diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 3cb1bcd2..fb9d2c67 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-08T09:54:03.295Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T09:56:18.619Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", diff --git a/core/src/exchanges/gemini-titan/normalizer.ts b/core/src/exchanges/gemini-titan/normalizer.ts index e91390a2..80cb576a 100644 --- a/core/src/exchanges/gemini-titan/normalizer.ts +++ b/core/src/exchanges/gemini-titan/normalizer.ts @@ -213,12 +213,19 @@ export class GeminiNormalizer implements IExchangeNormalizer ({ price: parseFloat(level.px), size: parseFloat(level.sz), + orderCount: level.n, })); const asks = rawAsks.map(level => ({ price: parseFloat(level.px), size: parseFloat(level.sz), + orderCount: level.n, })); return { diff --git a/core/src/exchanges/kalshi/fetcher.ts b/core/src/exchanges/kalshi/fetcher.ts index ee63a6c8..b697d87f 100644 --- a/core/src/exchanges/kalshi/fetcher.ts +++ b/core/src/exchanges/kalshi/fetcher.ts @@ -23,6 +23,8 @@ export interface KalshiRawMarket { last_price_dollars?: string; yes_ask_dollars?: string; yes_bid_dollars?: string; + yes_ask_size_fp?: string; + yes_bid_size_fp?: string; rules_primary?: string; rules_secondary?: string; expiration_time: string; diff --git a/core/src/exchanges/kalshi/normalizer.ts b/core/src/exchanges/kalshi/normalizer.ts index e694c208..ade69ea1 100644 --- a/core/src/exchanges/kalshi/normalizer.ts +++ b/core/src/exchanges/kalshi/normalizer.ts @@ -118,7 +118,12 @@ export class KalshiNormalizer implements IExchangeNormalizer, KALSHI_PROMOTED_MARKET_KEYS, - { series_ticker: event.series_ticker, series_title: event.series_title }, + { + yes_ask_size_fp: market.yes_ask_size_fp, + yes_bid_size_fp: market.yes_bid_size_fp, + series_ticker: event.series_ticker, + series_title: event.series_title, + }, ), } as UnifiedMarket; diff --git a/core/src/exchanges/limitless/fetcher.ts b/core/src/exchanges/limitless/fetcher.ts index 01819d9a..23738823 100644 --- a/core/src/exchanges/limitless/fetcher.ts +++ b/core/src/exchanges/limitless/fetcher.ts @@ -16,6 +16,10 @@ export interface LimitlessRawMarket { description?: string; tokens?: Record; prices?: number[]; + tradePrices?: { + buy?: { market?: number[]; limit?: number[] }; + sell?: { market?: number[]; limit?: number[] }; + }; expirationTimestamp?: string; volumeFormatted?: number; volume?: number; diff --git a/core/src/exchanges/limitless/utils.ts b/core/src/exchanges/limitless/utils.ts index 29a54076..099d37cb 100644 --- a/core/src/exchanges/limitless/utils.ts +++ b/core/src/exchanges/limitless/utils.ts @@ -73,9 +73,9 @@ export function mapMarketToUnified(market: any, context: LimitlessMarketContext throw new Error(`[limitless] Market "${market.slug}" is missing token addresses`); } - const prices = Array.isArray(market.prices) ? market.prices : []; - const yesPrice = prices[0] || 0; - const noPrice = prices[1] || 0; + const legacyPrices = Array.isArray(market.prices) ? market.prices : []; + const yesPrice = market.tradePrices?.buy?.market?.[0] ?? legacyPrices[0] ?? 0.5; + const noPrice = market.tradePrices?.sell?.market?.[0] ?? legacyPrices[1] ?? Math.max(0, Math.min(1, 1 - yesPrice)); const yesLabel = hasParentContext ? rawTitle : 'Yes'; const noLabel = hasParentContext ? `Not ${rawTitle}` : 'No'; diff --git a/core/src/exchanges/limitless/websocket.ts b/core/src/exchanges/limitless/websocket.ts index 61de1598..4a15efab 100644 --- a/core/src/exchanges/limitless/websocket.ts +++ b/core/src/exchanges/limitless/websocket.ts @@ -228,7 +228,7 @@ export class LimitlessWebSocket { await this.client.connect(); } - await this.client.subscribe('subscribe_order_events'); + await this.client.subscribe('subscribe_order_events' as any); await this.client.subscribe('subscribe_positions' as any); this.client.on('positions', callback); } diff --git a/core/src/server/openapi.yaml b/core/src/server/openapi.yaml index 6c1789f8..c82d3771 100644 --- a/core/src/server/openapi.yaml +++ b/core/src/server/openapi.yaml @@ -3199,6 +3199,8 @@ components: size: type: number description: contracts/shares + orderCount: + type: number required: - price - size diff --git a/core/src/types.ts b/core/src/types.ts index af3b74ec..e9bdda9a 100644 --- a/core/src/types.ts +++ b/core/src/types.ts @@ -171,6 +171,7 @@ export interface PriceCandle { export interface OrderLevel { price: number; // 0.0 to 1.0 (probability) size: number; // contracts/shares + orderCount?: number; } export interface OrderBook { diff --git a/core/test/normalizers/exchange-normalizers-3.test.ts b/core/test/normalizers/exchange-normalizers-3.test.ts index db37edc1..312dca3c 100644 --- a/core/test/normalizers/exchange-normalizers-3.test.ts +++ b/core/test/normalizers/exchange-normalizers-3.test.ts @@ -878,19 +878,21 @@ describe('HyperliquidNormalizer', () => { expect(book.asks[0].price).toBeLessThanOrEqual(book.asks[1].price); }); - test('bid prices and sizes are numbers', () => { + test('bid prices, sizes, and order counts are numbers', () => { const book = normalizer.normalizeOrderBook(rawBook, 'hl-outcome-42'); for (const b of book.bids) { expect(typeof b.price).toBe('number'); expect(typeof b.size).toBe('number'); + expect(typeof b.orderCount).toBe('number'); } }); - test('ask prices and sizes are numbers', () => { + test('ask prices, sizes, and order counts are numbers', () => { const book = normalizer.normalizeOrderBook(rawBook, 'hl-outcome-42'); for (const a of book.asks) { expect(typeof a.price).toBe('number'); expect(typeof a.size).toBe('number'); + expect(typeof a.orderCount).toBe('number'); } }); diff --git a/core/test/normalizers/exchange-normalizers.test.ts b/core/test/normalizers/exchange-normalizers.test.ts index 35f4a2b3..056551ad 100644 --- a/core/test/normalizers/exchange-normalizers.test.ts +++ b/core/test/normalizers/exchange-normalizers.test.ts @@ -381,6 +381,8 @@ describe('KalshiNormalizer', () => { last_price_dollars: '0.4540', yes_ask_dollars: '0.4600', yes_bid_dollars: '0.4500', + yes_ask_size_fp: '125.00', + yes_bid_size_fp: '90.00', previous_price_dollars: '0.4200', rules_primary: 'This market resolves YES if the Fed Funds Rate is above 4.75% on Jan 29, 2025.', rules_secondary: 'Data source: Federal Reserve', @@ -509,6 +511,11 @@ describe('KalshiNormalizer', () => { expect(market.liquidity).toBe(5000); }); + it('surfaces top-of-book depth sizes in sourceMetadata', () => { + expect((market.sourceMetadata as any).yes_ask_size_fp).toBe('125.00'); + expect((market.sourceMetadata as any).yes_bid_size_fp).toBe('90.00'); + }); + it('maps url to kalshi events URL', () => { expect(market.url).toBe('https://kalshi.com/events/FED-25JAN29'); }); @@ -800,6 +807,10 @@ describe('LimitlessNormalizer', () => { no: 'token-eth-flip-no', }, prices: [0.08, 0.92], + tradePrices: { + buy: { market: [0.11] }, + sell: { market: [0.89] }, + }, expirationTimestamp: '2025-12-31T23:59:59Z', volumeFormatted: 75000, volume: 1500000, @@ -863,14 +874,14 @@ describe('LimitlessNormalizer', () => { expect(market.outcomes[1].label).toBe('Not Will ETH flip BTC in 2025?'); }); - it('maps yes price from prices[0] as a number', () => { + it('maps yes price from directional tradePrices when available', () => { expect(typeof market.outcomes[0].price).toBe('number'); - expect(market.outcomes[0].price).toBeCloseTo(0.08); + expect(market.outcomes[0].price).toBeCloseTo(0.11); }); - it('maps no price from prices[1] as a number', () => { + it('maps no price from directional tradePrices when available', () => { expect(typeof market.outcomes[1].price).toBe('number'); - expect(market.outcomes[1].price).toBeCloseTo(0.92); + expect(market.outcomes[1].price).toBeCloseTo(0.89); }); it('prices sum to 1.0', () => { diff --git a/core/test/normalizers/gemini-titan-normalizer.test.ts b/core/test/normalizers/gemini-titan-normalizer.test.ts new file mode 100644 index 00000000..663bbd9d --- /dev/null +++ b/core/test/normalizers/gemini-titan-normalizer.test.ts @@ -0,0 +1,66 @@ +import { GeminiNormalizer } from '../../src/exchanges/gemini-titan/normalizer'; + +describe('GeminiTitanNormalizer', () => { + const normalizer = new GeminiNormalizer(); + + const rawEvent: any = { + id: 'evt-1', + title: 'Gemini prediction market event', + slug: 'gemini-prediction-market-event', + description: 'Event-level description fallback', + ticker: 'GEM-1', + status: 'active', + type: 'prediction-market', + liquidity: undefined, + volume24h: '1234.5', + volume: '9999.9', + contracts: [ + { + id: 'contract-1', + label: 'BTC above $100k', + description: { + nodeType: 'document', + content: [ + { + nodeType: 'paragraph', + content: [ + { nodeType: 'text', value: 'Contract rich-text description' }, + ], + }, + ], + }, + ticker: 'GEM-1-A', + instrumentSymbol: 'GEM-1-A', + status: 'active', + marketState: 'open', + prices: { + buy: { yes: '0.71', no: '0.29' }, + sell: { yes: '0.69', no: '0.31' }, + bestBid: '0.60', + bestAsk: '0.40', + lastTradePrice: '0.55', + }, + expiryDate: '2026-06-30T23:59:00Z', + }, + ], + }; + + test('extracts rich-text contract descriptions', () => { + const market = normalizer.normalizeMarket(rawEvent); + expect(market).not.toBeNull(); + expect(market!.description).toBe('Contract rich-text description'); + }); + + test('prefers directional buy/sell prices when present', () => { + const market = normalizer.normalizeMarket(rawEvent); + expect(market).not.toBeNull(); + expect(market!.outcomes[0].price).toBeCloseTo(0.71); + expect(market!.outcomes[1].price).toBeCloseTo(0.29); + }); + + test('falls back to volume fields when liquidity is absent', () => { + const market = normalizer.normalizeMarket(rawEvent); + expect(market).not.toBeNull(); + expect(market!.liquidity).toBeCloseTo(1234.5); + }); +}); diff --git a/docs/api-reference/openapi.json b/docs/api-reference/openapi.json index d4a3c329..628c6b9a 100644 --- a/docs/api-reference/openapi.json +++ b/docs/api-reference/openapi.json @@ -9790,6 +9790,9 @@ "size": { "type": "number", "description": "contracts/shares" + }, + "orderCount": { + "type": "number" } }, "required": [ diff --git a/sdks/python/API_REFERENCE.md b/sdks/python/API_REFERENCE.md index 1d0e55cf..7ecc5224 100644 --- a/sdks/python/API_REFERENCE.md +++ b/sdks/python/API_REFERENCE.md @@ -1576,6 +1576,7 @@ datetime: str # ISO 8601 datetime string of the snapshot (CCXT-compatible). class OrderLevel: price: float # 0.0 to 1.0 (probability) size: float # contracts/shares +order_count: float # ``` --- diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index b7b82934..a2d42f0b 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -1576,6 +1576,7 @@ datetime: string; // ISO 8601 datetime string of the snapshot (CCXT-compatible). interface OrderLevel { price: number; // 0.0 to 1.0 (probability) size: number; // contracts/shares +orderCount: number; // } ```