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
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [2.17.10] - 2026-02-25

### Added

- **Raw mode for price data**: Added an optional `{ mode: "raw" }` API argument to return raw exchange price values without normalization.

## [2.17.9] - 2026-02-25

### Fixed
Expand Down
108 changes: 83 additions & 25 deletions core/src/BaseExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export interface ApiDescriptor {
endpoints: Record<string, ApiEndpoint>;
}

export interface RequestOptions {
mode?: 'raw';
}

export interface ImplicitApiMethodInfo {
name: string;
method: string;
Expand Down Expand Up @@ -222,7 +226,7 @@ export abstract class PredictionMarketExchange {

// Snapshot state for cursor-based pagination
private _snapshotTTL: number;
private _snapshot?: { markets: UnifiedMarket[]; takenAt: number; id: string };
private _snapshot?: { markets: UnifiedMarket[]; takenAt: number; id: string; mode?: RequestOptions['mode'] };

get rateLimit(): number {
return this._rateLimit;
Expand Down Expand Up @@ -405,8 +409,11 @@ export abstract class PredictionMarketExchange {
* @example-python Get market by slug
* markets = exchange.fetch_markets(slug='will-trump-win')
*/
async fetchMarkets(params?: MarketFetchParams): Promise<UnifiedMarket[]> {
return this.fetchMarketsImpl(params);
async fetchMarkets(
params?: MarketFetchParams,
options?: RequestOptions,
): Promise<UnifiedMarket[]> {
return this.fetchMarketsImpl(params, options);
}

/**
Expand All @@ -424,9 +431,13 @@ export abstract class PredictionMarketExchange {
* @param params.cursor - Opaque cursor returned by a previous call
* @returns PaginatedMarketsResult with data, total, and optional nextCursor
*/
async fetchMarketsPaginated(params?: { limit?: number; cursor?: string }): Promise<PaginatedMarketsResult> {
async fetchMarketsPaginated(
params?: { limit?: number; cursor?: string },
options?: RequestOptions,
): Promise<PaginatedMarketsResult> {
const limit = params?.limit;
const cursor = params?.cursor;
const mode = options?.mode;

if (cursor) {
// Cursor encodes: snapshotId:offset
Expand All @@ -437,6 +448,7 @@ export abstract class PredictionMarketExchange {
if (
!this._snapshot ||
this._snapshot.id !== snapshotId ||
this._snapshot.mode !== mode ||
(this._snapshotTTL > 0 && Date.now() - this._snapshot.takenAt > this._snapshotTTL)
) {
throw new Error('Cursor has expired');
Expand All @@ -454,13 +466,15 @@ export abstract class PredictionMarketExchange {
if (
!this._snapshot ||
this._snapshotTTL === 0 ||
this._snapshot.mode !== mode ||
Date.now() - this._snapshot.takenAt > this._snapshotTTL
) {
const markets = await this.fetchMarketsImpl();
const markets = await this.fetchMarketsImpl(undefined, options);
this._snapshot = {
markets,
takenAt: Date.now(),
id: Math.random().toString(36).slice(2),
mode,
};
}

Expand Down Expand Up @@ -497,8 +511,11 @@ export abstract class PredictionMarketExchange {
* fed_event = events[0]
* print(fed_event.title, len(fed_event.markets), 'markets')
*/
async fetchEvents(params?: EventFetchParams): Promise<UnifiedEvent[]> {
return this.fetchEventsImpl(params ?? {});
async fetchEvents(
params?: EventFetchParams,
options?: RequestOptions,
): Promise<UnifiedEvent[]> {
return this.fetchEventsImpl(params ?? {}, options);
}

/**
Expand All @@ -518,7 +535,10 @@ export abstract class PredictionMarketExchange {
* @example-python Fetch by market ID
* market = exchange.fetch_market(market_id='663583')
*/
async fetchMarket(params?: MarketFetchParams): Promise<UnifiedMarket> {
async fetchMarket(
params?: MarketFetchParams,
options?: RequestOptions,
): Promise<UnifiedMarket> {
// Try to fetch from cache first if we have loaded markets and have an ID/slug
if (this.loadedMarkets) {
if (params?.marketId && this.markets[params.marketId]) {
Expand All @@ -529,7 +549,7 @@ export abstract class PredictionMarketExchange {
}
}

const markets = await this.fetchMarkets(params);
const markets = await this.fetchMarkets(params, options);
if (markets.length === 0) {
const identifier = params?.marketId || params?.outcomeId || params?.slug || params?.eventId || params?.query || 'unknown';
throw new MarketNotFound(identifier, this.name);
Expand All @@ -551,8 +571,11 @@ export abstract class PredictionMarketExchange {
* @example-python Fetch by event ID
* event = exchange.fetch_event(event_id='TRUMP25DEC')
*/
async fetchEvent(params?: EventFetchParams): Promise<UnifiedEvent> {
const events = await this.fetchEvents(params);
async fetchEvent(
params?: EventFetchParams,
options?: RequestOptions,
): Promise<UnifiedEvent> {
const events = await this.fetchEvents(params, options);
if (events.length === 0) {
const identifier = params?.eventId || params?.slug || params?.query || 'unknown';
throw new EventNotFound(identifier, this.name);
Expand All @@ -569,15 +592,21 @@ export abstract class PredictionMarketExchange {
* Implementation for fetching/searching markets.
* Exchanges should handle query, slug, and plain fetch cases based on params.
*/
protected async fetchMarketsImpl(params?: MarketFetchParams): Promise<UnifiedMarket[]> {
protected async fetchMarketsImpl(
params?: MarketFetchParams,
options?: RequestOptions,
): Promise<UnifiedMarket[]> {
throw new Error("Method fetchMarketsImpl not implemented.");
}

/**
* @internal
* Implementation for searching events by keyword.
*/
protected async fetchEventsImpl(params: EventFetchParams): Promise<UnifiedEvent[]> {
protected async fetchEventsImpl(
params: EventFetchParams,
options?: RequestOptions,
): Promise<UnifiedEvent[]> {
throw new Error("Method fetchEventsImpl not implemented.");
}

Expand Down Expand Up @@ -607,7 +636,11 @@ export abstract class PredictionMarketExchange {
* @notes Polymarket: outcomeId is the CLOB Token ID. Kalshi: outcomeId is the Market Ticker.
* @notes Resolution options: '1m' | '5m' | '15m' | '1h' | '6h' | '1d'
*/
async fetchOHLCV(id: string, params: OHLCVParams): Promise<PriceCandle[]> {
async fetchOHLCV(
id: string,
params: OHLCVParams,
options?: RequestOptions,
): Promise<PriceCandle[]> {
throw new Error("Method fetchOHLCV not implemented.");
}

Expand All @@ -630,7 +663,7 @@ export abstract class PredictionMarketExchange {
* print(f"Best ask: {book.asks[0].price}")
* print(f"Spread: {(book.asks[0].price - book.bids[0].price) * 100:.2f}%")
*/
async fetchOrderBook(id: string): Promise<OrderBook> {
async fetchOrderBook(id: string, options?: RequestOptions): Promise<OrderBook> {
throw new Error("Method fetchOrderBook not implemented.");
}

Expand All @@ -654,7 +687,11 @@ export abstract class PredictionMarketExchange {
*
* @notes Polymarket requires an API key for trade history. Use fetchOHLCV for public historical data.
*/
async fetchTrades(id: string, params: TradesParams | HistoryFilterParams): Promise<Trade[]> {
async fetchTrades(
id: string,
params: TradesParams | HistoryFilterParams,
options?: RequestOptions,
): Promise<Trade[]> {
// Deprecation warning for resolution parameter
if ('resolution' in params && params.resolution !== undefined) {
console.warn(
Expand Down Expand Up @@ -751,7 +788,7 @@ export abstract class PredictionMarketExchange {
* order = exchange.fetch_order('order-456')
* print(f"Filled: {order.filled}/{order.amount}")
*/
async fetchOrder(orderId: string): Promise<Order> {
async fetchOrder(orderId: string, options?: RequestOptions): Promise<Order> {
throw new Error("Method fetchOrder not implemented.");
}

Expand All @@ -778,19 +815,31 @@ export abstract class PredictionMarketExchange {
* @example-python Fetch orders for a specific market
* orders = exchange.fetch_open_orders('FED-25JAN')
*/
async fetchOpenOrders(marketId?: string): Promise<Order[]> {
async fetchOpenOrders(
marketId?: string,
options?: RequestOptions,
): Promise<Order[]> {
throw new Error("Method fetchOpenOrders not implemented.");
}

async fetchMyTrades(params?: MyTradesParams): Promise<UserTrade[]> {
async fetchMyTrades(
params?: MyTradesParams,
options?: RequestOptions,
): Promise<UserTrade[]> {
throw new Error("Method fetchMyTrades not implemented.");
}

async fetchClosedOrders(params?: OrderHistoryParams): Promise<Order[]> {
async fetchClosedOrders(
params?: OrderHistoryParams,
options?: RequestOptions,
): Promise<Order[]> {
throw new Error("Method fetchClosedOrders not implemented.");
}

async fetchAllOrders(params?: OrderHistoryParams): Promise<Order[]> {
async fetchAllOrders(
params?: OrderHistoryParams,
options?: RequestOptions,
): Promise<Order[]> {
throw new Error("Method fetchAllOrders not implemented.");
}

Expand All @@ -812,7 +861,7 @@ export abstract class PredictionMarketExchange {
* print(f"{pos.outcome_label}: {pos.size} @ ${pos.entry_price}")
* print(f"Unrealized P&L: ${pos.unrealized_pnl:.2f}")
*/
async fetchPositions(): Promise<Position[]> {
async fetchPositions(options?: RequestOptions): Promise<Position[]> {
throw new Error("Method fetchPositions not implemented.");
}

Expand All @@ -829,7 +878,7 @@ export abstract class PredictionMarketExchange {
* balances = exchange.fetch_balance()
* print(f"Available: ${balances[0].available}")
*/
async fetchBalance(): Promise<Balance[]> {
async fetchBalance(options?: RequestOptions): Promise<Balance[]> {
throw new Error("Method fetchBalance not implemented.");
}

Expand Down Expand Up @@ -1206,7 +1255,11 @@ export abstract class PredictionMarketExchange {
* book = exchange.watch_order_book(outcome.outcome_id)
* print(f"Bid: {book.bids[0].price} Ask: {book.asks[0].price}")
*/
async watchOrderBook(id: string, limit?: number): Promise<OrderBook> {
async watchOrderBook(
id: string,
limit?: number,
options?: RequestOptions,
): Promise<OrderBook> {
throw new Error(`watchOrderBook() is not supported by ${this.name}`);
}

Expand All @@ -1233,7 +1286,12 @@ export abstract class PredictionMarketExchange {
* for trade in trades:
* print(f"{trade.side} {trade.amount} @ {trade.price}")
*/
async watchTrades(id: string, since?: number, limit?: number): Promise<Trade[]> {
async watchTrades(
id: string,
since?: number,
limit?: number,
options?: RequestOptions,
): Promise<Trade[]> {
throw new Error(`watchTrades() is not supported by ${this.name}`);
}

Expand Down
5 changes: 3 additions & 2 deletions core/src/exchanges/baozi/fetchEvents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Connection } from '@solana/web3.js';
import { EventFetchParams } from '../../BaseExchange';
import { EventFetchParams, RequestOptions } from '../../BaseExchange';
import { UnifiedEvent } from '../../types';
import { fetchMarkets } from './fetchMarkets';
import { baoziErrorMapper } from './errors';
Expand All @@ -11,6 +11,7 @@ import { baoziErrorMapper } from './errors';
export async function fetchEvents(
connection: Connection,
params: EventFetchParams,
options?: RequestOptions,
): Promise<UnifiedEvent[]> {
try {
const markets = await fetchMarkets(connection, {
Expand All @@ -19,7 +20,7 @@ export async function fetchEvents(
offset: params.offset,
status: params.status,
searchIn: params.searchIn,
});
}, options);

return markets.map(m => {
const unifiedEvent = {
Expand Down
18 changes: 11 additions & 7 deletions core/src/exchanges/baozi/fetchMarkets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Connection } from '@solana/web3.js';
import { MarketFetchParams } from '../../BaseExchange';
import { MarketFetchParams, RequestOptions } from '../../BaseExchange';
import { UnifiedMarket } from '../../types';
import {
PROGRAM_ID,
Expand All @@ -20,10 +20,11 @@ const marketsCache = new Cache<UnifiedMarket[]>(30_000); // 30s TTL
export async function fetchMarkets(
connection: Connection,
params?: MarketFetchParams,
options?: RequestOptions,
): Promise<UnifiedMarket[]> {
try {
// Use cache for default (no-filter) fetches
if (!params?.query && !params?.slug) {
if (!params?.query && !params?.slug && options?.mode !== 'raw') {
const cached = marketsCache.get();
if (cached) {
return applyFilters(cached, params);
Expand All @@ -46,7 +47,7 @@ export async function fetchMarkets(
for (const account of booleanAccounts) {
try {
const parsed = parseMarket(account.account.data);
markets.push(mapBooleanToUnified(parsed, account.pubkey.toString()));
markets.push(mapBooleanToUnified(parsed, account.pubkey.toString(), options));
} catch {
// Skip malformed accounts
}
Expand All @@ -56,14 +57,16 @@ export async function fetchMarkets(
for (const account of raceAccounts) {
try {
const parsed = parseRaceMarket(account.account.data);
markets.push(mapRaceToUnified(parsed, account.pubkey.toString()));
markets.push(mapRaceToUnified(parsed, account.pubkey.toString(), options));
} catch {
// Skip malformed accounts
}
}

// Cache results
marketsCache.set(markets);
if (options?.mode !== 'raw') {
marketsCache.set(markets);
}

return applyFilters(markets, params);
} catch (error: any) {
Expand All @@ -74,6 +77,7 @@ export async function fetchMarkets(
export async function fetchSingleMarket(
connection: Connection,
pubkey: string,
options?: RequestOptions,
): Promise<UnifiedMarket | null> {
try {
const { PublicKey } = await import('@solana/web3.js');
Expand All @@ -87,13 +91,13 @@ export async function fetchSingleMarket(
// Check if it's a boolean market
if (Buffer.from(discriminator).equals(Buffer.from([219, 190, 213, 55, 0, 227, 198, 154]))) {
const parsed = parseMarket(data);
return mapBooleanToUnified(parsed, pubkey);
return mapBooleanToUnified(parsed, pubkey, options);
}

// Check if it's a race market
if (Buffer.from(discriminator).equals(Buffer.from([235, 196, 111, 75, 230, 113, 118, 238]))) {
const parsed = parseRaceMarket(data);
return mapRaceToUnified(parsed, pubkey);
return mapRaceToUnified(parsed, pubkey, options);
}

return null;
Expand Down
Loading
Loading