diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 3e176c67..cb9e5fc3 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-05T14:19:30.044Z. 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/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/feed_client.py b/sdks/python/pmxt/feed_client.py index ea9e741d..d5f0e87b 100644 --- a/sdks/python/pmxt/feed_client.py +++ b/sdks/python/pmxt/feed_client.py @@ -96,6 +96,9 @@ def __init__( if api_key: self._headers["Authorization"] = f"Bearer {api_key}" + def list_feeds(self) -> List[str]: + return self._request(f"{self._base_url}/api/feeds/") + def load_markets(self) -> Dict[str, Market]: data = self._get("loadMarkets", {}) return { @@ -172,13 +175,15 @@ def fetch_historical_prices( return [self._to_ticker(r) for r in data] def _get(self, method: str, params: Dict[str, Any]) -> Any: - filtered = {k: v for k, v in params.items() if v is not None} - qs = urllib.parse.urlencode(filtered) if filtered else "" url = f"{self._base_url}/api/feeds/{self._feed_name}/{method}" - if qs: - url += f"?{qs}" + return self._request(url, params) + + def _request(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any: + filtered = {k: v for k, v in (params or {}).items() if v is not None} + qs = urllib.parse.urlencode(filtered) if filtered else "" + request_url = f"{url}?{qs}" if qs else url - req = urllib.request.Request(url, headers=self._headers) + req = urllib.request.Request(request_url, headers=self._headers) try: with urllib.request.urlopen(req, timeout=15) as resp: body = json.loads(resp.read()) diff --git a/sdks/python/tests/test_feed_client.py b/sdks/python/tests/test_feed_client.py new file mode 100644 index 00000000..bf7f5886 --- /dev/null +++ b/sdks/python/tests/test_feed_client.py @@ -0,0 +1,37 @@ +import json +import urllib.request + +from pmxt.feed_client import FeedClient + + +class _FakeResponse: + def __init__(self, payload): + self._payload = payload + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def read(self): + return json.dumps(self._payload).encode("utf-8") + + +def test_list_feeds_hits_the_root_endpoint(monkeypatch): + captured = {} + + def fake_urlopen(req, timeout=15): + captured["url"] = req.full_url + captured["headers"] = dict(req.header_items()) + captured["timeout"] = timeout + return _FakeResponse({"success": True, "data": ["binance", "chainlink"]}) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + client = FeedClient("binance", base_url="http://localhost:3847") + + assert client.list_feeds() == ["binance", "chainlink"] + assert captured["url"] == "http://localhost:3847/api/feeds/" + assert captured["timeout"] == 15 + assert captured["headers"] == {} 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). diff --git a/sdks/typescript/pmxt/feed-client.ts b/sdks/typescript/pmxt/feed-client.ts index 33f69bad..19ef02dd 100644 --- a/sdks/typescript/pmxt/feed-client.ts +++ b/sdks/typescript/pmxt/feed-client.ts @@ -80,6 +80,10 @@ export class FeedClient { }; } + async listFeeds(): Promise { + return this.getRoot(); + } + async loadMarkets(): Promise> { return this.get>('loadMarkets', {}); } @@ -118,14 +122,22 @@ export class FeedClient { } private async get(method: string, params: Record): Promise { + return this.request(`${this.baseUrl}/api/feeds/${this.feedName}/${method}`, params); + } + + private async getRoot(params: Record = {}): Promise { + return this.request(`${this.baseUrl}/api/feeds/`, params); + } + + private async request(url: string, params: Record): Promise { const qs = Object.entries(params) .filter(([, v]) => v !== undefined && v !== null) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) .join('&'); - const url = `${this.baseUrl}/api/feeds/${this.feedName}/${method}${qs ? '?' + qs : ''}`; + const requestUrl = `${url}${qs ? '?' + qs : ''}`; - const response = await fetch(url, { + const response = await fetch(requestUrl, { headers: this.headers, signal: AbortSignal.timeout(30_000), }); diff --git a/sdks/typescript/tests/feed-client.test.ts b/sdks/typescript/tests/feed-client.test.ts new file mode 100644 index 00000000..6b276011 --- /dev/null +++ b/sdks/typescript/tests/feed-client.test.ts @@ -0,0 +1,28 @@ +import { FeedClient } from '../pmxt/feed-client'; + +describe('FeedClient.listFeeds', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('requests the feeds index and returns the available feed names', async () => { + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ + success: true, + data: ['binance', 'chainlink'], + }), + } as any); + + const client = new FeedClient('binance', { baseUrl: 'http://localhost:3847' }); + await expect(client.listFeeds()).resolves.toEqual(['binance', 'chainlink']); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + 'http://localhost:3847/api/feeds/', + expect.objectContaining({ + headers: {}, + }), + ); + fetchMock.mockRestore(); + }); +});