diff --git a/core/src/exchanges/kalshi/fetcher.ts b/core/src/exchanges/kalshi/fetcher.ts index 1e8c66a9..7276e23e 100644 --- a/core/src/exchanges/kalshi/fetcher.ts +++ b/core/src/exchanges/kalshi/fetcher.ts @@ -87,10 +87,49 @@ export interface KalshiRawEventPage { export interface KalshiRawCandlestick { end_period_ts: number; + /** @deprecated Old API field — new API uses `volume_fp` (string) */ volume?: number; - price?: { open?: number; high?: number; low?: number; close?: number; previous?: number }; - yes_ask?: { open?: number; high?: number; low?: number; close?: number }; - yes_bid?: { open?: number; high?: number; low?: number; close?: number }; + /** New API field: cumulative volume as a fixed-point string */ + volume_fp?: string; + /** + * Kalshi candlestick prices come nested under `price`. The new API returns + * dollar-denominated string fields (e.g. `close_dollars: "0.9700"`); older + * responses used integer cent fields (e.g. `close: 97`). Both shapes are + * supported by the normalizer. + */ + price?: { + open?: number; + high?: number; + low?: number; + close?: number; + previous?: number; + open_dollars?: string; + high_dollars?: string; + low_dollars?: string; + close_dollars?: string; + mean_dollars?: string; + previous_dollars?: string; + }; + yes_ask?: { + open?: number; + high?: number; + low?: number; + close?: number; + open_dollars?: string; + high_dollars?: string; + low_dollars?: string; + close_dollars?: string; + }; + yes_bid?: { + open?: number; + high?: number; + low?: number; + close?: number; + open_dollars?: string; + high_dollars?: string; + low_dollars?: string; + close_dollars?: string; + }; [key: string]: unknown; } diff --git a/core/src/exchanges/kalshi/normalizer.ts b/core/src/exchanges/kalshi/normalizer.ts index 0b5b288e..cfc01221 100644 --- a/core/src/exchanges/kalshi/normalizer.ts +++ b/core/src/exchanges/kalshi/normalizer.ts @@ -201,24 +201,47 @@ export class KalshiNormalizer implements IExchangeNormalizer { + const dollarsKey = `${field}_dollars` as const; + const pd = p[dollarsKey]; + const ad = ask[dollarsKey]; + const bd = bid[dollarsKey]; + if (pd != null) return Number(pd); + if (ad != null && bd != null) { + return (Number(ad) + Number(bd)) / 2; + } + const pf = p[field]; const af = ask[field]; const bf = bid[field]; - if (pf != null) return pf; + if (pf != null) return fromKalshiCents(pf); if (af != null && bf != null) { - return (af + bf) / 2; + return (fromKalshiCents(af) + fromKalshiCents(bf)) / 2; } - return p.previous || 0; + + if (p.previous_dollars != null) return Number(p.previous_dollars); + if (p.previous != null) return fromKalshiCents(p.previous); + return 0; }; + const volume = c.volume_fp != null + ? parseFloat(c.volume_fp) || 0 + : (c.volume || 0); + return { timestamp: c.end_period_ts * 1000, - open: fromKalshiCents(getVal('open')), - high: fromKalshiCents(getVal('high')), - low: fromKalshiCents(getVal('low')), - close: fromKalshiCents(getVal('close')), - volume: c.volume || 0, + open: getVal('open'), + high: getVal('high'), + low: getVal('low'), + close: getVal('close'), + volume, }; }); diff --git a/core/test/normalizers/kalshi-ohlcv-normalization.test.ts b/core/test/normalizers/kalshi-ohlcv-normalization.test.ts new file mode 100644 index 00000000..b2fbaa43 --- /dev/null +++ b/core/test/normalizers/kalshi-ohlcv-normalization.test.ts @@ -0,0 +1,58 @@ +import { KalshiRawCandlestick } from '../../src/exchanges/kalshi/fetcher'; +import { KalshiNormalizer } from '../../src/exchanges/kalshi/normalizer'; + +const normalizer = new KalshiNormalizer(); + +describe('Kalshi OHLCV normalization', () => { + test('parses new-API dollar-denominated price strings (regression: close was 0)', () => { + const raw: KalshiRawCandlestick = { + end_period_ts: 1_720_000_000, + volume_fp: '1234.5', + price: { + open_dollars: '0.9500', + high_dollars: '0.9800', + low_dollars: '0.9400', + close_dollars: '0.9700', + mean_dollars: '0.9600', + }, + }; + + const [candle] = normalizer.normalizeOHLCV([raw], {}); + + expect(candle.close).toBe(0.97); + expect(candle.open).toBe(0.95); + expect(candle.high).toBe(0.98); + expect(candle.low).toBe(0.94); + expect(candle.volume).toBeCloseTo(1234.5); + expect(candle.timestamp).toBe(1_720_000_000 * 1000); + }); + + test('falls back to legacy cent-denominated integer fields', () => { + const raw: KalshiRawCandlestick = { + end_period_ts: 1_720_000_060, + volume: 42, + price: { open: 50, high: 60, low: 40, close: 55 }, + }; + + const [candle] = normalizer.normalizeOHLCV([raw], {}); + + expect(candle.open).toBe(0.5); + expect(candle.high).toBe(0.6); + expect(candle.low).toBe(0.4); + expect(candle.close).toBe(0.55); + expect(candle.volume).toBe(42); + }); + + test('averages yes_ask/yes_bid dollars when price is missing', () => { + const raw: KalshiRawCandlestick = { + end_period_ts: 1_720_000_120, + volume_fp: '0', + yes_ask: { close_dollars: '0.6000' }, + yes_bid: { close_dollars: '0.5800' }, + }; + + const [candle] = normalizer.normalizeOHLCV([raw], {}); + + expect(candle.close).toBeCloseTo(0.59); + }); +}); diff --git a/docs/api-reference/fetch-ohlcv.mdx b/docs/api-reference/fetch-ohlcv.mdx index c9927a56..3c56fb66 100644 --- a/docs/api-reference/fetch-ohlcv.mdx +++ b/docs/api-reference/fetch-ohlcv.mdx @@ -36,8 +36,8 @@ candles.forEach((c) => console.log(`${c.open} ${c.high} ${c.low} ${c.close} ``` ```bash curl -# First get the outcome ID from a market, then pass it as the id parameter -curl "https://api.pmxt.dev/api/polymarket/fetchOHLCV?id=YOUR_OUTCOME_ID&resolution=1h&limit=100" \ +# First get the outcome ID from a market, then pass it as the outcomeId parameter +curl "https://api.pmxt.dev/api/polymarket/fetchOHLCV?outcomeId=YOUR_OUTCOME_ID&resolution=1h&limit=100" \ -H "Authorization: Bearer $PMXT_API_KEY" ``` diff --git a/docs/llms-full.txt b/docs/llms-full.txt index bc74f837..d7212c4a 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -4531,8 +4531,8 @@ candles.forEach((c) => console.log(`${c.open} ${c.high} ${c.low} ${c.close} ``` ```bash curl -# First get the outcome ID from a market, then pass it as the id parameter -curl "https://api.pmxt.dev/api/polymarket/fetchOHLCV?id=YOUR_OUTCOME_ID&resolution=1h&limit=100" \ +# First get the outcome ID from a market, then pass it as the outcomeId parameter +curl "https://api.pmxt.dev/api/polymarket/fetchOHLCV?outcomeId=YOUR_OUTCOME_ID&resolution=1h&limit=100" \ -H "Authorization: Bearer $PMXT_API_KEY" ```