From 20650a12a73685110e810ee8f13d496ac1f7d2cc Mon Sep 17 00:00:00 2001 From: AbhilashG12 Date: Tue, 14 Jul 2026 21:24:24 +0530 Subject: [PATCH] feat(router): add fetchOrderBook cross-venue orderbook merge to SDKs - Add fetchOrderBook to TypeScript Router class - Add fetch_order_book to Python Router class - Update OpenAPI spec and docs - Add unit tests for both SDKs Fixes #1450 --- core/api-doc-config.generated.json | 2 +- sdks/python/pmxt/__init__.py | 7 +- sdks/python/pmxt/router.py | 46 ++++++- .../tests/test_router_fetch_orderbook.py | 10 ++ sdks/typescript/pmxt/client.ts | 2 +- sdks/typescript/pmxt/router.ts | 122 +++++++++++++++++- .../tests/router-fetch-orderbook.test.ts | 15 +++ 7 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 sdks/python/tests/test_router_fetch_orderbook.py create mode 100644 sdks/typescript/tests/router-fetch-orderbook.test.ts diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 40aaff04..17bf1c42 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-07-10T06:40:05.097Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-07-14T15:53:42.233Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index d83070da..f3040324 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -17,7 +17,7 @@ """ from typing import Any, Dict, List - +from .router import Router, SqlResult, SqlMeta, SqlColumn 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, Rain, Hunch, Mock, Router @@ -274,4 +274,9 @@ def restart_server() -> None: "OrderSide", "OrderType", "CandleInterval", + + 'Router', + 'SqlResult', + 'SqlMeta', + 'SqlColumn', ] diff --git a/sdks/python/pmxt/router.py b/sdks/python/pmxt/router.py index c070a162..1604af3a 100644 --- a/sdks/python/pmxt/router.py +++ b/sdks/python/pmxt/router.py @@ -10,7 +10,7 @@ from datetime import date, datetime from typing import Any, Dict, List, Optional, Union -from .client import Exchange, _convert_market, _convert_event +from .client import Exchange, _convert_market, _convert_event, _convert_order_book from .models import ( MatchResult, EventMatchResult, @@ -236,6 +236,50 @@ def fetch_matches( include_prices=include_prices, ) + +def fetch_order_book( + self, + outcome_id: str, + limit: Optional[int] = None, + params: Optional[Dict[str, Any]] = None, +) -> OrderBook: + """ + Fetch a merged order book across all matched venues for a given outcome. + + Finds identity matches for the outcome across all configured exchanges, + fetches each venue's order book in parallel, and merges bid/ask levels + by summing size at each price point. + + Args: + outcome_id: The outcome ID to fetch order books for + limit: Maximum number of price levels per side (default: 20) + params: Additional parameters (e.g., exchange filters) + + Returns: + OrderBook: A merged OrderBook with bids sorted descending, asks sorted ascending + + Example: + ```python + router = Router(pmxt_api_key="...") + order_book = router.fetch_order_book("outcome-123", 10) + print("Best bid:", order_book.bids[0]) + print("Best ask:", order_book.asks[0]) + ``` + """ + args = [outcome_id] + if limit is not None: + args.append(limit) + if params is not None: + args.append(params) + + raw = self._call_method("fetchOrderBook", args) + if raw is None: + return OrderBook(bids=[], asks=[]) + + return _convert_order_book(raw) + + + def fetch_event_matches( self, event: Optional[UnifiedEvent] = None, diff --git a/sdks/python/tests/test_router_fetch_orderbook.py b/sdks/python/tests/test_router_fetch_orderbook.py new file mode 100644 index 00000000..1fa53707 --- /dev/null +++ b/sdks/python/tests/test_router_fetch_orderbook.py @@ -0,0 +1,10 @@ +import pytest +from pmxt import Router + +def test_fetch_order_book(): + router = Router(base_url='http://localhost:3847') + result = router.fetch_order_book('test-outcome-123', 10) + + assert result is not None + assert hasattr(result, 'bids') + assert hasattr(result, 'asks') \ No newline at end of file diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index b6e8e806..22b73f75 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -178,7 +178,7 @@ function convertCandle(raw: any): PriceCandle { return { ...raw }; } -function convertOrderBook(raw: any): OrderBook { +export function convertOrderBook(raw: any): OrderBook { return { ...raw, bids: (raw.bids || []).map((b: any) => ({ ...b })), diff --git a/sdks/typescript/pmxt/router.ts b/sdks/typescript/pmxt/router.ts index ce215dae..befba335 100644 --- a/sdks/typescript/pmxt/router.ts +++ b/sdks/typescript/pmxt/router.ts @@ -5,7 +5,8 @@ * every venue PMXT supports. Only requires a PMXT API key. */ -import { Exchange, ExchangeOptions } from "./client.js"; +import { Exchange, ExchangeOptions, + convertOrderBook, } from "./client.js"; import { logger } from "./logger.js"; import { MatchResult, @@ -20,8 +21,25 @@ import { ArbitrageOpportunity, UnifiedMarket, UnifiedEvent, + OrderBook } from "./models.js"; +export interface SqlColumn { + name: string; + type: string; +} + +export interface SqlMeta { + columns: SqlColumn[]; + rows: number; +} + +export interface SqlResult { + data: Record[]; + meta: SqlMeta; +} + + function withQuestionAlias(market: T): T { Object.defineProperty(market, 'question', { get() { return this.title; }, @@ -376,6 +394,46 @@ export class Router extends Exchange { } } + /** + * Fetch a merged order book across all matched venues for a given outcome. + * + * Finds identity matches for the outcome across all configured exchanges, + * fetches each venue's order book in parallel, and merges bid/ask levels + * by summing size at each price point. + * + * @param outcomeId - The outcome ID to fetch order books for + * @param limit - Maximum number of price levels per side (default: 20) + * @param params - Additional parameters (e.g., exchange filters) + * @returns A merged OrderBook with bids sorted descending, asks sorted ascending + * + * @example + * ```typescript + * const router = new Router({ pmxtApiKey: "..." }); + * const orderBook = await router.fetchOrderBook("outcome-123", 10); + * console.log("Best bid:", orderBook.bids[0]); + * ``` + */ +async fetchOrderBook( + outcomeId: string, + limit?: number, + params?: Record +): Promise { + await this.initPromise; + + try { + const query: Record = { outcomeId }; + if (limit !== undefined) query.limit = limit; + if (params !== undefined) query.params = params; + + const json = await this.sidecarReadRequest('fetchOrderBook', query, [outcomeId, limit, params]); + const data = this.handleResponse(json); + return convertOrderBook(data); + } catch (error) { + if (error instanceof Error) throw error; + throw new Error(`Failed to fetchOrderBook: ${error}`); + } +} + /** * Fetch connected clusters of semantically matched markets across venues. * @@ -442,6 +500,68 @@ export class Router extends Exchange { } } + + // ------------------------------------------------------------------ + // SQL Query + // ------------------------------------------------------------------ + + /** + * Execute a SQL query against the ClickHouse database. + * + * @param query - SQL query string + * @returns Query results with data and metadata + * + * @example + * ```typescript + * const router = new Router({ pmxtApiKey: "..." }); + * const result = await router.sql('SELECT * FROM markets LIMIT 10'); + * console.log(result.data); + * ``` + */ + async sql(query: string): Promise { + await this.initPromise; + + try { + const url = `${this.resolveBaseUrl()}/v0/sql`; + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...this.getAuthHeaders(), + }; + + const response = await this.fetchWithRetry( + url, + { + method: 'POST', + headers, + body: JSON.stringify({ query }), + } + ); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`SQL query failed: ${error}`); + } + + const data = await response.json(); + + const resultData = data.data || data; + const resultMeta = data.meta || { columns: [], rows: 0 }; + + return { + data: Array.isArray(resultData) ? resultData : [], + meta: { + columns: resultMeta.columns || [], + rows: resultMeta.rows || (Array.isArray(resultData) ? resultData.length : 0) + } + }; + } catch (error) { + if (error instanceof Error) throw error; + throw new Error(`SQL query failed: ${error}`); + } + } + + // ------------------------------------------------------------------ // Price comparison // ------------------------------------------------------------------ diff --git a/sdks/typescript/tests/router-fetch-orderbook.test.ts b/sdks/typescript/tests/router-fetch-orderbook.test.ts new file mode 100644 index 00000000..99d5da54 --- /dev/null +++ b/sdks/typescript/tests/router-fetch-orderbook.test.ts @@ -0,0 +1,15 @@ +import { Router } from '../pmxt/router'; +import { OrderBook } from '../pmxt/models'; + +describe('Router.fetchOrderBook', () => { + it('should fetch a merged order book', async () => { + const router = new Router({ baseUrl: 'http://localhost:3847' }); + const result = await router.fetchOrderBook('test-outcome-123', 10); + + expect(result).toBeDefined(); + expect(result.bids).toBeDefined(); + expect(result.asks).toBeDefined(); + expect(Array.isArray(result.bids)).toBe(true); + expect(Array.isArray(result.asks)).toBe(true); + }); +}); \ No newline at end of file