UNPKG

ccxt

Version:

A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go

1,035 lines (1,032 loc) 85.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var Exchange = require('./Exchange.js'); var Precise = require('./Precise.js'); var errors = require('./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`. */ class PredictionExchange extends Exchange.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 errors.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 errors.ExchangeError(this.id + ' filterEventsBySearchIn() missing title'); } if (checkTitle && (title.indexOf(q) >= 0)) { matched = true; break; } if (description === undefined) { throw new errors.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 errors.NotSupported(this.id + ' fetchEvents() is not supported yet'); } async fetchEvent(id, params = {}) { throw new errors.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 errors.BadSymbol(this.id + ' has no cached event ' + eventIdOrSlug + " - call fetchEvents ({ 'query': ... }) first"); } outcome(outcomeSymbol) { if (outcomeSymbol === undefined) { throw new errors.ArgumentsRequired(this.id + ' outcome() requires an outcomeSymbol argument'); } if ((this.outcomes === undefined) || this.isEmpty(this.outcomes)) { throw new errors.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 errors.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 errors.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 errors.BadSymbol)) { throw e; } } if (this.hasOutcome(outcomeSymbol)) { return this.safeOutcome(outcomeSymbol); } } throw new errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 errors.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 * @param {int} [limit] the maximum number of trades to fetch * @param {object} [params] extr