UNPKG

ccxt

Version:

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

998 lines (995 loc) 172 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var sha2_js = require('@noble/hashes/sha2.js'); var sha3_js = require('@noble/hashes/sha3.js'); var secp256k1_js = require('@noble/curves/secp256k1.js'); var polymarket$1 = require('../abstract/prediction/polymarket.js'); var crypto = require('../base/functions/crypto.js'); var number = require('../base/functions/number.js'); var Precise = require('../base/Precise.js'); var Cache = require('../base/ws/Cache.js'); var errors = require('../base/errors.js'); // ---------------------------------------------------------------------------- // --------------------------------------------------------------------------- /** * @class polymarket * @augments Exchange */ class polymarket extends polymarket$1["default"] { describe() { return this.deepExtend(super.describe(), { 'id': 'polymarket', 'name': 'Polymarket', 'countries': ['US'], 'rateLimit': 100, 'certified': false, 'pro': true, 'streaming': { // Polymarket's CLOB ws (market + user channels) has no protocol-level ping-pong; // it requires a text "PING" every 10s and replies "PONG" (see @see in describe) 'ping': this.ping, 'keepAlive': 10000, }, 'has': { 'CORS': undefined, 'spot': false, 'margin': false, 'swap': false, 'future': false, 'option': false, 'cancelAllOrders': true, 'cancelOrder': true, 'cancelOrders': true, 'createMarketBuyOrderWithCost': true, 'createOrder': true, 'createOrders': true, 'fetchBalance': true, 'fetchCurrencies': false, 'fetchDeposits': false, 'fetchEvent': true, // Custom: fetch a single Polymarket event by id/slug 'fetchEvents': true, // Custom: fetch Polymarket events 'fetchLedger': false, 'fetchMarkets': true, // Each outcome token = one market 'fetchMyTrades': true, 'fetchOHLCV': true, 'fetchOpenInterest': true, 'fetchOpenOrders': true, 'fetchOrder': true, 'fetchOrderBook': true, 'fetchOrderTrades': true, 'fetchPosition': true, 'fetchPositions': true, 'fetchStatus': true, 'fetchTicker': true, 'fetchTickers': true, 'fetchTime': true, 'fetchTrades': true, 'fetchTradingFee': true, 'fetchWithdrawals': false, 'prediction': true, // Prediction market support 'watchMyTrades': true, 'watchOrderBook': true, 'watchOrders': true, 'watchTicker': true, 'watchTrades': true, }, 'timeframes': { '1m': '1', '5m': '5', '1h': '60', '6h': '360', '1d': '1440', }, 'urls': { 'logo': 'https://github.com/user-attachments/assets/89e1a2c4-a682-44e7-ad50-9fb15b534437', 'api': { 'gamma': 'https://gamma-api.polymarket.com', 'clob': 'https://clob.polymarket.com', 'data': 'https://data-api.polymarket.com', 'ws': 'wss://ws-subscriptions-clob.polymarket.com/ws/market', 'wsUser': 'wss://ws-subscriptions-clob.polymarket.com/ws/user', }, 'www': 'https://polymarket.com', 'doc': ['https://docs.polymarket.com'], 'fees': 'https://docs.polymarket.com/#fees', }, 'api': { 'gamma': { 'public': { 'get': { 'status': 1, 'comments': 1, 'comments/{id}': 1, 'comments/user_address/{user_address}': 1, 'events': 1, 'events/creators': 1, 'events/creators/{id}': 1, 'events/keyset': 1, 'events/pagination': 1, 'events/results': 1, 'events/slug/{slug}': 1, 'events/{id}': 1, 'events/{id}/comments/count': 1, 'events/{id}/tags': 1, 'events/{id}/tweet-count': 1, 'markets': 1, 'markets/keyset': 1, 'markets/slug/{slug}': 1, 'markets/{id}': 1, 'markets/{id}/description': 1, 'markets/{id}/tags': 1, 'profiles/user_address/{user_address}': 1, 'public-profile': 1, 'public-search': 1, 'series': 1, 'series-summary/slug/{slug}': 1, 'series-summary/{id}': 1, 'series/{id}': 1, 'series/{id}/comments/count': 1, 'sports': 1, 'sports/market-types': 1, 'tags': 1, 'tags/slug/{slug}': 1, 'tags/slug/{slug}/related-tags': 1, 'tags/slug/{slug}/related-tags/tags': 1, 'tags/{id}': 1, 'tags/{id}/related-tags': 1, 'tags/{id}/related-tags/tags': 1, 'teams': 1, 'teams/{id}': 1, }, 'post': { 'markets/abridged': 1, 'markets/information': 1, }, }, }, 'clob': { 'public': { 'get': { 'book': 1, 'books': 1, 'builder/trades': 1, 'clob-markets/{condition_id}': 1, 'fee-rate': 1, 'fee-rate/{token_id}': 1, 'last-trade-price': 1, 'last-trades-prices': 1, 'markets-by-token/{token_id}': 1, 'markets/live-activity/{condition_id}': 1, 'midpoint': 1, 'midpoints': 1, 'neg-risk': 1, 'neg-risk/{token_id}': 1, 'price': 1, 'prices': 1, 'prices-history': 1, 'rebates/current': 1, 'rewards/markets/current': 1, 'rewards/markets/multi': 1, 'rewards/markets/{condition_id}': 1, 'sampling-markets': 1, 'sampling-simplified-markets': 1, 'simplified-markets': 1, 'spread': 1, 'tick-size': 1, 'tick-size/{token_id}': 1, 'time': 1, }, 'post': { 'batch-prices-history': 1, 'books': 1, 'last-trades-prices': 1, 'markets/live-activity': 1, 'midpoints': 1, 'prices': 1, 'spreads': 1, }, }, 'private': { 'get': { 'auth/api-keys': 1, 'auth/ban-status/closed-only': 1, 'auth/builder-api-key': 1, 'auth/derive-api-key': 1, 'balance-allowance': 1, 'balance-allowance/update': 1, 'data/order/{id}': 1, 'data/orders': 1, 'data/trades': 1, 'notifications': 1, 'order-scoring': 1, 'orders-scoring': 1, 'rewards/user': 1, 'rewards/user/markets': 1, 'rewards/user/percentages': 1, 'rewards/user/total': 1, }, 'post': { 'auth/api-key': 1, 'auth/builder-api-key': 1, 'heartbeats': 1, 'order': 1, 'orders': 1, 'v1/heartbeats': 1, }, 'delete': { 'auth/api-key': 1, 'auth/builder-api-key': 1, 'cancel-all': 1, 'cancel-market-orders': 1, 'notifications': 1, 'order': 1, 'orders': 1, }, }, }, 'data': { 'public': { 'get': { 'activity': 1, 'closed-positions': 1, 'holders': 1, 'live-volume': 1, 'oi': 1, 'other': 1, 'positions': 1, 'revisions': 1, 'traded': 1, 'trades': 1, 'v1/accounting/snapshot': 1, 'v1/activity/combos': 1, 'v1/builders/leaderboard': 1, 'v1/builders/volume': 1, 'v1/leaderboard': 1, 'v1/market-positions': 1, 'v1/positions/combos': 1, 'value': 1, }, }, }, 'combos': { 'public': { 'get': { 'v1/rfq/combo-markets': 1, }, }, 'private': { 'post': { 'v1/maker/confirmations': 1, 'v1/maker/quotes': 1, 'v1/maker/quotes/cancel': 1, }, }, }, }, 'exceptions': { 'exact': { 'not enough balance / allowance': errors.InsufficientFunds, 'invalid signature': errors.AuthenticationError, 'api key not found': errors.AuthenticationError, }, 'broad': { 'No orderbook exists': errors.BadSymbol, // gamma rejects non-token-id values in clob_token_ids with a 422 'invalid clob token ids': errors.BadSymbol, 'not enough balance': errors.InsufficientFunds, 'allowance': errors.InsufficientFunds, 'invalid amount': errors.InvalidOrder, 'invalid price': errors.InvalidOrder, 'minimum tick size': errors.InvalidOrder, // a FAK/FOK order that finds no match is killed (a normal order outcome, not a // transport outage) — map it to OrderNotFillable so callers don't retry as if down 'no orders found to match': errors.OrderNotFillable, 'could not be fully filled': errors.OrderNotFillable, 'geoblocked': errors.PermissionDenied, 'restricted jurisdiction': errors.PermissionDenied, 'Unauthorized': errors.AuthenticationError, }, }, 'requiredCredentials': { // dual auth: either pass the L2 api credentials directly // apiKey=POLY_API_KEY, secret=POLY_API_SECRET, password=POLY_PASSPHRASE // or a privateKey to derive them (see loadApiCredentials); none are // individually required, so validation happens in loadApiCredentials 'apiKey': false, 'secret': false, 'password': false, 'privateKey': false, // EOA private key, used to derive L2 creds + sign orders 'walletAddress': false, // Ethereum wallet address (POLY_ADDRESS) }, 'fees': { 'trading': { 'tierBased': false, 'percentage': true, 'maker': 0.0, 'taker': 0.0, }, }, 'options': { 'defaultFetchEventsLimit': 100, // gamma caps each /events response at 100, so paginate at that page size 'eventsPageSize': 100, // cap the cold-start listing: each gamma event is heavy (~90 KB with the HTML // description), so 200 events (2 pages, volume-ordered) is ~18 MB; raise via // params.limit or this option when you need a wider set 'fetchMarketsLimit': 200, 'maxFetchEventsLimit': 500, 'defaultEventStatus': 'active', // 'active' | 'closed' | 'all' // prices-history rejects startTs/endTs spans over 15 days regardless of fidelity 'maxPricesHistoryWindow': 1296000, // CTF Exchange V2 signing constants (Polygon); the V2 contracts are the same on // mainnet (137) and Amoy (80002), see @polymarket/clob-client config.ts 'chainId': 137, 'ctfExchangeName': 'Polymarket CTF Exchange', 'ctfExchangeVersion': '2', 'exchangeAddress': '0xE111180000d2663C0091e4f400237545B87B996B', 'negRiskExchangeAddress': '0xe2222d279d744050d28e00520010520000310F59', 'builder': '0xea409de8b037bb6ac664b6d12d6831b03cb04a37', 'builderFee': true, // when true, feeRate below is packed into the builder code's upper bytes 'feeRate': 0, // builder fee in bps, applied only when builderFee is true }, }); } /** * @method * @name polymarket#fetchMarkets * @description retrieves data on all markets for polymarket, each prediction market becomes one market with its outcome tokens listed under the outcomes key * @see https://docs.polymarket.com/api-reference/events/list-events * @see https://docs.polymarket.com/api-reference/search/search-markets-events-and-profiles * @param {object} [params] extra exchange-specific parameters * @param {string} [params.query] a single search term used to filter the fetched events * @param {string[]} [params.queries] multiple search terms (alternative to query) * @param {string[]} [params.tags] filter events by tag — human-readable labels ("Fed Rates") or slugs ("fed-rates") both work; multiple tags match ANY (one gamma listing per tag, unioned) * @param {string} [params.status] 'active', 'closed' or 'all', the status of the events to fetch, defaults to 'active' * @param {int} [params.limit] max number of events to fetch when no query is given (defaults to options.fetchMarketsLimit, 200); the listing is ordered by 24h volume so the most active markets come first — outcomes on lower-volume markets are resolvable on demand by their token id (fetchOutcome) * @returns {object[]} an array of objects representing market data */ async fetchMarkets(params = {}) { const queries = this.parseSearchQueries(params); const rest = this.omit(params, ['query', 'queries']); const queriesLength = queries.length; let rawEvents = []; if (queriesLength > 0) { rawEvents = await this.fetchRawEventsBySearch(queries, rest); } else { rawEvents = await this.fetchRawEventsList(rest); } const flatMarkets = []; const eventsDict = {}; for (let rei = 0; rei < rawEvents.length; rei++) { const rawEvent = rawEvents[rei]; const ccxtMarkets = this.parseEventToMarkets(rawEvent); for (let mi = 0; mi < ccxtMarkets.length; mi++) { flatMarkets.push(ccxtMarkets[mi]); } const parsedEvent = this.parseEvent(rawEvent); const eventSlug = this.safeString(rawEvent, 'slug'); if (eventSlug) { const eventKey = this.shortenSlug(eventSlug); eventsDict[eventKey] = parsedEvent; } } this.events = eventsDict; return flatMarkets; } /** * @ignore * @method * @name polymarket#fetchRawEventsBySearch * @description fetches raw gamma event objects matching the given search terms, paginating through all result pages * @see https://docs.polymarket.com/api-reference/search/search-markets-events-and-profiles * @param {string[]} queries search terms * @param {object} [params] extra exchange-specific parameters * @param {int} [params.limit] page size per search query, defaults to 50 * @returns {object[]} an array of raw gamma event objects */ async fetchRawEventsBySearch(queries, params = {}) { const resultLimit = this.safeInteger(params, 'limit'); // fixed page size (gamma's limit_per_type). do NOT tie it to `limit`: that made a small // limit fan out into many tiny-page requests (limit:1 -> ~one request per matching event). // tunable per-call via params.searchPageSize, else the exchange option, else 100 const optionPageSize = this.safeInteger(this.options, 'searchPageSize', 100); const pageSize = this.safeInteger(params, 'searchPageSize', optionPageSize); // map the unified sort/status onto the gamma search params const sort = this.safeString(params, 'sort'); let sortParam = 'volume'; if (sort === 'liquidity') { sortParam = 'liquidity'; } else if (sort === 'newest') { sortParam = 'startDate'; } const status = this.safeString(params, 'status', 'active'); let eventsStatus = 'active'; if ((status === 'closed') || (status === 'inactive')) { eventsStatus = 'closed'; } else if (status === 'all') { eventsStatus = undefined; } const rest = this.omit(params, ['limit', 'sort', 'status', 'searchIn', 'eventId', 'slug', 'query', 'queries', 'searchPageSize', 'maxSearchPages']); const seen = {}; const rawEvents = []; for (let qi = 0; qi < queries.length; qi++) { const q = queries[qi]; const baseRequest = { 'q': q, 'limit_per_type': pageSize, 'sort': sortParam, 'ascending': false }; if (eventsStatus !== undefined) { baseRequest['events_status'] = eventsStatus; } let firstRequest = { 'page': 1 }; firstRequest = this.extend(this.extend(firstRequest, baseRequest), rest); const first = await this.gammaPublicGetPublicSearch(firstRequest); const firstEvents = this.safeList(first, 'events', []); const firstEventsLength = firstEvents.length; const pagination = this.safeDict(first, 'pagination', {}); const totalResults = this.safeInteger(pagination, 'totalResults', firstEventsLength); let totalPages = Math.ceil(totalResults / pageSize); // only page as far as `limit` needs (applyEventFetchParams slices to it afterwards); // with no limit, cap the fan-out at options.maxSearchPages so a broad query stays bounded if (resultLimit !== undefined) { const limitPages = Math.ceil(resultLimit / pageSize); if (limitPages < totalPages) { totalPages = limitPages; } } else { const optionMaxPages = this.safeInteger(this.options, 'maxSearchPages', 5); const maxSearchPages = this.safeInteger(params, 'maxSearchPages', optionMaxPages); if (maxSearchPages < totalPages) { totalPages = maxSearchPages; } } const remainingPages = []; for (let p = 2; p <= totalPages; p++) { remainingPages.push(p); } const restPromises = []; for (let pi = 0; pi < remainingPages.length; pi++) { let pageRequest = { 'page': remainingPages[pi] }; pageRequest = this.extend(this.extend(pageRequest, baseRequest), rest); restPromises.push(this.gammaPublicGetPublicSearch(pageRequest)); } const restResponses = await Promise.all(restPromises); const allEvents = []; for (let fi = 0; fi < firstEvents.length; fi++) { allEvents.push(firstEvents[fi]); } for (let ri = 0; ri < restResponses.length; ri++) { const pageEvents = this.safeList(restResponses[ri], 'events', []); for (let ei = 0; ei < pageEvents.length; ei++) { allEvents.push(pageEvents[ei]); } } for (let ei = 0; ei < allEvents.length; ei++) { const rawEvent = allEvents[ei]; const eventId = this.safeString(rawEvent, 'id'); if (eventId && !(eventId in seen)) { seen[eventId] = true; rawEvents.push(rawEvent); } } } return rawEvents; } /** * @ignore * @method * @name polymarket#tagToSlug * @description converts a human-readable tag label into gamma's slug form, "Fed Rates" -> "fed-rates"; lowercase alphanumeric runs joined by single dashes, so a tag already in slug form passes through unchanged * @param {string} tag the tag label or slug * @returns {string} the gamma tag slug */ tagToSlug(tag) { const lower = tag.toLowerCase(); const allowed = 'abcdefghijklmnopqrstuvwxyz0123456789'; const chars = this.stringToCharsArray(lower); let slug = ''; let pendingSep = false; for (let i = 0; i < chars.length; i++) { const ch = chars[i]; if (allowed.indexOf(ch) >= 0) { if (pendingSep && (slug !== '')) { slug = slug + '-'; } slug = slug + ch; pendingSep = false; } else { pendingSep = true; } } if (slug === '') { // a tag with no alphanumerics at all — pass it through so gamma just returns no match return lower; } return slug; } /** * @ignore * @method * @name polymarket#fetchRawEventsList * @description fetches raw gamma event objects from the events listing endpoint, paginating in parallel * @see https://docs.polymarket.com/api-reference/events/list-events * @param {object} [params] extra exchange-specific parameters * @param {string} [params.status] 'active', 'closed' or 'all', defaults to options.defaultEventStatus * @param {int} [params.limit] max number of events to fetch (default options.fetchMarketsLimit); the listing is ordered by 24h volume so the most active markets come first * @returns {object[]} an array of raw gamma event objects */ async fetchRawEventsList(params = {}) { // gamma hard-caps each response at 100 events regardless of the requested limit, so the // page size must be that cap or pagination never advances (the > check below stays false) const pageSize = this.safeInteger(this.options, 'eventsPageSize', 100); // scope the listing: without a search query loadMarkets would otherwise dump every // active event (tens of thousands of markets). Cap to `limit` events (most-traded first). const limit = this.safeInteger(params, 'limit', this.safeInteger(this.options, 'fetchMarketsLimit', 200)); const maxPages = Math.ceil(limit / pageSize); const status = this.safeString(params, 'status', this.safeString(this.options, 'defaultEventStatus', 'active')); // sort maps to the gamma `order` field; 'volume' is the default ranking const sort = this.safeString(params, 'sort'); let order = 'volume'; if (sort === 'liquidity') { order = 'liquidity'; } else if (sort === 'newest') { order = 'startDate'; } const rest = this.omit(params, ['status', 'limit', 'sort', 'searchIn', 'eventId', 'slug', 'query', 'queries', 'tags']); let baseRequest = { 'limit': pageSize, 'order': order, 'ascending': false }; baseRequest = this.extend(baseRequest, rest); // push requested tags server-side (gamma accepts one tag_slug per request) so a tags-only // fetchEvents returns the tagged events rather than filtering the top-volume listing down // to nothing; multiple tags run one listing per tag, unioned and deduped by event id const requestedTags = this.safeList(params, 'tags', []); const requestedTagsLength = requestedTags.length; if (requestedTagsLength > 1) { const seen = {}; const unioned = []; for (let ti = 0; ti < requestedTagsLength; ti++) { const singleTagParams = this.extend({}, params); singleTagParams['tags'] = [requestedTags[ti]]; const tagEvents = await this.fetchRawEventsList(singleTagParams); for (let ei = 0; ei < tagEvents.length; ei++) { const rawEvent = tagEvents[ei]; const eventId = this.safeString(rawEvent, 'id'); if ((eventId !== undefined) && !(eventId in seen)) { seen[eventId] = true; unioned.push(rawEvent); } } } return unioned; } if (requestedTagsLength > 0) { // gamma matches tag_slug case-insensitively but only in slug form ("fed-rates"), // so human-readable labels ("Fed Rates") must be slugified first baseRequest['tag_slug'] = this.tagToSlug(this.safeString(requestedTags, 0)); } if (status === 'active') { baseRequest['active'] = true; baseRequest['closed'] = false; } else if ((status === 'closed') || (status === 'inactive')) { baseRequest['active'] = false; baseRequest['closed'] = true; } // 'all' — no active/closed filter // fetch page 1 first; if full, fire remaining pages in parallel let firstPageRequest = { 'offset': 0 }; firstPageRequest = this.extend(firstPageRequest, baseRequest); const firstPageResponse = await this.gammaPublicGetEvents(firstPageRequest); const firstPage = (firstPageResponse !== undefined) ? firstPageResponse : []; const firstPageLength = firstPage.length; const allRawEvents = []; for (let fi = 0; fi < firstPageLength; fi++) { allRawEvents.push(firstPage[fi]); } if (firstPageLength >= pageSize) { const offsets = []; for (let p = 1; p < maxPages; p++) { offsets.push(p * pageSize); } const restPromises = []; for (let oi = 0; oi < offsets.length; oi++) { let pageRequest = { 'offset': offsets[oi] }; pageRequest = this.extend(pageRequest, baseRequest); restPromises.push(this.gammaPublicGetEvents(pageRequest)); } const restPages = await Promise.all(restPromises); for (let ri = 0; ri < restPages.length; ri++) { const page = (restPages[ri] !== undefined) ? restPages[ri] : []; const pageLength = page.length; for (let pi = 0; pi < pageLength; pi++) { allRawEvents.push(page[pi]); } } } const allRawEventsLength = allRawEvents.length; if (allRawEventsLength > limit) { return this.arraySlice(allRawEvents, 0, limit); } return allRawEvents; } parseEventToMarkets(event) { const eventSlug = this.safeString(event, 'slug', this.safeString(event, 'id')); const rawMarkets = this.safeList(event, 'markets', []); const result = []; // // { // "id":"604489", // "question":"Will Trump visit China by October 31?", // "conditionId":"0x3d69cc559693ee46ba58da16e43c4e75b8da67b99c2e9a9d2f72bb0222d0f137", // "slug":"will-trump-visit-china-by-october-31", // "resolutionSource":"", // "endDate":"2025-10-31T00:00:00Z", // "startDate":"2025-09-19T21:22:54.912576Z", // "image":"https://polymarket-upload.s3.us-east-2.amazonaws.com/will-trump-visit-china-by-october-31-ujqWMja0Uizt.png", // "icon":"https://polymarket-upload.s3.us-east-2.amazonaws.com/will-trump-visit-china-by-october-31-ujqWMja0Uizt.png", // "description":"If U.S. President Donald Trump visits China by October 31, 2025, 11:59 PM ET, this market will resolve to \\""Yes\\"". Otherwise, this market will resolve to \\""No\\"".\\n\\nFor the purpose of this market, a \\""visit\\"" is defined as Trump physically entering the terrestrial or maritime territory of the listed country. Whether or not Trump enters the country's airspace during the timeframe of this market will have no bearing on a positive resolution.\\n\\nThe primary resolution source for this information will be official information from government of the United States of America, official information from Trump or released by his verified social media accounts (e.g. https://twitter.com/POTUS), however, a consensus of credible reporting will also be used.", // "outcomes":"[\\""Yes\\"", \\""No\\""]", // "outcomePrices":"[\\"0\\", \\"1\\"]", // "volume":"549414.493468", // "active":true, // "closed":true, // "marketMakerAddress":"", // "createdAt":"2025-09-19T19:32:28.841694Z", // "updatedAt":"2026-03-09T22:44:28.896812Z", // "closedTime":"2025-11-01 06:28:08+00", // "new":false, // "featured":false, // "submitted_by":"0x91430CaD2d3975766499717fA0D66A78D814E5c5", // "archived":false, // "resolvedBy":"0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7", // "restricted":true, // "groupItemTitle":"October 31, 2025", // "groupItemThreshold":"0", // "questionID":"0x2dd49c70f01a5e7c687a9820d606ff0d85fb50199735928d52d98b3a57586969", // "umaEndDate":"2025-11-01T06:28:08Z", // "enableOrderBook":true, // "orderPriceMinTickSize":"0.001", // "orderMinSize":"5", // "umaResolutionStatus":"resolved", // "volumeNum":"549414.493468", // "endDateIso":"2025-10-31", // "startDateIso":"2025-09-19", // "hasReviewedDates":true, // "volume1wk":"121949.66579699998", // "volume1mo":"535231.3545640002", // "volume1yr":"549414.4934680002", // "clobTokenIds":"[\\"45601394554497090173642354630373884477724604907691447337031201817815960365378\\", \\"102652665954921241745169157447280060113772926009745753029489061659654862632571\\"]", // "umaBond":"500", // "umaReward":"5", // "volume1wkClob":"121949.66579699998", // "volume1moClob":"535231.3545640002", // "volume1yrClob":"549414.4934680002", // "volumeClob":"549414.493468", // "customLiveness":"0", // "acceptingOrders":false, // "negRisk":false, // "negRiskRequestID":"", // "ready":false, // "funded":false, // "acceptingOrdersTimestamp":"2025-09-19T21:22:32Z", // "cyom":false, // "pagerDutyNotificationEnabled":false, // "approved":true, // "rewardsMinSize":"100", // "rewardsMaxSpread":"3.5", // "spread":"0.001", // "automaticallyResolved":true, // "oneDayPriceChange":"-0.0005", // "oneWeekPriceChange":"-0.007", // "oneMonthPriceChange":"-0.011", // "lastTradePrice":"1", // "bestAsk":"0.001", // "automaticallyActive":true, // "clearBookOnStart":true, // "seriesColor":"", // "showGmpSeries":false, // "showGmpOutcome":false, // "manualActivation":false, // "negRiskOther":false, // "umaResolutionStatuses":"[\\""proposed\\""]", // "pendingDeployment":false, // "deploying":false, // "deployingTimestamp":"2025-09-19T21:22:01.979023Z", // "rfqEnabled":false, // "holdingRewardsEnabled":false, // "feesEnabled":false, // "requiresTranslation":false, // "feeType":null // } // for (let mi = 0; mi < rawMarkets.length; mi++) { const market = rawMarkets[mi]; const conditionId = this.safeString(market, 'conditionId'); const marketId = this.safeString(market, 'id'); const marketSlug = this.safeString(market, 'slug', conditionId); const active = this.safeBool(market, 'active', false); const closed = this.safeBool(market, 'closed', false); // resolution: a closed/uma-resolved market settles each outcome price to 0 or 1 const marketResolved = closed || (this.safeStringLower(market, 'umaResolutionStatus') === 'resolved'); let resolvedOutcome = undefined; // gamma exposes the order-book tick as orderPriceMinTickSize; minimumTickSize is the clob alias const tickSize = this.safeNumber2(market, 'orderPriceMinTickSize', 'minimumTickSize', 0.01); // real per-market min order size (shares) and price tick — don't hardcode 1 / 0.01..0.99 const orderMinSize = this.safeNumber(market, 'orderMinSize', 1); const priceMax = this.parseNumber(Precise["default"].stringSub('1', this.numberToString(tickSize))); const negRisk = this.safeBool(market, 'negRisk', false); const endDate = this.safeString(market, 'endDate', this.safeString(market, 'end_date_iso')); // Gamma API returns these arrays as JSON-encoded strings let outcomeLabels = []; let clobTokenIds = []; let outcomePrices = []; const parsedOutcomes = this.parseJson(this.safeString(market, 'outcomes', '[]')); const parsedTokenIds = this.parseJson(this.safeString(market, 'clobTokenIds', '[]')); const parsedPrices = this.parseJson(this.safeString(market, 'outcomePrices', '[]')); let parsedOutcomesLength = undefined; if (parsedOutcomes !== undefined) { parsedOutcomesLength = parsedOutcomes.length; } let parsedTokenIdsLength = undefined; if (parsedTokenIds !== undefined) { parsedTokenIdsLength = parsedTokenIds.length; } let parsedPricesLength = undefined; if (parsedPrices !== undefined) { parsedPricesLength = parsedPrices.length; } if (parsedOutcomes && (parsedOutcomesLength !== undefined)) { outcomeLabels = parsedOutcomes; } if (parsedTokenIds && (parsedTokenIdsLength !== undefined)) { clobTokenIds = parsedTokenIds; } if (parsedPrices && (parsedPricesLength !== undefined)) { outcomePrices = parsedPrices; } const outcomeLabelsLength = outcomeLabels.length; const clobTokenIdsLength = clobTokenIds.length; if (outcomeLabelsLength === 0 || clobTokenIdsLength === 0) { continue; } // Market outcome (no outcome suffix) const marketSymbol = this.slugToMarketSymbol(eventSlug, marketSlug); // Build outcomes array const outcomes = []; for (let oi = 0; oi < outcomeLabels.length; oi++) { const outcomeLabel = outcomeLabels[oi]; const clobTokenId = clobTokenIds[oi]; const outcomePrice = this.safeNumber(outcomePrices, oi); if (!clobTokenId) { continue; } const outcomeHandle = this.slugToOutcomeSymbol(eventSlug, marketSlug, outcomeLabel); let winnerRaw = undefined; let settleFractionRaw = undefined; if (marketResolved && (outcomePrice !== undefined)) { // a genuinely-settled polymarket outcome is at 1 (won) or 0 (lost). a market // that is only closed-for-trading (not yet UMA-resolved) still has fractional // prices — don't report a fractional mid as a final settleFraction; leave the // outcome-level fields undefined until a decisive price exists if (outcomePrice >= 0.99) { winnerRaw = true; settleFractionRaw = 1; resolvedOutcome = outcomeHandle; } else if (outcomePrice <= 0.01) { winnerRaw = false; settleFractionRaw = 0; } } // effectively-final copies: Java emits the object literal below as an anonymous // inner class, which cannot capture a reassigned local const winner = winnerRaw; const settleFraction = settleFractionRaw; outcomes.push({ 'outcome': outcomeHandle, 'outcomeId': clobTokenId, 'market': marketSymbol, 'label': outcomeLabel, 'price': outcomePrice, 'active': active && !closed, 'winner': winner, 'settleFraction': settleFraction, // carry the order precision so createOrder needs no extra request 'precision': { 'amount': tickSize, 'price': tickSize, }, 'negRisk': negRisk, 'info': market, }); } const baseId = (conditionId !== undefined) ? conditionId : marketId; const marketType = (outcomeLabelsLength > 2) ? 'categorical' : 'binary'; // effectively-final copy for the market object literal below (reassigned in the loop) const marketResolvedOutcome = resolvedOutcome; result.push({ 'id': conditionId, 'market': marketSymbol, 'marketType': marketType, 'executionModel': 'clob', 'collateral': 'USDC', 'base': 'USDC', 'quote': 'USDC', 'settle': undefined, 'baseId': baseId, 'quoteId': 'USDC', 'settleId': undefined, 'type': 'prediction', 'spot': false, 'margin': false, 'swap': false, 'future': false, 'option': false, 'prediction': true, 'active': active && !closed, 'resolved': marketResolved, 'resolvedOutcome': marketResolvedOutcome, 'contract': false, 'linear': undefined, 'inverse': undefined, 'contractSize': undefined, 'expiry': endDate ? this.parse8601(endDate) : undefined, 'expiryDatetime': endDate, 'strike': undefined, 'optionType': undefined, 'taker': 0.0, 'maker': 0.0, 'percentage': true, 'tierBased': false, 'feeSide': 'get', 'precision': { 'amount': tickSize, 'price': tickSize, }, 'limits': { 'leverage': { 'min': 1, 'max': 1 }, 'amount': { 'min': orderMinSize, 'max': undefined }, 'price': { 'min': tickSize, 'max': priceMax }, 'cost': { 'min': undefined, 'max': undefined }, }, 'outcomes': outcomes, 'info': market, 'created': undefined, }); } return result; } /** * @ignore * @method * @name polymarket#fetchOutcome * @description resolves a single outcome by its CLOB token id in one request, so a cache miss * (a bare token id, or a valid outcome on a market outside the top-volume cold cache) recovers * instead of throwing BadSymbol. an outcome HANDLE ("MARKET:LABEL") carries no token id, so it * falls back to the base bulk load * @param {string} outcomeSymbol the outcome token id or handle * @returns {object} the resolved outcome object */ async fetchOutcome(outcomeSymbol) { // a bare CLOB token id has no ':' (an outcome handle is always "MARKET:LABEL") and no // searchable words — outcomeSearchQuery returns undefined only for id-like inputs, so // word-bearing junk like 'BTC/USDT' skips the gamma by-id lookup (which 422s on // non-ids) and falls through to the search path and its local BadSymbol below. // absence must be `< 0` — the php transpiler maps that to `=== false`, while a literal // `=== -1` passes through and never matches mb_strpos's false return if ((outcomeSymbol.indexOf(':') < 0) && (this.outcomeSearchQuery(outcomeSymbol) === undefined)) { const response = await this.gammaPublicGetMarkets({ 'clob_token_ids': outcomeSymbol }); const rawMarkets = (response !== undefined) ? response : []; const rawMarketsLength = rawMarkets.length; if (rawMarketsLength > 0) { if (this.markets === undefined) { this.markets = this.createSafeDictionary(); } const ccxtMarkets = this.parseEventToMarkets({ 'markets': rawMarkets }); const ccxtMarketsLength = ccxtMarkets.length; for (let i = 0; i < ccxtMarketsLength; i++) { const mkt = ccxtMarkets[i]; if (mkt === undefined) { throw new errors.ExchangeError(this.id + ' fetchOutcome() could not resolve mkt'); } this.markets[mkt['market']] = mkt; } this.populateOutcomes(); const byId = this.safeValue(this.outcomes_by_id, outcomeSymbol); if (byId !== undefined) { return byId; } } } return await super.fetchOutcome(outcomeSymbol); } /** * @ignore * @method * @name polymarket#fetchOutcomes * @description resolves several uncached outcomes at once — bare CLOB token ids are batched into gamma markets requests (repeated clob_token_ids params, 50 per request to keep the URL bounded); handle-shaped symbols fall back to the single fetch and its search path * @see https://docs.polymarket.com/api-reference/markets/list-markets * @param {string[]} outcomeSymbols outcome token ids or handles * @returns {object} the outcome cache */ async fetchOutcomes(outcomeSymbols) { const tokenIds = []; for (let i = 0; i < outcomeSymbols.length; i++) { const outcomeSymbol = outcomeSymbols[i]; // only id-like symbols (no ':', no searchable words) belong in the by-id batch — // see the same gate in fetchOutcome. absence must be `< 0` — the php transpiler // maps that to `=== false`, while a literal `=== -1` passes through and never // matches mb_strpos's false return if ((outcomeSymbol.indexOf(':') < 0) && (this.outcomeSearchQuery(outcomeSymbol) === undefined)) { tokenIds.push(outcomeSymbol); } } const tokenIdsLength = tokenIds.length; if (tokenIdsLength > 0) { if (this.markets === undefined) { this.markets = this.createSafeDictionary(); } // token ids are ~78 chars each, so cap the batch to keep the URL under common limits const chunkSize = this.safeInteger(this.options, 'fetchOutcomesBatchSize', 50); let startIndex = 0; while (startIndex < tokenIdsLength) { let endIndex = this.sum(startIndex, chunkSize); if (endIndex > tokenIdsLength) { endIndex = tokenIdsLength; } const chunk = []; for (let i = startIndex; i < endIndex; i++) { chunk.push(tokenIds[i]); } // gamma matches repeated clob_token_ids params — comma-joined ids are rejected // with a validation error, so the list rides through urlencodeWithArrayRepeat const response = await this.gammaPublicGetMarkets({ 'clob_token_ids': chunk, 'limit': chunkSize }); const rawMarkets = (response !== undefined) ? response : []; const ccxtMarkets = this.parseEventToMarkets({ 'markets': rawMarkets }); for (let i = 0; i < ccxtMarkets.length; i++) { const mkt = ccxtMarkets[i]; if (mkt === undefined) { throw new errors.ExchangeError(this.id + ' fetchOutcomes() could not resolve mkt'); } this.markets[mkt['market']] = mkt; } startIndex = this.sum(startIndex, chunkSize); } this.populateOutcomes(); } for (let i = 0; i < outcomeSymbols.length; i++) { if (!this.hasOutcome(outcomeSymbols[i])) { await this.fetchOutcome(outcomeSymbols[i]); } } return this.outcomes; } /** * @method * @name polymarket#fetchTicker * @description fetches the current mid-price and best bid/ask for a single outcome token * @see https://docs.polymarket.com/api-reference/data/get-midpoint-price * @see https://docs.polymarket.com/api-reference/market-data/get-order-book * @see https://docs.polymarket.com/api-reference/data/get-last-trade-price * @param {string} outcome unified outcome like TRUMP_DANCE_TODAY_997:YES or an outcome token id * @param {object} [params] extra parameters specific to the exchange API endpoint * @returns {object} a [prediction ticker structure](https://docs.ccxt.com/#/?id=prediction-ticker-structure) */ async fetchTicker(outcome, params = {}) { const outcomeObj = await this.loadOutcome(outcome); const tokenId = outcomeObj['outcomeId']; const promises = [ this.clobPublicGetMidpoint({ 'token_id': tokenId }), this.clobPublicGetBook({ 'token_id': tokenId }), this.clobPublicGetLastTradePrice({ 'token_id': tokenId }), ]; const [midpointResponse, bookResponse, lastTradeResponse] = await Promise.all(promises); const response = { 'midpoint': midpointResponse, 'book': bookResponse, 'lastTrade': lastTradeResponse }; // // { // "midpoint": { // "mid": "0.9985" // }, // "book": { // "market": "0x2d55f622bc12e23dc1f1bb4db8360c28c92155f9376bf73953c0756ee1387b2f", // "asset_id": "16718041887881762329859205887704087070587186248220606272297433440108449709696", //