ccxt
Version:
1,035 lines (1,033 loc) • 85.4 kB
JavaScript
// ----------------------------------------------------------------------------
// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
// EDIT THE CORRESPONDENT .ts FILE INSTEAD
// ----------------------------------------------------------------------------
import { BaseExchange } from './Exchange.js';
import { Precise } from './Precise.js';
import { ExchangeError, BadSymbol, NotSupported, ArgumentsRequired } from './errors.js';
// ----------------------------------------------------------------------------
/**
* @class PredictionExchange
* @augments BaseExchange
* @description Base class for prediction-market exchanges. It carries the
* prediction-specific state (events / outcomes) and helpers, and re-declares the
* single-market unified methods using an `outcome` symbol instead of a `symbol`.
*/
export default class PredictionExchange extends BaseExchange {
constructor() {
super(...arguments);
this.outcomes = undefined;
this.outcomes_by_id = undefined;
this.events = undefined;
this.events_by_slug = undefined;
}
// METHODS BELOW THIS LINE ARE TRANSPILED FROM TYPESCRIPT
describe() {
return this.deepExtend(super.describe(), {
'has': {
'prediction': true,
'approve': false,
'redeem': false,
'fetchEvent': false,
'fetchEvents': false,
'fetchOutcome': false,
'fetchSettlements': false,
'createOrder': false,
'createOrders': false,
'createLimitOrder': false,
'createMarketOrder': false,
'createMarketOrderWs': false,
'createMarketBuyOrderWithCost': false,
'cancelOrder': false,
'cancelOrders': false,
'cancelAllOrders': false,
'editOrder': false,
'fetchBalance': false,
'fetchOrder': false,
'fetchOrders': false,
'fetchOrdersByIds': false,
'fetchOrderTrades': false,
'fetchOpenOrders': false,
'fetchClosedOrders': false,
'fetchCanceledOrders': false,
'fetchMyTrades': false,
'fetchPosition': false,
'fetchPositions': false,
'fetchAccounts': false,
'fetchLedger': false,
'fetchDeposits': false,
'fetchWithdrawals': false,
'fetchMarkets': false,
'fetchCurrencies': false,
'fetchTicker': false,
'fetchTickers': false,
'fetchOrderBook': false,
'fetchL2OrderBook': false,
'fetchOHLCV': false,
'fetchTrades': false,
'fetchStatus': false,
'fetchTime': false,
'fetchOpenInterest': false,
'fetchTradingFee': false,
'watchTicker': false,
'watchTickers': false,
'watchOrderBook': false,
'watchTrades': false,
'watchOrders': false,
'watchMyTrades': false,
'watchOHLCV': false,
'watchPositions': false,
},
});
}
isPrediction() {
return this.safeBool(this.has, 'prediction', false) === true;
}
parseSearchQueries(params = {}) {
// accepts either `query` (a single search string) or `queries` (a list of strings)
const singleQuery = this.safeString(params, 'query');
if (singleQuery !== undefined) {
return [singleQuery];
}
return this.safeList(params, 'queries', []);
}
requireEventQuery(params = {}) {
// fetchEvents must be scoped by at least one selector — an unfiltered call would page the
// entire exchange. require one of query / queries / tags / eventId / slug, or one of the
// venue-specific scope params an exchange declares in options['eventScopeParams'],
// e.g. kalshi's category / series_ticker
const query = this.safeString(params, 'query');
const queries = this.safeList(params, 'queries', []);
const tags = this.safeList(params, 'tags', []);
const eventId = this.safeString(params, 'eventId');
const slug = this.safeString(params, 'slug');
const queriesLength = queries.length;
const tagsLength = tags.length;
if ((query !== undefined) || (queriesLength > 0) || (tagsLength > 0) || (eventId !== undefined) || (slug !== undefined)) {
return undefined;
}
const extraScopeParams = this.safeList(this.options, 'eventScopeParams', []);
const extraScopeParamsLength = extraScopeParams.length;
let extraNames = '';
for (let i = 0; i < extraScopeParamsLength; i++) {
const scopeKey = extraScopeParams[i];
if (scopeKey in params) {
return undefined;
}
extraNames = extraNames + ', ' + scopeKey;
}
throw new ArgumentsRequired(this.id + ' fetchEvents() requires at least one of query, queries, tags, eventId, slug' + extraNames + ' to scope the search');
}
applyEventFetchParams(events, params = {}, queries = undefined) {
// applies the unified fetchEvents options client-side (eventId/slug/status/searchIn/sort/limit)
// so exchanges whose API can't filter natively still support them consistently.
// every fetched event lands in the cache before filtering, so loadEvents()/event()
// serve them later without another request
this.setEvents(events);
let result = events;
const eventId = this.safeString(params, 'eventId');
const slug = this.safeString(params, 'slug');
if ((eventId !== undefined) || (slug !== undefined)) {
const filtered = [];
for (let i = 0; i < result.length; i++) {
const event = result[i];
const idMatch = (eventId !== undefined) && (this.safeString(event, 'id') === eventId);
const slugMatch = (slug !== undefined) && (this.safeString(event, 'slug') === slug);
if (idMatch || slugMatch) {
filtered.push(event);
}
}
result = filtered;
}
result = this.filterEventsByStatus(result, this.safeString(params, 'status'));
result = this.filterEventsByTags(result, this.safeList(params, 'tags'));
// own-line length read so the regex transpiler treats `queries` as an array (count())
// and not a string (strlen()); guard undefined since the default is undefined
let queriesLength = 0;
if (queries !== undefined) {
queriesLength = queries.length;
}
if (queriesLength > 0) {
result = this.filterEventsBySearchIn(result, queries, this.safeString(params, 'searchIn'));
}
const sort = this.safeString(params, 'sort');
if (sort !== undefined) {
let sortKey = undefined;
if (sort === 'volume') {
sortKey = 'volume';
}
else if (sort === 'liquidity') {
sortKey = 'liquidity';
}
else if (sort === 'newest') {
sortKey = 'created';
}
if (sortKey !== undefined) {
// normalize the sort key on every row first — sortBy reads it with a raw
// subscript, which raises KeyError/undefined-index in Python/PHP when a
// venue's parsed event omits the field (JS alone tolerates the miss)
for (let i = 0; i < result.length; i++) {
result[i][sortKey] = this.safeNumber(result[i], sortKey, 0);
}
result = this.sortBy(result, sortKey, true, 0);
}
}
const limit = this.safeInteger(params, 'limit');
if (limit !== undefined) {
// clamp to the result length: arraySlice(x, 0, limit) with limit > length panics in Go
// via reflect Slice, and throws in C#, unlike JS/Python which return the whole array
const resultLength = result.length;
let sliceEnd = limit;
if (sliceEnd > resultLength) {
sliceEnd = resultLength;
}
result = this.arraySlice(result, 0, sliceEnd);
}
return result;
}
filterEventsByStatus(events, status = undefined) {
// 'active' | 'inactive' | 'closed' | 'all' — 'inactive' and 'closed' are interchangeable
if ((status === undefined) || (status === 'all')) {
return events;
}
const wantActive = (status === 'active');
const result = [];
for (let i = 0; i < events.length; i++) {
const event = events[i];
const isActive = this.safeBool(event, 'active');
// keep events whose status is unknown (already filtered server-side, no `active` field)
if ((isActive === undefined) || (isActive === wantActive)) {
result.push(event);
}
}
return result;
}
filterEventsBySearchIn(events, queries, searchIn = undefined) {
// keep events whose title and/or description contains one of the queries (searchIn defaults to 'both')
// own-line length read so the regex transpiler uses count() (array) not strlen() (string)
let queriesLength = 0;
if (queries !== undefined) {
queriesLength = queries.length;
}
if ((searchIn === undefined) || (queries === undefined) || (queriesLength === 0)) {
return events;
}
const checkTitle = (searchIn === 'title') || (searchIn === 'both');
const checkDescription = (searchIn === 'description') || (searchIn === 'both');
const result = [];
for (let i = 0; i < events.length; i++) {
const event = events[i];
const title = this.safeStringLower(event, 'title', '');
const description = this.safeStringLower(event, 'description', '');
let matched = false;
for (let qi = 0; qi < queries.length; qi++) {
const q = queries[qi].toLowerCase();
if (title === undefined) {
throw new ExchangeError(this.id + ' filterEventsBySearchIn() missing title');
}
if (checkTitle && (title.indexOf(q) >= 0)) {
matched = true;
break;
}
if (description === undefined) {
throw new ExchangeError(this.id + ' filterEventsBySearchIn() missing description');
}
if (checkDescription && (description.indexOf(q) >= 0)) {
matched = true;
break;
}
}
if (matched) {
result.push(event);
}
}
return result;
}
normalizeTagKey(tag) {
// reduce a tag to lowercase alphanumeric words joined by single spaces ("Fed Rates" /
// "fed-rates" / "FED_RATES" all become "fed rates") so label, slug and handle spellings
// of the same tag compare equal — venues surface tags in different forms and callers
// pass any of them. keeping the word boundary avoids cross-word false positives that
// plain concatenation would create ("us open" vs "household")
const lower = tag.toLowerCase();
const allowed = 'abcdefghijklmnopqrstuvwxyz0123456789';
const chars = this.stringToCharsArray(lower);
let s = '';
let pendingSep = false;
for (let i = 0; i < chars.length; i++) {
const ch = chars[i];
if (allowed.indexOf(ch) >= 0) {
if (pendingSep && (s !== '')) {
s = s + ' ';
}
s = s + ch;
pendingSep = false;
}
else {
pendingSep = true;
}
}
return s;
}
filterEventsByTags(events, tags = undefined) {
// keep events carrying one of the requested tags; tolerant to string tags and to
// object tags ({ slug, title, ... }) since venues differ. no-op when no tags requested
if ((tags === undefined) || (tags.length === 0)) {
return events;
}
const wanted = [];
for (let i = 0; i < tags.length; i++) {
const wantedKey = this.normalizeTagKey(tags[i]);
if (wantedKey !== '') {
// an empty normalized key would substring-match every tag
wanted.push(wantedKey);
}
}
const result = [];
for (let i = 0; i < events.length; i++) {
const event = events[i];
const eventTags = this.safeList(event, 'tags', []);
let matched = false;
for (let ti = 0; ti < eventTags.length; ti++) {
const tag = eventTags[ti];
let tagLabel = undefined;
if (typeof tag === 'string') {
tagLabel = tag;
}
else {
tagLabel = this.safeString2(tag, 'slug', 'title');
}
if (tagLabel !== undefined) {
const tagKey = this.normalizeTagKey(tagLabel);
for (let wi = 0; wi < wanted.length; wi++) {
if (tagKey.indexOf(wanted[wi]) >= 0) {
matched = true;
break;
}
}
}
if (matched) {
break;
}
}
if (matched) {
result.push(event);
}
}
return result;
}
async fetchEvents(params = {}) {
throw new NotSupported(this.id + ' fetchEvents() is not supported yet');
}
async fetchEvent(id, params = {}) {
throw new NotSupported(this.id + ' fetchEvent() is not supported yet');
}
setEvents(events) {
// merge (not reset) so successive scoped fetchEvents calls accumulate into the cache.
// index by the unified `event` handle too (that's the identifier every outcome's `event`
// field carries), so getEvent (handle) resolves without each exchange hand-writing it
if (this.events === undefined) {
this.events = {};
}
if (this.events_by_slug === undefined) {
this.events_by_slug = {};
}
for (let i = 0; i < events.length; i++) {
const event = events[i];
const id = this.safeString(event, 'id');
const slug = this.safeString(event, 'slug');
const handle = this.safeString(event, 'event');
if (id !== undefined) {
this.events[id] = event;
}
if (handle !== undefined) {
this.events[handle] = event;
}
if (slug !== undefined) {
this.events_by_slug[slug] = event;
}
}
return this.events;
}
eventsList() {
// the cached events as a list; empty on a cold instance (this.events is keyed by both
// id and handle, so de-duplicate by identity before returning)
if (this.events === undefined) {
return [];
}
const result = [];
const seen = {};
const keys = Object.keys(this.events);
for (let i = 0; i < keys.length; i++) {
const event = this.events[keys[i]];
const identity = this.safeString2(event, 'id', 'event', keys[i]);
if (!(identity in seen)) {
seen[identity] = true;
result.push(event);
}
}
return result;
}
async loadEventsHelper(reload = false, params = {}) {
// note: the cache-hit shortcut ignores params, so events fetched under one scope are
// returned for a later differently-scoped call. events are scoped (unlike global
// markets), so prefer fetchEvents (params) directly when you need a specific scope
if (!reload && this.events) {
return this.events;
}
const events = await this.fetchEvents(params);
return this.setEvents(events);
}
async loadEvents(reload = false, params = {}) {
// cached entry point mirroring loadMarkets. unlike loadMarkets there is no cross-call
// promise coalescing: the promise-sharing idiom is not expressible in the transpiled
// base, so two truly concurrent first calls may fetch twice (both land in the cache)
return await this.loadEventsHelper(reload, params);
}
getEvent(eventIdOrSlug) {
// cache-only event resolver (the event analogue of this.outcome) - the cache fills
// through fetchEvents; this never fetches
if ((this.events !== undefined) && (eventIdOrSlug in this.events)) {
return this.events[eventIdOrSlug];
}
if ((this.events_by_slug !== undefined) && (eventIdOrSlug in this.events_by_slug)) {
return this.events_by_slug[eventIdOrSlug];
}
throw new BadSymbol(this.id + ' has no cached event ' + eventIdOrSlug + " - call fetchEvents ({ 'query': ... }) first");
}
outcome(outcomeSymbol) {
if (outcomeSymbol === undefined) {
throw new ArgumentsRequired(this.id + ' outcome() requires an outcomeSymbol argument');
}
if ((this.outcomes === undefined) || this.isEmpty(this.outcomes)) {
throw new ExchangeError(this.id + ' outcomes not loaded - call loadOutcomes () or an outcome-addressed method first');
}
if (outcomeSymbol in this.outcomes) {
return this.outcomes[outcomeSymbol];
}
if ((this.outcomes_by_id !== undefined) && (outcomeSymbol in this.outcomes_by_id)) {
return this.outcomes_by_id[outcomeSymbol];
}
throw new BadSymbol(this.id + ' does not have outcome ' + outcomeSymbol + ' - pass a known outcome handle or outcomeId, or call fetchEvents ()/loadOutcomes () first');
}
hasOutcome(outcomeIdOrSymbol) {
// sync cache-only membership probe — never throws and never fetches. this is the predicate
// behind loadOutcome's fast path and loadOutcomes' miss filter; safeOutcome (stub on miss)
// and outcome (throws on miss) are the accessors
if (outcomeIdOrSymbol === undefined) {
return false;
}
if ((this.outcomes !== undefined) && (outcomeIdOrSymbol in this.outcomes)) {
return true;
}
if ((this.outcomes_by_id !== undefined) && (outcomeIdOrSymbol in this.outcomes_by_id)) {
return true;
}
return false;
}
safeOutcome(outcomeIdOrSymbol, outcomeObj = undefined) {
if (outcomeIdOrSymbol !== undefined) {
if ((this.outcomes !== undefined) && (outcomeIdOrSymbol in this.outcomes)) {
return this.outcomes[outcomeIdOrSymbol];
}
if ((this.outcomes_by_id !== undefined) && (outcomeIdOrSymbol in this.outcomes_by_id)) {
return this.outcomes_by_id[outcomeIdOrSymbol];
}
}
if (outcomeObj !== undefined) {
return outcomeObj;
}
return { 'outcome': outcomeIdOrSymbol, 'outcomeId': outcomeIdOrSymbol, 'market': undefined, 'label': undefined, 'event': undefined, 'info': {} };
}
safeOutcomeSymbol(outcomeIdOrSymbol, outcomeObj = undefined) {
outcomeObj = this.safeOutcome(outcomeIdOrSymbol, outcomeObj);
return outcomeObj['outcome'];
}
shortenSlug(slug) {
const replacements = {
'federal-reserve': 'fed',
'interest-rates': 'rates',
'interest-rate': 'rate',
'basis-points': 'bps',
'basis-point': 'bp',
'executive-order': 'eo',
'united-states': 'us',
'united-kingdom': 'uk',
'european-union': 'eu',
'artificial-intelligence': 'ai',
'republican-party': 'gop',
'democratic-party': 'dems',
'stock-market': 'market',
'price-target': 'pt',
'market-cap': 'mcap',
'increase': 'hike',
'decrease': 'cut',
'higher': 'up',
'lower': 'down',
'greater': 'gt',
'less': 'lt',
'million': 'M',
'billion': 'B',
'trillion': 'T',
'percent': 'pct',
};
const stopWords = [
'will', 'the', 'a', 'an', 'after', 'before', 'in', 'at', 'by',
'of', 'there', 'be', 'to', 'or', 'and', 'for', 'on', 'its',
'that', 'this', 'from', 'with', 'as', 'is', 'are', 'was', 'were', '?', 'how', 'many', 'who', 'what', 'when', 'where', 'which', 'much',
];
const lower = (slug === undefined) ? '' : slug.toLowerCase();
const allowed = 'abcdefghijklmnopqrstuvwxyz0123456789';
const chars = this.stringToCharsArray(lower);
let s = '';
let lastDash = true; // start true to drop leading separators
for (let i = 0; i < chars.length; i++) {
const ch = chars[i];
if (allowed.indexOf(ch) >= 0) {
s = s + ch;
lastDash = false;
}
else if (!lastDash) {
s = s + '-';
lastDash = true;
}
}
const replacementKeys = Object.keys(replacements);
for (let i = 0; i < replacementKeys.length; i++) {
const replacementKey = replacementKeys[i];
const replacementValue = this.safeString(replacements, replacementKey);
if (replacementValue !== undefined) {
s = s.replaceAll(replacementKey, replacementValue);
}
}
const rawParts = s.split('-');
const parts = [];
for (let i = 0; i < rawParts.length; i++) {
const w = rawParts[i];
if (w.length > 0 && !this.inArray(w, stopWords)) {
parts.push(w);
}
}
const joined = parts.join('_');
return joined.toUpperCase();
}
slugToMarketSymbol(eventSlug, marketSlug) {
// eventSlug is nullable (Str): markets without a parent event (e.g. myriad's 1:1 markets)
// pass undefined — the body already collapses an absent event to just the market part.
// a strict `string` param would make PHP/typed transpilers throw on null before the body runs.
// qualify the market handle with its event so two events that share a market label
// — e.g. kalshi's KXFEDDECISION-28JAN and -27OCT both list "Cut 25bps" — do NOT collapse
// to the same handle — a collision silently overwrites markets in this.markets and would
// resolve an outcome to the wrong event (wrong-market trade). skip the prefix when the
// event slug is absent or identical to the market slug (e.g. myriad's 1:1 markets), so
// already-unique handles stay clean.
const marketPart = this.shortenSlug(marketSlug);
const eventPart = this.shortenSlug(eventSlug);
if ((eventPart === undefined) || (eventPart === '') || (eventPart === marketPart)) {
return marketPart;
}
return eventPart + '_' + marketPart;
}
slugToOutcomeSymbol(eventSlug, marketSlug, outcome) {
// build on slugToMarketSymbol so the outcome handle stays consistent with the market symbol
// — both event-qualified or both not — otherwise a qualified market + unqualified outcome mismatch.
// the label gets a light slug treatment (uppercase alphanumerics joined by '_', no stop-word
// removal so labels like "UP OR DOWN" survive intact) — venue labels with spaces or
// currency symbols ("JD Vance", a dollar-sign price) yield clean handles (JD_VANCE, 120)
// instead of leaking raw text into the outcome handle
if (outcome === undefined) {
outcome = '';
}
const upper = outcome.toUpperCase();
const allowed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const chars = this.stringToCharsArray(upper);
let label = '';
let pendingSep = false;
for (let i = 0; i < chars.length; i++) {
const ch = chars[i];
if (allowed.indexOf(ch) >= 0) {
if (pendingSep && (label !== '')) {
label = label + '_';
}
label = label + ch;
pendingSep = false;
}
else {
pendingSep = true;
}
}
if (label === '') {
// a label with no alphanumerics at all (unrealistic, but keep the :LABEL contract)
label = upper;
}
return this.slugToMarketSymbol(eventSlug, marketSlug) + ':' + label;
}
setMarkets(markets, currencies = undefined) {
// prediction market rows carry only the unified `market` handle — `symbol` is
// deprecated there. the base indexer keys this.markets/this.symbols by 'symbol',
// so alias the handle onto a shallow copy per row; the caller's rows stay symbol-free
const marketsList = this.toArray(markets);
const aliased = [];
for (let i = 0; i < marketsList.length; i++) {
const row = marketsList[i];
const copy = this.extend({}, row);
copy['symbol'] = this.safeString2(row, 'market', 'symbol');
aliased.push(copy);
}
const stored = super.setMarkets(aliased, currencies);
// strip the alias back off the stored rows — venues assemble user-visible event
// structures from this.markets (hyperliquid groups its outcome markets that way),
// so a leftover 'symbol' key would leak the deprecated field back to the caller
const marketKeys = Object.keys(stored);
for (let i = 0; i < marketKeys.length; i++) {
const key = marketKeys[i];
stored[key] = this.omit(stored[key], 'symbol');
}
this.populateOutcomes();
return stored;
}
indexMarketOutcomes(market) {
// index one market's outcome tokens into this.outcomes / this.outcomes_by_id,
// normalizing each to the canonical identity keys (outcome / outcomeId / market) so
// consumers and the safe* helpers stay uniform even when an exchange's parseMarket
// still emits the legacy symbol / id / marketSymbol keys. used both by populateOutcomes
// for a full rebuild and by on-demand single-market fetches (kalshi fetchOutcome), so a
// cache miss doesn't force a full O(markets x outcomes) rebuild per new outcome
if (this.outcomes === undefined) {
this.outcomes = {};
}
if (this.outcomes_by_id === undefined) {
this.outcomes_by_id = {};
}
const outcomesList = this.safeList(market, 'outcomes', []);
for (let j = 0; j < outcomesList.length; j++) {
const oc = outcomesList[j];
let ocSymbol = this.safeString2(oc, 'outcome', 'symbol');
const ocId = this.safeString2(oc, 'outcomeId', 'id');
// assign unconditionally — safeString2 keeps the canonical key when present
// and falls back to the legacy one, so this never clobbers and avoids a
// missing-key access that throws in Python/PHP, unlike TS undefined
oc['outcomeId'] = ocId;
oc['market'] = this.safeString2(oc, 'market', 'marketSymbol');
if (ocSymbol !== undefined) {
// shortenSlug is lossy, so two different markets can produce the same handle.
// on a real collision of same handle but different outcomeId, disambiguate the
// second one deterministically instead of silently overwriting the first —
// trading the wrong market would otherwise be indistinguishable
const existing = this.safeValue(this.outcomes, ocSymbol);
if (existing !== undefined) {
const existingId = this.safeString(existing, 'outcomeId');
if ((existingId !== undefined) && (ocId !== undefined) && (existingId !== ocId)) {
const idLen = ocId.length;
let suffix = ocId;
if (idLen > 6) {
suffix = ocId.slice(idLen - 6);
}
ocSymbol = ocSymbol + '_' + suffix.toUpperCase();
}
}
oc['outcome'] = ocSymbol;
this.outcomes[ocSymbol] = oc;
}
else {
oc['outcome'] = ocSymbol;
}
if (ocId !== undefined) {
this.outcomes_by_id[ocId] = oc;
}
}
}
populateOutcomes() {
// rebuild the whole outcome lookup cache from this.markets (each market carries its
// outcome tokens under the outcomes key) so cached market data works offline. no-op on
// a cold instance where markets are not loaded yet (avoids a null-access crash on the
// eventId/slug-only fetchEvents path)
this.outcomes = {};
this.outcomes_by_id = {};
if (this.markets === undefined) {
return;
}
const marketKeys = Object.keys(this.markets);
for (let i = 0; i < marketKeys.length; i++) {
this.indexMarketOutcomes(this.markets[marketKeys[i]]);
}
}
indexEventOutcomes(event) {
// register a single event's markets into this.markets and rebuild the outcome cache so the
// handles fetchEvent() returns resolve immediately in outcome-addressed methods (fetchTicker,
// createOrder, ...). without this, on a cold instance or a loadAllOutcomes:false venue
// such as kalshi, the returned handles are unusable — fetchTicker(ev.markets[0].outcomes[0].outcome)
// BadSymbols because the outcome was never cached
if (this.markets === undefined) {
this.markets = this.createSafeDictionary();
}
const markets = this.safeList(event, 'markets', []);
const marketsLength = markets.length;
for (let i = 0; i < marketsLength; i++) {
const m = markets[i];
const marketHandle = this.safeString2(m, 'market', 'symbol');
if (marketHandle !== undefined) {
this.markets[marketHandle] = m;
}
}
this.populateOutcomes();
}
async loadOutcomes(outcomes = undefined, reload = false, params = {}) {
// outcome-addressed methods call this first, mirroring loadMarkets(). two modes:
// - an `outcomes` list (scoped): sync-filter the cache and resolve ONLY the misses through
// fetchOutcomes — venues with a batch by-id endpoint (kalshi, polymarket) override it to
// collapse all misses into one request; a warm cache returns with zero per-outcome awaits
// - no `outcomes` (bulk): load the capped markets listing once and index every outcome —
// idempotent unless reload; only worth paying on venues whose whole universe is one
// cheap request (hyperliquid), or when the user explicitly wants the top-N set
// loadMarkets()/populateOutcomes() rebuild the lookup caches explicitly (the setMarkets
// override is not dispatched by the base loadMarkets under the Go/C#/Java transpilers)
if (outcomes !== undefined) {
let missing = [];
for (let i = 0; i < outcomes.length; i++) {
if (reload || !this.hasOutcome(outcomes[i])) {
missing.push(outcomes[i]);
}
}
let missingLength = missing.length;
const wasWarm = (this.outcomes !== undefined) && !this.isEmpty(this.outcomes);
const loadAll = this.safeBool(this.options, 'loadAllOutcomes', false);
if ((missingLength > 0) && loadAll && !wasWarm && !reload) {
// same trade-off as loadOutcome: on venues where the whole universe is one cheap
// request (hyperliquid), a cold miss bulk-warms once instead of fetching per outcome
await this.loadOutcomes();
const stillMissing = [];
for (let i = 0; i < missingLength; i++) {
if (!this.hasOutcome(missing[i])) {
stillMissing.push(missing[i]);
}
}
missing = stillMissing;
missingLength = missing.length;
}
if (missingLength > 0) {
await this.fetchOutcomes(missing);
}
return this.outcomes;
}
if (!reload && (this.outcomes !== undefined) && !this.isEmpty(this.outcomes)) {
return this.outcomes;
}
await this.loadMarkets(reload, params);
this.populateOutcomes();
return this.outcomes;
}
/**
* @ignore
* @method
* @name PredictionExchange#fetchOutcomes
* @description resolves several uncached outcomes. the base has no batch by-id endpoint, so it fetches them one by one through fetchOutcome (which throws BadSymbol for an unresolvable one); venues with a batch endpoint (kalshi, polymarket) override this to collapse the list into one request
* @param {string[]} outcomeSymbols the uncached outcome handles or ids to resolve
* @returns {object} the outcome cache
*/
async fetchOutcomes(outcomeSymbols) {
for (let i = 0; i < outcomeSymbols.length; i++) {
await this.fetchOutcome(outcomeSymbols[i]);
}
return this.outcomes;
}
async loadOutcome(outcomeSymbol, reload = false) {
// resolve a single outcome — the per-outcome analogue of loadMarkets()+market(). a cache hit
// returns at once (pass reload=true to skip the cache and refetch the outcome's metadata).
// on a miss, fetchOutcome resolves just the requested outcome on demand — a by-id fetch on
// venues with such an endpoint (kalshi, polymarket) or the venue's scoped search otherwise.
// options.loadAllOutcomes (default false) opts back into the legacy bulk warm-up: the first
// miss loads the whole (capped) listing once so later lookups are 0-network hits — only
// sane on venues whose full universe is one cheap request (hyperliquid)
if (outcomeSymbol === undefined) {
throw new ArgumentsRequired(this.id + ' loadOutcome() requires an outcomeSymbol argument');
}
if (!reload) {
if (this.hasOutcome(outcomeSymbol)) {
return this.safeOutcome(outcomeSymbol);
}
const wasWarm = (this.outcomes !== undefined) && !this.isEmpty(this.outcomes);
// if markets are already loaded (offline-injected, or loaded by loadMarkets/fetchEvents)
// but the outcome cache is cold, index them for free before hitting the network — this
// makes cold-cache resolution consistent across languages regardless of loadAllOutcomes
if (!wasWarm && (this.markets !== undefined) && !this.isEmpty(this.markets)) {
this.populateOutcomes();
if (this.hasOutcome(outcomeSymbol)) {
return this.safeOutcome(outcomeSymbol);
}
}
const loadAll = this.safeBool(this.options, 'loadAllOutcomes', false);
if (loadAll && !wasWarm) {
// a miss on a cold cache: bulk-load once so later lookups are 0-network hits.
// a miss on an already-warm cache is authoritative — the outcome genuinely isn't
// listed, so fall through to fetchOutcome (a real BadSymbol) rather than refetching
// the whole listing (which would mask typos and clobber offline-injected markets)
await this.loadOutcomes();
if (this.hasOutcome(outcomeSymbol)) {
return this.safeOutcome(outcomeSymbol);
}
}
}
return await this.fetchOutcome(outcomeSymbol);
}
outcomeSearchQuery(outcomeSymbol) {
// derive a human search query from a unified outcome handle (EVENT_MARKET:LABEL) so a
// cache miss can be resolved through the venue's scoped search instead of a bulk listing
// download. returns undefined for id-like inputs (numeric token ids, 0x hashes) that
// carry no searchable words
let marketPart = outcomeSymbol;
const colonIndex = outcomeSymbol.indexOf(':');
if (colonIndex >= 0) {
marketPart = outcomeSymbol.slice(0, colonIndex);
}
if (marketPart.indexOf('0x') === 0) {
return undefined;
}
// handles join words with '_' (slug-derived) or legacy '-' separated inputs (normalized below)
const normalized = marketPart.toLowerCase().replaceAll('-', '_');
const rawWords = normalized.split('_');
const words = [];
let hasLetters = false;
const letters = 'abcdefghijklmnopqrstuvwxyz';
for (let i = 0; i < rawWords.length; i++) {
const word = rawWords[i];
// inline .length so the php transpiler emits strlen() — the standalone
// `const n = str.length;` statement form wrongly becomes count() (array)
if (word.length === 0) {
continue;
}
let wordHasLetters = false;
const chars = this.stringToCharsArray(word);
for (let ci = 0; ci < chars.length; ci++) {
if (letters.indexOf(chars[ci]) >= 0) {
wordHasLetters = true;
break;
}
}
// the query is the handle's letter-bearing words only. standalone numeric
// tokens (slug timestamps, strikes, years) are venue artifacts that title searches don't
// reliably index — and since the result is re-checked against the EXACT handle,
// a broader query only adds recall, never a wrong match
if (!wordHasLetters) {
continue;
}
words.push(word);
hasLetters = true;
}
const wordsLength = words.length;
if ((wordsLength === 0) || !hasLetters) {
// a purely numeric/symbolic handle is an id, not searchable text
return undefined;
}
return words.join(' ');
}
async fetchOutcome(outcomeSymbol) {
// fetch just one outcome on demand — never through a bulk listing download. the base has
// no generic by-id endpoint, so it derives a search query from the handle and resolves it
// through the venue's own scoped fetchEvents (which caches everything it finds), then
// re-checks the cache. venues with a real by-id fetch (kalshi by ticker, polymarket by
// token id) override this with a cheaper single fetch and fall back to super on a miss.
const searchQuery = this.outcomeSearchQuery(outcomeSymbol);
if ((searchQuery !== undefined) && this.safeBool(this.has, 'fetchEvents', false)) {
const searchLimit = this.safeInteger(this.options, 'fetchOutcomeSearchLimit', 10);
try {
await this.fetchEvents({ 'query': searchQuery, 'limit': searchLimit });
}
catch (e) {
// a query with zero matches surfaces as BadSymbol on some venues — treat it as a
// plain miss (the guidance-rich throw below); let real transport errors propagate
if (!(e instanceof BadSymbol)) {
throw e;
}
}
if (this.hasOutcome(outcomeSymbol)) {
return this.safeOutcome(outcomeSymbol);
}
}
throw new BadSymbol(this.id + ' could not resolve outcome ' + outcomeSymbol + " — call fetchEvents ({ 'query': ... }) first, or pass a known outcomeId");
}
/**
* @method
* @name fetchTicker
* @description fetches a price ticker for a single prediction outcome
* @param {string} outcome unified outcome handle
* @param {object} [params] extra exchange-specific parameters
* @returns {object} a prediction [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
*/
async fetchTicker(outcome, params = {}) {
throw new NotSupported(this.id + ' fetchTicker() is not supported yet');
}
/**
* @method
* @name fetchTickers
* @description fetches price tickers for multiple prediction outcomes at once
* @param {string[]} [outcomes] unified outcome handles or outcome ids
* @param {object} [params] extra exchange-specific parameters
* @returns {object} a dictionary of prediction [ticker structures](https://docs.ccxt.com/#/?id=ticker-structure) indexed by outcome
*/
async fetchTickers(outcomes = undefined, params = {}) {
throw new NotSupported(this.id + ' fetchTickers() is not supported yet');
}
/**
* @method
* @name fetchOrderBook
* @description fetches the order book for a prediction outcome
* @param {string} outcome unified outcome handle
* @param {int} [limit] the maximum number of order book entries to return
* @param {object} [params] extra exchange-specific parameters
* @returns {object} a prediction [order book structure](https://docs.ccxt.com/#/?id=order-book-structure)
*/
async fetchOrderBook(outcome, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' fetchOrderBook() is not supported yet');
}
/**
* @method
* @name fetchOHLCV
* @description fetches historical candlestick data for a prediction outcome
* @param {string} outcome unified outcome handle
* @param {string} timeframe the length of time each candle represents
* @param {int} [since] timestamp in ms of the earliest candle to fetch
* @param {int} [limit] the maximum number of candles to fetch
* @param {object} [params] extra exchange-specific parameters
* @returns {int[][]} a list of candles ordered as timestamp, open, high, low, close, volume
*/
async fetchOHLCV(outcome, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
return await super.fetchOHLCV(outcome, timeframe, since, limit, params);
}
/**
* @method
* @name fetchTrades
* @description get the list of most recent trades for a prediction outcome
* @param {string} outcome unified outcome handle
* @param {int} [since] timestamp in ms of the earliest trade to fetch
* @param {int} [limit] the maximum number of trades to fetch
* @param {object} [params] extra exchange-specific parameters
* @returns {object[]} a list of prediction [trade structures](https://docs.ccxt.com/#/?id=public-trades)
*/
async fetchTrades(outcome, since = undefined, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' fetchTrades() is not supported yet');
}
/**
* @method
* @name createOrder
* @description create a trade order on a prediction outcome
* @param {string} outcome unified outcome handle
* @param {string} type 'market' or 'limit'
* @param {string} side 'buy' or 'sell'
* @param {float} amount how many shares of the outcome to trade
* @param {float} [price] the price at which the order is to be filled, in cost per share
* @param {object} [params] extra exchange-specific parameters
* @returns {object} a prediction [order structure](https://docs.ccxt.com/#/?id=order-structure)
*/
async createOrder(outcome, type, side, amount, price = undefined, params = {}) {
throw new NotSupported(this.id + ' createOrder() is not supported yet');
}
/**
* @method
* @name cancelOrder
* @description cancels an open order
* @param {string} id order id
* @param {string} [outcome] unified outcome handle
* @param {object} [params] extra exchange-specific parameters
* @returns {object} a prediction [order structure](https://docs.ccxt.com/#/?id=order-structure)
*/
async cancelOrder(id, outcome = undefined, params = {}) {
throw new NotSupported(this.id + ' cancelOrder() is not supported yet');
}
/**
* @method
* @name watchTicker
* @description watches a price ticker for a single prediction outcome
* @param {string} outcome unified outcome handle
* @param {object} [params] extra exchange-specific parameters
* @returns {object} a prediction [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
*/
async watchTicker(outcome, params = {}) {
throw new NotSupported(this.id + ' watchTicker() is not supported yet');
}
/**
* @method
* @name watchOrderBook
* @description watches the order book for a prediction outcome
* @param {string} outcome unified outcome handle
* @param {int} [limit] the maximum number of order book entries to return
* @param {object} [params] extra exchange-specific parameters
* @returns {object} a prediction [order book structure](https://docs.ccxt.com/#/?id=order-book-structure)
*/
async watchOrderBook(outcome, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' watchOrderBook() is not supported yet');
}
/**
* @method
* @name watchTrades
* @description watches the most recent trades for a prediction outcome
* @param {string} outcome unified outcome handle
* @param {int} [since] timestamp in ms of the earliest trade to fetch
* @param {int} [limit] the maximum number of trades to fetch
* @param {object} [params] extra exchange-specific parameters
* @returns {object[]} a list of prediction [trade structures](https://docs.ccxt.com/#/?id=public-trades)
*/
async watchTrades(outcome, since = undefined, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' watchTrades() is not supported yet');
}
/**
* @method
* @name fetchOrders
* @description fetches information on multiple orders made by the user
* @param {string} [outcome] unified outcome handle
* @param {int} [since] timestamp in ms of the earliest order to fetch
* @param {int} [limit] the maximum number of orders to fetch
* @param {object} [params] extra exchange-specific parameters
* @returns {object[]} a list of prediction [order structures](https://docs.ccxt.com/#/?id=order-structure)
*/
async fetchOrders(outcome = undefined, since = undefined, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' fetchOrders() is not supported yet');
}
/**
* @method
* @name fetchOpenOrders
* @description fetches information on the user's open orders
* @param {string} [outcome] unified outcome handle
* @param {int} [since] timestamp in ms of the earliest order to fetch
* @param {int} [limit] the maximum number of orders to fetch
* @param {object} [params] extra exchange-specific parameters
* @returns {object[]} a list of prediction [order structures](https://docs.ccxt.com/#/?id=order-structure)
*/
async fetchOpenOrders(outcome = undefined, since = undefined, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' fetchOpenOrders() is not supported yet');
}
/**
* @method
* @name fetchClosedOrders
* @description fetches information on multiple closed orders made by the user
* @param {string} [outcome] unified outcome handle
* @param {int} [since] timestamp in ms of the earliest order to fetch
* @param {int} [limit] the maximum number of orders to fetch
* @param {object} [params] extra exchange-specific parameters
* @returns {object[]} a list of prediction [order structures](https://docs.ccxt.com/#/?id=order-structure)
*/
async fetchClosedOrders(outcome = undefined, since = undefined, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' fetchClosedOrders() is not supported yet');
}
/**
* @method
* @name fetchOrderTrades
* @description fetch all the trades made from a single order
* @param {string} id order id
* @param {string} [outcome] unified outcome handle
* @param {int} [since] timestamp in ms of the earliest trade to fetch
* @param {int} [limit] the maximum number of trades to fetch
* @param {object} [params] extra exchange-specific parameters
* @returns {object[]} a list of prediction [trade structures](https://docs.ccxt.com/#/?id=trade-structure)
*/
async fetchOrderTrades(id, outcome = undefined, since = undefined, limit = undefined, params = {}) {
throw new NotSupported(this.id + ' fetchOrderTrades() is not supported yet');
}
/**
* @method
* @name fetchMyTrades
* @description fetch all trades made by the user
* @param {string} [outcome] unified outcome handle
* @param {int} [since] timestamp in ms of the earliest trade to fetch
* @par