diff --git a/core/scripts/generate-python-exchanges.js b/core/scripts/generate-python-exchanges.js index 3cf2cfcf..5bed28fb 100644 --- a/core/scripts/generate-python-exchanges.js +++ b/core/scripts/generate-python-exchanges.js @@ -30,8 +30,8 @@ const OVERRIDES = { function toClassName(name) { return name - .split('-') - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .split(/[-_]/) + .map(part => part.toLowerCase() === 'us' ? 'US' : part.charAt(0).toUpperCase() + part.slice(1)) .join(''); } @@ -244,7 +244,7 @@ init = init.replace( // Update the # Exchanges section in __all__ const allEntries = classNames.map(n => ` "${n}",`).join('\n'); init = init.replace( - /( # Exchanges\n)([\s\S]*?)( "Exchange",)/, + /( # Exchanges\r?\n)([\s\S]*?)( "Exchange",)/, `$1${allEntries}\n$3` ); diff --git a/core/src/exchanges/baozi/websocket.ts b/core/src/exchanges/baozi/websocket.ts index 80676ef1..01e51756 100644 --- a/core/src/exchanges/baozi/websocket.ts +++ b/core/src/exchanges/baozi/websocket.ts @@ -82,10 +82,7 @@ export class BaoziWebSocket { } const dataPromise = new Promise((resolve, reject) => { - if (!this.orderBookResolvers.has(marketPubkey)) { - this.orderBookResolvers.set(marketPubkey, []); - } - this.orderBookResolvers.get(marketPubkey)!.push({ resolve, reject }); + this.getOrderBookQueue(marketPubkey).push({ resolve, reject }); }); return withWatchTimeout( @@ -105,6 +102,17 @@ export class BaoziWebSocket { } } + private getOrderBookQueue(marketPubkey: string): QueuedPromise[] { + const resolvers = this.orderBookResolvers.get(marketPubkey); + if (resolvers) { + return resolvers; + } + + const queue: QueuedPromise[] = []; + this.orderBookResolvers.set(marketPubkey, queue); + return queue; + } + async close(connection: Connection): Promise { for (const [, subId] of this.subscriptions) { await connection.removeAccountChangeListener(subId); diff --git a/core/test/pipeline/baozi-websocket-resolvers.test.ts b/core/test/pipeline/baozi-websocket-resolvers.test.ts new file mode 100644 index 00000000..016c076c --- /dev/null +++ b/core/test/pipeline/baozi-websocket-resolvers.test.ts @@ -0,0 +1,13 @@ +import fs from 'fs'; +import path from 'path'; + +describe('Baozi WebSocket resolver queues', () => { + it('does not rely on a non-null assertion when queueing order book watchers', () => { + const source = fs.readFileSync( + path.resolve(__dirname, '../../src/exchanges/baozi/websocket.ts'), + 'utf8', + ); + + expect(source).not.toMatch(/orderBookResolvers\.get\([^)]*\)!/); + }); +}); diff --git a/sdks/python/API_REFERENCE.md b/sdks/python/API_REFERENCE.md index 1e0d0349..d49b9183 100644 --- a/sdks/python/API_REFERENCE.md +++ b/sdks/python/API_REFERENCE.md @@ -386,14 +386,14 @@ Fetch the order book (bids/asks) for a specific outcome. **Signature:** ```python -def fetch_order_book(outcome_id: str, limit: Optional[float] = None, params: Optional[Dict[str, Any]] = None) -> OrderBook: +def fetch_order_book(outcome_id: str, limit: Optional[float] = None, params: Optional[FetchOrderBookParams] = None) -> OrderBook: ``` **Parameters:** - `outcome_id` (str): The Outcome ID (outcomeId) or market slug -- `limit` (float) - **Optional**: Max number of bid/ask levels to return (CCXT-style). -- `params` (Dict[str, Any]) - **Optional**: Optional parameters: +- `limit` (float) - **Optional**: Max number of bid/ask levels to return. For range +- `params` ([FetchOrderBookParams](#fetchorderbookparams)) - **Optional**: Optional parameters: **Returns:** [OrderBook](#orderbook) - Order book with bids and asks. Returns OrderBook[] when @@ -922,7 +922,7 @@ exchange.unwatch_address(address="0xabc...") --- -### `test_dummy_method` +### `close` Close all WebSocket connections and clean up resources. @@ -930,19 +930,19 @@ Close all WebSocket connections and clean up resources. **Signature:** ```python -def test_dummy_method(param: Optional[str] = None) -> str: +def close() -> void: ``` **Parameters:** -- `param` (str) - **Optional**: param +- None -**Returns:** str - Result +**Returns:** void - Result **Example:** ```python -exchange.test_dummy_method(param="...") +exchange.close() ``` @@ -1966,17 +1966,31 @@ end: str # End of the time range limit: float # Maximum number of results to return ``` +--- +### `FetchOrderBookParams` + + + +```python +@dataclass +class FetchOrderBookParams: +side: str # Outcome side: 'yes' or 'no'. Required for exchanges like Limitless where the API returns a single orderbook per market. +outcome: str # Outcome alias: 'yes' or 'no', or an outcome token ID. When set, the first argument is treated as a market ID and this value selects which outcome's order book to fetch. Accepts the literal strings 'yes'/'no' (resolved via a market lookup) or a raw outcome token ID. +since: float # Unix timestamp (ms) — fetch a historical snapshot at or before this time, or the start of a range when combined with `until` (hosted API only). +until: float # Unix timestamp (ms) — end of a historical range. When combined with `since`, returns an array of reconstructed L2 OrderBook snapshots between `since` and `until` (hosted API only). +``` + --- ### `TradesParams` -Parameters for fetching trade history. No resolution parameter - trades are discrete events. + ```python @dataclass class TradesParams: start: str # Start of the time range end: str # End of the time range -limit: float # Maximum number of results to return +limit: float # Maximum number of results to return (max {@link MAX_TRADES_LIMIT}) ``` --- diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index f1996569..09ba6770 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -17,7 +17,7 @@ """ from .client import Exchange -from ._exchanges import Polymarket, Limitless, Kalshi, KalshiDemo, Probable, Baozi, Myriad, Opinion, Metaculus, Smarkets, Polymarket_us, Hyperliquid, GeminiTitan, Mock, Router +from ._exchanges import Polymarket, Limitless, Kalshi, KalshiDemo, Probable, Baozi, Myriad, Opinion, Metaculus, Smarkets, PolymarketUS, Hyperliquid, GeminiTitan, Mock, Router from .router import Router from .server_manager import ServerManager from .errors import ( @@ -143,7 +143,7 @@ def restart_server(): "Opinion", "Metaculus", "Smarkets", - "Polymarket_us", + "PolymarketUS", "Hyperliquid", "GeminiTitan", "Mock", diff --git a/sdks/python/pmxt/_exchanges.py b/sdks/python/pmxt/_exchanges.py index 03eece68..0f6fb9c5 100644 --- a/sdks/python/pmxt/_exchanges.py +++ b/sdks/python/pmxt/_exchanges.py @@ -365,8 +365,8 @@ def __init__( ) -class Polymarket_us(Exchange): - """Polymarket_us exchange client.""" +class PolymarketUS(Exchange): + """PolymarketUS exchange client.""" def __init__( self, @@ -377,7 +377,7 @@ def __init__( pmxt_api_key: Optional[str] = None, ): """ - Initialize Polymarket_us client. + Initialize PolymarketUS client. Args: api_key: API key for authentication (optional) diff --git a/sdks/python/scripts/generate-client-methods.js b/sdks/python/scripts/generate-client-methods.js index baf5a20e..fcb0e0af 100644 --- a/sdks/python/scripts/generate-client-methods.js +++ b/sdks/python/scripts/generate-client-methods.js @@ -387,6 +387,41 @@ function buildPyReturnLines(config) { } function generatePyMethod(name, params, config, sf) { + if (name === 'fetchOrderBook') { + return [ + ` 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:`, + ` params = {**(params or {}), **kwargs}`, + ` outcome_id = _compat_id(outcome_id, kwargs)`, + ` args.append(_resolve_outcome_id(outcome_id))`, + ` if limit is not None:`, + ` args.append(limit)`, + ` if params is not None:`, + ` if limit is None:`, + ` args.append(None)`, + ` args.append(params)`, + ` body: dict = {"args": args}`, + ` creds = self._get_credentials_dict()`, + ` if creds:`, + ` body["credentials"] = creds`, + ` url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/fetchOrderBook"`, + ` headers = {"Content-Type": "application/json", "Accept": "application/json"}`, + ` headers.update(self._get_auth_headers())`, + ` response = self._fetch_with_retry(`, + ` lambda: self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)`, + ` )`, + ` response.read()`, + ` data = self._handle_response(json.loads(response.data))`, + ` if isinstance(data, list):`, + ` return [_convert_order_book(d) for d in data]`, + ` return _convert_order_book(data)`, + ` except ApiException as e:`, + ` raise self._parse_api_exception(e) from None`, + ].join('\n'); + } + const snakeName = camelToSnake(name); const paramSig = buildPySignatureParams(params, sf); const hasParams = params.some(p => camelToSnake(p.name.getText(sf)) === 'params'); diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index ff18c56d..b4ada98d 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -389,14 +389,14 @@ Fetch the order book (bids/asks) for a specific outcome. **Signature:** ```typescript -async fetchOrderBook(outcomeId: string, limit?: number, params?: Record): Promise +async fetchOrderBook(outcomeId: string, limit?: number, params?: FetchOrderBookParams): Promise ``` **Parameters:** - `outcomeId` (string): The Outcome ID (outcomeId) or market slug -- `limit` (number) - **Optional**: Max number of bid/ask levels to return (CCXT-style). -- `params` (Record) - **Optional**: Optional parameters: +- `limit` (number) - **Optional**: Max number of bid/ask levels to return. For range +- `params` ([FetchOrderBookParams](#fetchorderbookparams)) - **Optional**: Optional parameters: **Returns:** Promise<[OrderBook](#orderbook)> - Order book with bids and asks. Returns OrderBook[] when @@ -925,7 +925,7 @@ await exchange.unwatchAddress("0xabc...") --- -### `testDummyMethod` +### `close` Close all WebSocket connections and clean up resources. @@ -933,19 +933,19 @@ Close all WebSocket connections and clean up resources. **Signature:** ```typescript -async testDummyMethod(param?: string): Promise +async close(): Promise ``` **Parameters:** -- `param` (string) - **Optional**: param +- None -**Returns:** Promise - Result +**Returns:** Promise - Result **Example:** ```typescript -await exchange.testDummyMethod({ param: "..." }) +await exchange.close() ``` @@ -1967,16 +1967,30 @@ limit?: number; // Maximum number of results to return } ``` +--- +### `FetchOrderBookParams` + + + +```typescript +interface FetchOrderBookParams { +side?: string; // Outcome side: 'yes' or 'no'. Required for exchanges like Limitless where the API returns a single orderbook per market. +outcome?: string; // Outcome alias: 'yes' or 'no', or an outcome token ID. When set, the first argument is treated as a market ID and this value selects which outcome's order book to fetch. Accepts the literal strings 'yes'/'no' (resolved via a market lookup) or a raw outcome token ID. +since?: number; // Unix timestamp (ms) — fetch a historical snapshot at or before this time, or the start of a range when combined with `until` (hosted API only). +until?: number; // Unix timestamp (ms) — end of a historical range. When combined with `since`, returns an array of reconstructed L2 OrderBook snapshots between `since` and `until` (hosted API only). +} +``` + --- ### `TradesParams` -Parameters for fetching trade history. No resolution parameter - trades are discrete events. + ```typescript interface TradesParams { start?: string; // Start of the time range end?: string; // End of the time range -limit?: number; // Maximum number of results to return +limit?: number; // Maximum number of results to return (max {@link MAX_TRADES_LIMIT}) } ``` diff --git a/sdks/typescript/scripts/generate-client-methods.js b/sdks/typescript/scripts/generate-client-methods.js index 4e9521ee..0ae168ee 100644 --- a/sdks/typescript/scripts/generate-client-methods.js +++ b/sdks/typescript/scripts/generate-client-methods.js @@ -381,6 +381,44 @@ function buildReturnLines(config) { } function generateMethod(name, params, config, sf) { + if (name === 'fetchOrderBook') { + return [ + ` async fetchOrderBook(outcomeId: string | MarketOutcome, limit?: number, params?: FetchOrderBookParams): Promise {`, + ` await this.initPromise;`, + ` try {`, + ` const args: any[] = [];`, + ` args.push(resolveOutcomeId(outcomeId));`, + ` if (limit !== undefined) args.push(limit);`, + ` if (params !== undefined) {`, + ` if (limit === undefined) args.push(null);`, + ` args.push(params);`, + ` }`, + ` const response = await this.fetchWithRetry(\`\${this.resolveBaseUrl()}/api/\${this.exchangeName}/fetchOrderBook\`, {`, + ` method: 'POST',`, + ` headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },`, + ` body: JSON.stringify({ args, credentials: this.getCredentials() }),`, + ` });`, + ` if (!response.ok) {`, + ` const body = await response.json().catch(() => ({}));`, + ` if (body.error && typeof body.error === "object") {`, + ` throw fromServerError(body.error);`, + ` }`, + ` throw new PmxtError(body.error?.message || response.statusText);`, + ` }`, + ` const json = await response.json();`, + ` const data = this.handleResponse(json);`, + ` if (Array.isArray(data)) {`, + ` return data.map(convertOrderBook);`, + ` }`, + ` return convertOrderBook(data);`, + ` } catch (error) {`, + ` if (error instanceof PmxtError) throw error;`, + ` throw new PmxtError(\`Failed to fetchOrderBook: \${error}\`);`, + ` }`, + ` }`, + ].join('\n'); + } + const sig = buildSignatureParams(params, sf); const argsCode = buildArgsLines(params, sf); const returnCode = buildReturnLines(config);