Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions core/src/exchanges/kalshi/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
39 changes: 31 additions & 8 deletions core/src/exchanges/kalshi/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,24 +201,47 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal
const ask = c.yes_ask || {};
const bid = c.yes_bid || {};

/**
* Kalshi's new candlestick API returns prices as dollar-denominated
* strings under `*_dollars` fields (e.g. `close_dollars: "0.9700"`).
* The legacy API returned integer cents under bare fields
* (e.g. `close: 97`). Prefer dollars when present; fall back to
* cents (converted via {@link fromKalshiCents}).
*/
const getVal = (field: OhlcField): number => {
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,
};
});

Expand Down
58 changes: 58 additions & 0 deletions core/test/normalizers/kalshi-ohlcv-normalization.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
4 changes: 2 additions & 2 deletions docs/api-reference/fetch-ohlcv.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
</CodeGroup>
4 changes: 2 additions & 2 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
</CodeGroup>
Expand Down
Loading