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
3 changes: 3 additions & 0 deletions .github/workflows/docs-sync-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
- name: Install dependencies
run: npm install

- name: Build core (capability map needs dist/)
run: npm run build --workspace=pmxt-core

- name: Generate OpenAPI (includes hosted docs spec)
run: npm run generate:openapi --workspace=pmxt-core

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/openapi-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- name: Install dependencies
run: npm install --workspace=pmxt-core

- name: Build core
run: npm run build --workspace=pmxt-core

- name: Regenerate OpenAPI spec
run: npm run generate:openapi --workspace=pmxt-core

Expand Down Expand Up @@ -66,6 +69,9 @@ jobs:
- name: Install dependencies
run: npm install --workspace=pmxt-core

- name: Build core
run: npm run build --workspace=pmxt-core

- name: Regenerate OpenAPI spec
run: npm run generate:openapi --workspace=pmxt-core

Expand Down
164 changes: 100 additions & 64 deletions core/api-doc-config.generated.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/scripts/generate-openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -2282,7 +2282,7 @@ function buildSpec(methodSpecs) {
name: 'exchange',
schema: {
type: 'string',
enum: ['polymarket', 'kalshi', 'kalshi-demo', 'limitless', 'probable', 'baozi', 'myriad', 'opinion', 'metaculus', 'smarkets', 'polymarket_us', 'router'],
enum: ['polymarket', 'kalshi', 'kalshi-demo', 'limitless', 'probable', 'baozi', 'myriad', 'opinion', 'metaculus', 'smarkets', 'polymarket_us', 'gemini-titan', 'hyperliquid', 'mock', 'router'],
},
required: true,
description: 'The prediction market exchange to target.',
Expand Down
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 @@ -847,6 +860,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 @@ -1341,7 +1367,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 @@ -1484,7 +1510,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 @@ -1542,9 +1568,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 @@ -1579,11 +1606,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 @@ -1601,7 +1632,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 @@ -1615,7 +1646,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
Loading
Loading