From 10e165d1cc55af2dc5488faa69416c1ad97a4995 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Tue, 2 Jun 2026 11:38:41 +0000 Subject: [PATCH 1/3] docs: clarify historical fetch order book usage --- docs/api-reference/fetch-order-book.mdx | 95 +++++++++++++++++-------- 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/docs/api-reference/fetch-order-book.mdx b/docs/api-reference/fetch-order-book.mdx index 1aea6e43..55b0cb4d 100644 --- a/docs/api-reference/fetch-order-book.mdx +++ b/docs/api-reference/fetch-order-book.mdx @@ -7,7 +7,11 @@ openapi: GET /api/{exchange}/fetchOrderBook ### Live order book -Fetch the current L2 order book for an outcome. If you already have an outcome token ID, pass it directly: +Fetch the current L2 order book for an outcome from the exchange's live order book endpoint. For Polymarket this is a live CLOB call: pass the outcome token ID directly and omit historical params. + + +`poly.fetch_order_book(token_id)` without `params.since` or `params.until` is live-only. It does not read PMXT Archive data and may fail for closed, resolved, or otherwise inactive Polymarket markets with an error such as `No orderbook exists`. + ```python Python @@ -28,15 +32,15 @@ console.log(`${book.bids.length} bids, ${book.asks.length} asks`); ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchOrderBook?outcomeId=104932610032177696635191871147557737718087870958469629338467406422339967452218" \ - -H "Authorization: Bearer $PMXT_API_KEY" + -H "Authorization: Bearer ***" ``` ### Historical snapshot -Get the order book at a specific point in time. Pass `since` as a Unix timestamp in milliseconds — returns the nearest snapshot at or before that time. +Get the order book at a specific point in time by passing `since` as a Unix timestamp in milliseconds. Historical Polymarket queries are served from the PMXT Archive and return the nearest reconstructed snapshot at or before that time. -For binary markets, you can pass the market ID and choose the side with `params.outcome`. Use `"yes"` or `"no"` instead of copying the long outcome token ID: +For binary markets, you can pass the Polymarket condition ID as the first argument and choose the side with `params.outcome`. Use `"yes"` or `"no"` instead of copying the long outcome token ID. ```python Python @@ -44,12 +48,9 @@ import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") -market = poly.fetch_market( - slug="will-spacex-starship-flight-test-12-launch-by-may-22-354-721" -) - +# Historical lookup against PMXT Archive, not the live CLOB. book = poly.fetch_order_book( - market.market_id, + "0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", params={"since": 1779487200000, "outcome": "yes"}, ) @@ -63,12 +64,9 @@ import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); -const market = await poly.fetchMarket({ - slug: "will-spacex-starship-flight-test-12-launch-by-may-22-354-721", -}); - +// Historical lookup against PMXT Archive, not the live CLOB. const book = await poly.fetchOrderBook( - market.marketId, + "0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", undefined, { since: 1779487200000, outcome: "yes" } ); @@ -79,9 +77,51 @@ console.log(` best ask: ${Math.min(...book.asks.map((a) => a.price)).toFixed(3) ```bash curl curl -X POST "https://api.pmxt.dev/api/polymarket/fetchOrderBook" \ - -H "Authorization: Bearer $PMXT_API_KEY" \ + -H "Authorization: Bearer ***" \ + -H "Content-Type: application/json" \ + -d '{"args":["0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", null, {"since": 1779487200000, "outcome": "yes"}]}' +``` + + +### Historical crypto 5m events + +Polymarket crypto 5-minute slugs such as `btc-updown-5m-1779481500` are event slugs. Use `fetch_event(slug=...)`, then select the nested market you want. Do not use `fetch_market(slug=...)` for these event-level slugs. + + +```python Python +import pmxt + +poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") +event = poly.fetch_event(slug="btc-updown-5m-1779481500") +market = event.markets[0] + +book = poly.fetch_order_book( + market.market_id, + params={"since": 1779487200000, "outcome": "yes"}, +) +print(f"{market.title}: {book.dt}") +``` + +```javascript JavaScript +import { Polymarket } from "pmxtjs"; + +const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); +const event = await poly.fetchEvent({ slug: "btc-updown-5m-1779481500" }); +const market = event.markets[0]; + +const book = await poly.fetchOrderBook( + market.marketId, + undefined, + { since: 1779487200000, outcome: "yes" } +); +console.log(`${market.title}: ${book.datetime}`); +``` + +```bash curl +curl -X POST "https://api.pmxt.dev/api/polymarket/fetchEvent" \ + -H "Authorization: Bearer ***" \ -H "Content-Type: application/json" \ - -d '{"args":["61b0ed20-7f42-41fd-af15-7b86153f6bb7", null, {"since": 1779487200000, "outcome": "yes"}]}' + -d '{"kwargs":{"slug":"btc-updown-5m-1779481500"}}' ``` @@ -89,17 +129,15 @@ curl -X POST "https://api.pmxt.dev/api/polymarket/fetchOrderBook" \ Pass both `since` and `until` to get an array of fully reconstructed L2 order book snapshots. Each snapshot is a complete book at that moment in time — not deltas. -Default 100 snapshots per request, max 1000. +The API returns up to `limit` snapshots from the PMXT Archive. Defaults to 100 snapshots per request, max 1000. For raw bulk downloads, use the Parquet files in the [PMXT Archive](https://archive.pmxt.dev) instead of trying to stream bulk data through the live order book endpoint. ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") - -market = poly.fetch_market( - slug="will-spacex-starship-flight-test-12-launch-by-may-22-354-721" -) +event = poly.fetch_event(slug="btc-updown-5m-1779481500") +market = event.markets[0] books = poly.fetch_order_book( market.market_id, @@ -107,6 +145,7 @@ books = poly.fetch_order_book( "since": 1779480000000, "until": 1779487200000, "outcome": "yes", + "limit": 100, } ) print(f"{len(books)} snapshots") @@ -118,15 +157,13 @@ for ob in books[:3]: import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); - -const market = await poly.fetchMarket({ - slug: "will-spacex-starship-flight-test-12-launch-by-may-22-354-721", -}); +const event = await poly.fetchEvent({ slug: "btc-updown-5m-1779481500" }); +const market = event.markets[0]; const books = await poly.fetchOrderBook( market.marketId, undefined, - { since: 1779480000000, until: 1779487200000, outcome: "yes" } + { since: 1779480000000, until: 1779487200000, outcome: "yes", limit: 100 } ); console.log(`${books.length} snapshots`); books.slice(0, 3).forEach((ob) => @@ -136,12 +173,12 @@ books.slice(0, 3).forEach((ob) => ```bash curl curl -X POST "https://api.pmxt.dev/api/polymarket/fetchOrderBook" \ - -H "Authorization: Bearer $PMXT_API_KEY" \ + -H "Authorization: Bearer ***" \ -H "Content-Type: application/json" \ - -d '{"args":["61b0ed20-7f42-41fd-af15-7b86153f6bb7", null, {"since": 1779480000000, "until": 1779487200000, "outcome": "yes"}]}' + -d '{"args":["0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", null, {"since": 1779480000000, "until": 1779487200000, "outcome": "yes", "limit": 100}]}' ``` -Historical order book data is backed by the [PMXT Archive](https://archive.pmxt.dev) — the same tick-level data available as Parquet files, but queryable directly from the API without downloading or parsing files. Supports Polymarket, Kalshi, Limitless, and Opinion. +Historical order book data is backed by the [PMXT Archive](https://archive.pmxt.dev) — the same tick-level data available as Parquet files, but queryable directly from the API without downloading or parsing files. Historical `fetchOrderBook` supports Polymarket, Kalshi, Limitless, and Opinion. From 69aaaeb405c900f9d0b747fb8363f98ddbc57fc8 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Fri, 5 Jun 2026 12:41:19 +0000 Subject: [PATCH 2/3] fix: resolve sdk drift parity sweep --- sdks/python/pmxt/__init__.py | 25 ++++++++ sdks/python/pmxt/_exchanges.py | 2 + sdks/python/pmxt/client.py | 79 ++++++++++-------------- sdks/python/pmxt/constants.py | 6 +- sdks/python/pmxt/errors.py | 35 +++++++++-- sdks/python/pmxt/feed_client.py | 2 +- sdks/python/pmxt/models.py | 76 +++++++++++++++++++++-- sdks/python/pmxt/router.py | 3 + sdks/python/pmxt/ws_client.py | 12 ++-- sdks/python/tests/test_converters.py | 14 +++++ sdks/python/tests/test_errors.py | 24 +++++++ sdks/python/tests/test_public_exports.py | 34 +++++++++- sdks/typescript/pmxt/server-manager.ts | 7 +++ 13 files changed, 254 insertions(+), 65 deletions(-) create mode 100644 sdks/python/tests/test_errors.py diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index 171a6ca8..deb28e39 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -19,6 +19,7 @@ from typing import Any, Dict, List from .client import Exchange +from .constants import ENV, ENV_BASE_URL, ENV_API_KEY from ._exchanges import Polymarket, Limitless, Kalshi, KalshiDemo, Probable, Baozi, Myriad, Opinion, Metaculus, Smarkets, PolymarketUS, Polymarket_us, Hyperliquid, GeminiTitan, SuiBets, Suibets, Mock, Router from .router import Router from .feed_client import FeedClient @@ -61,6 +62,13 @@ EventFilterCriteria, MarketFetchParams, EventFetchParams, + SeriesFetchParams, + TradesParams, + FetchOrderBookParams, + ExchangeOptions, + PolymarketOptions, + RouterOptions, + FeedClientOptions, MatchResult, EventMatchResult, MatchedMarketCluster, @@ -73,6 +81,9 @@ ExecutionPriceResult, MatchRelation, ClusterSortOption, + MatchedClusterSort, + FetchMatchedMarketClustersParams, + FetchMatchedEventClustersParams, SortOption, SearchIn, OrderSide, @@ -170,6 +181,14 @@ def restart_server() -> None: "Router", "Exchange", "FeedClient", + "ExchangeOptions", + "PolymarketOptions", + "RouterOptions", + "FeedClientOptions", + # Environment + "ENV", + "ENV_BASE_URL", + "ENV_API_KEY", # Server Management "ServerManager", "server", @@ -220,10 +239,16 @@ def restart_server() -> None: "SubscribedAddressSnapshot", "MatchRelation", "ClusterSortOption", + "MatchedClusterSort", + "FetchMatchedMarketClustersParams", + "FetchMatchedEventClustersParams", "MarketFilterCriteria", "EventFilterCriteria", "MarketFetchParams", "EventFetchParams", + "SeriesFetchParams", + "TradesParams", + "FetchOrderBookParams", "SortOption", "SearchIn", "OrderSide", diff --git a/sdks/python/pmxt/_exchanges.py b/sdks/python/pmxt/_exchanges.py index 5368bc6f..2854a8d2 100644 --- a/sdks/python/pmxt/_exchanges.py +++ b/sdks/python/pmxt/_exchanges.py @@ -547,3 +547,5 @@ def __init__( # Backwards-compatible aliases for exchange classes generated before underscore handling. Polymarket_us = PolymarketUS Suibets = SuiBets + +from .router import Router as Router diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index 741a8d92..957789a5 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -46,6 +46,9 @@ MarketFilterFunction, EventFilterCriteria, EventFilterFunction, + SeriesFetchParams, + TradesParams, + FetchOrderBookParams, SubscribedAddressSnapshot, FirehoseEvent, MatchResult, @@ -262,9 +265,9 @@ def _convert_subscription_snapshot(raw: Dict[str, Any]) -> SubscribedAddressSnap raw_positions = raw.get("positions") raw_balances = raw.get("balances") return _auto_convert(SubscribedAddressSnapshot, raw, - trades=[_convert_trade(t) for t in raw_trades] if raw_trades else None, - positions=[_convert_position(p) for p in raw_positions] if raw_positions else None, - balances=[_convert_balance(b) for b in raw_balances] if raw_balances else None, + trades=[_convert_trade(t) for t in (raw_trades or [])], + positions=[_convert_position(p) for p in (raw_positions or [])], + balances=[_convert_balance(b) for b in (raw_balances or [])], ) @@ -368,7 +371,7 @@ def __init__( effective_base_url = f"http://localhost:{actual_port}" except Exception as e: - raise Exception( + raise PmxtError( f"Failed to start PMXT server: {e}\n\n" f"Please ensure 'pmxt-core' is installed: npm install -g pmxt-core\n" f"Or start the server manually: pmxt-server" @@ -571,6 +574,7 @@ def _sidecar_read_request( same ``_parse_api_exception`` path as the POST fallback. """ base_url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/{method_name}" + query = _convert_params_to_camel(query) creds = self._get_credentials_dict() has_credentials = creds is not None @@ -772,7 +776,7 @@ def fetch_markets(self, params: Optional[dict] = None, **kwargs) -> List[Unified if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -795,7 +799,7 @@ def fetch_markets_paginated(self, params: Optional[dict] = None, **kwargs) -> Pa if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -810,7 +814,7 @@ def fetch_markets_paginated(self, params: Optional[dict] = None, **kwargs) -> Pa data = self._handle_response(json.loads(response.data)) return PaginatedMarketsResult( data=[_convert_market(m) for m in data.get("data", [])], - total=data.get("total", 0), + total=data.get("total"), next_cursor=data.get("nextCursor"), ) except ApiException as e: @@ -822,7 +826,7 @@ def fetch_events(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedE if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -839,13 +843,13 @@ def fetch_events(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedE except ApiException as e: raise self._parse_api_exception(e) from None - def fetch_series(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedSeries]: + def fetch_series(self, params: Optional[SeriesFetchParams] = None, **kwargs) -> List[UnifiedSeries]: try: args = [] if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -868,7 +872,7 @@ def fetch_market(self, params: Optional[dict] = None, **kwargs) -> UnifiedMarket if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -891,7 +895,7 @@ def fetch_event(self, params: Optional[dict] = None, **kwargs) -> UnifiedEvent: if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -908,7 +912,7 @@ def fetch_event(self, params: Optional[dict] = None, **kwargs) -> UnifiedEvent: except ApiException as e: raise self._parse_api_exception(e) from None - def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[float] = None, params: Optional[dict] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]: + def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[int] = None, params: Optional[FetchOrderBookParams] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]: try: args = [] if kwargs: @@ -920,7 +924,7 @@ def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, lim if params is not None: if limit is None: args.append(None) - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1020,13 +1024,13 @@ def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]: except ApiException as e: raise self._parse_api_exception(e) from None - def fetch_my_trades(self, params: Optional[dict] = None, **kwargs) -> List[UserTrade]: + def fetch_my_trades(self, params: Optional[TradesParams] = None, **kwargs) -> List[UserTrade]: try: args = [] if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1049,7 +1053,7 @@ def fetch_closed_orders(self, params: Optional[dict] = None, **kwargs) -> List[O if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1072,7 +1076,7 @@ def fetch_all_orders(self, params: Optional[dict] = None, **kwargs) -> List[Orde if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1194,7 +1198,7 @@ def fetch_market_matches(self, params: Optional[dict] = None, **kwargs) -> List[ if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1216,7 +1220,7 @@ def fetch_matches(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1239,7 +1243,7 @@ def fetch_event_matches(self, params: Optional[dict] = None, **kwargs) -> List[A if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1261,7 +1265,7 @@ def compare_market_prices(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1283,7 +1287,7 @@ def fetch_related_markets(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1306,7 +1310,7 @@ def fetch_matched_markets(self, params: Optional[dict] = None, **kwargs) -> List if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1329,7 +1333,7 @@ def fetch_matched_prices(self, params: Optional[dict] = None, **kwargs) -> List[ if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1351,7 +1355,7 @@ def fetch_hedges(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1374,7 +1378,7 @@ def fetch_arbitrage(self, params: Optional[dict] = None, **kwargs) -> List[Any]: if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1908,7 +1912,7 @@ def watch_order_book( if params: if limit is None: args.append(None) - args.append(params) + args.append(_convert_params_to_camel(params)) ws_data = self._watch_required_via_ws( "watch_order_book", @@ -1917,23 +1921,6 @@ def watch_order_book( ) return _convert_order_book(ws_data) - def unwatch_order_book(self, outcome_id: Union[str, "MarketOutcome"]) -> None: - """ - Unsubscribe from a previously watched order book stream. - - Args: - outcome_id: Outcome ID to stop watching - - Returns: - None - """ - outcome_id = _resolve_outcome_id(outcome_id) - self._unwatch_required_via_ws( - "unwatch_order_book", - "unwatchOrderBook", - [outcome_id], - ) - def watch_order_books( self, outcome_ids: List[Union[str, "MarketOutcome"]] = _UNSET, @@ -1985,7 +1972,7 @@ def watch_order_books( if params: if limit is None: args.append(None) - args.append(params) + args.append(_convert_params_to_camel(params)) raw_result = self._watch_batch_required_via_ws( "watch_order_books", diff --git a/sdks/python/pmxt/constants.py b/sdks/python/pmxt/constants.py index c8b93cd5..3a16303b 100644 --- a/sdks/python/pmxt/constants.py +++ b/sdks/python/pmxt/constants.py @@ -10,6 +10,7 @@ import os from typing import Mapping, NamedTuple, Optional +from types import SimpleNamespace #: The hosted pmxt production endpoint. #: @@ -28,8 +29,9 @@ #: Environment variable names. Centralised so tests and docs can reference #: a single source of truth. -ENV_BASE_URL = "PMXT_BASE_URL" -ENV_API_KEY = "PMXT_API_KEY" +ENV = SimpleNamespace(BASE_URL="PMXT_BASE_URL", API_KEY="PMXT_API_KEY") +ENV_BASE_URL = ENV.BASE_URL +ENV_API_KEY = ENV.API_KEY class ResolvedBaseUrl(NamedTuple): diff --git a/sdks/python/pmxt/errors.py b/sdks/python/pmxt/errors.py index 16d999c2..a0e38f8f 100644 --- a/sdks/python/pmxt/errors.py +++ b/sdks/python/pmxt/errors.py @@ -29,6 +29,10 @@ def __str__(self) -> str: # 4xx Client Errors +def _format_not_found_message(prefix: str, identifier: str) -> str: + return identifier if identifier.startswith(prefix) else f"{prefix}{identifier}" + + class BadRequest(PmxtError): """400 Bad Request - The request was malformed or contains invalid parameters.""" pass @@ -51,23 +55,38 @@ class NotFoundError(PmxtError): class OrderNotFound(NotFoundError): """404 Not Found - The requested order doesn't exist.""" - pass + def __init__(self, order_id: str, exchange: str | None = None): + super().__init__( + _format_not_found_message("Order not found: ", order_id), + code="ORDER_NOT_FOUND", + exchange=exchange, + ) class MarketNotFound(NotFoundError): """404 Not Found - The requested market doesn't exist.""" - pass + def __init__(self, market_id: str, exchange: str | None = None): + super().__init__( + _format_not_found_message("Market not found: ", market_id), + code="MARKET_NOT_FOUND", + exchange=exchange, + ) class EventNotFound(NotFoundError): """404 Not Found - The requested event doesn't exist.""" - pass + def __init__(self, identifier: str, exchange: str | None = None): + super().__init__( + _format_not_found_message("Event not found: ", identifier), + code="EVENT_NOT_FOUND", + exchange=exchange, + ) class RateLimitExceeded(PmxtError): """429 Too Many Requests - Rate limit exceeded.""" - def __init__(self, message: str, retry_after: int | None = None, **kwargs): + def __init__(self, message: str, retry_after: float | None = None, **kwargs): super().__init__(message, **kwargs) self.retry_after = retry_after @@ -94,12 +113,16 @@ def __init__(self, message: str, field: str | None = None, **kwargs): class NetworkError(PmxtError): """503 Service Unavailable - Network connectivity issues.""" - pass + + def __init__(self, message: str, exchange: str | None = None): + super().__init__(message, code="NETWORK_ERROR", retryable=True, exchange=exchange) class ExchangeNotAvailable(PmxtError): """503 Service Unavailable - Exchange is down or unreachable.""" - pass + + def __init__(self, message: str, exchange: str | None = None): + super().__init__(message, code="EXCHANGE_NOT_AVAILABLE", retryable=True, exchange=exchange) # Mapping from server error codes to error classes diff --git a/sdks/python/pmxt/feed_client.py b/sdks/python/pmxt/feed_client.py index ea9e741d..5ce92c5c 100644 --- a/sdks/python/pmxt/feed_client.py +++ b/sdks/python/pmxt/feed_client.py @@ -180,7 +180,7 @@ def _get(self, method: str, params: Dict[str, Any]) -> Any: req = urllib.request.Request(url, headers=self._headers) try: - with urllib.request.urlopen(req, timeout=15) as resp: + with urllib.request.urlopen(req, timeout=30) as resp: body = json.loads(resp.read()) except urllib.error.HTTPError as e: body = json.loads(e.read()) if e.fp else {} diff --git a/sdks/python/pmxt/models.py b/sdks/python/pmxt/models.py index 8dd12ac6..9d98e391 100644 --- a/sdks/python/pmxt/models.py +++ b/sdks/python/pmxt/models.py @@ -134,7 +134,7 @@ def question(self) -> str: return self.title -class MarketList(list): +class MarketList(List[UnifiedMarket]): """A list of UnifiedMarket objects with a convenience match() method.""" def match( @@ -543,9 +543,42 @@ class SubscribedAddressSnapshot: """Balances of this address""" balances: Optional[List[Balance]] = None -# ---------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# Public SDK option types +# ----------------------------------------------------------------------------- + +class ExchangeOptions(TypedDict, total=False): + """Constructor options shared by the exchange clients.""" + pmxt_api_key: str + base_url: str + auto_start_server: bool + api_key: str + private_key: str + api_token: str + proxy_address: str + signature_type: Union[str, int] + + +class PolymarketOptions(ExchangeOptions, total=False): + """Constructor options for Polymarket clients.""" + signature_type: Union[Literal["eoa", "poly-proxy", "gnosis-safe"], int] + + +class RouterOptions(TypedDict, total=False): + """Constructor options for Router clients.""" + pmxt_api_key: str + base_url: str + auto_start_server: bool + + +class FeedClientOptions(TypedDict, total=False): + """Constructor options for FeedClient.""" + pmxt_api_key: str + base_url: str + +# ----------------------------------------------------------------------------- # Filtering Types -# ---------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- from typing import TypedDict, Callable @@ -644,9 +677,35 @@ class EventFetchParams(TypedDict, total=False): filter: EventFilterCriteria -# ---------------------------------------------------------------------------- +class SeriesFetchParams(TypedDict, total=False): + """Parameters for fetching recurring venue series.""" + id: str + slug: str + query: str + recurrence: str + limit: int + offset: int + + +class TradesParams(TypedDict, total=False): + """Parameters for fetching public trade history.""" + since: int + until: int + limit: int + cursor: str + + +class FetchOrderBookParams(TypedDict, total=False): + """Parameters for historical order book queries.""" + side: Literal["yes", "no"] + outcome: str + since: int + until: int + + +# ----------------------------------------------------------------------------- # Router Types -# ---------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- MatchRelation = Literal["identity", "complement", "subset", "superset", "overlap", "disjoint"] ClusterSortOption = Literal["volume", "confidence"] @@ -655,6 +714,7 @@ class EventFetchParams(TypedDict, total=False): class MatchedMarketClusterParams(TypedDict, total=False): """Parameters for fetching matched market clusters.""" + market: UnifiedMarket market_id: str slug: str url: str @@ -677,6 +737,7 @@ class MatchedMarketClusterParams(TypedDict, total=False): class MatchedEventClusterParams(TypedDict, total=False): """Parameters for fetching matched event clusters.""" + event: UnifiedEvent event_id: str slug: str url: str @@ -740,6 +801,11 @@ def __getattr__(self, name: str) -> Any: return getattr(self.event, name) +MatchedClusterSort = ClusterSortOption +FetchMatchedMarketClustersParams = MatchedMarketClusterParams +FetchMatchedEventClustersParams = MatchedEventClusterParams + + @dataclass class MatchedMarketCluster: """A connected cluster of semantically matched markets across venues.""" diff --git a/sdks/python/pmxt/router.py b/sdks/python/pmxt/router.py index da2cc666..21142fb6 100644 --- a/sdks/python/pmxt/router.py +++ b/sdks/python/pmxt/router.py @@ -242,6 +242,7 @@ def fetch_event_matches( *, event_id: Optional[str] = None, slug: Optional[str] = None, + url: Optional[str] = None, query: Optional[str] = None, category: Optional[str] = None, relation: Optional[MatchRelation] = None, @@ -276,6 +277,8 @@ def fetch_event_matches( params["eventId"] = event_id if slug is not None: params["slug"] = slug + if url is not None: + params["url"] = url if query is not None: params["query"] = query if category is not None: diff --git a/sdks/python/pmxt/ws_client.py b/sdks/python/pmxt/ws_client.py index 329dd680..51a705c2 100644 --- a/sdks/python/pmxt/ws_client.py +++ b/sdks/python/pmxt/ws_client.py @@ -260,7 +260,8 @@ def subscribe( method: str, args: List[Any], credentials: Optional[Dict[str, Any]] = None, - timeout: float = 30.0, + timeout_ms: float = 30000.0, + timeout: Optional[float] = None, ) -> Dict[str, Any]: """Send a subscribe message and block until the first data event. @@ -302,7 +303,8 @@ def subscribe( self._ws.send(json.dumps(message)) - return self._wait_for_subscription_data(sub, timeout) + effective_timeout = timeout if timeout is not None else timeout_ms / 1000.0 + return self._wait_for_subscription_data(sub, effective_timeout) def subscribe_batch( self, @@ -310,7 +312,8 @@ def subscribe_batch( method: str, args: List[Any], credentials: Optional[Dict[str, Any]] = None, - timeout: float = 30.0, + timeout_ms: float = 30000.0, + timeout: Optional[float] = None, ) -> Dict[str, Any]: """Subscribe to a batch method (e.g. watchOrderBooks) and collect data events for all symbols. @@ -340,7 +343,8 @@ def subscribe_batch( # Wait for data event (the server may push one consolidated event # or multiple per-symbol events) - first_data = self._wait_for_subscription_data(sub, timeout) + effective_timeout = timeout if timeout is not None else timeout_ms / 1000.0 + first_data = self._wait_for_subscription_data(sub, effective_timeout) # Collect per-symbol data result: Dict[str, Any] = {} diff --git a/sdks/python/tests/test_converters.py b/sdks/python/tests/test_converters.py index 70090757..9de6c771 100644 --- a/sdks/python/tests/test_converters.py +++ b/sdks/python/tests/test_converters.py @@ -22,6 +22,7 @@ _convert_trade, _convert_user_trade, _convert_order, + _convert_subscription_snapshot, ) from pmxt.models import ( UnifiedMarket, @@ -34,6 +35,7 @@ UserTrade, Order, MarketList, + SubscribedAddressSnapshot, ) @@ -935,3 +937,15 @@ def test_partially_filled_order(self): assert order.filled == 30.0 assert order.remaining == 20.0 assert order.amount == 50.0 + + +class TestConvertSubscriptionSnapshot: + def test_missing_lists_default_to_empty_lists(self): + snapshot = _convert_subscription_snapshot({ + "address": "0xabc", + "timestamp": 123, + }) + assert isinstance(snapshot, SubscribedAddressSnapshot) + assert snapshot.trades == [] + assert snapshot.positions == [] + assert snapshot.balances == [] diff --git a/sdks/python/tests/test_errors.py b/sdks/python/tests/test_errors.py new file mode 100644 index 00000000..19974a77 --- /dev/null +++ b/sdks/python/tests/test_errors.py @@ -0,0 +1,24 @@ +from pmxt.errors import ( + EventNotFound, + ExchangeNotAvailable, + MarketNotFound, + NetworkError, + OrderNotFound, + RateLimitExceeded, +) + + +def test_not_found_errors_format_their_messages(): + assert str(OrderNotFound("abc-123")) == "Order not found: abc-123" + assert str(MarketNotFound("mkt-456")) == "Market not found: mkt-456" + assert str(EventNotFound("evt-789")) == "Event not found: evt-789" + + +def test_retryable_errors_are_marked_retryable(): + assert NetworkError("network down").retryable is True + assert ExchangeNotAvailable("venue offline").retryable is True + + +def test_rate_limit_retry_after_accepts_float_values(): + err = RateLimitExceeded("slow down", retry_after=1.5) + assert err.retry_after == 1.5 diff --git a/sdks/python/tests/test_public_exports.py b/sdks/python/tests/test_public_exports.py index c3ba6321..deaf9d32 100644 --- a/sdks/python/tests/test_public_exports.py +++ b/sdks/python/tests/test_public_exports.py @@ -25,7 +25,7 @@ def test_websocket_return_types_are_public_exports(): if isinstance(item, ast.Constant) and isinstance(item.value, str) ) - expected = {"FirehoseEvent", "SubscribedAddressSnapshot"} + expected = {"FirehoseEvent", "SubscribedAddressSnapshot", "ExchangeOptions", "PolymarketOptions", "RouterOptions", "FeedClientOptions", "SeriesFetchParams", "TradesParams", "FetchOrderBookParams", "MatchedClusterSort", "FetchMatchedMarketClustersParams", "FetchMatchedEventClustersParams"} assert expected <= imported_models assert expected <= public_exports @@ -99,6 +99,38 @@ def test_feed_client_is_top_level_public_export(): assert "FeedClient" in public_exports +def test_environment_constants_are_top_level_public_exports(): + init_path = Path(__file__).resolve().parents[1] / "pmxt" / "__init__.py" + tree = ast.parse(init_path.read_text(encoding="utf-8")) + + imported_modules = { + alias.name: node.module + for node in tree.body + if isinstance(node, ast.ImportFrom) + for alias in node.names + } + public_exports = set() + + for node in tree.body: + if ( + isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + and node.targets[0].id == "__all__" + and isinstance(node.value, ast.List) + ): + public_exports.update( + item.value + for item in node.value.elts + if isinstance(item, ast.Constant) and isinstance(item.value, str) + ) + + assert imported_modules["ENV"] == "constants" + assert imported_modules["ENV_BASE_URL"] == "constants" + assert imported_modules["ENV_API_KEY"] == "constants" + assert {"ENV", "ENV_BASE_URL", "ENV_API_KEY"} <= public_exports + + def test_polymarket_init_auth_is_generated(): exchanges_path = Path(__file__).resolve().parents[1] / "pmxt" / "_exchanges.py" tree = ast.parse(exchanges_path.read_text(encoding="utf-8")) diff --git a/sdks/typescript/pmxt/server-manager.ts b/sdks/typescript/pmxt/server-manager.ts index abcaadfd..e6a0f80b 100644 --- a/sdks/typescript/pmxt/server-manager.ts +++ b/sdks/typescript/pmxt/server-manager.ts @@ -121,6 +121,13 @@ export class ServerManager { } } + /** + * Backwards-compatible alias for `isServerRunning()`. + */ + async isServerAlive(): Promise { + return this.isServerRunning(); + } + /** * Wait for the server to be ready. * Requires a lock file to be present to avoid falsely matching an unrelated From ca8a31fe1d2933e3755838b822c2519248cab9a6 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Fri, 5 Jun 2026 12:49:37 +0000 Subject: [PATCH 3/3] chore: keep SDK drift generated outputs in sync --- core/api-doc-config.generated.json | 36 +++---- docs/api-reference/fetch-order-book.mdx | 95 ++++++------------- sdks/python/API_REFERENCE.md | 2 +- sdks/python/pmxt/client.py | 6 +- .../python/scripts/generate-client-methods.js | 8 +- sdks/typescript/API_REFERENCE.md | 2 +- 6 files changed, 57 insertions(+), 92 deletions(-) diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 3e176c67..f2b28a73 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,5 +1,5 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-02T00:34:44.916Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-05T12:49:16.156Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", @@ -566,7 +566,7 @@ "type": "UnifiedEvent[]", "description": "Filtered array of events" }, - "source": "BaseExchange.ts:1317" + "source": "BaseExchange.ts:1318" }, "watchOrderBook": { "summary": "Watch order book updates in real-time via WebSocket.", @@ -595,7 +595,7 @@ "type": "OrderBook", "description": "Promise that resolves with the current orderbook state" }, - "source": "BaseExchange.ts:1413" + "source": "BaseExchange.ts:1414" }, "watchOrderBooks": { "summary": "Watch multiple order books simultaneously via WebSocket.", @@ -624,7 +624,7 @@ "type": "Record", "description": "Promise that resolves with order books keyed by ID" }, - "source": "BaseExchange.ts:1426" + "source": "BaseExchange.ts:1427" }, "unwatchOrderBook": { "summary": "Unsubscribe from a previously watched order book stream.", @@ -641,7 +641,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1454" + "source": "BaseExchange.ts:1455" }, "watchTrades": { "summary": "Watch trade executions in real-time via WebSocket.", @@ -676,7 +676,7 @@ "type": "Trade[]", "description": "Promise that resolves with recent trades" }, - "source": "BaseExchange.ts:1467" + "source": "BaseExchange.ts:1468" }, "watchAddress": { "summary": "Stream activity for a public wallet address", @@ -699,7 +699,7 @@ "type": "SubscribedAddressSnapshot", "description": "Promise that resolves with the latest SubscribedAddressSnapshot snapshot" }, - "source": "BaseExchange.ts:1481" + "source": "BaseExchange.ts:1482" }, "unwatchAddress": { "summary": "Stop watching a previously registered wallet address and release its resource updates.", @@ -716,7 +716,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1494" + "source": "BaseExchange.ts:1495" }, "close": { "summary": "Close all WebSocket connections and clean up resources.", @@ -726,7 +726,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1503" + "source": "BaseExchange.ts:1504" }, "fetchMarketMatches": { "summary": "Find the same or related market on other venues. Two modes:", @@ -743,7 +743,7 @@ "type": "MatchResult[]", "description": "Array of matched markets with relation and confidence" }, - "source": "BaseExchange.ts:1517" + "source": "BaseExchange.ts:1518" }, "fetchMatches": { "summary": "fetchMatches", @@ -760,7 +760,7 @@ "type": "MatchResult[]", "description": "Result" }, - "source": "BaseExchange.ts:1533" + "source": "BaseExchange.ts:1534" }, "fetchEventMatches": { "summary": "Find the same or related event on other venues. Two modes:", @@ -777,7 +777,7 @@ "type": "EventMatchResult[]", "description": "Array of matched events with market-level match details" }, - "source": "BaseExchange.ts:1541" + "source": "BaseExchange.ts:1542" }, "compareMarketPrices": { "summary": "Compare live prices for the same market across venues. Finds identity matches and returns side-by-side best bid/ask prices so you can spot price differences at a glance.", @@ -794,7 +794,7 @@ "type": "PriceComparison[]", "description": "Array of price comparisons across venues" }, - "source": "BaseExchange.ts:1557" + "source": "BaseExchange.ts:1558" }, "fetchRelatedMarkets": { "summary": "Find related markets across venues. Discovers subset/superset market relationships", @@ -811,7 +811,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1567" + "source": "BaseExchange.ts:1568" }, "fetchMatchedMarkets": { "summary": "fetchMatchedMarkets", @@ -828,7 +828,7 @@ "type": "MatchedMarketPair[]", "description": "Result" }, - "source": "BaseExchange.ts:1578" + "source": "BaseExchange.ts:1579" }, "fetchMatchedPrices": { "summary": "fetchMatchedPrices", @@ -845,7 +845,7 @@ "type": "MatchedPricePair[]", "description": "Array of matched market pairs with prices from each venue" }, - "source": "BaseExchange.ts:1586" + "source": "BaseExchange.ts:1587" }, "fetchHedges": { "summary": "fetchHedges", @@ -862,7 +862,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1597" + "source": "BaseExchange.ts:1598" }, "fetchArbitrage": { "summary": "fetchArbitrage", @@ -879,7 +879,7 @@ "type": "ArbitrageOpportunity[]", "description": "Array of arbitrage opportunities sorted by spread" }, - "source": "BaseExchange.ts:1607" + "source": "BaseExchange.ts:1608" }, "watchPrices": { "summary": "Watch AMM price updates for a market address (Limitless only).", diff --git a/docs/api-reference/fetch-order-book.mdx b/docs/api-reference/fetch-order-book.mdx index 55b0cb4d..1aea6e43 100644 --- a/docs/api-reference/fetch-order-book.mdx +++ b/docs/api-reference/fetch-order-book.mdx @@ -7,11 +7,7 @@ openapi: GET /api/{exchange}/fetchOrderBook ### Live order book -Fetch the current L2 order book for an outcome from the exchange's live order book endpoint. For Polymarket this is a live CLOB call: pass the outcome token ID directly and omit historical params. - - -`poly.fetch_order_book(token_id)` without `params.since` or `params.until` is live-only. It does not read PMXT Archive data and may fail for closed, resolved, or otherwise inactive Polymarket markets with an error such as `No orderbook exists`. - +Fetch the current L2 order book for an outcome. If you already have an outcome token ID, pass it directly: ```python Python @@ -32,15 +28,15 @@ console.log(`${book.bids.length} bids, ${book.asks.length} asks`); ```bash curl curl "https://api.pmxt.dev/api/polymarket/fetchOrderBook?outcomeId=104932610032177696635191871147557737718087870958469629338467406422339967452218" \ - -H "Authorization: Bearer ***" + -H "Authorization: Bearer $PMXT_API_KEY" ``` ### Historical snapshot -Get the order book at a specific point in time by passing `since` as a Unix timestamp in milliseconds. Historical Polymarket queries are served from the PMXT Archive and return the nearest reconstructed snapshot at or before that time. +Get the order book at a specific point in time. Pass `since` as a Unix timestamp in milliseconds — returns the nearest snapshot at or before that time. -For binary markets, you can pass the Polymarket condition ID as the first argument and choose the side with `params.outcome`. Use `"yes"` or `"no"` instead of copying the long outcome token ID. +For binary markets, you can pass the market ID and choose the side with `params.outcome`. Use `"yes"` or `"no"` instead of copying the long outcome token ID: ```python Python @@ -48,9 +44,12 @@ import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") -# Historical lookup against PMXT Archive, not the live CLOB. +market = poly.fetch_market( + slug="will-spacex-starship-flight-test-12-launch-by-may-22-354-721" +) + book = poly.fetch_order_book( - "0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", + market.market_id, params={"since": 1779487200000, "outcome": "yes"}, ) @@ -64,9 +63,12 @@ import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); -// Historical lookup against PMXT Archive, not the live CLOB. +const market = await poly.fetchMarket({ + slug: "will-spacex-starship-flight-test-12-launch-by-may-22-354-721", +}); + const book = await poly.fetchOrderBook( - "0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", + market.marketId, undefined, { since: 1779487200000, outcome: "yes" } ); @@ -77,51 +79,9 @@ console.log(` best ask: ${Math.min(...book.asks.map((a) => a.price)).toFixed(3) ```bash curl curl -X POST "https://api.pmxt.dev/api/polymarket/fetchOrderBook" \ - -H "Authorization: Bearer ***" \ - -H "Content-Type: application/json" \ - -d '{"args":["0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", null, {"since": 1779487200000, "outcome": "yes"}]}' -``` - - -### Historical crypto 5m events - -Polymarket crypto 5-minute slugs such as `btc-updown-5m-1779481500` are event slugs. Use `fetch_event(slug=...)`, then select the nested market you want. Do not use `fetch_market(slug=...)` for these event-level slugs. - - -```python Python -import pmxt - -poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") -event = poly.fetch_event(slug="btc-updown-5m-1779481500") -market = event.markets[0] - -book = poly.fetch_order_book( - market.market_id, - params={"since": 1779487200000, "outcome": "yes"}, -) -print(f"{market.title}: {book.dt}") -``` - -```javascript JavaScript -import { Polymarket } from "pmxtjs"; - -const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); -const event = await poly.fetchEvent({ slug: "btc-updown-5m-1779481500" }); -const market = event.markets[0]; - -const book = await poly.fetchOrderBook( - market.marketId, - undefined, - { since: 1779487200000, outcome: "yes" } -); -console.log(`${market.title}: ${book.datetime}`); -``` - -```bash curl -curl -X POST "https://api.pmxt.dev/api/polymarket/fetchEvent" \ - -H "Authorization: Bearer ***" \ + -H "Authorization: Bearer $PMXT_API_KEY" \ -H "Content-Type: application/json" \ - -d '{"kwargs":{"slug":"btc-updown-5m-1779481500"}}' + -d '{"args":["61b0ed20-7f42-41fd-af15-7b86153f6bb7", null, {"since": 1779487200000, "outcome": "yes"}]}' ``` @@ -129,15 +89,17 @@ curl -X POST "https://api.pmxt.dev/api/polymarket/fetchEvent" \ Pass both `since` and `until` to get an array of fully reconstructed L2 order book snapshots. Each snapshot is a complete book at that moment in time — not deltas. -The API returns up to `limit` snapshots from the PMXT Archive. Defaults to 100 snapshots per request, max 1000. For raw bulk downloads, use the Parquet files in the [PMXT Archive](https://archive.pmxt.dev) instead of trying to stream bulk data through the live order book endpoint. +Default 100 snapshots per request, max 1000. ```python Python import pmxt poly = pmxt.Polymarket(pmxt_api_key="pmxt_...") -event = poly.fetch_event(slug="btc-updown-5m-1779481500") -market = event.markets[0] + +market = poly.fetch_market( + slug="will-spacex-starship-flight-test-12-launch-by-may-22-354-721" +) books = poly.fetch_order_book( market.market_id, @@ -145,7 +107,6 @@ books = poly.fetch_order_book( "since": 1779480000000, "until": 1779487200000, "outcome": "yes", - "limit": 100, } ) print(f"{len(books)} snapshots") @@ -157,13 +118,15 @@ for ob in books[:3]: import { Polymarket } from "pmxtjs"; const poly = new Polymarket({ pmxtApiKey: "pmxt_..." }); -const event = await poly.fetchEvent({ slug: "btc-updown-5m-1779481500" }); -const market = event.markets[0]; + +const market = await poly.fetchMarket({ + slug: "will-spacex-starship-flight-test-12-launch-by-may-22-354-721", +}); const books = await poly.fetchOrderBook( market.marketId, undefined, - { since: 1779480000000, until: 1779487200000, outcome: "yes", limit: 100 } + { since: 1779480000000, until: 1779487200000, outcome: "yes" } ); console.log(`${books.length} snapshots`); books.slice(0, 3).forEach((ob) => @@ -173,12 +136,12 @@ books.slice(0, 3).forEach((ob) => ```bash curl curl -X POST "https://api.pmxt.dev/api/polymarket/fetchOrderBook" \ - -H "Authorization: Bearer ***" \ + -H "Authorization: Bearer $PMXT_API_KEY" \ -H "Content-Type: application/json" \ - -d '{"args":["0xc704f74e2f9dfae70f770cb253ffadde10768eeab41233098bf5ac67995a94b5", null, {"since": 1779480000000, "until": 1779487200000, "outcome": "yes", "limit": 100}]}' + -d '{"args":["61b0ed20-7f42-41fd-af15-7b86153f6bb7", null, {"since": 1779480000000, "until": 1779487200000, "outcome": "yes"}]}' ``` -Historical order book data is backed by the [PMXT Archive](https://archive.pmxt.dev) — the same tick-level data available as Parquet files, but queryable directly from the API without downloading or parsing files. Historical `fetchOrderBook` supports Polymarket, Kalshi, Limitless, and Opinion. +Historical order book data is backed by the [PMXT Archive](https://archive.pmxt.dev) — the same tick-level data available as Parquet files, but queryable directly from the API without downloading or parsing files. Supports Polymarket, Kalshi, Limitless, and Opinion. diff --git a/sdks/python/API_REFERENCE.md b/sdks/python/API_REFERENCE.md index dac681ce..b20f3aa2 100644 --- a/sdks/python/API_REFERENCE.md +++ b/sdks/python/API_REFERENCE.md @@ -1456,7 +1456,7 @@ title: str # The market title (e.g., "Will BTC close above $100k on Dec 31?"). description: str # Long-form market description or resolution criteria. slug: str # URL-friendly slug for the market. outcomes: List[MarketOutcome] # The possible outcomes for this market. -resolution_date: str # When the market is scheduled to resolve. +resolution_date: str # When the market is scheduled to resolve. Optional because some venues do not publish a cutoff for every market (e.g. Opinion categorical children) — emit `undefined` rather than coercing to epoch. volume24h: float # Trading volume over the past 24 hours (USD). volume: float # Total / Lifetime volume liquidity: float # Current market liquidity (USD). diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index 957789a5..162c2065 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -843,7 +843,7 @@ def fetch_events(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedE except ApiException as e: raise self._parse_api_exception(e) from None - def fetch_series(self, params: Optional[SeriesFetchParams] = None, **kwargs) -> List[UnifiedSeries]: + def fetch_series(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedSeries]: try: args = [] if kwargs: @@ -912,7 +912,7 @@ def fetch_event(self, params: Optional[dict] = None, **kwargs) -> UnifiedEvent: except ApiException as e: raise self._parse_api_exception(e) from None - def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[int] = None, params: Optional[FetchOrderBookParams] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]: + def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[float] = None, params: Optional[dict] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]: try: args = [] if kwargs: @@ -1024,7 +1024,7 @@ def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]: except ApiException as e: raise self._parse_api_exception(e) from None - def fetch_my_trades(self, params: Optional[TradesParams] = None, **kwargs) -> List[UserTrade]: + def fetch_my_trades(self, params: Optional[dict] = None, **kwargs) -> List[UserTrade]: try: args = [] if kwargs: diff --git a/sdks/python/scripts/generate-client-methods.js b/sdks/python/scripts/generate-client-methods.js index b3d33700..d944555a 100644 --- a/sdks/python/scripts/generate-client-methods.js +++ b/sdks/python/scripts/generate-client-methods.js @@ -341,7 +341,9 @@ function buildPyArgsLines(params, sf) { ? `_resolve_outcome_id(${snakeName})` : isOutcomeIds ? `[_resolve_outcome_id(x) for x in ${snakeName}]` - : snakeName; + : tsName === 'params' + ? `_convert_params_to_camel(${snakeName})` + : snakeName; if (p.questionToken) { lines.push(` if ${snakeName} is not None:`); lines.push(` args.append(${value})`); @@ -376,7 +378,7 @@ function buildPyReturnLines(config) { `${i}data = self._handle_response(json.loads(response.data))`, `${i}return PaginatedMarketsResult(`, `${i} data=[_convert_market(m) for m in data.get("data", [])],`, - `${i} total=data.get("total", 0),`, + `${i} total=data.get("total"),`, `${i} next_cursor=data.get("nextCursor"),`, `${i})`, ].join('\n'); @@ -402,7 +404,7 @@ function generatePyMethod(name, params, config, sf) { ` if params is not None:`, ` if limit is None:`, ` args.append(None)`, - ` args.append(params)`, + ` args.append(_convert_params_to_camel(params))`, ` body: dict = {"args": args}`, ` creds = self._get_credentials_dict()`, ` if creds:`, diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index f8504e0a..ec2f3280 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -1456,7 +1456,7 @@ title: string; // The market title (e.g., "Will BTC close above $100k on Dec 31? description: string; // Long-form market description or resolution criteria. slug: string; // URL-friendly slug for the market. outcomes: MarketOutcome[]; // The possible outcomes for this market. -resolutionDate: string; // When the market is scheduled to resolve. +resolutionDate: string; // When the market is scheduled to resolve. Optional because some venues do not publish a cutoff for every market (e.g. Opinion categorical children) — emit `undefined` rather than coercing to epoch. volume24h: number; // Trading volume over the past 24 hours (USD). volume: number; // Total / Lifetime volume liquidity: number; // Current market liquidity (USD).