Skip to content
Open
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
152 changes: 91 additions & 61 deletions core/api-doc-config.generated.json

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions core/specs/kalshi/Kalshi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,56 @@ paths:
'500':
$ref: '#/components/responses/InternalServerError'

/markets/orderbooks:
get:
operationId: GetMarketOrderbooks
summary: Get Multiple Market Orderbooks
description: >-
Endpoint for getting the current order books for multiple markets in a
single request. The order book shows all active bid orders for both yes
and no sides of a binary market. It returns yes bids and no bids only
(no asks are returned). This is because in binary markets, a bid for yes
at price X is equivalent to an ask for no at price (100-X). For example,
a yes bid at 7¢ is the same as a no ask at 93¢, with identical contract
sizes. Each side shows price levels with their corresponding quantities
and order counts, organized from best to worst prices. Returns one
orderbook per requested market ticker.
tags:
- market
security:
- kalshiAccessKey: []
kalshiAccessSignature: []
kalshiAccessTimestamp: []
parameters:
- name: tickers
in: query
required: true
description: List of market tickers to fetch orderbooks for
schema:
type: array
items:
type: string
maxLength: 200
minItems: 1
maxItems: 100
style: form
explode: true
x-oapi-codegen-extra-tags:
validate: required,min=1,max=100,dive,max=200
responses:
'200':
description: Orderbooks retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/GetMarketOrderbooksResponse'
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'500':
$ref: '#/components/responses/InternalServerError'

/milestones/{milestone_id}:
get:
operationId: GetMilestone
Expand Down Expand Up @@ -6041,6 +6091,27 @@ components:
$ref: '#/components/schemas/OrderbookCountFp'
description: Orderbook with fixed-point contract counts (fp) in all price levels.

GetMarketOrderbooksResponse:
type: object
required:
- orderbooks
properties:
orderbooks:
type: array
items:
$ref: '#/components/schemas/MarketOrderbookFp'

MarketOrderbookFp:
type: object
required:
- ticker
- orderbook_fp
properties:
ticker:
type: string
orderbook_fp:
$ref: '#/components/schemas/OrderbookCountFp'

