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
23 changes: 15 additions & 8 deletions core/src/BaseExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,17 +826,24 @@ export abstract class PredictionMarketExchange {
}

/**
* Fetch the current order book (bids/asks) for a specific outcome.
* Essential for calculating spread, depth, and execution prices.
* Fetch the order book (bids/asks) for a specific outcome.
*
* @param outcomeId - The Outcome ID (outcomeId) or market slug
* @param side - Optional 'yes' or 'no' to explicitly indicate the
* outcome side. Required for exchanges where the API returns a
* single orderbook per market (e.g. Limitless) and the caller
* passes a slug instead of a token ID.
* @returns Current order book with bids and asks
* @param limit - Max number of bid/ask levels to return (CCXT-style).
* For range queries, limits the number of snapshots returned.
* @param params - Optional parameters:
* - `side`: 'yes' or 'no' — explicitly indicate the outcome side
* (required for exchanges like Limitless where the API returns a
* single orderbook per market).
* - `since`: Unix timestamp (ms) — fetch a historical snapshot from
* the archive at or before this time (hosted API only).
* - `until`: Unix timestamp (ms) — when combined with `since`,
* returns an array of OrderBook snapshots between `since` and
* `until` (hosted API only).
* @returns Order book with bids and asks. Returns OrderBook[] when
* both `since` and `until` are provided.
*/
async fetchOrderBook(outcomeId: string, side?: 'yes' | 'no'): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, limit?: number, params?: Record<string, any>): Promise<OrderBook> {
throw new Error("Method fetchOrderBook not implemented.");
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/baozi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class BaoziExchange extends PredictionMarketExchange {
return [];
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const rawMarket = await this.fetcher.fetchRawOrderBook(outcomeId);
return this.normalizer.normalizeOrderBook(rawMarket, outcomeId);
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/gemini-titan/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class GeminiTitanExchange extends PredictionMarketExchange {
.filter((e): e is UnifiedEvent => e !== null);
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const { instrumentSymbol } = fromOutcomeId(outcomeId);
const raw = await this.fetcher.fetchRawOrderBook(instrumentSymbol);
if (!raw) {
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/hyperliquid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class HyperliquidExchange extends PredictionMarketExchange {
.filter((e): e is UnifiedEvent => e !== null);
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const raw = await this.fetcher.fetchRawOrderBook(outcomeId);
return this.normalizer.normalizeOrderBook(raw, outcomeId);
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/kalshi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class KalshiExchange extends PredictionMarketExchange {
return this.normalizer.normalizeOHLCV(rawCandles, params);
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
validateIdFormat(outcomeId, "OrderBook");
const raw = await this.fetcher.fetchRawOrderBook(outcomeId);
return this.normalizer.normalizeOrderBook(raw, outcomeId);
Expand Down
3 changes: 2 additions & 1 deletion core/src/exchanges/limitless/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,15 @@ export class LimitlessExchange extends PredictionMarketExchange {
return this.normalizer.normalizeOHLCV!(rawPrices as any, params);
}

async fetchOrderBook(outcomeId: string, side?: 'yes' | 'no'): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, limit?: number, params?: Record<string, any>): Promise<OrderBook> {
const slug = await this.resolveSlug(outcomeId);
const rawOrderBook = await this.fetcher.fetchRawOrderBook!(slug);
const orderBook = this.normalizer.normalizeOrderBook!(rawOrderBook as any, outcomeId);

// The Limitless API always returns the Yes-side order book regardless
// of which token is queried. If the caller asked for the No token,
// flip: noBid = 1 - yesAsk, noAsk = 1 - yesBid.
const side = params?.side;
const isNoToken = side === 'no' || (!side && await this.isNoOutcome(outcomeId, slug));
if (isNoToken) {
return {
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/mock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export class MockExchange extends PredictionMarketExchange {
return limit !== undefined ? events.slice(offset, offset + limit) : events.slice(offset);
}

override async fetchOrderBook(id: string): Promise<OrderBook> {
override async fetchOrderBook(id: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const f = new SeededRng(id);
const midPrice = round(f.float(0.1, 0.9), 3);
const spread = round(f.float(0.005, 0.03), 3);
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/myriad/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class MyriadExchange extends PredictionMarketExchange {
return this.normalizer.normalizeOHLCV(rawMarket, params, parsedOutcomeId);
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const parts = outcomeId.split(':');
if (parts.length >= 3) {
const [networkId, marketId, oid] = parts;
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/opinion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class OpinionExchange extends PredictionMarketExchange {
return this.normalizer.normalizeOHLCV({ history: rawPoints }, params);
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const raw = await this.fetcher.fetchRawOrderBook(outcomeId);
return this.normalizer.normalizeOrderBook(raw, outcomeId);
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/polymarket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class PolymarketExchange extends PredictionMarketExchange {
return this.normalizer.normalizeOHLCV(raw, params);
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
validateIdFormat(outcomeId, 'OrderBook');
validateOutcomeId(outcomeId, 'OrderBook');
const raw = await this.fetcher.fetchRawOrderBook(outcomeId);
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/polymarket_us/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class PolymarketUSExchange extends PredictionMarketExchange {
});
}

override async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
override async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
return this.run(async () => {
const slug = this.slugFromId(outcomeId);
const book = await this.client.markets.book(slug);
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/probable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export class ProbableExchange extends PredictionMarketExchange {
return event;
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const raw = await this.fetcher.fetchRawOrderBook(outcomeId);
return this.normalizer.normalizeOrderBook(raw, outcomeId);
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/smarkets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class SmarketsExchange extends PredictionMarketExchange {
.slice(0, limit);
}

async fetchOrderBook(outcomeId: string): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, _limit?: number, _params?: Record<string, any>): Promise<OrderBook> {
const raw = await this.fetcher.fetchRawOrderBook(outcomeId);
return this.normalizer.normalizeOrderBook(raw, outcomeId);
}
Expand Down
8 changes: 4 additions & 4 deletions core/src/router/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ export class Router extends PredictionMarketExchange {
// Unified orderbook (cross-exchange merge)
// -----------------------------------------------------------------------

async fetchOrderBook(outcomeId: string, side?: 'yes' | 'no'): Promise<OrderBook> {
async fetchOrderBook(outcomeId: string, limit?: number, params?: Record<string, any>): Promise<OrderBook> {
const exchangeNames = Object.keys(this.exchanges);
if (exchangeNames.length === 0) {
throw new Error(
'Router requires exchange instances for fetchOrderBook. Pass exchanges in RouterOptions.',
);
}

const resolvedSide = side ?? 'yes';
const resolvedSide = params?.side ?? 'yes';

// Find identity matches across venues
const matches = await this.fetchMarketMatches({
Expand All @@ -138,7 +138,7 @@ export class Router extends PredictionMarketExchange {

fetchPromises.push(
exchange
.fetchOrderBook(outcome.outcomeId, resolvedSide)
.fetchOrderBook(outcome.outcomeId, undefined, { side: resolvedSide })
.then((book) => ({ book, venue: venueName, error: null }))
.catch((error: unknown) => ({ book: null, venue: venueName, error })),
);
Expand All @@ -149,7 +149,7 @@ export class Router extends PredictionMarketExchange {
if (matchedVenues.has(name)) continue;
fetchPromises.push(
exchange
.fetchOrderBook(outcomeId, resolvedSide)
.fetchOrderBook(outcomeId, undefined, { side: resolvedSide })
.then((book) => ({ book, venue: name, error: null }))
.catch((error: unknown) => ({ book: null, venue: name, error })),
);
Expand Down
64 changes: 64 additions & 0 deletions core/src/router/e2e-orderbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* E2E test for Router.fetchOrderBook — run with:
* npx ts-node src/router/e2e-orderbook.ts
*
* Requires PMXT_API_KEY in env.
*/
import { Router } from './Router';
import { PolymarketExchange } from '../exchanges/polymarket';
import { LimitlessExchange } from '../exchanges/limitless';

async function main() {
const apiKey = process.env.PMXT_API_KEY;
if (!apiKey) {
console.error('Set PMXT_API_KEY');
process.exit(1);
}

const polymarket = new PolymarketExchange({});
const limitless = new LimitlessExchange({});

const router = new Router({
apiKey,
exchanges: { polymarket, limitless },
});

// Morocco FIFA World Cup market on Polymarket
const marketId = 'f017596d-4d53-49d5-a7d6-36ed9c37fdc4';

console.log('Fetching unified orderbook for Morocco (Polymarket + Limitless)...');
console.log(`Input market ID: ${marketId}`);
console.log('---');

const book = await router.fetchOrderBook(marketId, undefined, { side: 'yes' });

console.log(`Bids: ${book.bids.length} levels`);
console.log(`Asks: ${book.asks.length} levels`);
console.log('Top 5 bids:', book.bids.slice(0, 5));
console.log('Top 5 asks:', book.asks.slice(0, 5));

// Verify we got data from BOTH exchanges
// Polymarket top bid was 0.016, Limitless had 0.002
// If merged correctly, we should see both
const hasPoly = book.bids.some((b) => b.price === 0.016);
const hasLimitless = book.bids.some((b) => b.price === 0.002);

console.log('---');
console.log(`Has Polymarket levels: ${hasPoly}`);
console.log(`Has Limitless levels: ${hasLimitless}`);

if (hasPoly && hasLimitless) {
console.log('SUCCESS: Merged orderbook contains levels from both exchanges');
} else if (!hasPoly && hasLimitless) {
console.log('PARTIAL: Only Limitless book (source market fetch failed)');
} else if (hasPoly && !hasLimitless) {
console.log('PARTIAL: Only Polymarket book (matched market fetch failed)');
} else {
console.log('FAIL: No data from either exchange');
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
2 changes: 2 additions & 0 deletions core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export interface OrderBook {
asks: OrderLevel[];
/** Unix timestamp in milliseconds when the snapshot was taken. */
timestamp?: number;
/** ISO 8601 datetime string of the snapshot (CCXT-compatible). */
datetime?: string;
}

export interface Trade {
Expand Down
Loading
Loading