GetEventsResponse:
type: object
required:
Expand Down
51 changes: 41 additions & 10 deletions core/src/BaseExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ export interface ExchangeHas {
fetchOHLCV: ExchangeCapability;
/** Whether this exchange supports fetching the order book. */
fetchOrderBook: ExchangeCapability;
/** Whether this exchange supports fetching multiple market order books. */
fetchOrderBooks: ExchangeCapability;
/** Whether this exchange supports fetching public trades. */
fetchTrades: ExchangeCapability;
/** Whether this exchange supports creating orders. */
Expand Down Expand Up @@ -422,7 +424,18 @@ export abstract class PredictionMarketExchange {
this.http = axios.create({
headers: {
'User-Agent': `pmxt (https://github.com/pmxt-dev/pmxt)`
}
},
paramsSerializer: {
serialize: (params) => {
const sp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null) continue;
if (Array.isArray(v)) v.forEach((x) => sp.append(k, String(x)));
else sp.append(k, String(v));
}
return sp.toString();
},
},
});
this._throttler = new Throttler({
refillRate: 1 / this._rateLimit,
Expand Down Expand Up @@ -840,6 +853,19 @@ export abstract class PredictionMarketExchange {
throw new Error("Method fetchOrderBook not implemented.");
}

/**
* Batch variant of {@link fetchOrderBook}. Fetches order books for
* multiple outcomes in a single request where the exchange supports it.
*
* @param outcomeIds - List of Outcome IDs (outcomeId). Each id must be in the
* exchange's native format; market slugs are not accepted here.
* @returns A map keyed by the input id (preserving the caller's exact
* string) to its order book. Throws `NotFound` if any id has no book.
*/
async fetchOrderBooks(outcomeIds: string[]): Promise<Record<string, OrderBook>> {
throw new Error("Method fetchOrderBooks not implemented.");
}

/**
* Fetch raw trade history for a specific outcome.
*
Expand Down Expand Up @@ -1334,7 +1360,7 @@ export abstract class PredictionMarketExchange {
* Close all WebSocket connections and clean up resources.
* Call this when you're done streaming to properly release connections.
*/

/**
* Test method for auto-generation verification.
*/
Expand Down Expand Up @@ -1477,7 +1503,7 @@ export abstract class PredictionMarketExchange {
* Provides a typed entry point so unified methods can delegate to the implicit API
* without casting to `any` everywhere.
*/
protected async callApi(operationId: string, params?: Record<string, any>): Promise<any> {
protected async callApi(operationId: string, params?: Record<string, any> | any[]): Promise<any> {
const method = (this as any)[operationId];
if (typeof method !== 'function') {
throw new Error(`Implicit API method "${operationId}" not found on ${this.name}`);
Expand Down Expand Up @@ -1535,9 +1561,10 @@ export abstract class PredictionMarketExchange {
name: string,
endpoint: ApiEndpoint,
resolvedBaseUrl: string
): (params?: Record<string, any>) => Promise<any> {
return async (params?: Record<string, any>): Promise<any> => {
const allParams = { ...(params || {}) };
): (params?: Record<string, any> | any[]) => Promise<any> {
return async (params?: Record<string, any> | any[]): Promise<any> => {
const isArray = Array.isArray(params);
const allParams: Record<string, any> = isArray ? {} : { ...(params || {}) };

// Substitute path parameters like {ticker} from params
let resolvedPath = endpoint.path.replace(/\{([^}]+)\}/g, (_match, key) => {
Expand Down Expand Up @@ -1572,11 +1599,15 @@ export abstract class PredictionMarketExchange {
headers,
});
} else {
// POST/PUT/PATCH: remaining params go to JSON body
// POST/PUT/PATCH: array payloads go through as-is; object
// payloads send remaining params.
const body = isArray
? params
: (Object.keys(allParams).length > 0 ? allParams : undefined);
response = await this.http.request({
method: method as any,
url,
data: Object.keys(allParams).length > 0 ? allParams : undefined,
data: body,
headers: { 'Content-Type': 'application/json', ...headers },
});
}
Expand All @@ -1594,7 +1625,7 @@ export abstract class PredictionMarketExchange {

/** All keys that appear in ExchangeHas -- kept in sync via the exhaustive check below. */
private static readonly _capabilityKeys: readonly (keyof ExchangeHas)[] = [
'fetchMarkets', 'fetchEvents', 'fetchOHLCV', 'fetchOrderBook',
'fetchMarkets', 'fetchEvents', 'fetchOHLCV', 'fetchOrderBook', 'fetchOrderBooks',
'fetchTrades', 'createOrder', 'cancelOrder', 'fetchOrder',
'fetchOpenOrders', 'fetchPositions', 'fetchBalance',
'watchAddress', 'unwatchAddress', 'watchOrderBook', 'watchOrderBooks',
Expand All @@ -1608,7 +1639,7 @@ export abstract class PredictionMarketExchange {
// ExchangeHas but is missing from _capabilityKeys above.
private static readonly _exhaustiveCheck: Record<keyof ExchangeHas, true> = {
fetchMarkets: true, fetchEvents: true, fetchOHLCV: true,
fetchOrderBook: true, fetchTrades: true, createOrder: true,
fetchOrderBook: true, fetchOrderBooks: true, fetchTrades: true, createOrder: true,
cancelOrder: true, fetchOrder: true, fetchOpenOrders: true,
fetchPositions: true, fetchBalance: true, watchAddress: true,
unwatchAddress: true, watchOrderBook: true, watchOrderBooks: true, unwatchOrderBook: true,
Expand Down
41 changes: 39 additions & 2 deletions core/src/exchanges/kalshi/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Auto-generated from /Users/ndmeiri/Developer/pmxt/core/specs/kalshi/Kalshi.yaml
* Generated at: 2026-04-21T22:01:26.550Z
* Auto-generated from /home/zihao/pmxt/core/specs/kalshi/Kalshi.yaml
* Generated at: 2026-05-10T23:00:51.402Z
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
*/
export const kalshiApiSpec = {
Expand Down Expand Up @@ -1735,6 +1735,43 @@ export const kalshiApiSpec = {
]
}
},
"/markets/orderbooks": {
"get": {
"operationId": "GetMarketOrderbooks",
"summary": "Get Multiple Market Orderbooks",
"tags": [
"market"
],
"security": [
{
"kalshiAccessKey": [],
"kalshiAccessSignature": [],
"kalshiAccessTimestamp": []
}
],
"parameters": [
{
"name": "tickers",
"in": "query",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string",
"maxLength": 200
},
"minItems": 1,
"maxItems": 100
},
"style": "form",
"explode": true,
"x-oapi-codegen-extra-tags": {
"validate": "required,min=1,max=100,dive,max=200"
}
}
]
}
},
"/milestones/{milestone_id}": {
"get": {
"operationId": "GetMilestone",
Expand Down
35 changes: 32 additions & 3 deletions core/src/exchanges/kalshi/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MarketFilterParams, EventFetchParams, OHLCVParams, TradesParams, MyTradesParams } from '../../BaseExchange';
import { IExchangeFetcher, FetcherContext } from '../interfaces';
import { EventFetchParams, MarketFilterParams, MyTradesParams, OHLCVParams, TradesParams } from '../../BaseExchange';
import { FetcherContext, IExchangeFetcher } from '../interfaces';
import { kalshiErrorMapper } from './errors';
import { NotFound } from '../../errors';
import { validateIdFormat } from '../../utils/validation';
Expand Down Expand Up @@ -31,6 +31,7 @@ export interface KalshiRawMarket {
volume_fp?: string;
open_interest_fp?: string;
close_time?: string;

[key: string]: unknown;
}

Expand All @@ -43,6 +44,7 @@ export interface KalshiRawEvent {
tags?: string[];
series_ticker?: string;
markets?: KalshiRawMarket[];

[key: string]: unknown;
}

Expand All @@ -52,6 +54,7 @@ export interface KalshiRawCandlestick {
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 };

[key: string]: unknown;
}

Expand All @@ -60,6 +63,15 @@ export interface KalshiRawOrderBookFp {
no_dollars?: string[][];
}

export interface KalshiRawOrderBook {
ticker: string;
orderbook_fp: KalshiRawOrderBookFp;
}

export interface KalshiRawOrderBooks {
orderbooks: KalshiRawOrderBook[];
}

export interface KalshiRawTrade {
trade_id: string;
created_time: string;
Expand All @@ -72,6 +84,7 @@ export interface KalshiRawTrade {
/** New API field: count as a string e.g. "424.00" */
count_fp?: string;
taker_side: string;

[key: string]: unknown;
}

Expand All @@ -86,6 +99,7 @@ export interface KalshiRawFill {
count_fp?: string;
side: string;
order_id: string;

[key: string]: unknown;
}

Expand All @@ -99,6 +113,7 @@ export interface KalshiRawOrder {
remaining_count?: number;
status?: string;
created_time: string;

[key: string]: unknown;
}

Expand All @@ -109,6 +124,7 @@ export interface KalshiRawPosition {
market_price?: number;
market_exposure?: number;
realized_pnl?: number;

[key: string]: unknown;
}

Expand Down Expand Up @@ -255,7 +271,7 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw

// -- OrderBook -------------------------------------------------------------

async fetchRawOrderBook(id: string): Promise<{ orderbook_fp: KalshiRawOrderBookFp }> {
async fetchRawOrderBook(id: string): Promise<KalshiRawOrderBook> {
validateIdFormat(id, 'OrderBook');
const ticker = id.replace(/-NO$/, '');
const data = await this.ctx.callApi('GetMarketOrderbook', { ticker });
Expand All @@ -266,6 +282,19 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
return data;
}

async fetchRawOrderBooks(ids: string[]): Promise<KalshiRawOrderBook[]> {
ids.forEach((id) => validateIdFormat(id, 'OrderBook'));
const tickers = [...new Set(ids.map(id => id.replace(/-NO$/, '')))];
const data: KalshiRawOrderBooks = await this.ctx.callApi('GetMarketOrderbooks', { tickers });
const orderBooks = data.orderbooks;
if (tickers.length !== orderBooks.length) {
const returned = new Set(orderBooks.map(item => item.ticker));
const missing = tickers.filter(t => !returned.has(t));
throw new NotFound(`Order book not found for tickers ${missing.join(', ')}`, 'Kalshi');
}
return orderBooks;
}

// -- Trades ----------------------------------------------------------------

async fetchRawTrades(id: string, params: TradesParams): Promise<KalshiRawTrade[]> {
Expand Down
Loading
Loading