From 5ab0ca4b12afd45a790d075bfad70058b85d5772 Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 11:04:27 -0500 Subject: [PATCH 01/13] feat(core): add raw mode request option to exchange API --- core/src/BaseExchange.ts | 108 +++++++++++++++----- core/src/exchanges/baozi/fetchEvents.ts | 5 +- core/src/exchanges/baozi/fetchMarkets.ts | 18 ++-- core/src/exchanges/baozi/fetchOrderBook.ts | 4 +- core/src/exchanges/baozi/index.ts | 31 +++--- core/src/exchanges/baozi/websocket.ts | 11 +- core/src/exchanges/kalshi/fetchEvents.ts | 21 ++-- core/src/exchanges/kalshi/fetchMarkets.ts | 23 +++-- core/src/exchanges/kalshi/fetchOHLCV.ts | 13 ++- core/src/exchanges/kalshi/fetchOrderBook.ts | 16 ++- core/src/exchanges/kalshi/fetchTrades.ts | 7 +- core/src/exchanges/kalshi/index.ts | 102 ++++++++++++------ core/src/exchanges/kalshi/websocket.ts | 37 +++++-- core/src/exchanges/myriad/index.ts | 37 +++++-- core/src/exchanges/myriad/websocket.ts | 11 +- 15 files changed, 312 insertions(+), 132 deletions(-) diff --git a/core/src/BaseExchange.ts b/core/src/BaseExchange.ts index f382d3a..6772fcb 100644 --- a/core/src/BaseExchange.ts +++ b/core/src/BaseExchange.ts @@ -20,6 +20,10 @@ export interface ApiDescriptor { endpoints: Record; } +export interface RequestOptions { + mode?: 'raw'; +} + export interface ImplicitApiMethodInfo { name: string; method: string; @@ -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; @@ -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 { - return this.fetchMarketsImpl(params); + async fetchMarkets( + params?: MarketFetchParams, + options?: RequestOptions, + ): Promise { + return this.fetchMarketsImpl(params, options); } /** @@ -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 { + async fetchMarketsPaginated( + params?: { limit?: number; cursor?: string }, + options?: RequestOptions, + ): Promise { const limit = params?.limit; const cursor = params?.cursor; + const mode = options?.mode; if (cursor) { // Cursor encodes: snapshotId:offset @@ -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'); @@ -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, }; } @@ -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 { - return this.fetchEventsImpl(params ?? {}); + async fetchEvents( + params?: EventFetchParams, + options?: RequestOptions, + ): Promise { + return this.fetchEventsImpl(params ?? {}, options); } /** @@ -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 { + async fetchMarket( + params?: MarketFetchParams, + options?: RequestOptions, + ): Promise { // 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]) { @@ -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); @@ -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 { - const events = await this.fetchEvents(params); + async fetchEvent( + params?: EventFetchParams, + options?: RequestOptions, + ): Promise { + 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); @@ -569,7 +592,10 @@ 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 { + protected async fetchMarketsImpl( + params?: MarketFetchParams, + options?: RequestOptions, + ): Promise { throw new Error("Method fetchMarketsImpl not implemented."); } @@ -577,7 +603,10 @@ export abstract class PredictionMarketExchange { * @internal * Implementation for searching events by keyword. */ - protected async fetchEventsImpl(params: EventFetchParams): Promise { + protected async fetchEventsImpl( + params: EventFetchParams, + options?: RequestOptions, + ): Promise { throw new Error("Method fetchEventsImpl not implemented."); } @@ -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 { + async fetchOHLCV( + id: string, + params: OHLCVParams, + options?: RequestOptions, + ): Promise { throw new Error("Method fetchOHLCV not implemented."); } @@ -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 { + async fetchOrderBook(id: string, options?: RequestOptions): Promise { throw new Error("Method fetchOrderBook not implemented."); } @@ -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 { + async fetchTrades( + id: string, + params: TradesParams | HistoryFilterParams, + options?: RequestOptions, + ): Promise { // Deprecation warning for resolution parameter if ('resolution' in params && params.resolution !== undefined) { console.warn( @@ -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 { + async fetchOrder(orderId: string, options?: RequestOptions): Promise { throw new Error("Method fetchOrder not implemented."); } @@ -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 { + async fetchOpenOrders( + marketId?: string, + options?: RequestOptions, + ): Promise { throw new Error("Method fetchOpenOrders not implemented."); } - async fetchMyTrades(params?: MyTradesParams): Promise { + async fetchMyTrades( + params?: MyTradesParams, + options?: RequestOptions, + ): Promise { throw new Error("Method fetchMyTrades not implemented."); } - async fetchClosedOrders(params?: OrderHistoryParams): Promise { + async fetchClosedOrders( + params?: OrderHistoryParams, + options?: RequestOptions, + ): Promise { throw new Error("Method fetchClosedOrders not implemented."); } - async fetchAllOrders(params?: OrderHistoryParams): Promise { + async fetchAllOrders( + params?: OrderHistoryParams, + options?: RequestOptions, + ): Promise { throw new Error("Method fetchAllOrders not implemented."); } @@ -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 { + async fetchPositions(options?: RequestOptions): Promise { throw new Error("Method fetchPositions not implemented."); } @@ -829,7 +878,7 @@ export abstract class PredictionMarketExchange { * balances = exchange.fetch_balance() * print(f"Available: ${balances[0].available}") */ - async fetchBalance(): Promise { + async fetchBalance(options?: RequestOptions): Promise { throw new Error("Method fetchBalance not implemented."); } @@ -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 { + async watchOrderBook( + id: string, + limit?: number, + options?: RequestOptions, + ): Promise { throw new Error(`watchOrderBook() is not supported by ${this.name}`); } @@ -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 { + async watchTrades( + id: string, + since?: number, + limit?: number, + options?: RequestOptions, + ): Promise { throw new Error(`watchTrades() is not supported by ${this.name}`); } diff --git a/core/src/exchanges/baozi/fetchEvents.ts b/core/src/exchanges/baozi/fetchEvents.ts index b353cc1..d9457d8 100644 --- a/core/src/exchanges/baozi/fetchEvents.ts +++ b/core/src/exchanges/baozi/fetchEvents.ts @@ -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'; @@ -11,6 +11,7 @@ import { baoziErrorMapper } from './errors'; export async function fetchEvents( connection: Connection, params: EventFetchParams, + options?: RequestOptions, ): Promise { try { const markets = await fetchMarkets(connection, { @@ -19,7 +20,7 @@ export async function fetchEvents( offset: params.offset, status: params.status, searchIn: params.searchIn, - }); + }, options); return markets.map(m => { const unifiedEvent = { diff --git a/core/src/exchanges/baozi/fetchMarkets.ts b/core/src/exchanges/baozi/fetchMarkets.ts index 7a9284b..681d1b2 100644 --- a/core/src/exchanges/baozi/fetchMarkets.ts +++ b/core/src/exchanges/baozi/fetchMarkets.ts @@ -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, @@ -20,10 +20,11 @@ const marketsCache = new Cache(30_000); // 30s TTL export async function fetchMarkets( connection: Connection, params?: MarketFetchParams, + options?: RequestOptions, ): Promise { 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); @@ -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 } @@ -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) { @@ -74,6 +77,7 @@ export async function fetchMarkets( export async function fetchSingleMarket( connection: Connection, pubkey: string, + options?: RequestOptions, ): Promise { try { const { PublicKey } = await import('@solana/web3.js'); @@ -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; diff --git a/core/src/exchanges/baozi/fetchOrderBook.ts b/core/src/exchanges/baozi/fetchOrderBook.ts index a28a7d9..e3cdd4d 100644 --- a/core/src/exchanges/baozi/fetchOrderBook.ts +++ b/core/src/exchanges/baozi/fetchOrderBook.ts @@ -2,6 +2,7 @@ import { Connection } from '@solana/web3.js'; import { OrderBook } from '../../types'; import { fetchSingleMarket } from './fetchMarkets'; import { baoziErrorMapper } from './errors'; +import { RequestOptions } from '../../BaseExchange'; /** * Pari-mutuel markets don't have a real order book. @@ -15,10 +16,11 @@ import { baoziErrorMapper } from './errors'; export async function fetchOrderBook( connection: Connection, outcomeId: string, + options?: RequestOptions, ): Promise { try { const marketPubkey = outcomeId.replace(/-YES$|-NO$|-\d+$/, ''); - const market = await fetchSingleMarket(connection, marketPubkey); + const market = await fetchSingleMarket(connection, marketPubkey, options); if (!market) { throw new Error(`Market not found: ${marketPubkey}`); diff --git a/core/src/exchanges/baozi/index.ts b/core/src/exchanges/baozi/index.ts index 79a2f82..e8963b1 100644 --- a/core/src/exchanges/baozi/index.ts +++ b/core/src/exchanges/baozi/index.ts @@ -13,6 +13,7 @@ import { HistoryFilterParams, TradesParams, ExchangeCredentials, + RequestOptions, } from '../../BaseExchange'; import { UnifiedMarket, @@ -117,20 +118,26 @@ export class BaoziExchange extends PredictionMarketExchange { // Market Data // ----------------------------------------------------------------------- - protected async fetchMarketsImpl(params?: MarketFetchParams): Promise { - return fetchMarkets(this.connection, params); + protected async fetchMarketsImpl( + params?: MarketFetchParams, + options?: RequestOptions, + ): Promise { + return fetchMarkets(this.connection, params, options); } - protected async fetchEventsImpl(params: EventFetchParams): Promise { - return fetchEvents(this.connection, params); + protected async fetchEventsImpl( + params: EventFetchParams, + options?: RequestOptions, + ): Promise { + return fetchEvents(this.connection, params, options); } async fetchOHLCV(): Promise { return fetchOHLCV(); } - async fetchOrderBook(id: string): Promise { - return fetchOrderBook(this.connection, id); + async fetchOrderBook(id: string, options?: RequestOptions): Promise { + return fetchOrderBook(this.connection, id, options); } async fetchTrades(): Promise { @@ -141,7 +148,7 @@ export class BaoziExchange extends PredictionMarketExchange { // User Data // ----------------------------------------------------------------------- - async fetchBalance(): Promise { + async fetchBalance(options?: RequestOptions): Promise { try { const auth = this.ensureAuth(); const lamports = await this.connection.getBalance(auth.getPublicKey()); @@ -158,7 +165,7 @@ export class BaoziExchange extends PredictionMarketExchange { } } - async fetchPositions(): Promise { + async fetchPositions(options?: RequestOptions): Promise { try { const auth = this.ensureAuth(); const userPubkey = auth.getPublicKey(); @@ -197,7 +204,7 @@ export class BaoziExchange extends PredictionMarketExchange { const marketInfo = await this.connection.getAccountInfo(marketPda); if (marketInfo) { const market = parseMarket(marketInfo.data); - const unified = mapBooleanToUnified(market, marketPda.toString()); + const unified = mapBooleanToUnified(market, marketPda.toString(), options); currentYesPrice = unified.yes?.price ?? 0; currentNoPrice = unified.no?.price ?? 0; marketTitle = market.question; @@ -253,7 +260,7 @@ export class BaoziExchange extends PredictionMarketExchange { const marketInfo = await this.connection.getAccountInfo(racePda); if (marketInfo) { const raceMarket = parseRaceMarket(marketInfo.data); - const unified = mapRaceToUnified(raceMarket, racePdaStr); + const unified = mapRaceToUnified(raceMarket, racePdaStr, options); outcomePrices = unified.outcomes.map(o => o.price); outcomeLabels = unified.outcomes.map(o => o.label); } @@ -497,11 +504,11 @@ export class BaoziExchange extends PredictionMarketExchange { // WebSocket // ----------------------------------------------------------------------- - async watchOrderBook(id: string): Promise { + async watchOrderBook(id: string, limit?: number, options?: RequestOptions): Promise { if (!this.ws) { this.ws = new BaoziWebSocket(); } - return this.ws.watchOrderBook(this.connection, id); + return this.ws.watchOrderBook(this.connection, id, options); } async watchTrades(): Promise { diff --git a/core/src/exchanges/baozi/websocket.ts b/core/src/exchanges/baozi/websocket.ts index 3b15ee3..4114ece 100644 --- a/core/src/exchanges/baozi/websocket.ts +++ b/core/src/exchanges/baozi/websocket.ts @@ -1,5 +1,6 @@ import { Connection, PublicKey } from '@solana/web3.js'; import { OrderBook } from '../../types'; +import { RequestOptions } from '../../BaseExchange'; import { MARKET_DISCRIMINATOR, RACE_MARKET_DISCRIMINATOR, @@ -23,7 +24,11 @@ export class BaoziWebSocket { private orderBookResolvers = new Map[]>(); private subscriptions = new Map(); - async watchOrderBook(connection: Connection, outcomeId: string): Promise { + async watchOrderBook( + connection: Connection, + outcomeId: string, + options?: RequestOptions, + ): Promise { const marketPubkey = outcomeId.replace(/-YES$|-NO$|-\d+$/, ''); const marketKey = new PublicKey(marketPubkey); @@ -38,10 +43,10 @@ export class BaoziWebSocket { if (Buffer.from(discriminator).equals(MARKET_DISCRIMINATOR)) { const parsed = parseMarket(data); - market = mapBooleanToUnified(parsed, marketPubkey); + market = mapBooleanToUnified(parsed, marketPubkey, options); } else if (Buffer.from(discriminator).equals(RACE_MARKET_DISCRIMINATOR)) { const parsed = parseRaceMarket(data); - market = mapRaceToUnified(parsed, marketPubkey); + market = mapRaceToUnified(parsed, marketPubkey, options); } if (!market) return; diff --git a/core/src/exchanges/kalshi/fetchEvents.ts b/core/src/exchanges/kalshi/fetchEvents.ts index ccd9d3a..8578392 100644 --- a/core/src/exchanges/kalshi/fetchEvents.ts +++ b/core/src/exchanges/kalshi/fetchEvents.ts @@ -1,4 +1,4 @@ -import { EventFetchParams } from "../../BaseExchange"; +import { EventFetchParams, RequestOptions } from "../../BaseExchange"; import { UnifiedEvent, UnifiedMarket } from "../../types"; import { mapMarketToUnified } from "./utils"; import { kalshiErrorMapper } from "./errors"; @@ -11,6 +11,7 @@ type CallApi = ( async function fetchEventByTicker( eventTicker: string, callApi: CallApi, + options?: RequestOptions, ): Promise { const normalizedTicker = eventTicker.toUpperCase(); const data = await callApi("GetEvent", { @@ -24,7 +25,7 @@ async function fetchEventByTicker( const markets: UnifiedMarket[] = []; if (event.markets) { for (const market of event.markets) { - const unifiedMarket = mapMarketToUnified(event, market); + const unifiedMarket = mapMarketToUnified(event, market, options); if (unifiedMarket) { markets.push(unifiedMarket); } @@ -46,11 +47,14 @@ async function fetchEventByTicker( return [unifiedEvent]; } -function rawEventToUnified(event: any): UnifiedEvent { +function rawEventToUnified( + event: any, + options?: RequestOptions, +): UnifiedEvent { const markets: UnifiedMarket[] = []; if (event.markets) { for (const market of event.markets) { - const unifiedMarket = mapMarketToUnified(event, market); + const unifiedMarket = mapMarketToUnified(event, market, options); if (unifiedMarket) { markets.push(unifiedMarket); } @@ -106,16 +110,17 @@ async function fetchAllWithStatus( export async function fetchEvents( params: EventFetchParams, callApi: CallApi, + options?: RequestOptions, ): Promise { try { // Handle eventId lookup (direct API call) if (params.eventId) { - return await fetchEventByTicker(params.eventId, callApi); + return await fetchEventByTicker(params.eventId, callApi, options); } // Handle slug lookup (slug IS the event ticker on Kalshi) if (params.slug) { - return await fetchEventByTicker(params.slug, callApi); + return await fetchEventByTicker(params.slug, callApi, options); } const status = params?.status || "active"; @@ -152,7 +157,9 @@ export async function fetchEvents( const sort = params?.sort || "volume"; const sorted = sortRawEvents(filtered, sort); - const unifiedEvents: UnifiedEvent[] = sorted.map(rawEventToUnified); + const unifiedEvents: UnifiedEvent[] = sorted.map((event) => + rawEventToUnified(event, options), + ); return unifiedEvents.slice(0, limit); } catch (error: any) { throw kalshiErrorMapper.mapError(error); diff --git a/core/src/exchanges/kalshi/fetchMarkets.ts b/core/src/exchanges/kalshi/fetchMarkets.ts index eb4c434..86d5713 100644 --- a/core/src/exchanges/kalshi/fetchMarkets.ts +++ b/core/src/exchanges/kalshi/fetchMarkets.ts @@ -1,4 +1,4 @@ -import { MarketFetchParams } from "../../BaseExchange"; +import { MarketFetchParams, RequestOptions } from "../../BaseExchange"; import { UnifiedMarket } from "../../types"; import { mapMarketToUnified } from "./utils"; import { kalshiErrorMapper } from "./errors"; @@ -104,36 +104,37 @@ export function resetCache(): void { export async function fetchMarkets( params: MarketFetchParams | undefined, callApi: CallApi, + options?: RequestOptions, ): Promise { try { // Handle marketId lookup (Kalshi marketId is the ticker) if (params?.marketId) { - return await fetchMarketsBySlug(params.marketId, callApi); + return await fetchMarketsBySlug(params.marketId, callApi, options); } // Handle slug-based lookup (event ticker) if (params?.slug) { - return await fetchMarketsBySlug(params.slug, callApi); + return await fetchMarketsBySlug(params.slug, callApi, options); } // Handle outcomeId lookup (strip -NO suffix, use as ticker) if (params?.outcomeId) { const ticker = params.outcomeId.replace(/-NO$/, ""); - return await fetchMarketsBySlug(ticker, callApi); + return await fetchMarketsBySlug(ticker, callApi, options); } // Handle eventId lookup (event ticker works the same way) if (params?.eventId) { - return await fetchMarketsBySlug(params.eventId, callApi); + return await fetchMarketsBySlug(params.eventId, callApi, options); } // Handle query-based search if (params?.query) { - return await searchMarkets(params.query, params, callApi); + return await searchMarkets(params.query, params, callApi, options); } // Default: fetch markets - return await fetchMarketsDefault(params, callApi); + return await fetchMarketsDefault(params, callApi, options); } catch (error: any) { throw kalshiErrorMapper.mapError(error); } @@ -142,6 +143,7 @@ export async function fetchMarkets( async function fetchMarketsBySlug( eventTicker: string, callApi: CallApi, + options?: RequestOptions, ): Promise { // Kalshi API expects uppercase tickers, but URLs use lowercase const normalizedTicker = eventTicker.toUpperCase(); @@ -174,7 +176,7 @@ async function fetchMarketsBySlug( const markets = event.markets || []; for (const market of markets) { - const unifiedMarket = mapMarketToUnified(event, market); + const unifiedMarket = mapMarketToUnified(event, market, options); if (unifiedMarket) { unifiedMarkets.push(unifiedMarket); } @@ -187,12 +189,14 @@ async function searchMarkets( query: string, params: MarketFetchParams | undefined, callApi: CallApi, + options?: RequestOptions, ): Promise { // We must fetch ALL markets to search them locally since we don't have server-side search const searchLimit = 250000; const markets = await fetchMarketsDefault( { ...params, limit: searchLimit }, callApi, + options, ); const lowerQuery = query.toLowerCase(); const searchIn = params?.searchIn || "title"; // Default to title-only search @@ -215,6 +219,7 @@ async function searchMarkets( async function fetchMarketsDefault( params: MarketFetchParams | undefined, callApi: CallApi, + options?: RequestOptions, ): Promise { const limit = params?.limit || 250000; const offset = params?.offset || 0; @@ -285,7 +290,7 @@ async function fetchMarketsDefault( const markets = event.markets || []; for (const market of markets) { - const unifiedMarket = mapMarketToUnified(event, market); + const unifiedMarket = mapMarketToUnified(event, market, options); if (unifiedMarket) { allMarkets.push(unifiedMarket); } diff --git a/core/src/exchanges/kalshi/fetchOHLCV.ts b/core/src/exchanges/kalshi/fetchOHLCV.ts index 4e8cdd3..d750245 100644 --- a/core/src/exchanges/kalshi/fetchOHLCV.ts +++ b/core/src/exchanges/kalshi/fetchOHLCV.ts @@ -1,13 +1,15 @@ -import { OHLCVParams } from "../../BaseExchange"; +import { OHLCVParams, RequestOptions } from "../../BaseExchange"; import { PriceCandle } from "../../types"; import { mapIntervalToKalshi } from "./utils"; import { validateIdFormat } from "../../utils/validation"; import { kalshiErrorMapper } from "./errors"; +import { getKalshiPriceContext, fromKalshiCents } from "./price"; export async function fetchOHLCV( id: string, params: OHLCVParams, callApi: (operationId: string, params?: Record) => Promise, + options?: RequestOptions, ): Promise { validateIdFormat(id, "OHLCV"); @@ -72,6 +74,7 @@ export async function fetchOHLCV( end_ts: endTs, }); const candles = data.candlesticks || []; + const priceContext = getKalshiPriceContext(options); const mappedCandles: PriceCandle[] = candles.map((c: any) => { // Priority: @@ -98,10 +101,10 @@ export async function fetchOHLCV( return { timestamp: c.end_period_ts * 1000, - open: getVal("open") / 100, - high: getVal("high") / 100, - low: getVal("low") / 100, - close: getVal("close") / 100, + open: fromKalshiCents(getVal("open"), priceContext), + high: fromKalshiCents(getVal("high"), priceContext), + low: fromKalshiCents(getVal("low"), priceContext), + close: fromKalshiCents(getVal("close"), priceContext), volume: c.volume || 0, }; }); diff --git a/core/src/exchanges/kalshi/fetchOrderBook.ts b/core/src/exchanges/kalshi/fetchOrderBook.ts index 2fd5881..a73e069 100644 --- a/core/src/exchanges/kalshi/fetchOrderBook.ts +++ b/core/src/exchanges/kalshi/fetchOrderBook.ts @@ -3,14 +3,22 @@ import { OrderBook } from "../../types"; import { validateIdFormat } from "../../utils/validation"; import { kalshiErrorMapper } from "./errors"; import { getMarketsUrl } from "./config"; +import { RequestOptions } from "../../BaseExchange"; +import { + getKalshiPriceContext, + fromKalshiCents, + invertKalshiCents, +} from "./price"; export async function fetchOrderBook( baseUrl: string, id: string, + options?: RequestOptions, ): Promise { validateIdFormat(id, "OrderBook"); try { + const priceContext = getKalshiPriceContext(options); // Check if this is a NO outcome request const isNoOutcome = id.endsWith("-NO"); const ticker = id.replace(/-NO$/, ""); @@ -31,12 +39,12 @@ export async function fetchOrderBook( // - Bids: people buying NO (use data.no directly) // - Asks: people selling NO = people buying YES (invert data.yes) bids = (data.no || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0], priceContext), size: level[1], })); asks = (data.yes || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, // Invert YES price to get NO ask price + price: invertKalshiCents(level[0], priceContext), // Invert YES price to get NO ask price size: level[1], })); } else { @@ -44,12 +52,12 @@ export async function fetchOrderBook( // - Bids: people buying YES (use data.yes directly) // - Asks: people selling YES = people buying NO (invert data.no) bids = (data.yes || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0], priceContext), size: level[1], })); asks = (data.no || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, // Invert NO price to get YES ask price + price: invertKalshiCents(level[0], priceContext), // Invert NO price to get YES ask price size: level[1], })); } diff --git a/core/src/exchanges/kalshi/fetchTrades.ts b/core/src/exchanges/kalshi/fetchTrades.ts index ef1fd44..9b17e5f 100644 --- a/core/src/exchanges/kalshi/fetchTrades.ts +++ b/core/src/exchanges/kalshi/fetchTrades.ts @@ -1,15 +1,18 @@ import axios from "axios"; -import { HistoryFilterParams, TradesParams } from "../../BaseExchange"; +import { HistoryFilterParams, TradesParams, RequestOptions } from "../../BaseExchange"; import { Trade } from "../../types"; import { kalshiErrorMapper } from "./errors"; import { getMarketsUrl } from "./config"; +import { getKalshiPriceContext, fromKalshiCents } from "./price"; export async function fetchTrades( baseUrl: string, id: string, params: TradesParams | HistoryFilterParams, + options?: RequestOptions, ): Promise { try { + const priceContext = getKalshiPriceContext(options); const ticker = id.replace(/-NO$/, ""); const url = getMarketsUrl(baseUrl, undefined, ["trades"]); const response = await axios.get(url, { @@ -23,7 +26,7 @@ export async function fetchTrades( return trades.map((t: any) => ({ id: t.trade_id, timestamp: new Date(t.created_time).getTime(), - price: t.yes_price / 100, + price: fromKalshiCents(t.yes_price, priceContext), amount: t.count, side: t.taker_side === "yes" ? "buy" : "sell", })); diff --git a/core/src/exchanges/kalshi/index.ts b/core/src/exchanges/kalshi/index.ts index f6ae1a4..152c45b 100644 --- a/core/src/exchanges/kalshi/index.ts +++ b/core/src/exchanges/kalshi/index.ts @@ -5,6 +5,7 @@ import { OHLCVParams, TradesParams, ExchangeCredentials, + RequestOptions, EventFetchParams, MyTradesParams, OrderHistoryParams, @@ -32,6 +33,11 @@ import { AuthenticationError } from "../../errors"; import { parseOpenApiSpec } from "../../utils/openapi"; import { kalshiApiSpec } from "./api"; import { getKalshiConfig, KalshiApiConfig, KALSHI_PATHS } from "./config"; +import { + getKalshiPriceContext, + fromKalshiCents, + invertKalshiCents, +} from "./price"; // Re-export for external use export type { KalshiWebSocketConfig }; @@ -146,26 +152,30 @@ export class KalshiExchange extends PredictionMarketExchange { protected async fetchMarketsImpl( params?: MarketFilterParams, + options?: RequestOptions, ): Promise { - return fetchMarkets(params, this.callApi.bind(this)); + return fetchMarkets(params, this.callApi.bind(this), options); } protected async fetchEventsImpl( params: EventFetchParams, + options?: RequestOptions, ): Promise { - return fetchEvents(params, this.callApi.bind(this)); + return fetchEvents(params, this.callApi.bind(this), options); } async fetchOHLCV( id: string, params: OHLCVParams, + options?: RequestOptions, ): Promise { - return fetchOHLCV(id, params, this.callApi.bind(this)); + return fetchOHLCV(id, params, this.callApi.bind(this), options); } - async fetchOrderBook(id: string): Promise { + async fetchOrderBook(id: string, options?: RequestOptions): Promise { validateIdFormat(id, "OrderBook"); + const priceContext = getKalshiPriceContext(options); const isNoOutcome = id.endsWith("-NO"); const ticker = id.replace(/-NO$/, ""); const data = (await this.callApi("GetMarketOrderbook", { ticker })) @@ -176,20 +186,20 @@ export class KalshiExchange extends PredictionMarketExchange { if (isNoOutcome) { bids = (data.no || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0], priceContext), size: level[1], })); asks = (data.yes || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, + price: invertKalshiCents(level[0], priceContext), size: level[1], })); } else { bids = (data.yes || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0], priceContext), size: level[1], })); asks = (data.no || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, + price: invertKalshiCents(level[0], priceContext), size: level[1], })); } @@ -203,6 +213,7 @@ export class KalshiExchange extends PredictionMarketExchange { async fetchTrades( id: string, params: TradesParams | HistoryFilterParams, + options?: RequestOptions, ): Promise { if ("resolution" in params && params.resolution !== undefined) { console.warn( @@ -210,6 +221,7 @@ export class KalshiExchange extends PredictionMarketExchange { "It will be removed in v3.0.0. Please remove it from your code.", ); } + const priceContext = getKalshiPriceContext(options); const ticker = id.replace(/-NO$/, ""); const data = await this.callApi("GetTrades", { ticker, @@ -219,7 +231,7 @@ export class KalshiExchange extends PredictionMarketExchange { return trades.map((t: any) => ({ id: t.trade_id, timestamp: new Date(t.created_time).getTime(), - price: t.yes_price / 100, + price: fromKalshiCents(t.yes_price, priceContext), amount: t.count, side: t.taker_side === "yes" ? "buy" : "sell", })); @@ -229,10 +241,11 @@ export class KalshiExchange extends PredictionMarketExchange { // User Data Methods // ---------------------------------------------------------------------------- - async fetchBalance(): Promise { + async fetchBalance(options?: RequestOptions): Promise { const data = await this.callApi("GetBalance"); - const available = data.balance / 100; - const total = data.portfolio_value / 100; + const priceContext = getKalshiPriceContext(options); + const available = fromKalshiCents(data.balance, priceContext); + const total = fromKalshiCents(data.portfolio_value, priceContext); return [ { currency: "USD", @@ -291,12 +304,12 @@ export class KalshiExchange extends PredictionMarketExchange { }; } - async fetchOrder(orderId: string): Promise { + async fetchOrder(orderId: string, options?: RequestOptions): Promise { const data = await this.callApi("GetOrder", { order_id: orderId }); - return this.mapKalshiOrder(data.order); + return this.mapKalshiOrder(data.order, options); } - async fetchOpenOrders(marketId?: string): Promise { + async fetchOpenOrders(marketId?: string, options?: RequestOptions): Promise { const queryParams: Record = { status: "resting" }; if (marketId) { queryParams.ticker = marketId; @@ -304,10 +317,13 @@ export class KalshiExchange extends PredictionMarketExchange { const data = await this.callApi("GetOrders", queryParams); const orders = data.orders || []; - return orders.map((order: any) => this.mapKalshiOrder(order)); + return orders.map((order: any) => this.mapKalshiOrder(order, options)); } - async fetchMyTrades(params?: MyTradesParams): Promise { + async fetchMyTrades( + params?: MyTradesParams, + options?: RequestOptions, + ): Promise { const queryParams: Record = {}; if (params?.outcomeId || params?.marketId) { queryParams.ticker = (params.outcomeId || params.marketId)!.replace( @@ -323,17 +339,21 @@ export class KalshiExchange extends PredictionMarketExchange { if (params?.cursor) queryParams.cursor = params.cursor; const data = await this.callApi("GetFills", queryParams); + const priceContext = getKalshiPriceContext(options); return (data.fills || []).map((f: any) => ({ id: f.fill_id, timestamp: new Date(f.created_time).getTime(), - price: f.yes_price / 100, + price: fromKalshiCents(f.yes_price, priceContext), amount: f.count, side: f.side === "yes" ? ("buy" as const) : ("sell" as const), orderId: f.order_id, })); } - async fetchClosedOrders(params?: OrderHistoryParams): Promise { + async fetchClosedOrders( + params?: OrderHistoryParams, + options?: RequestOptions, + ): Promise { const queryParams: Record = {}; if (params?.marketId) queryParams.ticker = params.marketId; if (params?.until) @@ -342,10 +362,13 @@ export class KalshiExchange extends PredictionMarketExchange { if (params?.cursor) queryParams.cursor = params.cursor; const data = await this.callApi("GetHistoricalOrders", queryParams); - return (data.orders || []).map((o: any) => this.mapKalshiOrder(o)); + return (data.orders || []).map((o: any) => this.mapKalshiOrder(o, options)); } - async fetchAllOrders(params?: OrderHistoryParams): Promise { + async fetchAllOrders( + params?: OrderHistoryParams, + options?: RequestOptions, + ): Promise { const queryParams: Record = {}; if (params?.marketId) queryParams.ticker = params.marketId; if (params?.since) @@ -370,20 +393,21 @@ export class KalshiExchange extends PredictionMarketExchange { ]) { if (!seen.has(o.order_id)) { seen.add(o.order_id); - all.push(this.mapKalshiOrder(o)); + all.push(this.mapKalshiOrder(o, options)); } } return all.sort((a, b) => b.timestamp - a.timestamp); } - async fetchPositions(): Promise { + async fetchPositions(options?: RequestOptions): Promise { const data = await this.callApi("GetPositions"); const positions = data.market_positions || []; + const priceContext = getKalshiPriceContext(options); return positions.map((pos: any) => { const absPosition = Math.abs(pos.position); const entryPrice = - absPosition > 0 ? pos.total_cost / absPosition / 100 : 0; + absPosition > 0 ? fromKalshiCents(pos.total_cost, priceContext) / absPosition : 0; return { marketId: pos.ticker, @@ -391,22 +415,31 @@ export class KalshiExchange extends PredictionMarketExchange { outcomeLabel: pos.ticker, size: pos.position, entryPrice, - currentPrice: pos.market_price ? pos.market_price / 100 : entryPrice, - unrealizedPnL: pos.market_exposure ? pos.market_exposure / 100 : 0, - realizedPnL: pos.realized_pnl ? pos.realized_pnl / 100 : 0, + currentPrice: pos.market_price + ? fromKalshiCents(pos.market_price, priceContext) + : entryPrice, + unrealizedPnL: pos.market_exposure + ? fromKalshiCents(pos.market_exposure, priceContext) + : 0, + realizedPnL: pos.realized_pnl + ? fromKalshiCents(pos.realized_pnl, priceContext) + : 0, }; }); } // Helper to map a raw Kalshi order object to a unified Order - private mapKalshiOrder(order: any): Order { + private mapKalshiOrder(order: any, options?: RequestOptions): Order { + const priceContext = getKalshiPriceContext(options); return { id: order.order_id, marketId: order.ticker, outcomeId: order.ticker, side: order.side === "yes" ? "buy" : "sell", type: order.type === "limit" ? "limit" : "market", - price: order.yes_price ? order.yes_price / 100 : undefined, + price: order.yes_price + ? fromKalshiCents(order.yes_price, priceContext) + : undefined, amount: order.count, status: this.mapKalshiOrderStatus(order.status), filled: order.count - (order.remaining_count || 0), @@ -439,7 +472,11 @@ export class KalshiExchange extends PredictionMarketExchange { private ws?: KalshiWebSocket; - async watchOrderBook(id: string, limit?: number): Promise { + async watchOrderBook( + id: string, + limit?: number, + options?: RequestOptions, + ): Promise { const auth = this.ensureAuth(); if (!this.ws) { @@ -452,13 +489,14 @@ export class KalshiExchange extends PredictionMarketExchange { } // Normalize ticker (strip -NO suffix if present) const marketTicker = id.replace(/-NO$/, ""); - return this.ws.watchOrderBook(marketTicker); + return this.ws.watchOrderBook(marketTicker, options); } async watchTrades( id: string, since?: number, limit?: number, + options?: RequestOptions, ): Promise { const auth = this.ensureAuth(); @@ -472,7 +510,7 @@ export class KalshiExchange extends PredictionMarketExchange { } // Normalize ticker (strip -NO suffix if present) const marketTicker = id.replace(/-NO$/, ""); - return this.ws.watchTrades(marketTicker); + return this.ws.watchTrades(marketTicker, options); } async close(): Promise { diff --git a/core/src/exchanges/kalshi/websocket.ts b/core/src/exchanges/kalshi/websocket.ts index 85d6f63..5d6cba5 100644 --- a/core/src/exchanges/kalshi/websocket.ts +++ b/core/src/exchanges/kalshi/websocket.ts @@ -1,6 +1,12 @@ import WebSocket from "ws"; import { OrderBook, Trade, OrderLevel } from "../../types"; +import { RequestOptions } from "../../BaseExchange"; import { KalshiAuth } from "./auth"; +import { + getKalshiPriceContext, + fromKalshiCents, + invertKalshiCents, +} from "./price"; interface QueuedPromise { resolve: (value: T | PromiseLike) => void; @@ -26,6 +32,8 @@ export class KalshiWebSocket { private orderBookResolvers = new Map[]>(); private tradeResolvers = new Map[]>(); private orderBooks = new Map(); + private orderBookOptions = new Map(); + private tradeOptions = new Map(); private subscribedOrderBookTickers = new Set(); private subscribedTradeTickers = new Set(); private messageIdCounter = 1; @@ -222,6 +230,8 @@ export class KalshiWebSocket { private handleOrderbookSnapshot(data: any) { const ticker = data.market_ticker; + const options = this.orderBookOptions.get(ticker); + const priceContext = getKalshiPriceContext(options); // Kalshi orderbook structure: // yes: [{ price: number (cents), quantity: number }, ...] @@ -229,7 +239,7 @@ export class KalshiWebSocket { const bids: OrderLevel[] = (data.yes || []) .map((level: any) => { - const price = (level.price || level[0]) / 100; + const price = fromKalshiCents(level.price || level[0], priceContext); const size = (level.quantity !== undefined ? level.quantity @@ -242,7 +252,7 @@ export class KalshiWebSocket { const asks: OrderLevel[] = (data.no || []) .map((level: any) => { - const price = (100 - (level.price || level[0])) / 100; + const price = invertKalshiCents(level.price || level[0], priceContext); const size = (level.quantity !== undefined ? level.quantity @@ -266,6 +276,8 @@ export class KalshiWebSocket { private handleOrderbookDelta(data: any) { const ticker = data.market_ticker; const existing = this.orderBooks.get(ticker); + const options = this.orderBookOptions.get(ticker); + const priceContext = getKalshiPriceContext(options); if (!existing) { // No snapshot yet, skip delta @@ -274,7 +286,7 @@ export class KalshiWebSocket { // Apply delta updates // Kalshi sends: { price: number, delta: number, side: 'yes' | 'no' } - const price = data.price / 100; + const price = fromKalshiCents(data.price, priceContext); const delta = data.delta !== undefined ? data.delta @@ -286,8 +298,8 @@ export class KalshiWebSocket { if (side === "yes") { this.applyDelta(existing.bids, price, delta, "desc"); } else { - const yesPrice = (100 - data.price) / 100; - this.applyDelta(existing.asks, yesPrice, delta, "asc"); + const invertedPrice = invertKalshiCents(data.price, priceContext); + this.applyDelta(existing.asks, invertedPrice, delta, "asc"); } existing.timestamp = Date.now(); @@ -330,6 +342,8 @@ export class KalshiWebSocket { private handleTrade(data: any) { const ticker = data.market_ticker; + const options = this.tradeOptions.get(ticker); + const priceContext = getKalshiPriceContext(options); // Kalshi trade structure: // { trade_id, market_ticker, yes_price, no_price, count, created_time, taker_side } @@ -364,8 +378,8 @@ export class KalshiWebSocket { timestamp, price: data.yes_price || data.price - ? (data.yes_price || data.price) / 100 - : 0.5, + ? fromKalshiCents(data.yes_price || data.price, priceContext) + : priceContext.defaultPrice, amount: data.count || data.size || 0, side: data.taker_side === "yes" || data.side === "buy" @@ -390,7 +404,10 @@ export class KalshiWebSocket { } } - async watchOrderBook(ticker: string): Promise { + async watchOrderBook( + ticker: string, + options?: RequestOptions, + ): Promise { // Ensure connection if (!this.isConnected) { await this.connect(); @@ -401,6 +418,7 @@ export class KalshiWebSocket { this.subscribedOrderBookTickers.add(ticker); this.subscribeToOrderbook(Array.from(this.subscribedOrderBookTickers)); } + this.orderBookOptions.set(ticker, options); // Return a promise that resolves on the next orderbook update return new Promise((resolve, reject) => { @@ -411,7 +429,7 @@ export class KalshiWebSocket { }); } - async watchTrades(ticker: string): Promise { + async watchTrades(ticker: string, options?: RequestOptions): Promise { // Ensure connection if (!this.isConnected) { await this.connect(); @@ -422,6 +440,7 @@ export class KalshiWebSocket { this.subscribedTradeTickers.add(ticker); this.subscribeToTrades(Array.from(this.subscribedTradeTickers)); } + this.tradeOptions.set(ticker, options); // Return a promise that resolves on the next trade return new Promise((resolve, reject) => { diff --git a/core/src/exchanges/myriad/index.ts b/core/src/exchanges/myriad/index.ts index b16f912..93f2fe6 100644 --- a/core/src/exchanges/myriad/index.ts +++ b/core/src/exchanges/myriad/index.ts @@ -1,4 +1,4 @@ -import { PredictionMarketExchange, MarketFilterParams, HistoryFilterParams, OHLCVParams, TradesParams, ExchangeCredentials, EventFetchParams, MyTradesParams } from '../../BaseExchange'; +import { PredictionMarketExchange, MarketFilterParams, HistoryFilterParams, OHLCVParams, TradesParams, ExchangeCredentials, EventFetchParams, MyTradesParams, RequestOptions } from '../../BaseExchange'; import { UnifiedMarket, UnifiedEvent, PriceCandle, OrderBook, Trade, UserTrade, Balance, Order, Position, CreateOrderParams } from '../../types'; import { fetchMarkets } from './fetchMarkets'; import { fetchEvents } from './fetchEvents'; @@ -11,6 +11,7 @@ import { AuthenticationError } from '../../errors'; import { BASE_URL } from './utils'; import { parseOpenApiSpec } from '../../utils/openapi'; import { myriadApiSpec } from './api'; +import { resolveMyriadPrice } from './price'; export class MyriadExchange extends PredictionMarketExchange { override readonly has = { @@ -95,7 +96,11 @@ export class MyriadExchange extends PredictionMarketExchange { return fetchOrderBook(id, this.callApi.bind(this)); } - async fetchTrades(id: string, params: TradesParams | HistoryFilterParams): Promise { + async fetchTrades( + id: string, + params: TradesParams | HistoryFilterParams, + options?: RequestOptions, + ): Promise { if ('resolution' in params && params.resolution !== undefined) { console.warn( '[pmxt] Warning: The "resolution" parameter is deprecated for fetchTrades() and will be ignored. ' + @@ -140,13 +145,13 @@ export class MyriadExchange extends PredictionMarketExchange { return filtered.map((t: any, index: number) => ({ id: `${t.blockNumber || t.timestamp}-${index}`, timestamp: (t.timestamp || 0) * 1000, - price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, + price: resolveMyriadPrice(t, options), amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); } - async fetchMyTrades(params?: MyTradesParams): Promise { + async fetchMyTrades(params?: MyTradesParams, options?: RequestOptions): Promise { const walletAddress = this.ensureAuth().walletAddress; if (!walletAddress) { throw new AuthenticationError( @@ -169,7 +174,7 @@ export class MyriadExchange extends PredictionMarketExchange { return tradeEvents.map((t: any, i: number) => ({ id: `${t.blockNumber || t.timestamp}-${i}`, timestamp: (t.timestamp || 0) * 1000, - price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, + price: resolveMyriadPrice(t, options), amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); @@ -238,7 +243,7 @@ export class MyriadExchange extends PredictionMarketExchange { return []; // AMM: no open orders } - async fetchPositions(): Promise { + async fetchPositions(options?: RequestOptions): Promise { const walletAddress = this.ensureAuth().walletAddress; if (!walletAddress) { throw new AuthenticationError( @@ -256,7 +261,7 @@ export class MyriadExchange extends PredictionMarketExchange { outcomeLabel: pos.outcomeTitle || `Outcome ${pos.outcomeId}`, size: Number(pos.shares || 0), entryPrice: Number(pos.price || 0), - currentPrice: Number(pos.value || 0) / Math.max(Number(pos.shares || 1), 1), + currentPrice: resolveMyriadPrice(pos, options), unrealizedPnL: Number(pos.profit || 0), })); } @@ -290,20 +295,29 @@ export class MyriadExchange extends PredictionMarketExchange { // WebSocket (poll-based) // ------------------------------------------------------------------------ - async watchOrderBook(id: string, _limit?: number): Promise { + async watchOrderBook( + id: string, + _limit?: number, + options?: RequestOptions, + ): Promise { this.ensureAuth(); if (!this.ws) { this.ws = new MyriadWebSocket(this.callApi.bind(this)); } - return this.ws.watchOrderBook(id); + return this.ws.watchOrderBook(id, options); } - async watchTrades(id: string, _since?: number, _limit?: number): Promise { + async watchTrades( + id: string, + _since?: number, + _limit?: number, + options?: RequestOptions, + ): Promise { this.ensureAuth(); if (!this.ws) { this.ws = new MyriadWebSocket(this.callApi.bind(this)); } - return this.ws.watchTrades(id); + return this.ws.watchTrades(id, options); } async close(): Promise { @@ -312,4 +326,5 @@ export class MyriadExchange extends PredictionMarketExchange { this.ws = undefined; } } + } diff --git a/core/src/exchanges/myriad/websocket.ts b/core/src/exchanges/myriad/websocket.ts index 8073ded..af11bc9 100644 --- a/core/src/exchanges/myriad/websocket.ts +++ b/core/src/exchanges/myriad/websocket.ts @@ -1,5 +1,7 @@ import { OrderBook, Trade } from '../../types'; +import { RequestOptions } from '../../BaseExchange'; import { fetchOrderBook } from './fetchOrderBook'; +import { resolveMyriadPrice } from './price'; // Myriad API v2 does not expose a WebSocket endpoint. // We implement a poll-based fallback that resolves promises @@ -15,6 +17,7 @@ export class MyriadWebSocket { private orderBookResolvers: Map void)[]> = new Map(); private tradeResolvers: Map void)[]> = new Map(); private lastTradeTimestamp: Map = new Map(); + private tradeOptions: Map = new Map(); private closed = false; constructor(callApi: (operationId: string, params?: Record) => Promise, pollInterval?: number) { @@ -22,7 +25,7 @@ export class MyriadWebSocket { this.pollInterval = pollInterval || DEFAULT_POLL_INTERVAL; } - async watchOrderBook(id: string): Promise { + async watchOrderBook(id: string, _options?: RequestOptions): Promise { if (this.closed) throw new Error('WebSocket connection is closed'); return new Promise((resolve) => { @@ -37,7 +40,7 @@ export class MyriadWebSocket { }); } - async watchTrades(id: string): Promise { + async watchTrades(id: string, options?: RequestOptions): Promise { if (this.closed) throw new Error('WebSocket connection is closed'); return new Promise((resolve) => { @@ -46,6 +49,7 @@ export class MyriadWebSocket { } this.tradeResolvers.get(id)!.push(resolve); + this.tradeOptions.set(id, options); if (!this.tradeTimers.has(id)) { this.startTradePolling(id); } @@ -92,6 +96,7 @@ export class MyriadWebSocket { private startTradePolling(id: string): void { const poll = async () => { try { + const options = this.tradeOptions.get(id); const parts = id.split(':'); const [networkId, marketId] = parts; const outcomeId = parts.length >= 3 ? parts[2] : undefined; @@ -116,7 +121,7 @@ export class MyriadWebSocket { const trades: Trade[] = filtered.map((t: any, index: number) => ({ id: `${t.blockNumber || t.timestamp}-${index}`, timestamp: (t.timestamp || 0) * 1000, - price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, + price: resolveMyriadPrice(t, options), amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); From 5d492a45ed92d2da6a1b13d706f37ebccd1e0414 Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 11:04:47 -0500 Subject: [PATCH 02/13] refactor(core): extract reusable price conversion helpers for raw/normalized modes --- core/src/exchanges/baozi/price.ts | 30 ++++++++++++++++++++++ core/src/exchanges/baozi/utils.ts | 32 ++++++++++++++--------- core/src/exchanges/kalshi/price.ts | 41 ++++++++++++++++++++++++++++++ core/src/exchanges/kalshi/utils.ts | 36 +++++++++++++++++++++----- core/src/exchanges/myriad/price.ts | 15 +++++++++++ 5 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 core/src/exchanges/baozi/price.ts create mode 100644 core/src/exchanges/kalshi/price.ts create mode 100644 core/src/exchanges/myriad/price.ts diff --git a/core/src/exchanges/baozi/price.ts b/core/src/exchanges/baozi/price.ts new file mode 100644 index 0000000..6885b83 --- /dev/null +++ b/core/src/exchanges/baozi/price.ts @@ -0,0 +1,30 @@ +import { RequestOptions } from "../../BaseExchange"; +import { MarketOutcome } from "../../types"; + +export function clampBaoziPrice( + value: number, + options?: RequestOptions, +): number { + if (options?.mode === "raw") { + return value; + } + return Math.min(Math.max(value, 0), 1); +} + +export function normalizeBaoziOutcomes( + outcomes: MarketOutcome[], + options?: RequestOptions, +): void { + if (options?.mode === "raw") { + return; + } + + const sum = outcomes.reduce((acc, item) => acc + item.price, 0); + if (sum <= 0) { + return; + } + + for (const outcome of outcomes) { + outcome.price = outcome.price / sum; + } +} diff --git a/core/src/exchanges/baozi/utils.ts b/core/src/exchanges/baozi/utils.ts index a55acaf..f476562 100644 --- a/core/src/exchanges/baozi/utils.ts +++ b/core/src/exchanges/baozi/utils.ts @@ -2,7 +2,12 @@ import { PublicKey } from '@solana/web3.js'; import bs58 from 'bs58'; import { createHash } from 'crypto'; import { UnifiedMarket, MarketOutcome } from '../../types'; +import { RequestOptions } from '../../BaseExchange'; import { addBinaryOutcomes } from '../../utils/market-utils'; +import { + clampBaoziPrice, + normalizeBaoziOutcomes, +} from './price'; // --------------------------------------------------------------------------- // Constants @@ -367,7 +372,11 @@ export function parseRacePosition(data: Buffer | Uint8Array): BaoziRacePosition // Mapping to Unified Types // --------------------------------------------------------------------------- -export function mapBooleanToUnified(market: BaoziMarket, pubkey: string): UnifiedMarket { +export function mapBooleanToUnified( + market: BaoziMarket, + pubkey: string, + options?: RequestOptions, +): UnifiedMarket { const totalPool = market.yesPool + market.noPool; const totalPoolSol = Number(totalPool) / LAMPORTS_PER_SOL; @@ -387,13 +396,13 @@ export function mapBooleanToUnified(market: BaoziMarket, pubkey: string): Unifie outcomeId: `${pubkey}-YES`, marketId: pubkey, label: 'Yes', - price: yesPrice, + price: clampBaoziPrice(yesPrice, options), }, { outcomeId: `${pubkey}-NO`, marketId: pubkey, label: 'No', - price: noPrice, + price: clampBaoziPrice(noPrice, options), }, ]; @@ -415,7 +424,11 @@ export function mapBooleanToUnified(market: BaoziMarket, pubkey: string): Unifie return um; } -export function mapRaceToUnified(market: BaoziRaceMarket, pubkey: string): UnifiedMarket { +export function mapRaceToUnified( + market: BaoziRaceMarket, + pubkey: string, + options?: RequestOptions, +): UnifiedMarket { const totalPoolSol = Number(market.totalPool) / LAMPORTS_PER_SOL; const outcomes: MarketOutcome[] = []; @@ -431,17 +444,12 @@ export function mapRaceToUnified(market: BaoziRaceMarket, pubkey: string): Unifi outcomeId: `${pubkey}-${i}`, marketId: pubkey, label: market.outcomeLabels[i] || `Outcome ${i + 1}`, - price: Math.min(Math.max(price, 0), 1), + price: clampBaoziPrice(price, options), }); } - // Normalize prices to sum to 1 - const priceSum = outcomes.reduce((s, o) => s + o.price, 0); - if (priceSum > 0) { - for (const o of outcomes) { - o.price = o.price / priceSum; - } - } + // Normalize prices to sum to 1 in non-raw mode. + normalizeBaoziOutcomes(outcomes, options); const um: UnifiedMarket = { marketId: pubkey, diff --git a/core/src/exchanges/kalshi/price.ts b/core/src/exchanges/kalshi/price.ts new file mode 100644 index 0000000..ab60795 --- /dev/null +++ b/core/src/exchanges/kalshi/price.ts @@ -0,0 +1,41 @@ +import { RequestOptions } from "../../BaseExchange"; + +export interface KalshiPriceContext { + isRaw: boolean; + scale: number; + unit: number; + defaultPrice: number; +} + +export function getKalshiPriceContext( + options?: RequestOptions, +): KalshiPriceContext { + const isRaw = options?.mode === "raw"; + return { + isRaw, + scale: isRaw ? 1 : 100, + unit: isRaw ? 100 : 1, + defaultPrice: isRaw ? 50 : 0.5, + }; +} + +export function fromKalshiCents( + priceInCents: number, + context: KalshiPriceContext, +): number { + return priceInCents / context.scale; +} + +export function invertKalshiCents( + priceInCents: number, + context: KalshiPriceContext, +): number { + return context.unit - fromKalshiCents(priceInCents, context); +} + +export function invertKalshiUnified( + price: number, + context: KalshiPriceContext, +): number { + return context.unit - price; +} diff --git a/core/src/exchanges/kalshi/utils.ts b/core/src/exchanges/kalshi/utils.ts index e0e589c..43450fa 100644 --- a/core/src/exchanges/kalshi/utils.ts +++ b/core/src/exchanges/kalshi/utils.ts @@ -1,20 +1,32 @@ +import { RequestOptions } from "../../BaseExchange"; import { UnifiedMarket, MarketOutcome, CandleInterval } from "../../types"; import { addBinaryOutcomes } from "../../utils/market-utils"; +import { + getKalshiPriceContext, + fromKalshiCents, + invertKalshiUnified, +} from "./price"; export function mapMarketToUnified( event: any, market: any, + options?: RequestOptions, ): UnifiedMarket | null { if (!market) return null; + const priceContext = getKalshiPriceContext(options); + // Calculate price - let price = 0.5; + let price = priceContext.defaultPrice; if (market.last_price) { - price = market.last_price / 100; + price = fromKalshiCents(market.last_price, priceContext); } else if (market.yes_ask && market.yes_bid) { - price = (market.yes_ask + market.yes_bid) / 200; + price = + (fromKalshiCents(market.yes_ask, priceContext) + + fromKalshiCents(market.yes_bid, priceContext)) / + 2; } else if (market.yes_ask) { - price = market.yes_ask / 100; + price = fromKalshiCents(market.yes_ask, priceContext); } // Extract candidate name @@ -25,7 +37,19 @@ export function mapMarketToUnified( // Calculate 24h change let priceChange = 0; - if ( + if (priceContext.isRaw) { + if ( + market.previous_price !== undefined && + market.last_price !== undefined + ) { + priceChange = market.last_price - market.previous_price; + } else if ( + market.previous_price_dollars !== undefined && + market.last_price_dollars !== undefined + ) { + priceChange = market.last_price_dollars - market.previous_price_dollars; + } + } else if ( market.previous_price_dollars !== undefined && market.last_price_dollars !== undefined ) { @@ -44,7 +68,7 @@ export function mapMarketToUnified( outcomeId: `${market.ticker}-NO`, marketId: market.ticker, label: candidateName ? `Not ${candidateName}` : "No", - price: 1 - price, + price: invertKalshiUnified(price, priceContext), priceChange24h: -priceChange, // Inverse change for No? simplified assumption }, ]; diff --git a/core/src/exchanges/myriad/price.ts b/core/src/exchanges/myriad/price.ts new file mode 100644 index 0000000..5cb43d0 --- /dev/null +++ b/core/src/exchanges/myriad/price.ts @@ -0,0 +1,15 @@ +import { RequestOptions } from "../../BaseExchange"; + +export function resolveMyriadPrice(event: any, options?: RequestOptions): number { + if ( + options?.mode === "raw" && + event.price !== undefined && + event.price !== null + ) { + return Number(event.price); + } + + const shares = Math.max(Number(event.shares || 1), 1); + return Number(event.value || 0) / shares; +} + From bc5369ae272e343955edff00fff42c8be20212e7 Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 11:05:12 -0500 Subject: [PATCH 03/13] test(core): cover raw-mode price helpers and kalshi raw orderbook behavior --- core/src/exchanges/baozi/price.test.ts | 42 ++++++++++++++++++++ core/src/exchanges/kalshi/kalshi.test.ts | 50 ++++++++++++++++++++++++ core/src/exchanges/kalshi/price.test.ts | 42 ++++++++++++++++++++ core/src/exchanges/myriad/price.test.ts | 25 ++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 core/src/exchanges/baozi/price.test.ts create mode 100644 core/src/exchanges/kalshi/price.test.ts create mode 100644 core/src/exchanges/myriad/price.test.ts diff --git a/core/src/exchanges/baozi/price.test.ts b/core/src/exchanges/baozi/price.test.ts new file mode 100644 index 0000000..106c648 --- /dev/null +++ b/core/src/exchanges/baozi/price.test.ts @@ -0,0 +1,42 @@ +import { + clampBaoziPrice, + normalizeBaoziOutcomes, +} from "./price"; +import { MarketOutcome } from "../../types"; + +describe("baozi price helpers", () => { + it("clamps values in normalized mode", () => { + expect(clampBaoziPrice(1.2)).toBe(1); + expect(clampBaoziPrice(-0.1)).toBe(0); + expect(clampBaoziPrice(0.3)).toBe(0.3); + }); + + it("does not clamp in raw mode", () => { + expect(clampBaoziPrice(1.2, { mode: "raw" })).toBe(1.2); + expect(clampBaoziPrice(-0.1, { mode: "raw" })).toBe(-0.1); + }); + + it("normalizes outcomes in normalized mode", () => { + const outcomes: MarketOutcome[] = [ + { outcomeId: "a", marketId: "m", label: "A", price: 45 }, + { outcomeId: "b", marketId: "m", label: "B", price: 55 }, + ]; + + normalizeBaoziOutcomes(outcomes); + + expect(outcomes[0].price).toBeCloseTo(0.45); + expect(outcomes[1].price).toBeCloseTo(0.55); + }); + + it("skips normalization in raw mode", () => { + const outcomes: MarketOutcome[] = [ + { outcomeId: "a", marketId: "m", label: "A", price: 45 }, + { outcomeId: "b", marketId: "m", label: "B", price: 55 }, + ]; + + normalizeBaoziOutcomes(outcomes, { mode: "raw" }); + + expect(outcomes[0].price).toBe(45); + expect(outcomes[1].price).toBe(55); + }); +}); diff --git a/core/src/exchanges/kalshi/kalshi.test.ts b/core/src/exchanges/kalshi/kalshi.test.ts index 97014cb..c2cefe5 100644 --- a/core/src/exchanges/kalshi/kalshi.test.ts +++ b/core/src/exchanges/kalshi/kalshi.test.ts @@ -105,6 +105,56 @@ describe("KalshiExchange", () => { const markets = await exchange.fetchMarkets(); expect(markets).toBeDefined(); }); + + it("should return raw prices for fetchTrades when mode is raw", async () => { + const mockResponse = { + data: { + trades: [ + { + trade_id: "trade-1", + created_time: "2026-01-13T12:00:00Z", + yes_price: 55, + count: 10, + taker_side: "yes", + }, + ], + }, + }; + (mockAxiosInstance.request as jest.Mock).mockResolvedValue( + mockResponse, + ); + + const trades = await exchange.fetchTrades( + "TEST-MARKET", + { limit: 1 }, + { mode: "raw" }, + ); + + expect(trades).toHaveLength(1); + expect(trades[0].price).toBe(55); + }); + + it("should return raw prices for fetchOrderBook when mode is raw", async () => { + exchange = new KalshiExchange(mockCredentials); + const mockResponse = { + data: { + orderbook: { + yes: [[55, 10]], + no: [[45, 5]], + }, + }, + }; + (mockAxiosInstance.request as jest.Mock).mockResolvedValue( + mockResponse, + ); + + const book = await exchange.fetchOrderBook("TEST-MARKET", { + mode: "raw", + }); + + expect(book.bids[0].price).toBe(55); + expect(book.asks[0].price).toBe(55); + }); }); describe("Trading Methods", () => { diff --git a/core/src/exchanges/kalshi/price.test.ts b/core/src/exchanges/kalshi/price.test.ts new file mode 100644 index 0000000..7932e0a --- /dev/null +++ b/core/src/exchanges/kalshi/price.test.ts @@ -0,0 +1,42 @@ +import { + getKalshiPriceContext, + fromKalshiCents, + invertKalshiCents, + invertKalshiUnified, +} from "./price"; + +describe("kalshi price helpers", () => { + it("returns normalized context by default", () => { + const context = getKalshiPriceContext(); + + expect(context.isRaw).toBe(false); + expect(context.scale).toBe(100); + expect(context.unit).toBe(1); + expect(context.defaultPrice).toBe(0.5); + }); + + it("returns raw context when mode is raw", () => { + const context = getKalshiPriceContext({ mode: "raw" }); + + expect(context.isRaw).toBe(true); + expect(context.scale).toBe(1); + expect(context.unit).toBe(100); + expect(context.defaultPrice).toBe(50); + }); + + it("converts cents to unified normalized prices", () => { + const context = getKalshiPriceContext(); + + expect(fromKalshiCents(55, context)).toBe(0.55); + expect(invertKalshiCents(45, context)).toBe(0.55); + expect(invertKalshiUnified(0.45, context)).toBe(0.55); + }); + + it("converts cents to raw prices without scaling", () => { + const context = getKalshiPriceContext({ mode: "raw" }); + + expect(fromKalshiCents(55, context)).toBe(55); + expect(invertKalshiCents(45, context)).toBe(55); + expect(invertKalshiUnified(45, context)).toBe(55); + }); +}); diff --git a/core/src/exchanges/myriad/price.test.ts b/core/src/exchanges/myriad/price.test.ts new file mode 100644 index 0000000..9e4e5bb --- /dev/null +++ b/core/src/exchanges/myriad/price.test.ts @@ -0,0 +1,25 @@ +import { resolveMyriadPrice } from "./price"; + +describe("myriad price helpers", () => { + it("uses raw event.price when raw mode is requested and present", () => { + const value = resolveMyriadPrice( + { price: "42.5", value: 100, shares: 2 }, + { mode: "raw" }, + ); + expect(value).toBe(42.5); + }); + + it("falls back to value/shares when raw price is missing", () => { + const value = resolveMyriadPrice( + { value: 100, shares: 4 }, + { mode: "raw" }, + ); + expect(value).toBe(25); + }); + + it("keeps previous fallback behavior when shares are missing or zero", () => { + expect(resolveMyriadPrice({ value: 12, shares: 0 })).toBe(12); + expect(resolveMyriadPrice({ value: 7 })).toBe(7); + }); +}); + From 19ccfc41c9773b2f7075d5fbb57e1feea96dd1f8 Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 11:05:28 -0500 Subject: [PATCH 04/13] feat(sdk-typescript): support raw mode in optional request args handling --- sdks/typescript/pmxt/args.ts | 29 +++++++++++ sdks/typescript/pmxt/client.ts | 93 ++++++++++++++++++---------------- sdks/typescript/pmxt/models.ts | 9 +++- 3 files changed, 85 insertions(+), 46 deletions(-) create mode 100644 sdks/typescript/pmxt/args.ts diff --git a/sdks/typescript/pmxt/args.ts b/sdks/typescript/pmxt/args.ts new file mode 100644 index 0000000..837d64d --- /dev/null +++ b/sdks/typescript/pmxt/args.ts @@ -0,0 +1,29 @@ +import { RequestOptions } from "./models.js"; + +export function buildArgsWithOptionalOptions( + primary?: any, + options?: RequestOptions, +): any[] { + if (options !== undefined) { + return [primary ?? null, options]; + } + return primary !== undefined ? [primary] : []; +} + +export function withTrailingOptions( + args: any[], + options: RequestOptions | undefined, + optionalArgCount: number, +): any[] { + if (options === undefined) { + return args; + } + + while (args.length < optionalArgCount) { + args.push(null); + } + + args.push(options); + return args; +} + diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index df892f6..ae553f0 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -34,9 +34,11 @@ import { MarketFilterFunction, EventFilterCriteria, EventFilterFunction, + RequestOptions, } from "./models.js"; import { ServerManager } from "./server-manager.js"; +import { buildArgsWithOptionalOptions, withTrailingOptions } from "./args.js"; // Converter functions function convertMarket(raw: any): UnifiedMarket { @@ -379,11 +381,10 @@ export abstract class Exchange { } } - async fetchMarkets(params?: any): Promise { + async fetchMarkets(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarkets`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -401,11 +402,10 @@ export abstract class Exchange { } } - async fetchMarketsPaginated(params?: any): Promise { + async fetchMarketsPaginated(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarketsPaginated`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -427,11 +427,10 @@ export abstract class Exchange { } } - async fetchEvents(params?: any): Promise { + async fetchEvents(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchEvents`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -449,11 +448,10 @@ export abstract class Exchange { } } - async fetchMarket(params?: any): Promise { + async fetchMarket(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarket`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -471,11 +469,10 @@ export abstract class Exchange { } } - async fetchEvent(params?: any): Promise { + async fetchEvent(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchEvent`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -493,11 +490,10 @@ export abstract class Exchange { } } - async fetchOrderBook(id: string): Promise { + async fetchOrderBook(id: string, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - args.push(id); + const args = withTrailingOptions([id], options, 2); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchOrderBook`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -537,11 +533,10 @@ export abstract class Exchange { } } - async fetchOrder(orderId: string): Promise { + async fetchOrder(orderId: string, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - args.push(orderId); + const args = withTrailingOptions([orderId], options, 2); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchOrder`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -559,11 +554,10 @@ export abstract class Exchange { } } - async fetchOpenOrders(marketId?: string): Promise { + async fetchOpenOrders(marketId?: string, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (marketId !== undefined) args.push(marketId); + const args = buildArgsWithOptionalOptions(marketId, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchOpenOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -581,11 +575,10 @@ export abstract class Exchange { } } - async fetchMyTrades(params?: any): Promise { + async fetchMyTrades(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMyTrades`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -603,11 +596,10 @@ export abstract class Exchange { } } - async fetchClosedOrders(params?: any): Promise { + async fetchClosedOrders(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchClosedOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -625,11 +617,10 @@ export abstract class Exchange { } } - async fetchAllOrders(params?: any): Promise { + async fetchAllOrders(params?: any, options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params, options); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchAllOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -647,10 +638,10 @@ export abstract class Exchange { } } - async fetchPositions(): Promise { + async fetchPositions(options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; + const args = options !== undefined ? [options] : []; const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchPositions`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -668,10 +659,10 @@ export abstract class Exchange { } } - async fetchBalance(): Promise { + async fetchBalance(options?: RequestOptions): Promise { await this.initPromise; try { - const args: any[] = []; + const args = options !== undefined ? [options] : []; const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchBalance`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -730,7 +721,8 @@ export abstract class Exchange { */ async fetchOHLCV( outcomeId: string, - params: any + params: any, + options?: RequestOptions ): Promise { await this.initPromise; try { @@ -746,7 +738,7 @@ export abstract class Exchange { } const requestBody: FetchOHLCVRequest = { - args: [outcomeId, paramsDict], + args: withTrailingOptions([outcomeId, paramsDict], options, 3), credentials: this.getCredentials() }; @@ -773,17 +765,21 @@ export abstract class Exchange { */ async fetchTrades( outcomeId: string, - params: any + params: any, + options?: RequestOptions ): Promise { await this.initPromise; try { - const paramsDict: any = { resolution: params.resolution }; - if (params.limit) { + const paramsDict: any = {}; + if (params?.resolution !== undefined) { + paramsDict.resolution = params.resolution; + } + if (params?.limit !== undefined) { paramsDict.limit = params.limit; } const requestBody: FetchTradesRequest = { - args: [outcomeId, paramsDict], + args: withTrailingOptions([outcomeId, paramsDict], options, 3), credentials: this.getCredentials() }; @@ -821,13 +817,18 @@ export abstract class Exchange { * } * ``` */ - async watchOrderBook(outcomeId: string, limit?: number): Promise { + async watchOrderBook( + outcomeId: string, + limit?: number, + options?: RequestOptions + ): Promise { await this.initPromise; try { const args: any[] = [outcomeId]; if (limit !== undefined) { args.push(limit); } + withTrailingOptions(args, options, 2); const requestBody: any = { args, @@ -871,7 +872,8 @@ export abstract class Exchange { async watchTrades( outcomeId: string, since?: number, - limit?: number + limit?: number, + options?: RequestOptions ): Promise { await this.initPromise; try { @@ -882,6 +884,7 @@ export abstract class Exchange { if (limit !== undefined) { args.push(limit); } + withTrailingOptions(args, options, 3); const requestBody: any = { args, diff --git a/sdks/typescript/pmxt/models.ts b/sdks/typescript/pmxt/models.ts index 95db590..4dce320 100644 --- a/sdks/typescript/pmxt/models.ts +++ b/sdks/typescript/pmxt/models.ts @@ -1,9 +1,16 @@ /** * Data models for PMXT TypeScript SDK. - * + * * These are clean TypeScript interfaces that provide a user-friendly API. */ + /** + * Request options for API calls. + */ +export interface RequestOptions { + mode?: 'raw'; +} + /** * A single tradeable outcome within a market. */ From 1d8b83445d39006422a7eb6c0db34102db1f99c9 Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 11:05:37 -0500 Subject: [PATCH 05/13] test(sdk-typescript): add unit tests for args helper normalization --- sdks/typescript/tests/client-args.test.ts | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 sdks/typescript/tests/client-args.test.ts diff --git a/sdks/typescript/tests/client-args.test.ts b/sdks/typescript/tests/client-args.test.ts new file mode 100644 index 0000000..8b8dff0 --- /dev/null +++ b/sdks/typescript/tests/client-args.test.ts @@ -0,0 +1,40 @@ +import { describe, test, expect } from "@jest/globals"; +import { + buildArgsWithOptionalOptions, + withTrailingOptions, +} from "../pmxt/args"; + +describe("client args helpers", () => { + test("buildArgsWithOptionalOptions handles optional options and primary", () => { + expect(buildArgsWithOptionalOptions(undefined, undefined)).toEqual([]); + expect(buildArgsWithOptionalOptions({ q: 1 }, undefined)).toEqual([{ q: 1 }]); + expect(buildArgsWithOptionalOptions(undefined, { mode: "raw" })).toEqual([ + null, + { mode: "raw" }, + ]); + expect(buildArgsWithOptionalOptions({ q: 1 }, { mode: "raw" })).toEqual([ + { q: 1 }, + { mode: "raw" }, + ]); + }); + + test("withTrailingOptions pads missing optional args before options", () => { + expect(withTrailingOptions(["id"], undefined, 2)).toEqual(["id"]); + expect(withTrailingOptions(["id"], { mode: "raw" }, 2)).toEqual([ + "id", + null, + { mode: "raw" }, + ]); + expect(withTrailingOptions(["id", 10], { mode: "raw" }, 2)).toEqual([ + "id", + 10, + { mode: "raw" }, + ]); + expect(withTrailingOptions(["id", 100], { mode: "raw" }, 3)).toEqual([ + "id", + 100, + null, + { mode: "raw" }, + ]); + }); +}); From 102529d01845e811dddb669cbbbf3bba919dab8d Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 11:05:47 -0500 Subject: [PATCH 06/13] docs: document raw mode in README/api docs and migration notes --- changelog.md | 6 ++ core/src/server/openapi.yaml | 147 ++++++++++++++++++++++--------- docs/MIGRATE_FROM_DOMEAPI.md | 2 +- readme.md | 2 + sdks/typescript/API_REFERENCE.md | 11 +++ sdks/typescript/README.md | 2 +- 6 files changed, 128 insertions(+), 42 deletions(-) diff --git a/changelog.md b/changelog.md index bc7e67a..611cabe 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/core/src/server/openapi.yaml b/core/src/server/openapi.yaml index 8295a32..5e1a5ae 100644 --- a/core/src/server/openapi.yaml +++ b/core/src/server/openapi.yaml @@ -82,9 +82,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - $ref: '#/components/schemas/MarketFilterParams' + oneOf: + - $ref: '#/components/schemas/MarketFilterParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -119,14 +122,17 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - type: object - properties: - limit: - type: number - cursor: - type: string + oneOf: + - type: object + properties: + limit: + type: number + cursor: + type: string + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -162,9 +168,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - $ref: '#/components/schemas/EventFetchParams' + oneOf: + - $ref: '#/components/schemas/EventFetchParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -199,9 +208,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - $ref: '#/components/schemas/MarketFilterParams' + oneOf: + - $ref: '#/components/schemas/MarketFilterParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -234,9 +246,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - $ref: '#/components/schemas/EventFetchParams' + oneOf: + - $ref: '#/components/schemas/EventFetchParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -270,11 +285,12 @@ paths: args: type: array minItems: 2 - maxItems: 2 + maxItems: 3 items: oneOf: - type: string - $ref: '#/components/schemas/OHLCVParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -309,10 +325,12 @@ paths: properties: args: type: array - maxItems: 1 - items: - type: string minItems: 1 + maxItems: 2 + items: + oneOf: + - type: string + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -348,13 +366,14 @@ paths: args: type: array minItems: 2 - maxItems: 2 + maxItems: 3 items: oneOf: - type: string - oneOf: - $ref: '#/components/schemas/TradesParams' - $ref: '#/components/schemas/HistoryFilterParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -461,10 +480,12 @@ paths: properties: args: type: array - maxItems: 1 - items: - type: string minItems: 1 + maxItems: 2 + items: + oneOf: + - type: string + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -497,9 +518,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - type: string + oneOf: + - type: string + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -532,9 +556,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - $ref: '#/components/schemas/MyTradesParams' + oneOf: + - $ref: '#/components/schemas/MyTradesParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -566,9 +593,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - $ref: '#/components/schemas/OrderHistoryParams' + oneOf: + - $ref: '#/components/schemas/OrderHistoryParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -600,9 +630,12 @@ paths: properties: args: type: array - maxItems: 1 + minItems: 0 + maxItems: 2 items: - $ref: '#/components/schemas/OrderHistoryParams' + oneOf: + - $ref: '#/components/schemas/OrderHistoryParams' + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -634,8 +667,9 @@ paths: properties: args: type: array - maxItems: 0 - items: {} + maxItems: 1 + items: + type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -668,8 +702,9 @@ paths: properties: args: type: array - maxItems: 0 - items: {} + maxItems: 1 + items: + type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -883,11 +918,12 @@ paths: args: type: array minItems: 1 - maxItems: 2 + maxItems: 3 items: oneOf: - type: string - type: number + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -923,12 +959,13 @@ paths: args: type: array minItems: 1 - maxItems: 3 + maxItems: 4 items: oneOf: - type: string - type: number - type: number + - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -950,6 +987,39 @@ paths: description: >- Watch trade executions in real-time via WebSocket. Returns a promise that resolves with the next trade(s). Call repeatedly in a loop to stream updates (CCXT Pro pattern). + '/api/{exchange}/testDummyMethod': + post: + summary: Test Dummy Method + operationId: testDummyMethod + parameters: + - $ref: '#/components/parameters/ExchangeParam' + requestBody: + content: + application/json: + schema: + title: TestDummyMethodRequest + type: object + properties: + args: + type: array + maxItems: 1 + items: + type: string + credentials: + $ref: '#/components/schemas/ExchangeCredentials' + responses: + '200': + description: Test Dummy Method response + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/BaseResponse' + - type: object + properties: + data: + type: string + description: Test method for auto-generation verification. '/api/{exchange}/close': post: summary: Close @@ -976,9 +1046,6 @@ paths: application/json: schema: $ref: '#/components/schemas/BaseResponse' - description: >- - Close all WebSocket connections and clean up resources. Call this when you're done streaming to properly release - connections. components: parameters: ExchangeParam: diff --git a/docs/MIGRATE_FROM_DOMEAPI.md b/docs/MIGRATE_FROM_DOMEAPI.md index 89e3477..9460dc8 100644 --- a/docs/MIGRATE_FROM_DOMEAPI.md +++ b/docs/MIGRATE_FROM_DOMEAPI.md @@ -94,7 +94,7 @@ markets = poly.fetch_markets(query='Trump') price = markets[0].yes.price # 0.0 to 1.0 ``` -> Note: DomeAPI returns price as a raw value. pmxt prices are always 0.0-1.0 (probability). Multiply by 100 for percentage. +> Note: DomeAPI returns price as a raw value. pmxt prices default to 0.0-1.0 (probability). To return raw exchange values, pass `{ mode: "raw" }` as the final argument (e.g., `fetchMarkets(params, { mode: "raw" })`). --- diff --git a/readme.md b/readme.md index cc82397..8fe16aa 100644 --- a/readme.md +++ b/readme.md @@ -148,6 +148,8 @@ const warsh = fedEvent.markets.match('Kevin Warsh'); console.log(`Price: ${warsh.yes?.price}`); ``` +Prices are normalized to `0.0-1.0` by default. To return raw exchange values, pass `{ mode: "raw" }` as the final argument to fetch methods (e.g., `fetchMarkets(params, { mode: "raw" })`). + ## Trading pmxt supports unified trading across exchanges. diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index 511c590..088dc56 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -28,6 +28,17 @@ console.log(markets[0].title); --- +## Raw Mode + +Prices are normalized to `0.0-1.0` by default. To return raw exchange values, pass `{ mode: "raw" }` as the final argument to fetch methods. + +```typescript +const markets = await poly.fetchMarkets({ query: "Trump" }, { mode: "raw" }); +const orderBook = await kalshi.fetchOrderBook("FED-25JAN", { mode: "raw" }); +``` + +--- + ## Server Management The SDK provides global functions to manage the background sidecar server. This is useful for clearing state or diff --git a/sdks/typescript/README.md b/sdks/typescript/README.md index bebcabd..79262b0 100644 --- a/sdks/typescript/README.md +++ b/sdks/typescript/README.md @@ -136,7 +136,7 @@ For complete API documentation and examples, see: ## Important Notes - **Use `outcome.outcomeId`, not `market.marketId`** for deep-dive methods (fetchOHLCV, fetchOrderBook, fetchTrades) -- **Prices are 0.0 to 1.0** (multiply by 100 for percentages) +- **Prices are 0.0 to 1.0 by default** (multiply by 100 for percentages). Use `{ mode: 'raw' }` as the final argument to return raw exchange values. - **Timestamps are Unix milliseconds** - **Volumes are in USD** From 495032e16083a1b03f2cc09faefc943b54ca0f35 Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 12:03:37 -0500 Subject: [PATCH 07/13] feat(python-sdk): add raw price mode support across exchange methods --- sdks/python/pmxt/__init__.py | 2 + sdks/python/pmxt/client.py | 274 ++++++++++++++++++++++++++--------- sdks/python/pmxt/models.py | 10 +- 3 files changed, 216 insertions(+), 70 deletions(-) diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index 72a6c74..f4f7f19 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -33,6 +33,7 @@ Order, Position, Balance, + RequestOptions, ) @@ -80,4 +81,5 @@ def restart_server(): "Order", "Position", "Balance", + "RequestOptions", ] diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index de3244e..56d1efc 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -7,7 +7,7 @@ import os import sys -from typing import List, Optional, Dict, Any, Literal, Union +from typing import List, Optional, Dict, Any, Literal, Union, Sequence from datetime import datetime from abc import ABC, abstractmethod import json @@ -226,7 +226,7 @@ def __init__( base_url: str = "http://localhost:3847", auto_start_server: bool = True, proxy_address: Optional[str] = None, - signature_type: Optional[Any] = None, + signature_type: Optional[Union[int, str]] = None, ): """ Initialize an exchange client. @@ -330,6 +330,45 @@ def _get_credentials_dict(self) -> Optional[Dict[str, Any]]: creds["signatureType"] = self.signature_type return creds if creds else None + def _build_request_options( + self, + mode: Optional[Literal["normalized", "raw"]], + ) -> Optional[Dict[str, str]]: + """Build validated request options.""" + if mode is None: + return None + if mode not in ("normalized", "raw"): + raise ValueError("mode must be either 'normalized' or 'raw'") + return {"mode": mode} + + def _build_args_with_optional_options( + self, + required_args: Sequence[object], + optional_args: Optional[Sequence[object]] = None, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> List[object]: + """ + Build args while preserving optional argument positions when options are present. + """ + args: List[object] = list(required_args) + optional_values = list(optional_args or []) + options = self._build_request_options(mode) + + if options is not None: + args.extend(optional_values) + args.append(options) + return args + + last_non_none = -1 + for i, value in enumerate(optional_values): + if value is not None: + last_non_none = i + + if last_non_none >= 0: + args.extend(optional_values[: last_non_none + 1]) + + return args + @property def has(self) -> Dict[str, Any]: """ @@ -363,11 +402,20 @@ def has(self) -> Dict[str, Any]: # Low-Level API Access - def _call_method(self, method_name: str, params: Optional[Dict[str, Any]] = None) -> Any: + def _call_method( + self, + method_name: str, + params: Optional[Dict[str, object]] = None, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> Any: """Call any exchange method on the server by name.""" try: url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/{method_name}" - body: Dict[str, Any] = {"args": [params] if params is not None else []} + optional_params = params + if mode is not None and optional_params is None: + optional_params = {} + args = self._build_args_with_optional_options([], [optional_params], mode) + body: Dict[str, object] = {"args": args} creds = self._get_credentials_dict() if creds: body["credentials"] = creds @@ -462,7 +510,12 @@ def load_markets(self, reload: bool = False) -> Dict[str, UnifiedMarket]: self._loaded_markets = True return self.markets - def fetch_markets(self, query: Optional[str] = None, **kwargs) -> List[UnifiedMarket]: + def fetch_markets( + self, + query: Optional[str] = None, + mode: Optional[Literal["normalized", "raw"]] = None, + **kwargs, + ) -> List[UnifiedMarket]: """ Get active markets from the exchange. @@ -477,10 +530,8 @@ def fetch_markets(self, query: Optional[str] = None, **kwargs) -> List[UnifiedMa >>> markets = exchange.fetch_markets("Trump", limit=20, sort="volume") """ try: - body_dict = {"args": []} - # Prepare arguments - search_params = {} + search_params: Dict[str, object] = {} if query: search_params["query"] = query @@ -488,8 +539,13 @@ def fetch_markets(self, query: Optional[str] = None, **kwargs) -> List[UnifiedMa for key, value in kwargs.items(): search_params[key] = value - if search_params: - body_dict["args"] = [search_params] + params_arg: Optional[Dict[str, object]] = ( + search_params if search_params else None + ) + if mode is not None and params_arg is None: + params_arg = {} + args = self._build_args_with_optional_options([], [params_arg], mode) + body_dict = {"args": args} # Add credentials if available creds = self._get_credentials_dict() @@ -508,7 +564,12 @@ def fetch_markets(self, query: Optional[str] = None, **kwargs) -> List[UnifiedMa except ApiException as e: raise Exception(f"Failed to fetch markets: {self._extract_api_error(e)}") from None - def fetch_events(self, query: Optional[str] = None, **kwargs) -> List[UnifiedEvent]: + def fetch_events( + self, + query: Optional[str] = None, + mode: Optional[Literal["normalized", "raw"]] = None, + **kwargs, + ) -> List[UnifiedEvent]: """ Fetch events with optional keyword search. Events group related markets together. @@ -524,10 +585,8 @@ def fetch_events(self, query: Optional[str] = None, **kwargs) -> List[UnifiedEve >>> events = exchange.fetch_events("Election", limit=10) """ try: - body_dict = {"args": []} - # Prepare arguments - search_params = {} + search_params: Dict[str, object] = {} if query: search_params["query"] = query @@ -535,8 +594,13 @@ def fetch_events(self, query: Optional[str] = None, **kwargs) -> List[UnifiedEve for key, value in kwargs.items(): search_params[key] = value - if search_params: - body_dict["args"] = [search_params] + params_arg: Optional[Dict[str, object]] = ( + search_params if search_params else None + ) + if mode is not None and params_arg is None: + params_arg = {} + args = self._build_args_with_optional_options([], [params_arg], mode) + body_dict = {"args": args} # Add credentials if available creds = self._get_credentials_dict() @@ -562,7 +626,8 @@ def fetch_market( event_id: Optional[str] = None, slug: Optional[str] = None, query: Optional[str] = None, - **kwargs + mode: Optional[Literal["normalized", "raw"]] = None, + **kwargs, ) -> UnifiedMarket: """ Fetch a single market by lookup parameters. @@ -587,7 +652,7 @@ def fetch_market( >>> market = exchange.fetch_market(slug='will-trump-win') """ try: - search_params = {} + search_params: Dict[str, object] = {} if market_id: search_params["marketId"] = market_id if outcome_id: @@ -608,7 +673,13 @@ def fetch_market( camel_key = key_map.get(key, key) search_params[camel_key] = value - body_dict = {"args": [search_params] if search_params else []} + params_arg: Optional[Dict[str, object]] = ( + search_params if search_params else None + ) + if mode is not None and params_arg is None: + params_arg = {} + args = self._build_args_with_optional_options([], [params_arg], mode) + body_dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -639,7 +710,8 @@ def fetch_event( event_id: Optional[str] = None, slug: Optional[str] = None, query: Optional[str] = None, - **kwargs + mode: Optional[Literal["normalized", "raw"]] = None, + **kwargs, ) -> UnifiedEvent: """ Fetch a single event by lookup parameters. @@ -662,7 +734,7 @@ def fetch_event( >>> event = exchange.fetch_event(slug='us-election') """ try: - search_params = {} + search_params: Dict[str, object] = {} if event_id: search_params["eventId"] = event_id if slug: @@ -678,7 +750,13 @@ def fetch_event( camel_key = key_map.get(key, key) search_params[camel_key] = value - body_dict = {"args": [search_params] if search_params else []} + params_arg: Optional[Dict[str, object]] = ( + search_params if search_params else None + ) + if mode is not None and params_arg is None: + params_arg = {} + args = self._build_args_with_optional_options([], [params_arg], mode) + body_dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -927,7 +1005,8 @@ def fetch_ohlcv( limit: Optional[int] = None, start: Optional[datetime] = None, end: Optional[datetime] = None, - **kwargs + mode: Optional[Literal["normalized", "raw"]] = None, + **kwargs, ) -> List[PriceCandle]: """ Get historical price candles. @@ -957,7 +1036,7 @@ def fetch_ohlcv( ... ) """ try: - params_dict = {} + params_dict: Dict[str, object] = {} if resolution: params_dict["resolution"] = resolution if start: @@ -972,7 +1051,12 @@ def fetch_ohlcv( if key not in params_dict: params_dict[key] = value - request_body_dict = {"args": [outcome_id, params_dict]} + args = self._build_args_with_optional_options( + [outcome_id], + [params_dict], + mode, + ) + request_body_dict = {"args": args} request_body = internal_models.FetchOHLCVRequest.from_dict(request_body_dict) response = self._api.fetch_ohlcv( @@ -985,7 +1069,11 @@ def fetch_ohlcv( except ApiException as e: raise Exception(f"Failed to fetch OHLCV: {self._extract_api_error(e)}") from None - def fetch_order_book(self, outcome_id: str) -> OrderBook: + def fetch_order_book( + self, + outcome_id: str, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> OrderBook: """ Get current order book for an outcome. @@ -1001,7 +1089,8 @@ def fetch_order_book(self, outcome_id: str) -> OrderBook: >>> print(f"Best ask: {order_book.asks[0].price}") """ try: - body_dict = {"args": [outcome_id]} + args = self._build_args_with_optional_options([outcome_id], [], mode) + body_dict = {"args": args} request_body = internal_models.FetchOrderBookRequest.from_dict(body_dict) response = self._api.fetch_order_book( @@ -1019,7 +1108,8 @@ def fetch_trades( outcome_id: str, limit: Optional[int] = None, since: Optional[int] = None, - **kwargs + mode: Optional[Literal["normalized", "raw"]] = None, + **kwargs, ) -> List[Trade]: """ Get trade history for an outcome. @@ -1039,7 +1129,7 @@ def fetch_trades( >>> trades = exchange.fetch_trades(outcome_id, limit=50) """ try: - params_dict = {} + params_dict: Dict[str, object] = {} if limit: params_dict["limit"] = limit if since: @@ -1050,7 +1140,12 @@ def fetch_trades( if key not in params_dict: params_dict[key] = value - request_body_dict = {"args": [outcome_id, params_dict]} + args = self._build_args_with_optional_options( + [outcome_id], + [params_dict], + mode, + ) + request_body_dict = {"args": args} request_body = internal_models.FetchTradesRequest.from_dict(request_body_dict) response = self._api.fetch_trades( @@ -1065,7 +1160,12 @@ def fetch_trades( # WebSocket Streaming Methods - def watch_order_book(self, outcome_id: str, limit: Optional[int] = None) -> OrderBook: + def watch_order_book( + self, + outcome_id: str, + limit: Optional[int] = None, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> OrderBook: """ Watch real-time order book updates via WebSocket. @@ -1087,9 +1187,11 @@ def watch_order_book(self, outcome_id: str, limit: Optional[int] = None) -> Orde ... print(f"Best ask: {order_book.asks[0].price}") """ try: - args = [outcome_id] - if limit is not None: - args.append(limit) + args = self._build_args_with_optional_options( + [outcome_id], + [limit], + mode, + ) body_dict = {"args": args} @@ -1114,7 +1216,8 @@ def watch_trades( self, outcome_id: str, since: Optional[int] = None, - limit: Optional[int] = None + limit: Optional[int] = None, + mode: Optional[Literal["normalized", "raw"]] = None, ) -> List[Trade]: """ Watch real-time trade updates via WebSocket. @@ -1138,11 +1241,11 @@ def watch_trades( ... print(f"Trade: {trade.price} @ {trade.amount}") """ try: - args = [outcome_id] - if since is not None: - args.append(since) - if limit is not None: - args.append(limit) + args = self._build_args_with_optional_options( + [outcome_id], + [since, limit], + mode, + ) body_dict = {"args": args} @@ -1384,7 +1487,11 @@ def cancel_order(self, order_id: str) -> Order: except ApiException as e: raise Exception(f"Failed to cancel order: {self._extract_api_error(e)}") from None - def fetch_order(self, order_id: str) -> Order: + def fetch_order( + self, + order_id: str, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> Order: """ Get details of a specific order. @@ -1395,7 +1502,8 @@ def fetch_order(self, order_id: str) -> Order: Order details """ try: - body_dict = {"args": [order_id]} + args = self._build_args_with_optional_options([order_id], [], mode) + body_dict = {"args": args} # Add credentials if available creds = self._get_credentials_dict() @@ -1414,7 +1522,11 @@ def fetch_order(self, order_id: str) -> Order: except ApiException as e: raise Exception(f"Failed to fetch order: {self._extract_api_error(e)}") from None - def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]: + def fetch_open_orders( + self, + market_id: Optional[str] = None, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> List[Order]: """ Get all open orders, optionally filtered by market. @@ -1425,9 +1537,7 @@ def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]: List of open orders """ try: - args = [] - if market_id: - args.append(market_id) + args = self._build_args_with_optional_options([], [market_id], mode) body_dict = {"args": args} @@ -1452,9 +1562,10 @@ def fetch_my_trades( self, outcome_id: Optional[str] = None, market_id: Optional[str] = None, - since: Optional[Any] = None, + since: Optional[Union[datetime, int, float, str]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, + mode: Optional[Literal["normalized", "raw"]] = None, ) -> List[UserTrade]: """ Get trades made by the authenticated user. @@ -1472,26 +1583,30 @@ def fetch_my_trades( Example: trades = exchange.fetch_my_trades(limit=50) """ - params: Dict[str, Any] = {} + params: Dict[str, object] = {} if outcome_id is not None: params["outcomeId"] = outcome_id if market_id is not None: params["marketId"] = market_id if since is not None: - params["since"] = since.isoformat() if hasattr(since, "isoformat") else since + if isinstance(since, datetime): + params["since"] = since.isoformat() + else: + params["since"] = since if limit is not None: params["limit"] = limit if cursor is not None: params["cursor"] = cursor - data = self._call_method("fetchMyTrades", params or None) + data = self._call_method("fetchMyTrades", params or None, mode) return [_convert_user_trade(t) for t in (data or [])] def fetch_closed_orders( self, market_id: Optional[str] = None, - since: Optional[Any] = None, - until: Optional[Any] = None, + since: Optional[Union[datetime, int, float, str]] = None, + until: Optional[Union[datetime, int, float, str]] = None, limit: Optional[int] = None, + mode: Optional[Literal["normalized", "raw"]] = None, ) -> List[Order]: """ Get filled and cancelled orders. @@ -1508,24 +1623,31 @@ def fetch_closed_orders( Example: orders = exchange.fetch_closed_orders(market_id="some-market") """ - params: Dict[str, Any] = {} + params: Dict[str, object] = {} if market_id is not None: params["marketId"] = market_id if since is not None: - params["since"] = since.isoformat() if hasattr(since, "isoformat") else since + if isinstance(since, datetime): + params["since"] = since.isoformat() + else: + params["since"] = since if until is not None: - params["until"] = until.isoformat() if hasattr(until, "isoformat") else until + if isinstance(until, datetime): + params["until"] = until.isoformat() + else: + params["until"] = until if limit is not None: params["limit"] = limit - data = self._call_method("fetchClosedOrders", params or None) + data = self._call_method("fetchClosedOrders", params or None, mode) return [_convert_order(o) for o in (data or [])] def fetch_all_orders( self, market_id: Optional[str] = None, - since: Optional[Any] = None, - until: Optional[Any] = None, + since: Optional[Union[datetime, int, float, str]] = None, + until: Optional[Union[datetime, int, float, str]] = None, limit: Optional[int] = None, + mode: Optional[Literal["normalized", "raw"]] = None, ) -> List[Order]: """ Get all orders (open + closed), sorted newest-first. @@ -1542,22 +1664,29 @@ def fetch_all_orders( Example: orders = exchange.fetch_all_orders() """ - params: Dict[str, Any] = {} + params: Dict[str, object] = {} if market_id is not None: params["marketId"] = market_id if since is not None: - params["since"] = since.isoformat() if hasattr(since, "isoformat") else since + if isinstance(since, datetime): + params["since"] = since.isoformat() + else: + params["since"] = since if until is not None: - params["until"] = until.isoformat() if hasattr(until, "isoformat") else until + if isinstance(until, datetime): + params["until"] = until.isoformat() + else: + params["until"] = until if limit is not None: params["limit"] = limit - data = self._call_method("fetchAllOrders", params or None) + data = self._call_method("fetchAllOrders", params or None, mode) return [_convert_order(o) for o in (data or [])] def fetch_markets_paginated( self, limit: Optional[int] = None, cursor: Optional[str] = None, + mode: Optional[Literal["normalized", "raw"]] = None, ) -> PaginatedMarketsResult: """ Fetch markets with cursor-based pagination. @@ -1577,12 +1706,12 @@ def fetch_markets_paginated( while page.next_cursor: page = exchange.fetch_markets_paginated(limit=100, cursor=page.next_cursor) """ - params: Dict[str, Any] = {} + params: Dict[str, object] = {} if limit is not None: params["limit"] = limit if cursor is not None: params["cursor"] = cursor - raw = self._call_method("fetchMarketsPaginated", params or None) + raw = self._call_method("fetchMarketsPaginated", params or None, mode) return PaginatedMarketsResult( data=[_convert_market(m) for m in raw.get("data", [])], total=raw.get("total", 0), @@ -1591,7 +1720,10 @@ def fetch_markets_paginated( # Account Methods - def fetch_positions(self) -> List[Position]: + def fetch_positions( + self, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> List[Position]: """ Get current positions across all markets. @@ -1599,7 +1731,8 @@ def fetch_positions(self) -> List[Position]: List of positions """ try: - body_dict = {"args": []} + args = self._build_args_with_optional_options([], [], mode) + body_dict = {"args": args} # Add credentials if available creds = self._get_credentials_dict() @@ -1618,7 +1751,10 @@ def fetch_positions(self) -> List[Position]: except ApiException as e: raise Exception(f"Failed to fetch positions: {self._extract_api_error(e)}") from None - def fetch_balance(self) -> List[Balance]: + def fetch_balance( + self, + mode: Optional[Literal["normalized", "raw"]] = None, + ) -> List[Balance]: """ Get account balance. @@ -1626,7 +1762,8 @@ def fetch_balance(self) -> List[Balance]: List of balances (by currency) """ try: - body_dict = {"args": []} + args = self._build_args_with_optional_options([], [], mode) + body_dict = {"args": args} # Add credentials if available creds = self._get_credentials_dict() @@ -1717,4 +1854,3 @@ def get_execution_price_detailed( return _convert_execution_result(data) except Exception as e: raise Exception(f"Failed to get execution price: {self._extract_api_error(e)}") from None - diff --git a/sdks/python/pmxt/models.py b/sdks/python/pmxt/models.py index ffbd68c..1262be5 100644 --- a/sdks/python/pmxt/models.py +++ b/sdks/python/pmxt/models.py @@ -16,6 +16,15 @@ OrderSide = Literal["buy", "sell"] OrderType = Literal["market", "limit"] OutcomeType = Literal["yes", "no", "up", "down"] +PriceMode = Literal["normalized", "raw"] + + +@dataclass +class RequestOptions: + """Optional request options for exchange methods.""" + + mode: Optional[PriceMode] = None + """Price mode. Use 'raw' to skip normalization when supported.""" @dataclass @@ -447,4 +456,3 @@ class EventFilterCriteria(TypedDict, total=False): total_volume: MinMax EventFilterFunction = Callable[[UnifiedEvent], bool] - From e316eb2c58cbe9f3621836bb65bf6d662807ddbf Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 12:03:49 -0500 Subject: [PATCH 08/13] test(python-sdk): add raw mode args coverage and method signature checks --- sdks/python/tests/test_raw_mode_args.py | 98 +++++++++++++++++++++++++ sdks/python/tests/test_status.py | 16 ++++ 2 files changed, 114 insertions(+) create mode 100644 sdks/python/tests/test_raw_mode_args.py diff --git a/sdks/python/tests/test_raw_mode_args.py b/sdks/python/tests/test_raw_mode_args.py new file mode 100644 index 0000000..c59d7f1 --- /dev/null +++ b/sdks/python/tests/test_raw_mode_args.py @@ -0,0 +1,98 @@ +import json +import sys +import types +import unittest + +if "pmxt_internal" not in sys.modules: + fake_pmxt_internal = types.ModuleType("pmxt_internal") + + class _FakeApiClient: + def __init__(self, configuration=None): + self.configuration = configuration + self.default_headers = {} + + def call_api(self, **kwargs): + return _FakeResponse({"success": True, "data": {}}) + + class _FakeConfiguration: + def __init__(self, host="http://localhost:3847"): + self.host = host + + class _FakeDefaultApi: + def __init__(self, api_client=None): + self.api_client = api_client + + class _FakeApiException(Exception): + pass + + fake_pmxt_internal.ApiClient = _FakeApiClient + fake_pmxt_internal.Configuration = _FakeConfiguration + fake_pmxt_internal.models = types.SimpleNamespace() + sys.modules["pmxt_internal"] = fake_pmxt_internal + + fake_api_pkg = types.ModuleType("pmxt_internal.api") + sys.modules["pmxt_internal.api"] = fake_api_pkg + + fake_default_api = types.ModuleType("pmxt_internal.api.default_api") + fake_default_api.DefaultApi = _FakeDefaultApi + sys.modules["pmxt_internal.api.default_api"] = fake_default_api + + fake_exceptions = types.ModuleType("pmxt_internal.exceptions") + fake_exceptions.ApiException = _FakeApiException + sys.modules["pmxt_internal.exceptions"] = fake_exceptions + +from pmxt.client import Exchange + + +class _FakeResponse: + def __init__(self, payload): + self.data = json.dumps(payload).encode("utf-8") + + def read(self): + return None + + +class TestRawModeArgs(unittest.TestCase): + def setUp(self): + self.exchange = Exchange("polymarket", auto_start_server=False) + + def test_build_request_options_rejects_invalid_mode(self): + with self.assertRaises(ValueError): + self.exchange._build_request_options("invalid") + + def test_build_args_preserves_optional_positions_in_raw_mode(self): + args = self.exchange._build_args_with_optional_options( + ["outcome-id"], + [None, 50], + "raw", + ) + self.assertEqual(args, ["outcome-id", None, 50, {"mode": "raw"}]) + + def test_build_args_trims_trailing_none_without_mode(self): + args = self.exchange._build_args_with_optional_options( + ["outcome-id"], + [10, None], + None, + ) + self.assertEqual(args, ["outcome-id", 10]) + + def test_call_method_sends_empty_params_when_only_mode_is_set(self): + captured = {} + + def fake_call_api(**kwargs): + captured.update(kwargs) + return _FakeResponse({"success": True, "data": {"ok": True}}) + + self.exchange._api_client.call_api = fake_call_api + result = self.exchange._call_method( + "fetchMarketsPaginated", + params=None, + mode="raw", + ) + + self.assertEqual(result, {"ok": True}) + self.assertEqual(captured["body"]["args"], [{}, {"mode": "raw"}]) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdks/python/tests/test_status.py b/sdks/python/tests/test_status.py index affb751..81aa938 100644 --- a/sdks/python/tests/test_status.py +++ b/sdks/python/tests/test_status.py @@ -1,5 +1,6 @@ import unittest +import inspect from pmxt import Polymarket, Kalshi class TestStatusParams(unittest.TestCase): @@ -17,5 +18,20 @@ def test_fetch_events_status_signature(self): params = {"query": "Politics", "status": "active"} self.assertEqual(params["status"], "active") + def test_raw_mode_signature(self): + """Verify raw mode is exposed on key market-data methods.""" + methods = [ + Polymarket.fetch_markets, + Polymarket.fetch_events, + Polymarket.fetch_ohlcv, + Polymarket.fetch_order_book, + Polymarket.fetch_trades, + Kalshi.watch_order_book, + Kalshi.watch_trades, + ] + for method in methods: + signature = inspect.signature(method) + self.assertIn("mode", signature.parameters) + if __name__ == '__main__': unittest.main() From 4f66c4adf5b5602ef0272dd356c59a0492a1361e Mon Sep 17 00:00:00 2001 From: atrtde Date: Wed, 25 Feb 2026 12:03:59 -0500 Subject: [PATCH 09/13] docs(python-sdk): document raw mode usage --- sdks/python/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sdks/python/README.md b/sdks/python/README.md index f48ab9f..ff611f9 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -158,6 +158,20 @@ print(f"Available: ${balances[0].available}") - `get_execution_price(order_book, side, amount)` - Get execution price - `get_execution_price_detailed(order_book, side, amount)` - Get detailed execution info +### Raw Price Mode + +Use `mode="raw"` to get source exchange prices without normalization. + +```python +# Kalshi raw cents +book = kalshi.fetch_order_book("KXBTC-100K-YES", mode="raw") +print(book.bids[0].price) # e.g. 55 instead of 0.55 + +# Polymarket normalized default +book = poly.fetch_order_book(outcome_id) +print(book.bids[0].price) # e.g. 0.55 +``` + ### Trading Methods (require authentication) - `create_order(params)` - Place a new order From 5e7ad1a65e688214b235e787802b46ddc8b7b9fc Mon Sep 17 00:00:00 2001 From: "Samuel EF. Tinnerholm" Date: Sun, 1 Mar 2026 11:49:25 +0200 Subject: [PATCH 10/13] revert: remove all raw mode changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts all raw-price-mode commits from the PR. The mode option, RequestOptions threading, and all raw/normalized branching have been removed from core, TypeScript SDK, Python SDK, tests, and docs. Reverted commits (newest → oldest): - 4f66c4a docs(python-sdk): document raw mode usage - e316eb2 test(python-sdk): add raw mode args coverage - 495032e feat(python-sdk): add raw price mode support - 102529d docs: document raw mode in README/api docs - 1d8b834 test(sdk-typescript): add unit tests for args helper - 19ccfc4 feat(sdk-typescript): support raw mode in optional request args - bc5369a test(core): cover raw-mode price helpers - 5d492a4 refactor(core): extract reusable price conversion helpers - 5ab0ca4 feat(core): add raw mode request option to exchange API --- changelog.md | 6 - core/src/BaseExchange.ts | 108 ++------ core/src/exchanges/baozi/fetchEvents.ts | 5 +- core/src/exchanges/baozi/fetchMarkets.ts | 18 +- core/src/exchanges/baozi/fetchOrderBook.ts | 4 +- core/src/exchanges/baozi/index.ts | 31 +-- core/src/exchanges/baozi/price.test.ts | 42 --- core/src/exchanges/baozi/price.ts | 30 --- core/src/exchanges/baozi/utils.ts | 32 +-- core/src/exchanges/baozi/websocket.ts | 11 +- core/src/exchanges/kalshi/fetchEvents.ts | 21 +- core/src/exchanges/kalshi/fetchMarkets.ts | 23 +- core/src/exchanges/kalshi/fetchOHLCV.ts | 13 +- core/src/exchanges/kalshi/fetchOrderBook.ts | 16 +- core/src/exchanges/kalshi/fetchTrades.ts | 7 +- core/src/exchanges/kalshi/index.ts | 102 +++----- core/src/exchanges/kalshi/kalshi.test.ts | 50 ---- core/src/exchanges/kalshi/price.test.ts | 42 --- core/src/exchanges/kalshi/price.ts | 41 --- core/src/exchanges/kalshi/utils.ts | 36 +-- core/src/exchanges/kalshi/websocket.ts | 37 +-- core/src/exchanges/myriad/index.ts | 37 +-- core/src/exchanges/myriad/price.test.ts | 25 -- core/src/exchanges/myriad/price.ts | 15 -- core/src/exchanges/myriad/websocket.ts | 11 +- core/src/server/openapi.yaml | 147 +++-------- docs/MIGRATE_FROM_DOMEAPI.md | 2 +- readme.md | 2 - sdks/python/README.md | 14 - sdks/python/pmxt/__init__.py | 2 - sdks/python/pmxt/client.py | 274 +++++--------------- sdks/python/pmxt/models.py | 10 +- sdks/python/tests/test_raw_mode_args.py | 98 ------- sdks/python/tests/test_status.py | 16 -- sdks/typescript/API_REFERENCE.md | 11 - sdks/typescript/README.md | 2 +- sdks/typescript/pmxt/args.ts | 29 --- sdks/typescript/pmxt/client.ts | 93 ++++--- sdks/typescript/pmxt/models.ts | 9 +- sdks/typescript/tests/client-args.test.ts | 40 --- 40 files changed, 308 insertions(+), 1204 deletions(-) delete mode 100644 core/src/exchanges/baozi/price.test.ts delete mode 100644 core/src/exchanges/baozi/price.ts delete mode 100644 core/src/exchanges/kalshi/price.test.ts delete mode 100644 core/src/exchanges/kalshi/price.ts delete mode 100644 core/src/exchanges/myriad/price.test.ts delete mode 100644 core/src/exchanges/myriad/price.ts delete mode 100644 sdks/python/tests/test_raw_mode_args.py delete mode 100644 sdks/typescript/pmxt/args.ts delete mode 100644 sdks/typescript/tests/client-args.test.ts diff --git a/changelog.md b/changelog.md index 611cabe..bc7e67a 100644 --- a/changelog.md +++ b/changelog.md @@ -2,12 +2,6 @@ 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 diff --git a/core/src/BaseExchange.ts b/core/src/BaseExchange.ts index 6772fcb..f382d3a 100644 --- a/core/src/BaseExchange.ts +++ b/core/src/BaseExchange.ts @@ -20,10 +20,6 @@ export interface ApiDescriptor { endpoints: Record; } -export interface RequestOptions { - mode?: 'raw'; -} - export interface ImplicitApiMethodInfo { name: string; method: string; @@ -226,7 +222,7 @@ export abstract class PredictionMarketExchange { // Snapshot state for cursor-based pagination private _snapshotTTL: number; - private _snapshot?: { markets: UnifiedMarket[]; takenAt: number; id: string; mode?: RequestOptions['mode'] }; + private _snapshot?: { markets: UnifiedMarket[]; takenAt: number; id: string }; get rateLimit(): number { return this._rateLimit; @@ -409,11 +405,8 @@ export abstract class PredictionMarketExchange { * @example-python Get market by slug * markets = exchange.fetch_markets(slug='will-trump-win') */ - async fetchMarkets( - params?: MarketFetchParams, - options?: RequestOptions, - ): Promise { - return this.fetchMarketsImpl(params, options); + async fetchMarkets(params?: MarketFetchParams): Promise { + return this.fetchMarketsImpl(params); } /** @@ -431,13 +424,9 @@ 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 }, - options?: RequestOptions, - ): Promise { + async fetchMarketsPaginated(params?: { limit?: number; cursor?: string }): Promise { const limit = params?.limit; const cursor = params?.cursor; - const mode = options?.mode; if (cursor) { // Cursor encodes: snapshotId:offset @@ -448,7 +437,6 @@ 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'); @@ -466,15 +454,13 @@ 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(undefined, options); + const markets = await this.fetchMarketsImpl(); this._snapshot = { markets, takenAt: Date.now(), id: Math.random().toString(36).slice(2), - mode, }; } @@ -511,11 +497,8 @@ export abstract class PredictionMarketExchange { * fed_event = events[0] * print(fed_event.title, len(fed_event.markets), 'markets') */ - async fetchEvents( - params?: EventFetchParams, - options?: RequestOptions, - ): Promise { - return this.fetchEventsImpl(params ?? {}, options); + async fetchEvents(params?: EventFetchParams): Promise { + return this.fetchEventsImpl(params ?? {}); } /** @@ -535,10 +518,7 @@ export abstract class PredictionMarketExchange { * @example-python Fetch by market ID * market = exchange.fetch_market(market_id='663583') */ - async fetchMarket( - params?: MarketFetchParams, - options?: RequestOptions, - ): Promise { + async fetchMarket(params?: MarketFetchParams): Promise { // 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]) { @@ -549,7 +529,7 @@ export abstract class PredictionMarketExchange { } } - const markets = await this.fetchMarkets(params, options); + const markets = await this.fetchMarkets(params); if (markets.length === 0) { const identifier = params?.marketId || params?.outcomeId || params?.slug || params?.eventId || params?.query || 'unknown'; throw new MarketNotFound(identifier, this.name); @@ -571,11 +551,8 @@ export abstract class PredictionMarketExchange { * @example-python Fetch by event ID * event = exchange.fetch_event(event_id='TRUMP25DEC') */ - async fetchEvent( - params?: EventFetchParams, - options?: RequestOptions, - ): Promise { - const events = await this.fetchEvents(params, options); + async fetchEvent(params?: EventFetchParams): Promise { + const events = await this.fetchEvents(params); if (events.length === 0) { const identifier = params?.eventId || params?.slug || params?.query || 'unknown'; throw new EventNotFound(identifier, this.name); @@ -592,10 +569,7 @@ 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, - options?: RequestOptions, - ): Promise { + protected async fetchMarketsImpl(params?: MarketFetchParams): Promise { throw new Error("Method fetchMarketsImpl not implemented."); } @@ -603,10 +577,7 @@ export abstract class PredictionMarketExchange { * @internal * Implementation for searching events by keyword. */ - protected async fetchEventsImpl( - params: EventFetchParams, - options?: RequestOptions, - ): Promise { + protected async fetchEventsImpl(params: EventFetchParams): Promise { throw new Error("Method fetchEventsImpl not implemented."); } @@ -636,11 +607,7 @@ 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, - options?: RequestOptions, - ): Promise { + async fetchOHLCV(id: string, params: OHLCVParams): Promise { throw new Error("Method fetchOHLCV not implemented."); } @@ -663,7 +630,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, options?: RequestOptions): Promise { + async fetchOrderBook(id: string): Promise { throw new Error("Method fetchOrderBook not implemented."); } @@ -687,11 +654,7 @@ 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, - options?: RequestOptions, - ): Promise { + async fetchTrades(id: string, params: TradesParams | HistoryFilterParams): Promise { // Deprecation warning for resolution parameter if ('resolution' in params && params.resolution !== undefined) { console.warn( @@ -788,7 +751,7 @@ export abstract class PredictionMarketExchange { * order = exchange.fetch_order('order-456') * print(f"Filled: {order.filled}/{order.amount}") */ - async fetchOrder(orderId: string, options?: RequestOptions): Promise { + async fetchOrder(orderId: string): Promise { throw new Error("Method fetchOrder not implemented."); } @@ -815,31 +778,19 @@ export abstract class PredictionMarketExchange { * @example-python Fetch orders for a specific market * orders = exchange.fetch_open_orders('FED-25JAN') */ - async fetchOpenOrders( - marketId?: string, - options?: RequestOptions, - ): Promise { + async fetchOpenOrders(marketId?: string): Promise { throw new Error("Method fetchOpenOrders not implemented."); } - async fetchMyTrades( - params?: MyTradesParams, - options?: RequestOptions, - ): Promise { + async fetchMyTrades(params?: MyTradesParams): Promise { throw new Error("Method fetchMyTrades not implemented."); } - async fetchClosedOrders( - params?: OrderHistoryParams, - options?: RequestOptions, - ): Promise { + async fetchClosedOrders(params?: OrderHistoryParams): Promise { throw new Error("Method fetchClosedOrders not implemented."); } - async fetchAllOrders( - params?: OrderHistoryParams, - options?: RequestOptions, - ): Promise { + async fetchAllOrders(params?: OrderHistoryParams): Promise { throw new Error("Method fetchAllOrders not implemented."); } @@ -861,7 +812,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(options?: RequestOptions): Promise { + async fetchPositions(): Promise { throw new Error("Method fetchPositions not implemented."); } @@ -878,7 +829,7 @@ export abstract class PredictionMarketExchange { * balances = exchange.fetch_balance() * print(f"Available: ${balances[0].available}") */ - async fetchBalance(options?: RequestOptions): Promise { + async fetchBalance(): Promise { throw new Error("Method fetchBalance not implemented."); } @@ -1255,11 +1206,7 @@ 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, - options?: RequestOptions, - ): Promise { + async watchOrderBook(id: string, limit?: number): Promise { throw new Error(`watchOrderBook() is not supported by ${this.name}`); } @@ -1286,12 +1233,7 @@ export abstract class PredictionMarketExchange { * for trade in trades: * print(f"{trade.side} {trade.amount} @ {trade.price}") */ - async watchTrades( - id: string, - since?: number, - limit?: number, - options?: RequestOptions, - ): Promise { + async watchTrades(id: string, since?: number, limit?: number): Promise { throw new Error(`watchTrades() is not supported by ${this.name}`); } diff --git a/core/src/exchanges/baozi/fetchEvents.ts b/core/src/exchanges/baozi/fetchEvents.ts index d9457d8..b353cc1 100644 --- a/core/src/exchanges/baozi/fetchEvents.ts +++ b/core/src/exchanges/baozi/fetchEvents.ts @@ -1,5 +1,5 @@ import { Connection } from '@solana/web3.js'; -import { EventFetchParams, RequestOptions } from '../../BaseExchange'; +import { EventFetchParams } from '../../BaseExchange'; import { UnifiedEvent } from '../../types'; import { fetchMarkets } from './fetchMarkets'; import { baoziErrorMapper } from './errors'; @@ -11,7 +11,6 @@ import { baoziErrorMapper } from './errors'; export async function fetchEvents( connection: Connection, params: EventFetchParams, - options?: RequestOptions, ): Promise { try { const markets = await fetchMarkets(connection, { @@ -20,7 +19,7 @@ export async function fetchEvents( offset: params.offset, status: params.status, searchIn: params.searchIn, - }, options); + }); return markets.map(m => { const unifiedEvent = { diff --git a/core/src/exchanges/baozi/fetchMarkets.ts b/core/src/exchanges/baozi/fetchMarkets.ts index 681d1b2..7a9284b 100644 --- a/core/src/exchanges/baozi/fetchMarkets.ts +++ b/core/src/exchanges/baozi/fetchMarkets.ts @@ -1,5 +1,5 @@ import { Connection } from '@solana/web3.js'; -import { MarketFetchParams, RequestOptions } from '../../BaseExchange'; +import { MarketFetchParams } from '../../BaseExchange'; import { UnifiedMarket } from '../../types'; import { PROGRAM_ID, @@ -20,11 +20,10 @@ const marketsCache = new Cache(30_000); // 30s TTL export async function fetchMarkets( connection: Connection, params?: MarketFetchParams, - options?: RequestOptions, ): Promise { try { // Use cache for default (no-filter) fetches - if (!params?.query && !params?.slug && options?.mode !== 'raw') { + if (!params?.query && !params?.slug) { const cached = marketsCache.get(); if (cached) { return applyFilters(cached, params); @@ -47,7 +46,7 @@ export async function fetchMarkets( for (const account of booleanAccounts) { try { const parsed = parseMarket(account.account.data); - markets.push(mapBooleanToUnified(parsed, account.pubkey.toString(), options)); + markets.push(mapBooleanToUnified(parsed, account.pubkey.toString())); } catch { // Skip malformed accounts } @@ -57,16 +56,14 @@ export async function fetchMarkets( for (const account of raceAccounts) { try { const parsed = parseRaceMarket(account.account.data); - markets.push(mapRaceToUnified(parsed, account.pubkey.toString(), options)); + markets.push(mapRaceToUnified(parsed, account.pubkey.toString())); } catch { // Skip malformed accounts } } // Cache results - if (options?.mode !== 'raw') { - marketsCache.set(markets); - } + marketsCache.set(markets); return applyFilters(markets, params); } catch (error: any) { @@ -77,7 +74,6 @@ export async function fetchMarkets( export async function fetchSingleMarket( connection: Connection, pubkey: string, - options?: RequestOptions, ): Promise { try { const { PublicKey } = await import('@solana/web3.js'); @@ -91,13 +87,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, options); + return mapBooleanToUnified(parsed, pubkey); } // 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, options); + return mapRaceToUnified(parsed, pubkey); } return null; diff --git a/core/src/exchanges/baozi/fetchOrderBook.ts b/core/src/exchanges/baozi/fetchOrderBook.ts index e3cdd4d..a28a7d9 100644 --- a/core/src/exchanges/baozi/fetchOrderBook.ts +++ b/core/src/exchanges/baozi/fetchOrderBook.ts @@ -2,7 +2,6 @@ import { Connection } from '@solana/web3.js'; import { OrderBook } from '../../types'; import { fetchSingleMarket } from './fetchMarkets'; import { baoziErrorMapper } from './errors'; -import { RequestOptions } from '../../BaseExchange'; /** * Pari-mutuel markets don't have a real order book. @@ -16,11 +15,10 @@ import { RequestOptions } from '../../BaseExchange'; export async function fetchOrderBook( connection: Connection, outcomeId: string, - options?: RequestOptions, ): Promise { try { const marketPubkey = outcomeId.replace(/-YES$|-NO$|-\d+$/, ''); - const market = await fetchSingleMarket(connection, marketPubkey, options); + const market = await fetchSingleMarket(connection, marketPubkey); if (!market) { throw new Error(`Market not found: ${marketPubkey}`); diff --git a/core/src/exchanges/baozi/index.ts b/core/src/exchanges/baozi/index.ts index e8963b1..79a2f82 100644 --- a/core/src/exchanges/baozi/index.ts +++ b/core/src/exchanges/baozi/index.ts @@ -13,7 +13,6 @@ import { HistoryFilterParams, TradesParams, ExchangeCredentials, - RequestOptions, } from '../../BaseExchange'; import { UnifiedMarket, @@ -118,26 +117,20 @@ export class BaoziExchange extends PredictionMarketExchange { // Market Data // ----------------------------------------------------------------------- - protected async fetchMarketsImpl( - params?: MarketFetchParams, - options?: RequestOptions, - ): Promise { - return fetchMarkets(this.connection, params, options); + protected async fetchMarketsImpl(params?: MarketFetchParams): Promise { + return fetchMarkets(this.connection, params); } - protected async fetchEventsImpl( - params: EventFetchParams, - options?: RequestOptions, - ): Promise { - return fetchEvents(this.connection, params, options); + protected async fetchEventsImpl(params: EventFetchParams): Promise { + return fetchEvents(this.connection, params); } async fetchOHLCV(): Promise { return fetchOHLCV(); } - async fetchOrderBook(id: string, options?: RequestOptions): Promise { - return fetchOrderBook(this.connection, id, options); + async fetchOrderBook(id: string): Promise { + return fetchOrderBook(this.connection, id); } async fetchTrades(): Promise { @@ -148,7 +141,7 @@ export class BaoziExchange extends PredictionMarketExchange { // User Data // ----------------------------------------------------------------------- - async fetchBalance(options?: RequestOptions): Promise { + async fetchBalance(): Promise { try { const auth = this.ensureAuth(); const lamports = await this.connection.getBalance(auth.getPublicKey()); @@ -165,7 +158,7 @@ export class BaoziExchange extends PredictionMarketExchange { } } - async fetchPositions(options?: RequestOptions): Promise { + async fetchPositions(): Promise { try { const auth = this.ensureAuth(); const userPubkey = auth.getPublicKey(); @@ -204,7 +197,7 @@ export class BaoziExchange extends PredictionMarketExchange { const marketInfo = await this.connection.getAccountInfo(marketPda); if (marketInfo) { const market = parseMarket(marketInfo.data); - const unified = mapBooleanToUnified(market, marketPda.toString(), options); + const unified = mapBooleanToUnified(market, marketPda.toString()); currentYesPrice = unified.yes?.price ?? 0; currentNoPrice = unified.no?.price ?? 0; marketTitle = market.question; @@ -260,7 +253,7 @@ export class BaoziExchange extends PredictionMarketExchange { const marketInfo = await this.connection.getAccountInfo(racePda); if (marketInfo) { const raceMarket = parseRaceMarket(marketInfo.data); - const unified = mapRaceToUnified(raceMarket, racePdaStr, options); + const unified = mapRaceToUnified(raceMarket, racePdaStr); outcomePrices = unified.outcomes.map(o => o.price); outcomeLabels = unified.outcomes.map(o => o.label); } @@ -504,11 +497,11 @@ export class BaoziExchange extends PredictionMarketExchange { // WebSocket // ----------------------------------------------------------------------- - async watchOrderBook(id: string, limit?: number, options?: RequestOptions): Promise { + async watchOrderBook(id: string): Promise { if (!this.ws) { this.ws = new BaoziWebSocket(); } - return this.ws.watchOrderBook(this.connection, id, options); + return this.ws.watchOrderBook(this.connection, id); } async watchTrades(): Promise { diff --git a/core/src/exchanges/baozi/price.test.ts b/core/src/exchanges/baozi/price.test.ts deleted file mode 100644 index 106c648..0000000 --- a/core/src/exchanges/baozi/price.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - clampBaoziPrice, - normalizeBaoziOutcomes, -} from "./price"; -import { MarketOutcome } from "../../types"; - -describe("baozi price helpers", () => { - it("clamps values in normalized mode", () => { - expect(clampBaoziPrice(1.2)).toBe(1); - expect(clampBaoziPrice(-0.1)).toBe(0); - expect(clampBaoziPrice(0.3)).toBe(0.3); - }); - - it("does not clamp in raw mode", () => { - expect(clampBaoziPrice(1.2, { mode: "raw" })).toBe(1.2); - expect(clampBaoziPrice(-0.1, { mode: "raw" })).toBe(-0.1); - }); - - it("normalizes outcomes in normalized mode", () => { - const outcomes: MarketOutcome[] = [ - { outcomeId: "a", marketId: "m", label: "A", price: 45 }, - { outcomeId: "b", marketId: "m", label: "B", price: 55 }, - ]; - - normalizeBaoziOutcomes(outcomes); - - expect(outcomes[0].price).toBeCloseTo(0.45); - expect(outcomes[1].price).toBeCloseTo(0.55); - }); - - it("skips normalization in raw mode", () => { - const outcomes: MarketOutcome[] = [ - { outcomeId: "a", marketId: "m", label: "A", price: 45 }, - { outcomeId: "b", marketId: "m", label: "B", price: 55 }, - ]; - - normalizeBaoziOutcomes(outcomes, { mode: "raw" }); - - expect(outcomes[0].price).toBe(45); - expect(outcomes[1].price).toBe(55); - }); -}); diff --git a/core/src/exchanges/baozi/price.ts b/core/src/exchanges/baozi/price.ts deleted file mode 100644 index 6885b83..0000000 --- a/core/src/exchanges/baozi/price.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { RequestOptions } from "../../BaseExchange"; -import { MarketOutcome } from "../../types"; - -export function clampBaoziPrice( - value: number, - options?: RequestOptions, -): number { - if (options?.mode === "raw") { - return value; - } - return Math.min(Math.max(value, 0), 1); -} - -export function normalizeBaoziOutcomes( - outcomes: MarketOutcome[], - options?: RequestOptions, -): void { - if (options?.mode === "raw") { - return; - } - - const sum = outcomes.reduce((acc, item) => acc + item.price, 0); - if (sum <= 0) { - return; - } - - for (const outcome of outcomes) { - outcome.price = outcome.price / sum; - } -} diff --git a/core/src/exchanges/baozi/utils.ts b/core/src/exchanges/baozi/utils.ts index f476562..a55acaf 100644 --- a/core/src/exchanges/baozi/utils.ts +++ b/core/src/exchanges/baozi/utils.ts @@ -2,12 +2,7 @@ import { PublicKey } from '@solana/web3.js'; import bs58 from 'bs58'; import { createHash } from 'crypto'; import { UnifiedMarket, MarketOutcome } from '../../types'; -import { RequestOptions } from '../../BaseExchange'; import { addBinaryOutcomes } from '../../utils/market-utils'; -import { - clampBaoziPrice, - normalizeBaoziOutcomes, -} from './price'; // --------------------------------------------------------------------------- // Constants @@ -372,11 +367,7 @@ export function parseRacePosition(data: Buffer | Uint8Array): BaoziRacePosition // Mapping to Unified Types // --------------------------------------------------------------------------- -export function mapBooleanToUnified( - market: BaoziMarket, - pubkey: string, - options?: RequestOptions, -): UnifiedMarket { +export function mapBooleanToUnified(market: BaoziMarket, pubkey: string): UnifiedMarket { const totalPool = market.yesPool + market.noPool; const totalPoolSol = Number(totalPool) / LAMPORTS_PER_SOL; @@ -396,13 +387,13 @@ export function mapBooleanToUnified( outcomeId: `${pubkey}-YES`, marketId: pubkey, label: 'Yes', - price: clampBaoziPrice(yesPrice, options), + price: yesPrice, }, { outcomeId: `${pubkey}-NO`, marketId: pubkey, label: 'No', - price: clampBaoziPrice(noPrice, options), + price: noPrice, }, ]; @@ -424,11 +415,7 @@ export function mapBooleanToUnified( return um; } -export function mapRaceToUnified( - market: BaoziRaceMarket, - pubkey: string, - options?: RequestOptions, -): UnifiedMarket { +export function mapRaceToUnified(market: BaoziRaceMarket, pubkey: string): UnifiedMarket { const totalPoolSol = Number(market.totalPool) / LAMPORTS_PER_SOL; const outcomes: MarketOutcome[] = []; @@ -444,12 +431,17 @@ export function mapRaceToUnified( outcomeId: `${pubkey}-${i}`, marketId: pubkey, label: market.outcomeLabels[i] || `Outcome ${i + 1}`, - price: clampBaoziPrice(price, options), + price: Math.min(Math.max(price, 0), 1), }); } - // Normalize prices to sum to 1 in non-raw mode. - normalizeBaoziOutcomes(outcomes, options); + // Normalize prices to sum to 1 + const priceSum = outcomes.reduce((s, o) => s + o.price, 0); + if (priceSum > 0) { + for (const o of outcomes) { + o.price = o.price / priceSum; + } + } const um: UnifiedMarket = { marketId: pubkey, diff --git a/core/src/exchanges/baozi/websocket.ts b/core/src/exchanges/baozi/websocket.ts index 4114ece..3b15ee3 100644 --- a/core/src/exchanges/baozi/websocket.ts +++ b/core/src/exchanges/baozi/websocket.ts @@ -1,6 +1,5 @@ import { Connection, PublicKey } from '@solana/web3.js'; import { OrderBook } from '../../types'; -import { RequestOptions } from '../../BaseExchange'; import { MARKET_DISCRIMINATOR, RACE_MARKET_DISCRIMINATOR, @@ -24,11 +23,7 @@ export class BaoziWebSocket { private orderBookResolvers = new Map[]>(); private subscriptions = new Map(); - async watchOrderBook( - connection: Connection, - outcomeId: string, - options?: RequestOptions, - ): Promise { + async watchOrderBook(connection: Connection, outcomeId: string): Promise { const marketPubkey = outcomeId.replace(/-YES$|-NO$|-\d+$/, ''); const marketKey = new PublicKey(marketPubkey); @@ -43,10 +38,10 @@ export class BaoziWebSocket { if (Buffer.from(discriminator).equals(MARKET_DISCRIMINATOR)) { const parsed = parseMarket(data); - market = mapBooleanToUnified(parsed, marketPubkey, options); + market = mapBooleanToUnified(parsed, marketPubkey); } else if (Buffer.from(discriminator).equals(RACE_MARKET_DISCRIMINATOR)) { const parsed = parseRaceMarket(data); - market = mapRaceToUnified(parsed, marketPubkey, options); + market = mapRaceToUnified(parsed, marketPubkey); } if (!market) return; diff --git a/core/src/exchanges/kalshi/fetchEvents.ts b/core/src/exchanges/kalshi/fetchEvents.ts index 8578392..ccd9d3a 100644 --- a/core/src/exchanges/kalshi/fetchEvents.ts +++ b/core/src/exchanges/kalshi/fetchEvents.ts @@ -1,4 +1,4 @@ -import { EventFetchParams, RequestOptions } from "../../BaseExchange"; +import { EventFetchParams } from "../../BaseExchange"; import { UnifiedEvent, UnifiedMarket } from "../../types"; import { mapMarketToUnified } from "./utils"; import { kalshiErrorMapper } from "./errors"; @@ -11,7 +11,6 @@ type CallApi = ( async function fetchEventByTicker( eventTicker: string, callApi: CallApi, - options?: RequestOptions, ): Promise { const normalizedTicker = eventTicker.toUpperCase(); const data = await callApi("GetEvent", { @@ -25,7 +24,7 @@ async function fetchEventByTicker( const markets: UnifiedMarket[] = []; if (event.markets) { for (const market of event.markets) { - const unifiedMarket = mapMarketToUnified(event, market, options); + const unifiedMarket = mapMarketToUnified(event, market); if (unifiedMarket) { markets.push(unifiedMarket); } @@ -47,14 +46,11 @@ async function fetchEventByTicker( return [unifiedEvent]; } -function rawEventToUnified( - event: any, - options?: RequestOptions, -): UnifiedEvent { +function rawEventToUnified(event: any): UnifiedEvent { const markets: UnifiedMarket[] = []; if (event.markets) { for (const market of event.markets) { - const unifiedMarket = mapMarketToUnified(event, market, options); + const unifiedMarket = mapMarketToUnified(event, market); if (unifiedMarket) { markets.push(unifiedMarket); } @@ -110,17 +106,16 @@ async function fetchAllWithStatus( export async function fetchEvents( params: EventFetchParams, callApi: CallApi, - options?: RequestOptions, ): Promise { try { // Handle eventId lookup (direct API call) if (params.eventId) { - return await fetchEventByTicker(params.eventId, callApi, options); + return await fetchEventByTicker(params.eventId, callApi); } // Handle slug lookup (slug IS the event ticker on Kalshi) if (params.slug) { - return await fetchEventByTicker(params.slug, callApi, options); + return await fetchEventByTicker(params.slug, callApi); } const status = params?.status || "active"; @@ -157,9 +152,7 @@ export async function fetchEvents( const sort = params?.sort || "volume"; const sorted = sortRawEvents(filtered, sort); - const unifiedEvents: UnifiedEvent[] = sorted.map((event) => - rawEventToUnified(event, options), - ); + const unifiedEvents: UnifiedEvent[] = sorted.map(rawEventToUnified); return unifiedEvents.slice(0, limit); } catch (error: any) { throw kalshiErrorMapper.mapError(error); diff --git a/core/src/exchanges/kalshi/fetchMarkets.ts b/core/src/exchanges/kalshi/fetchMarkets.ts index 86d5713..eb4c434 100644 --- a/core/src/exchanges/kalshi/fetchMarkets.ts +++ b/core/src/exchanges/kalshi/fetchMarkets.ts @@ -1,4 +1,4 @@ -import { MarketFetchParams, RequestOptions } from "../../BaseExchange"; +import { MarketFetchParams } from "../../BaseExchange"; import { UnifiedMarket } from "../../types"; import { mapMarketToUnified } from "./utils"; import { kalshiErrorMapper } from "./errors"; @@ -104,37 +104,36 @@ export function resetCache(): void { export async function fetchMarkets( params: MarketFetchParams | undefined, callApi: CallApi, - options?: RequestOptions, ): Promise { try { // Handle marketId lookup (Kalshi marketId is the ticker) if (params?.marketId) { - return await fetchMarketsBySlug(params.marketId, callApi, options); + return await fetchMarketsBySlug(params.marketId, callApi); } // Handle slug-based lookup (event ticker) if (params?.slug) { - return await fetchMarketsBySlug(params.slug, callApi, options); + return await fetchMarketsBySlug(params.slug, callApi); } // Handle outcomeId lookup (strip -NO suffix, use as ticker) if (params?.outcomeId) { const ticker = params.outcomeId.replace(/-NO$/, ""); - return await fetchMarketsBySlug(ticker, callApi, options); + return await fetchMarketsBySlug(ticker, callApi); } // Handle eventId lookup (event ticker works the same way) if (params?.eventId) { - return await fetchMarketsBySlug(params.eventId, callApi, options); + return await fetchMarketsBySlug(params.eventId, callApi); } // Handle query-based search if (params?.query) { - return await searchMarkets(params.query, params, callApi, options); + return await searchMarkets(params.query, params, callApi); } // Default: fetch markets - return await fetchMarketsDefault(params, callApi, options); + return await fetchMarketsDefault(params, callApi); } catch (error: any) { throw kalshiErrorMapper.mapError(error); } @@ -143,7 +142,6 @@ export async function fetchMarkets( async function fetchMarketsBySlug( eventTicker: string, callApi: CallApi, - options?: RequestOptions, ): Promise { // Kalshi API expects uppercase tickers, but URLs use lowercase const normalizedTicker = eventTicker.toUpperCase(); @@ -176,7 +174,7 @@ async function fetchMarketsBySlug( const markets = event.markets || []; for (const market of markets) { - const unifiedMarket = mapMarketToUnified(event, market, options); + const unifiedMarket = mapMarketToUnified(event, market); if (unifiedMarket) { unifiedMarkets.push(unifiedMarket); } @@ -189,14 +187,12 @@ async function searchMarkets( query: string, params: MarketFetchParams | undefined, callApi: CallApi, - options?: RequestOptions, ): Promise { // We must fetch ALL markets to search them locally since we don't have server-side search const searchLimit = 250000; const markets = await fetchMarketsDefault( { ...params, limit: searchLimit }, callApi, - options, ); const lowerQuery = query.toLowerCase(); const searchIn = params?.searchIn || "title"; // Default to title-only search @@ -219,7 +215,6 @@ async function searchMarkets( async function fetchMarketsDefault( params: MarketFetchParams | undefined, callApi: CallApi, - options?: RequestOptions, ): Promise { const limit = params?.limit || 250000; const offset = params?.offset || 0; @@ -290,7 +285,7 @@ async function fetchMarketsDefault( const markets = event.markets || []; for (const market of markets) { - const unifiedMarket = mapMarketToUnified(event, market, options); + const unifiedMarket = mapMarketToUnified(event, market); if (unifiedMarket) { allMarkets.push(unifiedMarket); } diff --git a/core/src/exchanges/kalshi/fetchOHLCV.ts b/core/src/exchanges/kalshi/fetchOHLCV.ts index d750245..4e8cdd3 100644 --- a/core/src/exchanges/kalshi/fetchOHLCV.ts +++ b/core/src/exchanges/kalshi/fetchOHLCV.ts @@ -1,15 +1,13 @@ -import { OHLCVParams, RequestOptions } from "../../BaseExchange"; +import { OHLCVParams } from "../../BaseExchange"; import { PriceCandle } from "../../types"; import { mapIntervalToKalshi } from "./utils"; import { validateIdFormat } from "../../utils/validation"; import { kalshiErrorMapper } from "./errors"; -import { getKalshiPriceContext, fromKalshiCents } from "./price"; export async function fetchOHLCV( id: string, params: OHLCVParams, callApi: (operationId: string, params?: Record) => Promise, - options?: RequestOptions, ): Promise { validateIdFormat(id, "OHLCV"); @@ -74,7 +72,6 @@ export async function fetchOHLCV( end_ts: endTs, }); const candles = data.candlesticks || []; - const priceContext = getKalshiPriceContext(options); const mappedCandles: PriceCandle[] = candles.map((c: any) => { // Priority: @@ -101,10 +98,10 @@ export async function fetchOHLCV( return { timestamp: c.end_period_ts * 1000, - open: fromKalshiCents(getVal("open"), priceContext), - high: fromKalshiCents(getVal("high"), priceContext), - low: fromKalshiCents(getVal("low"), priceContext), - close: fromKalshiCents(getVal("close"), priceContext), + open: getVal("open") / 100, + high: getVal("high") / 100, + low: getVal("low") / 100, + close: getVal("close") / 100, volume: c.volume || 0, }; }); diff --git a/core/src/exchanges/kalshi/fetchOrderBook.ts b/core/src/exchanges/kalshi/fetchOrderBook.ts index a73e069..2fd5881 100644 --- a/core/src/exchanges/kalshi/fetchOrderBook.ts +++ b/core/src/exchanges/kalshi/fetchOrderBook.ts @@ -3,22 +3,14 @@ import { OrderBook } from "../../types"; import { validateIdFormat } from "../../utils/validation"; import { kalshiErrorMapper } from "./errors"; import { getMarketsUrl } from "./config"; -import { RequestOptions } from "../../BaseExchange"; -import { - getKalshiPriceContext, - fromKalshiCents, - invertKalshiCents, -} from "./price"; export async function fetchOrderBook( baseUrl: string, id: string, - options?: RequestOptions, ): Promise { validateIdFormat(id, "OrderBook"); try { - const priceContext = getKalshiPriceContext(options); // Check if this is a NO outcome request const isNoOutcome = id.endsWith("-NO"); const ticker = id.replace(/-NO$/, ""); @@ -39,12 +31,12 @@ export async function fetchOrderBook( // - Bids: people buying NO (use data.no directly) // - Asks: people selling NO = people buying YES (invert data.yes) bids = (data.no || []).map((level: number[]) => ({ - price: fromKalshiCents(level[0], priceContext), + price: level[0] / 100, size: level[1], })); asks = (data.yes || []).map((level: number[]) => ({ - price: invertKalshiCents(level[0], priceContext), // Invert YES price to get NO ask price + price: 1 - level[0] / 100, // Invert YES price to get NO ask price size: level[1], })); } else { @@ -52,12 +44,12 @@ export async function fetchOrderBook( // - Bids: people buying YES (use data.yes directly) // - Asks: people selling YES = people buying NO (invert data.no) bids = (data.yes || []).map((level: number[]) => ({ - price: fromKalshiCents(level[0], priceContext), + price: level[0] / 100, size: level[1], })); asks = (data.no || []).map((level: number[]) => ({ - price: invertKalshiCents(level[0], priceContext), // Invert NO price to get YES ask price + price: 1 - level[0] / 100, // Invert NO price to get YES ask price size: level[1], })); } diff --git a/core/src/exchanges/kalshi/fetchTrades.ts b/core/src/exchanges/kalshi/fetchTrades.ts index 9b17e5f..ef1fd44 100644 --- a/core/src/exchanges/kalshi/fetchTrades.ts +++ b/core/src/exchanges/kalshi/fetchTrades.ts @@ -1,18 +1,15 @@ import axios from "axios"; -import { HistoryFilterParams, TradesParams, RequestOptions } from "../../BaseExchange"; +import { HistoryFilterParams, TradesParams } from "../../BaseExchange"; import { Trade } from "../../types"; import { kalshiErrorMapper } from "./errors"; import { getMarketsUrl } from "./config"; -import { getKalshiPriceContext, fromKalshiCents } from "./price"; export async function fetchTrades( baseUrl: string, id: string, params: TradesParams | HistoryFilterParams, - options?: RequestOptions, ): Promise { try { - const priceContext = getKalshiPriceContext(options); const ticker = id.replace(/-NO$/, ""); const url = getMarketsUrl(baseUrl, undefined, ["trades"]); const response = await axios.get(url, { @@ -26,7 +23,7 @@ export async function fetchTrades( return trades.map((t: any) => ({ id: t.trade_id, timestamp: new Date(t.created_time).getTime(), - price: fromKalshiCents(t.yes_price, priceContext), + price: t.yes_price / 100, amount: t.count, side: t.taker_side === "yes" ? "buy" : "sell", })); diff --git a/core/src/exchanges/kalshi/index.ts b/core/src/exchanges/kalshi/index.ts index 152c45b..f6ae1a4 100644 --- a/core/src/exchanges/kalshi/index.ts +++ b/core/src/exchanges/kalshi/index.ts @@ -5,7 +5,6 @@ import { OHLCVParams, TradesParams, ExchangeCredentials, - RequestOptions, EventFetchParams, MyTradesParams, OrderHistoryParams, @@ -33,11 +32,6 @@ import { AuthenticationError } from "../../errors"; import { parseOpenApiSpec } from "../../utils/openapi"; import { kalshiApiSpec } from "./api"; import { getKalshiConfig, KalshiApiConfig, KALSHI_PATHS } from "./config"; -import { - getKalshiPriceContext, - fromKalshiCents, - invertKalshiCents, -} from "./price"; // Re-export for external use export type { KalshiWebSocketConfig }; @@ -152,30 +146,26 @@ export class KalshiExchange extends PredictionMarketExchange { protected async fetchMarketsImpl( params?: MarketFilterParams, - options?: RequestOptions, ): Promise { - return fetchMarkets(params, this.callApi.bind(this), options); + return fetchMarkets(params, this.callApi.bind(this)); } protected async fetchEventsImpl( params: EventFetchParams, - options?: RequestOptions, ): Promise { - return fetchEvents(params, this.callApi.bind(this), options); + return fetchEvents(params, this.callApi.bind(this)); } async fetchOHLCV( id: string, params: OHLCVParams, - options?: RequestOptions, ): Promise { - return fetchOHLCV(id, params, this.callApi.bind(this), options); + return fetchOHLCV(id, params, this.callApi.bind(this)); } - async fetchOrderBook(id: string, options?: RequestOptions): Promise { + async fetchOrderBook(id: string): Promise { validateIdFormat(id, "OrderBook"); - const priceContext = getKalshiPriceContext(options); const isNoOutcome = id.endsWith("-NO"); const ticker = id.replace(/-NO$/, ""); const data = (await this.callApi("GetMarketOrderbook", { ticker })) @@ -186,20 +176,20 @@ export class KalshiExchange extends PredictionMarketExchange { if (isNoOutcome) { bids = (data.no || []).map((level: number[]) => ({ - price: fromKalshiCents(level[0], priceContext), + price: level[0] / 100, size: level[1], })); asks = (data.yes || []).map((level: number[]) => ({ - price: invertKalshiCents(level[0], priceContext), + price: 1 - level[0] / 100, size: level[1], })); } else { bids = (data.yes || []).map((level: number[]) => ({ - price: fromKalshiCents(level[0], priceContext), + price: level[0] / 100, size: level[1], })); asks = (data.no || []).map((level: number[]) => ({ - price: invertKalshiCents(level[0], priceContext), + price: 1 - level[0] / 100, size: level[1], })); } @@ -213,7 +203,6 @@ export class KalshiExchange extends PredictionMarketExchange { async fetchTrades( id: string, params: TradesParams | HistoryFilterParams, - options?: RequestOptions, ): Promise { if ("resolution" in params && params.resolution !== undefined) { console.warn( @@ -221,7 +210,6 @@ export class KalshiExchange extends PredictionMarketExchange { "It will be removed in v3.0.0. Please remove it from your code.", ); } - const priceContext = getKalshiPriceContext(options); const ticker = id.replace(/-NO$/, ""); const data = await this.callApi("GetTrades", { ticker, @@ -231,7 +219,7 @@ export class KalshiExchange extends PredictionMarketExchange { return trades.map((t: any) => ({ id: t.trade_id, timestamp: new Date(t.created_time).getTime(), - price: fromKalshiCents(t.yes_price, priceContext), + price: t.yes_price / 100, amount: t.count, side: t.taker_side === "yes" ? "buy" : "sell", })); @@ -241,11 +229,10 @@ export class KalshiExchange extends PredictionMarketExchange { // User Data Methods // ---------------------------------------------------------------------------- - async fetchBalance(options?: RequestOptions): Promise { + async fetchBalance(): Promise { const data = await this.callApi("GetBalance"); - const priceContext = getKalshiPriceContext(options); - const available = fromKalshiCents(data.balance, priceContext); - const total = fromKalshiCents(data.portfolio_value, priceContext); + const available = data.balance / 100; + const total = data.portfolio_value / 100; return [ { currency: "USD", @@ -304,12 +291,12 @@ export class KalshiExchange extends PredictionMarketExchange { }; } - async fetchOrder(orderId: string, options?: RequestOptions): Promise { + async fetchOrder(orderId: string): Promise { const data = await this.callApi("GetOrder", { order_id: orderId }); - return this.mapKalshiOrder(data.order, options); + return this.mapKalshiOrder(data.order); } - async fetchOpenOrders(marketId?: string, options?: RequestOptions): Promise { + async fetchOpenOrders(marketId?: string): Promise { const queryParams: Record = { status: "resting" }; if (marketId) { queryParams.ticker = marketId; @@ -317,13 +304,10 @@ export class KalshiExchange extends PredictionMarketExchange { const data = await this.callApi("GetOrders", queryParams); const orders = data.orders || []; - return orders.map((order: any) => this.mapKalshiOrder(order, options)); + return orders.map((order: any) => this.mapKalshiOrder(order)); } - async fetchMyTrades( - params?: MyTradesParams, - options?: RequestOptions, - ): Promise { + async fetchMyTrades(params?: MyTradesParams): Promise { const queryParams: Record = {}; if (params?.outcomeId || params?.marketId) { queryParams.ticker = (params.outcomeId || params.marketId)!.replace( @@ -339,21 +323,17 @@ export class KalshiExchange extends PredictionMarketExchange { if (params?.cursor) queryParams.cursor = params.cursor; const data = await this.callApi("GetFills", queryParams); - const priceContext = getKalshiPriceContext(options); return (data.fills || []).map((f: any) => ({ id: f.fill_id, timestamp: new Date(f.created_time).getTime(), - price: fromKalshiCents(f.yes_price, priceContext), + price: f.yes_price / 100, amount: f.count, side: f.side === "yes" ? ("buy" as const) : ("sell" as const), orderId: f.order_id, })); } - async fetchClosedOrders( - params?: OrderHistoryParams, - options?: RequestOptions, - ): Promise { + async fetchClosedOrders(params?: OrderHistoryParams): Promise { const queryParams: Record = {}; if (params?.marketId) queryParams.ticker = params.marketId; if (params?.until) @@ -362,13 +342,10 @@ export class KalshiExchange extends PredictionMarketExchange { if (params?.cursor) queryParams.cursor = params.cursor; const data = await this.callApi("GetHistoricalOrders", queryParams); - return (data.orders || []).map((o: any) => this.mapKalshiOrder(o, options)); + return (data.orders || []).map((o: any) => this.mapKalshiOrder(o)); } - async fetchAllOrders( - params?: OrderHistoryParams, - options?: RequestOptions, - ): Promise { + async fetchAllOrders(params?: OrderHistoryParams): Promise { const queryParams: Record = {}; if (params?.marketId) queryParams.ticker = params.marketId; if (params?.since) @@ -393,21 +370,20 @@ export class KalshiExchange extends PredictionMarketExchange { ]) { if (!seen.has(o.order_id)) { seen.add(o.order_id); - all.push(this.mapKalshiOrder(o, options)); + all.push(this.mapKalshiOrder(o)); } } return all.sort((a, b) => b.timestamp - a.timestamp); } - async fetchPositions(options?: RequestOptions): Promise { + async fetchPositions(): Promise { const data = await this.callApi("GetPositions"); const positions = data.market_positions || []; - const priceContext = getKalshiPriceContext(options); return positions.map((pos: any) => { const absPosition = Math.abs(pos.position); const entryPrice = - absPosition > 0 ? fromKalshiCents(pos.total_cost, priceContext) / absPosition : 0; + absPosition > 0 ? pos.total_cost / absPosition / 100 : 0; return { marketId: pos.ticker, @@ -415,31 +391,22 @@ export class KalshiExchange extends PredictionMarketExchange { outcomeLabel: pos.ticker, size: pos.position, entryPrice, - currentPrice: pos.market_price - ? fromKalshiCents(pos.market_price, priceContext) - : entryPrice, - unrealizedPnL: pos.market_exposure - ? fromKalshiCents(pos.market_exposure, priceContext) - : 0, - realizedPnL: pos.realized_pnl - ? fromKalshiCents(pos.realized_pnl, priceContext) - : 0, + currentPrice: pos.market_price ? pos.market_price / 100 : entryPrice, + unrealizedPnL: pos.market_exposure ? pos.market_exposure / 100 : 0, + realizedPnL: pos.realized_pnl ? pos.realized_pnl / 100 : 0, }; }); } // Helper to map a raw Kalshi order object to a unified Order - private mapKalshiOrder(order: any, options?: RequestOptions): Order { - const priceContext = getKalshiPriceContext(options); + private mapKalshiOrder(order: any): Order { return { id: order.order_id, marketId: order.ticker, outcomeId: order.ticker, side: order.side === "yes" ? "buy" : "sell", type: order.type === "limit" ? "limit" : "market", - price: order.yes_price - ? fromKalshiCents(order.yes_price, priceContext) - : undefined, + price: order.yes_price ? order.yes_price / 100 : undefined, amount: order.count, status: this.mapKalshiOrderStatus(order.status), filled: order.count - (order.remaining_count || 0), @@ -472,11 +439,7 @@ export class KalshiExchange extends PredictionMarketExchange { private ws?: KalshiWebSocket; - async watchOrderBook( - id: string, - limit?: number, - options?: RequestOptions, - ): Promise { + async watchOrderBook(id: string, limit?: number): Promise { const auth = this.ensureAuth(); if (!this.ws) { @@ -489,14 +452,13 @@ export class KalshiExchange extends PredictionMarketExchange { } // Normalize ticker (strip -NO suffix if present) const marketTicker = id.replace(/-NO$/, ""); - return this.ws.watchOrderBook(marketTicker, options); + return this.ws.watchOrderBook(marketTicker); } async watchTrades( id: string, since?: number, limit?: number, - options?: RequestOptions, ): Promise { const auth = this.ensureAuth(); @@ -510,7 +472,7 @@ export class KalshiExchange extends PredictionMarketExchange { } // Normalize ticker (strip -NO suffix if present) const marketTicker = id.replace(/-NO$/, ""); - return this.ws.watchTrades(marketTicker, options); + return this.ws.watchTrades(marketTicker); } async close(): Promise { diff --git a/core/src/exchanges/kalshi/kalshi.test.ts b/core/src/exchanges/kalshi/kalshi.test.ts index c2cefe5..97014cb 100644 --- a/core/src/exchanges/kalshi/kalshi.test.ts +++ b/core/src/exchanges/kalshi/kalshi.test.ts @@ -105,56 +105,6 @@ describe("KalshiExchange", () => { const markets = await exchange.fetchMarkets(); expect(markets).toBeDefined(); }); - - it("should return raw prices for fetchTrades when mode is raw", async () => { - const mockResponse = { - data: { - trades: [ - { - trade_id: "trade-1", - created_time: "2026-01-13T12:00:00Z", - yes_price: 55, - count: 10, - taker_side: "yes", - }, - ], - }, - }; - (mockAxiosInstance.request as jest.Mock).mockResolvedValue( - mockResponse, - ); - - const trades = await exchange.fetchTrades( - "TEST-MARKET", - { limit: 1 }, - { mode: "raw" }, - ); - - expect(trades).toHaveLength(1); - expect(trades[0].price).toBe(55); - }); - - it("should return raw prices for fetchOrderBook when mode is raw", async () => { - exchange = new KalshiExchange(mockCredentials); - const mockResponse = { - data: { - orderbook: { - yes: [[55, 10]], - no: [[45, 5]], - }, - }, - }; - (mockAxiosInstance.request as jest.Mock).mockResolvedValue( - mockResponse, - ); - - const book = await exchange.fetchOrderBook("TEST-MARKET", { - mode: "raw", - }); - - expect(book.bids[0].price).toBe(55); - expect(book.asks[0].price).toBe(55); - }); }); describe("Trading Methods", () => { diff --git a/core/src/exchanges/kalshi/price.test.ts b/core/src/exchanges/kalshi/price.test.ts deleted file mode 100644 index 7932e0a..0000000 --- a/core/src/exchanges/kalshi/price.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - getKalshiPriceContext, - fromKalshiCents, - invertKalshiCents, - invertKalshiUnified, -} from "./price"; - -describe("kalshi price helpers", () => { - it("returns normalized context by default", () => { - const context = getKalshiPriceContext(); - - expect(context.isRaw).toBe(false); - expect(context.scale).toBe(100); - expect(context.unit).toBe(1); - expect(context.defaultPrice).toBe(0.5); - }); - - it("returns raw context when mode is raw", () => { - const context = getKalshiPriceContext({ mode: "raw" }); - - expect(context.isRaw).toBe(true); - expect(context.scale).toBe(1); - expect(context.unit).toBe(100); - expect(context.defaultPrice).toBe(50); - }); - - it("converts cents to unified normalized prices", () => { - const context = getKalshiPriceContext(); - - expect(fromKalshiCents(55, context)).toBe(0.55); - expect(invertKalshiCents(45, context)).toBe(0.55); - expect(invertKalshiUnified(0.45, context)).toBe(0.55); - }); - - it("converts cents to raw prices without scaling", () => { - const context = getKalshiPriceContext({ mode: "raw" }); - - expect(fromKalshiCents(55, context)).toBe(55); - expect(invertKalshiCents(45, context)).toBe(55); - expect(invertKalshiUnified(45, context)).toBe(55); - }); -}); diff --git a/core/src/exchanges/kalshi/price.ts b/core/src/exchanges/kalshi/price.ts deleted file mode 100644 index ab60795..0000000 --- a/core/src/exchanges/kalshi/price.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { RequestOptions } from "../../BaseExchange"; - -export interface KalshiPriceContext { - isRaw: boolean; - scale: number; - unit: number; - defaultPrice: number; -} - -export function getKalshiPriceContext( - options?: RequestOptions, -): KalshiPriceContext { - const isRaw = options?.mode === "raw"; - return { - isRaw, - scale: isRaw ? 1 : 100, - unit: isRaw ? 100 : 1, - defaultPrice: isRaw ? 50 : 0.5, - }; -} - -export function fromKalshiCents( - priceInCents: number, - context: KalshiPriceContext, -): number { - return priceInCents / context.scale; -} - -export function invertKalshiCents( - priceInCents: number, - context: KalshiPriceContext, -): number { - return context.unit - fromKalshiCents(priceInCents, context); -} - -export function invertKalshiUnified( - price: number, - context: KalshiPriceContext, -): number { - return context.unit - price; -} diff --git a/core/src/exchanges/kalshi/utils.ts b/core/src/exchanges/kalshi/utils.ts index 43450fa..e0e589c 100644 --- a/core/src/exchanges/kalshi/utils.ts +++ b/core/src/exchanges/kalshi/utils.ts @@ -1,32 +1,20 @@ -import { RequestOptions } from "../../BaseExchange"; import { UnifiedMarket, MarketOutcome, CandleInterval } from "../../types"; import { addBinaryOutcomes } from "../../utils/market-utils"; -import { - getKalshiPriceContext, - fromKalshiCents, - invertKalshiUnified, -} from "./price"; export function mapMarketToUnified( event: any, market: any, - options?: RequestOptions, ): UnifiedMarket | null { if (!market) return null; - const priceContext = getKalshiPriceContext(options); - // Calculate price - let price = priceContext.defaultPrice; + let price = 0.5; if (market.last_price) { - price = fromKalshiCents(market.last_price, priceContext); + price = market.last_price / 100; } else if (market.yes_ask && market.yes_bid) { - price = - (fromKalshiCents(market.yes_ask, priceContext) + - fromKalshiCents(market.yes_bid, priceContext)) / - 2; + price = (market.yes_ask + market.yes_bid) / 200; } else if (market.yes_ask) { - price = fromKalshiCents(market.yes_ask, priceContext); + price = market.yes_ask / 100; } // Extract candidate name @@ -37,19 +25,7 @@ export function mapMarketToUnified( // Calculate 24h change let priceChange = 0; - if (priceContext.isRaw) { - if ( - market.previous_price !== undefined && - market.last_price !== undefined - ) { - priceChange = market.last_price - market.previous_price; - } else if ( - market.previous_price_dollars !== undefined && - market.last_price_dollars !== undefined - ) { - priceChange = market.last_price_dollars - market.previous_price_dollars; - } - } else if ( + if ( market.previous_price_dollars !== undefined && market.last_price_dollars !== undefined ) { @@ -68,7 +44,7 @@ export function mapMarketToUnified( outcomeId: `${market.ticker}-NO`, marketId: market.ticker, label: candidateName ? `Not ${candidateName}` : "No", - price: invertKalshiUnified(price, priceContext), + price: 1 - price, priceChange24h: -priceChange, // Inverse change for No? simplified assumption }, ]; diff --git a/core/src/exchanges/kalshi/websocket.ts b/core/src/exchanges/kalshi/websocket.ts index 5d6cba5..85d6f63 100644 --- a/core/src/exchanges/kalshi/websocket.ts +++ b/core/src/exchanges/kalshi/websocket.ts @@ -1,12 +1,6 @@ import WebSocket from "ws"; import { OrderBook, Trade, OrderLevel } from "../../types"; -import { RequestOptions } from "../../BaseExchange"; import { KalshiAuth } from "./auth"; -import { - getKalshiPriceContext, - fromKalshiCents, - invertKalshiCents, -} from "./price"; interface QueuedPromise { resolve: (value: T | PromiseLike) => void; @@ -32,8 +26,6 @@ export class KalshiWebSocket { private orderBookResolvers = new Map[]>(); private tradeResolvers = new Map[]>(); private orderBooks = new Map(); - private orderBookOptions = new Map(); - private tradeOptions = new Map(); private subscribedOrderBookTickers = new Set(); private subscribedTradeTickers = new Set(); private messageIdCounter = 1; @@ -230,8 +222,6 @@ export class KalshiWebSocket { private handleOrderbookSnapshot(data: any) { const ticker = data.market_ticker; - const options = this.orderBookOptions.get(ticker); - const priceContext = getKalshiPriceContext(options); // Kalshi orderbook structure: // yes: [{ price: number (cents), quantity: number }, ...] @@ -239,7 +229,7 @@ export class KalshiWebSocket { const bids: OrderLevel[] = (data.yes || []) .map((level: any) => { - const price = fromKalshiCents(level.price || level[0], priceContext); + const price = (level.price || level[0]) / 100; const size = (level.quantity !== undefined ? level.quantity @@ -252,7 +242,7 @@ export class KalshiWebSocket { const asks: OrderLevel[] = (data.no || []) .map((level: any) => { - const price = invertKalshiCents(level.price || level[0], priceContext); + const price = (100 - (level.price || level[0])) / 100; const size = (level.quantity !== undefined ? level.quantity @@ -276,8 +266,6 @@ export class KalshiWebSocket { private handleOrderbookDelta(data: any) { const ticker = data.market_ticker; const existing = this.orderBooks.get(ticker); - const options = this.orderBookOptions.get(ticker); - const priceContext = getKalshiPriceContext(options); if (!existing) { // No snapshot yet, skip delta @@ -286,7 +274,7 @@ export class KalshiWebSocket { // Apply delta updates // Kalshi sends: { price: number, delta: number, side: 'yes' | 'no' } - const price = fromKalshiCents(data.price, priceContext); + const price = data.price / 100; const delta = data.delta !== undefined ? data.delta @@ -298,8 +286,8 @@ export class KalshiWebSocket { if (side === "yes") { this.applyDelta(existing.bids, price, delta, "desc"); } else { - const invertedPrice = invertKalshiCents(data.price, priceContext); - this.applyDelta(existing.asks, invertedPrice, delta, "asc"); + const yesPrice = (100 - data.price) / 100; + this.applyDelta(existing.asks, yesPrice, delta, "asc"); } existing.timestamp = Date.now(); @@ -342,8 +330,6 @@ export class KalshiWebSocket { private handleTrade(data: any) { const ticker = data.market_ticker; - const options = this.tradeOptions.get(ticker); - const priceContext = getKalshiPriceContext(options); // Kalshi trade structure: // { trade_id, market_ticker, yes_price, no_price, count, created_time, taker_side } @@ -378,8 +364,8 @@ export class KalshiWebSocket { timestamp, price: data.yes_price || data.price - ? fromKalshiCents(data.yes_price || data.price, priceContext) - : priceContext.defaultPrice, + ? (data.yes_price || data.price) / 100 + : 0.5, amount: data.count || data.size || 0, side: data.taker_side === "yes" || data.side === "buy" @@ -404,10 +390,7 @@ export class KalshiWebSocket { } } - async watchOrderBook( - ticker: string, - options?: RequestOptions, - ): Promise { + async watchOrderBook(ticker: string): Promise { // Ensure connection if (!this.isConnected) { await this.connect(); @@ -418,7 +401,6 @@ export class KalshiWebSocket { this.subscribedOrderBookTickers.add(ticker); this.subscribeToOrderbook(Array.from(this.subscribedOrderBookTickers)); } - this.orderBookOptions.set(ticker, options); // Return a promise that resolves on the next orderbook update return new Promise((resolve, reject) => { @@ -429,7 +411,7 @@ export class KalshiWebSocket { }); } - async watchTrades(ticker: string, options?: RequestOptions): Promise { + async watchTrades(ticker: string): Promise { // Ensure connection if (!this.isConnected) { await this.connect(); @@ -440,7 +422,6 @@ export class KalshiWebSocket { this.subscribedTradeTickers.add(ticker); this.subscribeToTrades(Array.from(this.subscribedTradeTickers)); } - this.tradeOptions.set(ticker, options); // Return a promise that resolves on the next trade return new Promise((resolve, reject) => { diff --git a/core/src/exchanges/myriad/index.ts b/core/src/exchanges/myriad/index.ts index 93f2fe6..b16f912 100644 --- a/core/src/exchanges/myriad/index.ts +++ b/core/src/exchanges/myriad/index.ts @@ -1,4 +1,4 @@ -import { PredictionMarketExchange, MarketFilterParams, HistoryFilterParams, OHLCVParams, TradesParams, ExchangeCredentials, EventFetchParams, MyTradesParams, RequestOptions } from '../../BaseExchange'; +import { PredictionMarketExchange, MarketFilterParams, HistoryFilterParams, OHLCVParams, TradesParams, ExchangeCredentials, EventFetchParams, MyTradesParams } from '../../BaseExchange'; import { UnifiedMarket, UnifiedEvent, PriceCandle, OrderBook, Trade, UserTrade, Balance, Order, Position, CreateOrderParams } from '../../types'; import { fetchMarkets } from './fetchMarkets'; import { fetchEvents } from './fetchEvents'; @@ -11,7 +11,6 @@ import { AuthenticationError } from '../../errors'; import { BASE_URL } from './utils'; import { parseOpenApiSpec } from '../../utils/openapi'; import { myriadApiSpec } from './api'; -import { resolveMyriadPrice } from './price'; export class MyriadExchange extends PredictionMarketExchange { override readonly has = { @@ -96,11 +95,7 @@ export class MyriadExchange extends PredictionMarketExchange { return fetchOrderBook(id, this.callApi.bind(this)); } - async fetchTrades( - id: string, - params: TradesParams | HistoryFilterParams, - options?: RequestOptions, - ): Promise { + async fetchTrades(id: string, params: TradesParams | HistoryFilterParams): Promise { if ('resolution' in params && params.resolution !== undefined) { console.warn( '[pmxt] Warning: The "resolution" parameter is deprecated for fetchTrades() and will be ignored. ' + @@ -145,13 +140,13 @@ export class MyriadExchange extends PredictionMarketExchange { return filtered.map((t: any, index: number) => ({ id: `${t.blockNumber || t.timestamp}-${index}`, timestamp: (t.timestamp || 0) * 1000, - price: resolveMyriadPrice(t, options), + price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); } - async fetchMyTrades(params?: MyTradesParams, options?: RequestOptions): Promise { + async fetchMyTrades(params?: MyTradesParams): Promise { const walletAddress = this.ensureAuth().walletAddress; if (!walletAddress) { throw new AuthenticationError( @@ -174,7 +169,7 @@ export class MyriadExchange extends PredictionMarketExchange { return tradeEvents.map((t: any, i: number) => ({ id: `${t.blockNumber || t.timestamp}-${i}`, timestamp: (t.timestamp || 0) * 1000, - price: resolveMyriadPrice(t, options), + price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); @@ -243,7 +238,7 @@ export class MyriadExchange extends PredictionMarketExchange { return []; // AMM: no open orders } - async fetchPositions(options?: RequestOptions): Promise { + async fetchPositions(): Promise { const walletAddress = this.ensureAuth().walletAddress; if (!walletAddress) { throw new AuthenticationError( @@ -261,7 +256,7 @@ export class MyriadExchange extends PredictionMarketExchange { outcomeLabel: pos.outcomeTitle || `Outcome ${pos.outcomeId}`, size: Number(pos.shares || 0), entryPrice: Number(pos.price || 0), - currentPrice: resolveMyriadPrice(pos, options), + currentPrice: Number(pos.value || 0) / Math.max(Number(pos.shares || 1), 1), unrealizedPnL: Number(pos.profit || 0), })); } @@ -295,29 +290,20 @@ export class MyriadExchange extends PredictionMarketExchange { // WebSocket (poll-based) // ------------------------------------------------------------------------ - async watchOrderBook( - id: string, - _limit?: number, - options?: RequestOptions, - ): Promise { + async watchOrderBook(id: string, _limit?: number): Promise { this.ensureAuth(); if (!this.ws) { this.ws = new MyriadWebSocket(this.callApi.bind(this)); } - return this.ws.watchOrderBook(id, options); + return this.ws.watchOrderBook(id); } - async watchTrades( - id: string, - _since?: number, - _limit?: number, - options?: RequestOptions, - ): Promise { + async watchTrades(id: string, _since?: number, _limit?: number): Promise { this.ensureAuth(); if (!this.ws) { this.ws = new MyriadWebSocket(this.callApi.bind(this)); } - return this.ws.watchTrades(id, options); + return this.ws.watchTrades(id); } async close(): Promise { @@ -326,5 +312,4 @@ export class MyriadExchange extends PredictionMarketExchange { this.ws = undefined; } } - } diff --git a/core/src/exchanges/myriad/price.test.ts b/core/src/exchanges/myriad/price.test.ts deleted file mode 100644 index 9e4e5bb..0000000 --- a/core/src/exchanges/myriad/price.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { resolveMyriadPrice } from "./price"; - -describe("myriad price helpers", () => { - it("uses raw event.price when raw mode is requested and present", () => { - const value = resolveMyriadPrice( - { price: "42.5", value: 100, shares: 2 }, - { mode: "raw" }, - ); - expect(value).toBe(42.5); - }); - - it("falls back to value/shares when raw price is missing", () => { - const value = resolveMyriadPrice( - { value: 100, shares: 4 }, - { mode: "raw" }, - ); - expect(value).toBe(25); - }); - - it("keeps previous fallback behavior when shares are missing or zero", () => { - expect(resolveMyriadPrice({ value: 12, shares: 0 })).toBe(12); - expect(resolveMyriadPrice({ value: 7 })).toBe(7); - }); -}); - diff --git a/core/src/exchanges/myriad/price.ts b/core/src/exchanges/myriad/price.ts deleted file mode 100644 index 5cb43d0..0000000 --- a/core/src/exchanges/myriad/price.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { RequestOptions } from "../../BaseExchange"; - -export function resolveMyriadPrice(event: any, options?: RequestOptions): number { - if ( - options?.mode === "raw" && - event.price !== undefined && - event.price !== null - ) { - return Number(event.price); - } - - const shares = Math.max(Number(event.shares || 1), 1); - return Number(event.value || 0) / shares; -} - diff --git a/core/src/exchanges/myriad/websocket.ts b/core/src/exchanges/myriad/websocket.ts index af11bc9..8073ded 100644 --- a/core/src/exchanges/myriad/websocket.ts +++ b/core/src/exchanges/myriad/websocket.ts @@ -1,7 +1,5 @@ import { OrderBook, Trade } from '../../types'; -import { RequestOptions } from '../../BaseExchange'; import { fetchOrderBook } from './fetchOrderBook'; -import { resolveMyriadPrice } from './price'; // Myriad API v2 does not expose a WebSocket endpoint. // We implement a poll-based fallback that resolves promises @@ -17,7 +15,6 @@ export class MyriadWebSocket { private orderBookResolvers: Map void)[]> = new Map(); private tradeResolvers: Map void)[]> = new Map(); private lastTradeTimestamp: Map = new Map(); - private tradeOptions: Map = new Map(); private closed = false; constructor(callApi: (operationId: string, params?: Record) => Promise, pollInterval?: number) { @@ -25,7 +22,7 @@ export class MyriadWebSocket { this.pollInterval = pollInterval || DEFAULT_POLL_INTERVAL; } - async watchOrderBook(id: string, _options?: RequestOptions): Promise { + async watchOrderBook(id: string): Promise { if (this.closed) throw new Error('WebSocket connection is closed'); return new Promise((resolve) => { @@ -40,7 +37,7 @@ export class MyriadWebSocket { }); } - async watchTrades(id: string, options?: RequestOptions): Promise { + async watchTrades(id: string): Promise { if (this.closed) throw new Error('WebSocket connection is closed'); return new Promise((resolve) => { @@ -49,7 +46,6 @@ export class MyriadWebSocket { } this.tradeResolvers.get(id)!.push(resolve); - this.tradeOptions.set(id, options); if (!this.tradeTimers.has(id)) { this.startTradePolling(id); } @@ -96,7 +92,6 @@ export class MyriadWebSocket { private startTradePolling(id: string): void { const poll = async () => { try { - const options = this.tradeOptions.get(id); const parts = id.split(':'); const [networkId, marketId] = parts; const outcomeId = parts.length >= 3 ? parts[2] : undefined; @@ -121,7 +116,7 @@ export class MyriadWebSocket { const trades: Trade[] = filtered.map((t: any, index: number) => ({ id: `${t.blockNumber || t.timestamp}-${index}`, timestamp: (t.timestamp || 0) * 1000, - price: resolveMyriadPrice(t, options), + price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); diff --git a/core/src/server/openapi.yaml b/core/src/server/openapi.yaml index 5e1a5ae..8295a32 100644 --- a/core/src/server/openapi.yaml +++ b/core/src/server/openapi.yaml @@ -82,12 +82,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - $ref: '#/components/schemas/MarketFilterParams' - - type: object + $ref: '#/components/schemas/MarketFilterParams' credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -122,17 +119,14 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - type: object - properties: - limit: - type: number - cursor: - type: string - - type: object + type: object + properties: + limit: + type: number + cursor: + type: string credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -168,12 +162,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - $ref: '#/components/schemas/EventFetchParams' - - type: object + $ref: '#/components/schemas/EventFetchParams' credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -208,12 +199,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - $ref: '#/components/schemas/MarketFilterParams' - - type: object + $ref: '#/components/schemas/MarketFilterParams' credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -246,12 +234,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - $ref: '#/components/schemas/EventFetchParams' - - type: object + $ref: '#/components/schemas/EventFetchParams' credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -285,12 +270,11 @@ paths: args: type: array minItems: 2 - maxItems: 3 + maxItems: 2 items: oneOf: - type: string - $ref: '#/components/schemas/OHLCVParams' - - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -325,12 +309,10 @@ paths: properties: args: type: array - minItems: 1 - maxItems: 2 + maxItems: 1 items: - oneOf: - - type: string - - type: object + type: string + minItems: 1 credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -366,14 +348,13 @@ paths: args: type: array minItems: 2 - maxItems: 3 + maxItems: 2 items: oneOf: - type: string - oneOf: - $ref: '#/components/schemas/TradesParams' - $ref: '#/components/schemas/HistoryFilterParams' - - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -480,12 +461,10 @@ paths: properties: args: type: array - minItems: 1 - maxItems: 2 + maxItems: 1 items: - oneOf: - - type: string - - type: object + type: string + minItems: 1 credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -518,12 +497,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - type: string - - type: object + type: string credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -556,12 +532,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - $ref: '#/components/schemas/MyTradesParams' - - type: object + $ref: '#/components/schemas/MyTradesParams' credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -593,12 +566,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - $ref: '#/components/schemas/OrderHistoryParams' - - type: object + $ref: '#/components/schemas/OrderHistoryParams' credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -630,12 +600,9 @@ paths: properties: args: type: array - minItems: 0 - maxItems: 2 + maxItems: 1 items: - oneOf: - - $ref: '#/components/schemas/OrderHistoryParams' - - type: object + $ref: '#/components/schemas/OrderHistoryParams' credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -667,9 +634,8 @@ paths: properties: args: type: array - maxItems: 1 - items: - type: object + maxItems: 0 + items: {} credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -702,9 +668,8 @@ paths: properties: args: type: array - maxItems: 1 - items: - type: object + maxItems: 0 + items: {} credentials: $ref: '#/components/schemas/ExchangeCredentials' responses: @@ -918,12 +883,11 @@ paths: args: type: array minItems: 1 - maxItems: 3 + maxItems: 2 items: oneOf: - type: string - type: number - - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -959,13 +923,12 @@ paths: args: type: array minItems: 1 - maxItems: 4 + maxItems: 3 items: oneOf: - type: string - type: number - type: number - - type: object credentials: $ref: '#/components/schemas/ExchangeCredentials' required: @@ -987,39 +950,6 @@ paths: description: >- Watch trade executions in real-time via WebSocket. Returns a promise that resolves with the next trade(s). Call repeatedly in a loop to stream updates (CCXT Pro pattern). - '/api/{exchange}/testDummyMethod': - post: - summary: Test Dummy Method - operationId: testDummyMethod - parameters: - - $ref: '#/components/parameters/ExchangeParam' - requestBody: - content: - application/json: - schema: - title: TestDummyMethodRequest - type: object - properties: - args: - type: array - maxItems: 1 - items: - type: string - credentials: - $ref: '#/components/schemas/ExchangeCredentials' - responses: - '200': - description: Test Dummy Method response - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/BaseResponse' - - type: object - properties: - data: - type: string - description: Test method for auto-generation verification. '/api/{exchange}/close': post: summary: Close @@ -1046,6 +976,9 @@ paths: application/json: schema: $ref: '#/components/schemas/BaseResponse' + description: >- + Close all WebSocket connections and clean up resources. Call this when you're done streaming to properly release + connections. components: parameters: ExchangeParam: diff --git a/docs/MIGRATE_FROM_DOMEAPI.md b/docs/MIGRATE_FROM_DOMEAPI.md index 9460dc8..89e3477 100644 --- a/docs/MIGRATE_FROM_DOMEAPI.md +++ b/docs/MIGRATE_FROM_DOMEAPI.md @@ -94,7 +94,7 @@ markets = poly.fetch_markets(query='Trump') price = markets[0].yes.price # 0.0 to 1.0 ``` -> Note: DomeAPI returns price as a raw value. pmxt prices default to 0.0-1.0 (probability). To return raw exchange values, pass `{ mode: "raw" }` as the final argument (e.g., `fetchMarkets(params, { mode: "raw" })`). +> Note: DomeAPI returns price as a raw value. pmxt prices are always 0.0-1.0 (probability). Multiply by 100 for percentage. --- diff --git a/readme.md b/readme.md index 8fe16aa..cc82397 100644 --- a/readme.md +++ b/readme.md @@ -148,8 +148,6 @@ const warsh = fedEvent.markets.match('Kevin Warsh'); console.log(`Price: ${warsh.yes?.price}`); ``` -Prices are normalized to `0.0-1.0` by default. To return raw exchange values, pass `{ mode: "raw" }` as the final argument to fetch methods (e.g., `fetchMarkets(params, { mode: "raw" })`). - ## Trading pmxt supports unified trading across exchanges. diff --git a/sdks/python/README.md b/sdks/python/README.md index ff611f9..f48ab9f 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -158,20 +158,6 @@ print(f"Available: ${balances[0].available}") - `get_execution_price(order_book, side, amount)` - Get execution price - `get_execution_price_detailed(order_book, side, amount)` - Get detailed execution info -### Raw Price Mode - -Use `mode="raw"` to get source exchange prices without normalization. - -```python -# Kalshi raw cents -book = kalshi.fetch_order_book("KXBTC-100K-YES", mode="raw") -print(book.bids[0].price) # e.g. 55 instead of 0.55 - -# Polymarket normalized default -book = poly.fetch_order_book(outcome_id) -print(book.bids[0].price) # e.g. 0.55 -``` - ### Trading Methods (require authentication) - `create_order(params)` - Place a new order diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index f4f7f19..72a6c74 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -33,7 +33,6 @@ Order, Position, Balance, - RequestOptions, ) @@ -81,5 +80,4 @@ def restart_server(): "Order", "Position", "Balance", - "RequestOptions", ] diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index 56d1efc..de3244e 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -7,7 +7,7 @@ import os import sys -from typing import List, Optional, Dict, Any, Literal, Union, Sequence +from typing import List, Optional, Dict, Any, Literal, Union from datetime import datetime from abc import ABC, abstractmethod import json @@ -226,7 +226,7 @@ def __init__( base_url: str = "http://localhost:3847", auto_start_server: bool = True, proxy_address: Optional[str] = None, - signature_type: Optional[Union[int, str]] = None, + signature_type: Optional[Any] = None, ): """ Initialize an exchange client. @@ -330,45 +330,6 @@ def _get_credentials_dict(self) -> Optional[Dict[str, Any]]: creds["signatureType"] = self.signature_type return creds if creds else None - def _build_request_options( - self, - mode: Optional[Literal["normalized", "raw"]], - ) -> Optional[Dict[str, str]]: - """Build validated request options.""" - if mode is None: - return None - if mode not in ("normalized", "raw"): - raise ValueError("mode must be either 'normalized' or 'raw'") - return {"mode": mode} - - def _build_args_with_optional_options( - self, - required_args: Sequence[object], - optional_args: Optional[Sequence[object]] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> List[object]: - """ - Build args while preserving optional argument positions when options are present. - """ - args: List[object] = list(required_args) - optional_values = list(optional_args or []) - options = self._build_request_options(mode) - - if options is not None: - args.extend(optional_values) - args.append(options) - return args - - last_non_none = -1 - for i, value in enumerate(optional_values): - if value is not None: - last_non_none = i - - if last_non_none >= 0: - args.extend(optional_values[: last_non_none + 1]) - - return args - @property def has(self) -> Dict[str, Any]: """ @@ -402,20 +363,11 @@ def has(self) -> Dict[str, Any]: # Low-Level API Access - def _call_method( - self, - method_name: str, - params: Optional[Dict[str, object]] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> Any: + def _call_method(self, method_name: str, params: Optional[Dict[str, Any]] = None) -> Any: """Call any exchange method on the server by name.""" try: url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/{method_name}" - optional_params = params - if mode is not None and optional_params is None: - optional_params = {} - args = self._build_args_with_optional_options([], [optional_params], mode) - body: Dict[str, object] = {"args": args} + body: Dict[str, Any] = {"args": [params] if params is not None else []} creds = self._get_credentials_dict() if creds: body["credentials"] = creds @@ -510,12 +462,7 @@ def load_markets(self, reload: bool = False) -> Dict[str, UnifiedMarket]: self._loaded_markets = True return self.markets - def fetch_markets( - self, - query: Optional[str] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - **kwargs, - ) -> List[UnifiedMarket]: + def fetch_markets(self, query: Optional[str] = None, **kwargs) -> List[UnifiedMarket]: """ Get active markets from the exchange. @@ -530,8 +477,10 @@ def fetch_markets( >>> markets = exchange.fetch_markets("Trump", limit=20, sort="volume") """ try: + body_dict = {"args": []} + # Prepare arguments - search_params: Dict[str, object] = {} + search_params = {} if query: search_params["query"] = query @@ -539,13 +488,8 @@ def fetch_markets( for key, value in kwargs.items(): search_params[key] = value - params_arg: Optional[Dict[str, object]] = ( - search_params if search_params else None - ) - if mode is not None and params_arg is None: - params_arg = {} - args = self._build_args_with_optional_options([], [params_arg], mode) - body_dict = {"args": args} + if search_params: + body_dict["args"] = [search_params] # Add credentials if available creds = self._get_credentials_dict() @@ -564,12 +508,7 @@ def fetch_markets( except ApiException as e: raise Exception(f"Failed to fetch markets: {self._extract_api_error(e)}") from None - def fetch_events( - self, - query: Optional[str] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - **kwargs, - ) -> List[UnifiedEvent]: + def fetch_events(self, query: Optional[str] = None, **kwargs) -> List[UnifiedEvent]: """ Fetch events with optional keyword search. Events group related markets together. @@ -585,8 +524,10 @@ def fetch_events( >>> events = exchange.fetch_events("Election", limit=10) """ try: + body_dict = {"args": []} + # Prepare arguments - search_params: Dict[str, object] = {} + search_params = {} if query: search_params["query"] = query @@ -594,13 +535,8 @@ def fetch_events( for key, value in kwargs.items(): search_params[key] = value - params_arg: Optional[Dict[str, object]] = ( - search_params if search_params else None - ) - if mode is not None and params_arg is None: - params_arg = {} - args = self._build_args_with_optional_options([], [params_arg], mode) - body_dict = {"args": args} + if search_params: + body_dict["args"] = [search_params] # Add credentials if available creds = self._get_credentials_dict() @@ -626,8 +562,7 @@ def fetch_market( event_id: Optional[str] = None, slug: Optional[str] = None, query: Optional[str] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - **kwargs, + **kwargs ) -> UnifiedMarket: """ Fetch a single market by lookup parameters. @@ -652,7 +587,7 @@ def fetch_market( >>> market = exchange.fetch_market(slug='will-trump-win') """ try: - search_params: Dict[str, object] = {} + search_params = {} if market_id: search_params["marketId"] = market_id if outcome_id: @@ -673,13 +608,7 @@ def fetch_market( camel_key = key_map.get(key, key) search_params[camel_key] = value - params_arg: Optional[Dict[str, object]] = ( - search_params if search_params else None - ) - if mode is not None and params_arg is None: - params_arg = {} - args = self._build_args_with_optional_options([], [params_arg], mode) - body_dict = {"args": args} + body_dict = {"args": [search_params] if search_params else []} creds = self._get_credentials_dict() if creds: @@ -710,8 +639,7 @@ def fetch_event( event_id: Optional[str] = None, slug: Optional[str] = None, query: Optional[str] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - **kwargs, + **kwargs ) -> UnifiedEvent: """ Fetch a single event by lookup parameters. @@ -734,7 +662,7 @@ def fetch_event( >>> event = exchange.fetch_event(slug='us-election') """ try: - search_params: Dict[str, object] = {} + search_params = {} if event_id: search_params["eventId"] = event_id if slug: @@ -750,13 +678,7 @@ def fetch_event( camel_key = key_map.get(key, key) search_params[camel_key] = value - params_arg: Optional[Dict[str, object]] = ( - search_params if search_params else None - ) - if mode is not None and params_arg is None: - params_arg = {} - args = self._build_args_with_optional_options([], [params_arg], mode) - body_dict = {"args": args} + body_dict = {"args": [search_params] if search_params else []} creds = self._get_credentials_dict() if creds: @@ -1005,8 +927,7 @@ def fetch_ohlcv( limit: Optional[int] = None, start: Optional[datetime] = None, end: Optional[datetime] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - **kwargs, + **kwargs ) -> List[PriceCandle]: """ Get historical price candles. @@ -1036,7 +957,7 @@ def fetch_ohlcv( ... ) """ try: - params_dict: Dict[str, object] = {} + params_dict = {} if resolution: params_dict["resolution"] = resolution if start: @@ -1051,12 +972,7 @@ def fetch_ohlcv( if key not in params_dict: params_dict[key] = value - args = self._build_args_with_optional_options( - [outcome_id], - [params_dict], - mode, - ) - request_body_dict = {"args": args} + request_body_dict = {"args": [outcome_id, params_dict]} request_body = internal_models.FetchOHLCVRequest.from_dict(request_body_dict) response = self._api.fetch_ohlcv( @@ -1069,11 +985,7 @@ def fetch_ohlcv( except ApiException as e: raise Exception(f"Failed to fetch OHLCV: {self._extract_api_error(e)}") from None - def fetch_order_book( - self, - outcome_id: str, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> OrderBook: + def fetch_order_book(self, outcome_id: str) -> OrderBook: """ Get current order book for an outcome. @@ -1089,8 +1001,7 @@ def fetch_order_book( >>> print(f"Best ask: {order_book.asks[0].price}") """ try: - args = self._build_args_with_optional_options([outcome_id], [], mode) - body_dict = {"args": args} + body_dict = {"args": [outcome_id]} request_body = internal_models.FetchOrderBookRequest.from_dict(body_dict) response = self._api.fetch_order_book( @@ -1108,8 +1019,7 @@ def fetch_trades( outcome_id: str, limit: Optional[int] = None, since: Optional[int] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - **kwargs, + **kwargs ) -> List[Trade]: """ Get trade history for an outcome. @@ -1129,7 +1039,7 @@ def fetch_trades( >>> trades = exchange.fetch_trades(outcome_id, limit=50) """ try: - params_dict: Dict[str, object] = {} + params_dict = {} if limit: params_dict["limit"] = limit if since: @@ -1140,12 +1050,7 @@ def fetch_trades( if key not in params_dict: params_dict[key] = value - args = self._build_args_with_optional_options( - [outcome_id], - [params_dict], - mode, - ) - request_body_dict = {"args": args} + request_body_dict = {"args": [outcome_id, params_dict]} request_body = internal_models.FetchTradesRequest.from_dict(request_body_dict) response = self._api.fetch_trades( @@ -1160,12 +1065,7 @@ def fetch_trades( # WebSocket Streaming Methods - def watch_order_book( - self, - outcome_id: str, - limit: Optional[int] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> OrderBook: + def watch_order_book(self, outcome_id: str, limit: Optional[int] = None) -> OrderBook: """ Watch real-time order book updates via WebSocket. @@ -1187,11 +1087,9 @@ def watch_order_book( ... print(f"Best ask: {order_book.asks[0].price}") """ try: - args = self._build_args_with_optional_options( - [outcome_id], - [limit], - mode, - ) + args = [outcome_id] + if limit is not None: + args.append(limit) body_dict = {"args": args} @@ -1216,8 +1114,7 @@ def watch_trades( self, outcome_id: str, since: Optional[int] = None, - limit: Optional[int] = None, - mode: Optional[Literal["normalized", "raw"]] = None, + limit: Optional[int] = None ) -> List[Trade]: """ Watch real-time trade updates via WebSocket. @@ -1241,11 +1138,11 @@ def watch_trades( ... print(f"Trade: {trade.price} @ {trade.amount}") """ try: - args = self._build_args_with_optional_options( - [outcome_id], - [since, limit], - mode, - ) + args = [outcome_id] + if since is not None: + args.append(since) + if limit is not None: + args.append(limit) body_dict = {"args": args} @@ -1487,11 +1384,7 @@ def cancel_order(self, order_id: str) -> Order: except ApiException as e: raise Exception(f"Failed to cancel order: {self._extract_api_error(e)}") from None - def fetch_order( - self, - order_id: str, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> Order: + def fetch_order(self, order_id: str) -> Order: """ Get details of a specific order. @@ -1502,8 +1395,7 @@ def fetch_order( Order details """ try: - args = self._build_args_with_optional_options([order_id], [], mode) - body_dict = {"args": args} + body_dict = {"args": [order_id]} # Add credentials if available creds = self._get_credentials_dict() @@ -1522,11 +1414,7 @@ def fetch_order( except ApiException as e: raise Exception(f"Failed to fetch order: {self._extract_api_error(e)}") from None - def fetch_open_orders( - self, - market_id: Optional[str] = None, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> List[Order]: + def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]: """ Get all open orders, optionally filtered by market. @@ -1537,7 +1425,9 @@ def fetch_open_orders( List of open orders """ try: - args = self._build_args_with_optional_options([], [market_id], mode) + args = [] + if market_id: + args.append(market_id) body_dict = {"args": args} @@ -1562,10 +1452,9 @@ def fetch_my_trades( self, outcome_id: Optional[str] = None, market_id: Optional[str] = None, - since: Optional[Union[datetime, int, float, str]] = None, + since: Optional[Any] = None, limit: Optional[int] = None, cursor: Optional[str] = None, - mode: Optional[Literal["normalized", "raw"]] = None, ) -> List[UserTrade]: """ Get trades made by the authenticated user. @@ -1583,30 +1472,26 @@ def fetch_my_trades( Example: trades = exchange.fetch_my_trades(limit=50) """ - params: Dict[str, object] = {} + params: Dict[str, Any] = {} if outcome_id is not None: params["outcomeId"] = outcome_id if market_id is not None: params["marketId"] = market_id if since is not None: - if isinstance(since, datetime): - params["since"] = since.isoformat() - else: - params["since"] = since + params["since"] = since.isoformat() if hasattr(since, "isoformat") else since if limit is not None: params["limit"] = limit if cursor is not None: params["cursor"] = cursor - data = self._call_method("fetchMyTrades", params or None, mode) + data = self._call_method("fetchMyTrades", params or None) return [_convert_user_trade(t) for t in (data or [])] def fetch_closed_orders( self, market_id: Optional[str] = None, - since: Optional[Union[datetime, int, float, str]] = None, - until: Optional[Union[datetime, int, float, str]] = None, + since: Optional[Any] = None, + until: Optional[Any] = None, limit: Optional[int] = None, - mode: Optional[Literal["normalized", "raw"]] = None, ) -> List[Order]: """ Get filled and cancelled orders. @@ -1623,31 +1508,24 @@ def fetch_closed_orders( Example: orders = exchange.fetch_closed_orders(market_id="some-market") """ - params: Dict[str, object] = {} + params: Dict[str, Any] = {} if market_id is not None: params["marketId"] = market_id if since is not None: - if isinstance(since, datetime): - params["since"] = since.isoformat() - else: - params["since"] = since + params["since"] = since.isoformat() if hasattr(since, "isoformat") else since if until is not None: - if isinstance(until, datetime): - params["until"] = until.isoformat() - else: - params["until"] = until + params["until"] = until.isoformat() if hasattr(until, "isoformat") else until if limit is not None: params["limit"] = limit - data = self._call_method("fetchClosedOrders", params or None, mode) + data = self._call_method("fetchClosedOrders", params or None) return [_convert_order(o) for o in (data or [])] def fetch_all_orders( self, market_id: Optional[str] = None, - since: Optional[Union[datetime, int, float, str]] = None, - until: Optional[Union[datetime, int, float, str]] = None, + since: Optional[Any] = None, + until: Optional[Any] = None, limit: Optional[int] = None, - mode: Optional[Literal["normalized", "raw"]] = None, ) -> List[Order]: """ Get all orders (open + closed), sorted newest-first. @@ -1664,29 +1542,22 @@ def fetch_all_orders( Example: orders = exchange.fetch_all_orders() """ - params: Dict[str, object] = {} + params: Dict[str, Any] = {} if market_id is not None: params["marketId"] = market_id if since is not None: - if isinstance(since, datetime): - params["since"] = since.isoformat() - else: - params["since"] = since + params["since"] = since.isoformat() if hasattr(since, "isoformat") else since if until is not None: - if isinstance(until, datetime): - params["until"] = until.isoformat() - else: - params["until"] = until + params["until"] = until.isoformat() if hasattr(until, "isoformat") else until if limit is not None: params["limit"] = limit - data = self._call_method("fetchAllOrders", params or None, mode) + data = self._call_method("fetchAllOrders", params or None) return [_convert_order(o) for o in (data or [])] def fetch_markets_paginated( self, limit: Optional[int] = None, cursor: Optional[str] = None, - mode: Optional[Literal["normalized", "raw"]] = None, ) -> PaginatedMarketsResult: """ Fetch markets with cursor-based pagination. @@ -1706,12 +1577,12 @@ def fetch_markets_paginated( while page.next_cursor: page = exchange.fetch_markets_paginated(limit=100, cursor=page.next_cursor) """ - params: Dict[str, object] = {} + params: Dict[str, Any] = {} if limit is not None: params["limit"] = limit if cursor is not None: params["cursor"] = cursor - raw = self._call_method("fetchMarketsPaginated", params or None, mode) + raw = self._call_method("fetchMarketsPaginated", params or None) return PaginatedMarketsResult( data=[_convert_market(m) for m in raw.get("data", [])], total=raw.get("total", 0), @@ -1720,10 +1591,7 @@ def fetch_markets_paginated( # Account Methods - def fetch_positions( - self, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> List[Position]: + def fetch_positions(self) -> List[Position]: """ Get current positions across all markets. @@ -1731,8 +1599,7 @@ def fetch_positions( List of positions """ try: - args = self._build_args_with_optional_options([], [], mode) - body_dict = {"args": args} + body_dict = {"args": []} # Add credentials if available creds = self._get_credentials_dict() @@ -1751,10 +1618,7 @@ def fetch_positions( except ApiException as e: raise Exception(f"Failed to fetch positions: {self._extract_api_error(e)}") from None - def fetch_balance( - self, - mode: Optional[Literal["normalized", "raw"]] = None, - ) -> List[Balance]: + def fetch_balance(self) -> List[Balance]: """ Get account balance. @@ -1762,8 +1626,7 @@ def fetch_balance( List of balances (by currency) """ try: - args = self._build_args_with_optional_options([], [], mode) - body_dict = {"args": args} + body_dict = {"args": []} # Add credentials if available creds = self._get_credentials_dict() @@ -1854,3 +1717,4 @@ def get_execution_price_detailed( return _convert_execution_result(data) except Exception as e: raise Exception(f"Failed to get execution price: {self._extract_api_error(e)}") from None + diff --git a/sdks/python/pmxt/models.py b/sdks/python/pmxt/models.py index 1262be5..ffbd68c 100644 --- a/sdks/python/pmxt/models.py +++ b/sdks/python/pmxt/models.py @@ -16,15 +16,6 @@ OrderSide = Literal["buy", "sell"] OrderType = Literal["market", "limit"] OutcomeType = Literal["yes", "no", "up", "down"] -PriceMode = Literal["normalized", "raw"] - - -@dataclass -class RequestOptions: - """Optional request options for exchange methods.""" - - mode: Optional[PriceMode] = None - """Price mode. Use 'raw' to skip normalization when supported.""" @dataclass @@ -456,3 +447,4 @@ class EventFilterCriteria(TypedDict, total=False): total_volume: MinMax EventFilterFunction = Callable[[UnifiedEvent], bool] + diff --git a/sdks/python/tests/test_raw_mode_args.py b/sdks/python/tests/test_raw_mode_args.py deleted file mode 100644 index c59d7f1..0000000 --- a/sdks/python/tests/test_raw_mode_args.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -import sys -import types -import unittest - -if "pmxt_internal" not in sys.modules: - fake_pmxt_internal = types.ModuleType("pmxt_internal") - - class _FakeApiClient: - def __init__(self, configuration=None): - self.configuration = configuration - self.default_headers = {} - - def call_api(self, **kwargs): - return _FakeResponse({"success": True, "data": {}}) - - class _FakeConfiguration: - def __init__(self, host="http://localhost:3847"): - self.host = host - - class _FakeDefaultApi: - def __init__(self, api_client=None): - self.api_client = api_client - - class _FakeApiException(Exception): - pass - - fake_pmxt_internal.ApiClient = _FakeApiClient - fake_pmxt_internal.Configuration = _FakeConfiguration - fake_pmxt_internal.models = types.SimpleNamespace() - sys.modules["pmxt_internal"] = fake_pmxt_internal - - fake_api_pkg = types.ModuleType("pmxt_internal.api") - sys.modules["pmxt_internal.api"] = fake_api_pkg - - fake_default_api = types.ModuleType("pmxt_internal.api.default_api") - fake_default_api.DefaultApi = _FakeDefaultApi - sys.modules["pmxt_internal.api.default_api"] = fake_default_api - - fake_exceptions = types.ModuleType("pmxt_internal.exceptions") - fake_exceptions.ApiException = _FakeApiException - sys.modules["pmxt_internal.exceptions"] = fake_exceptions - -from pmxt.client import Exchange - - -class _FakeResponse: - def __init__(self, payload): - self.data = json.dumps(payload).encode("utf-8") - - def read(self): - return None - - -class TestRawModeArgs(unittest.TestCase): - def setUp(self): - self.exchange = Exchange("polymarket", auto_start_server=False) - - def test_build_request_options_rejects_invalid_mode(self): - with self.assertRaises(ValueError): - self.exchange._build_request_options("invalid") - - def test_build_args_preserves_optional_positions_in_raw_mode(self): - args = self.exchange._build_args_with_optional_options( - ["outcome-id"], - [None, 50], - "raw", - ) - self.assertEqual(args, ["outcome-id", None, 50, {"mode": "raw"}]) - - def test_build_args_trims_trailing_none_without_mode(self): - args = self.exchange._build_args_with_optional_options( - ["outcome-id"], - [10, None], - None, - ) - self.assertEqual(args, ["outcome-id", 10]) - - def test_call_method_sends_empty_params_when_only_mode_is_set(self): - captured = {} - - def fake_call_api(**kwargs): - captured.update(kwargs) - return _FakeResponse({"success": True, "data": {"ok": True}}) - - self.exchange._api_client.call_api = fake_call_api - result = self.exchange._call_method( - "fetchMarketsPaginated", - params=None, - mode="raw", - ) - - self.assertEqual(result, {"ok": True}) - self.assertEqual(captured["body"]["args"], [{}, {"mode": "raw"}]) - - -if __name__ == "__main__": - unittest.main() diff --git a/sdks/python/tests/test_status.py b/sdks/python/tests/test_status.py index 81aa938..affb751 100644 --- a/sdks/python/tests/test_status.py +++ b/sdks/python/tests/test_status.py @@ -1,6 +1,5 @@ import unittest -import inspect from pmxt import Polymarket, Kalshi class TestStatusParams(unittest.TestCase): @@ -18,20 +17,5 @@ def test_fetch_events_status_signature(self): params = {"query": "Politics", "status": "active"} self.assertEqual(params["status"], "active") - def test_raw_mode_signature(self): - """Verify raw mode is exposed on key market-data methods.""" - methods = [ - Polymarket.fetch_markets, - Polymarket.fetch_events, - Polymarket.fetch_ohlcv, - Polymarket.fetch_order_book, - Polymarket.fetch_trades, - Kalshi.watch_order_book, - Kalshi.watch_trades, - ] - for method in methods: - signature = inspect.signature(method) - self.assertIn("mode", signature.parameters) - if __name__ == '__main__': unittest.main() diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index 088dc56..511c590 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -28,17 +28,6 @@ console.log(markets[0].title); --- -## Raw Mode - -Prices are normalized to `0.0-1.0` by default. To return raw exchange values, pass `{ mode: "raw" }` as the final argument to fetch methods. - -```typescript -const markets = await poly.fetchMarkets({ query: "Trump" }, { mode: "raw" }); -const orderBook = await kalshi.fetchOrderBook("FED-25JAN", { mode: "raw" }); -``` - ---- - ## Server Management The SDK provides global functions to manage the background sidecar server. This is useful for clearing state or diff --git a/sdks/typescript/README.md b/sdks/typescript/README.md index 79262b0..bebcabd 100644 --- a/sdks/typescript/README.md +++ b/sdks/typescript/README.md @@ -136,7 +136,7 @@ For complete API documentation and examples, see: ## Important Notes - **Use `outcome.outcomeId`, not `market.marketId`** for deep-dive methods (fetchOHLCV, fetchOrderBook, fetchTrades) -- **Prices are 0.0 to 1.0 by default** (multiply by 100 for percentages). Use `{ mode: 'raw' }` as the final argument to return raw exchange values. +- **Prices are 0.0 to 1.0** (multiply by 100 for percentages) - **Timestamps are Unix milliseconds** - **Volumes are in USD** diff --git a/sdks/typescript/pmxt/args.ts b/sdks/typescript/pmxt/args.ts deleted file mode 100644 index 837d64d..0000000 --- a/sdks/typescript/pmxt/args.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { RequestOptions } from "./models.js"; - -export function buildArgsWithOptionalOptions( - primary?: any, - options?: RequestOptions, -): any[] { - if (options !== undefined) { - return [primary ?? null, options]; - } - return primary !== undefined ? [primary] : []; -} - -export function withTrailingOptions( - args: any[], - options: RequestOptions | undefined, - optionalArgCount: number, -): any[] { - if (options === undefined) { - return args; - } - - while (args.length < optionalArgCount) { - args.push(null); - } - - args.push(options); - return args; -} - diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index ae553f0..df892f6 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -34,11 +34,9 @@ import { MarketFilterFunction, EventFilterCriteria, EventFilterFunction, - RequestOptions, } from "./models.js"; import { ServerManager } from "./server-manager.js"; -import { buildArgsWithOptionalOptions, withTrailingOptions } from "./args.js"; // Converter functions function convertMarket(raw: any): UnifiedMarket { @@ -381,10 +379,11 @@ export abstract class Exchange { } } - async fetchMarkets(params?: any, options?: RequestOptions): Promise { + async fetchMarkets(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarkets`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -402,10 +401,11 @@ export abstract class Exchange { } } - async fetchMarketsPaginated(params?: any, options?: RequestOptions): Promise { + async fetchMarketsPaginated(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarketsPaginated`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -427,10 +427,11 @@ export abstract class Exchange { } } - async fetchEvents(params?: any, options?: RequestOptions): Promise { + async fetchEvents(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchEvents`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -448,10 +449,11 @@ export abstract class Exchange { } } - async fetchMarket(params?: any, options?: RequestOptions): Promise { + async fetchMarket(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarket`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -469,10 +471,11 @@ export abstract class Exchange { } } - async fetchEvent(params?: any, options?: RequestOptions): Promise { + async fetchEvent(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchEvent`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -490,10 +493,11 @@ export abstract class Exchange { } } - async fetchOrderBook(id: string, options?: RequestOptions): Promise { + async fetchOrderBook(id: string): Promise { await this.initPromise; try { - const args = withTrailingOptions([id], options, 2); + const args: any[] = []; + args.push(id); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchOrderBook`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -533,10 +537,11 @@ export abstract class Exchange { } } - async fetchOrder(orderId: string, options?: RequestOptions): Promise { + async fetchOrder(orderId: string): Promise { await this.initPromise; try { - const args = withTrailingOptions([orderId], options, 2); + const args: any[] = []; + args.push(orderId); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchOrder`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -554,10 +559,11 @@ export abstract class Exchange { } } - async fetchOpenOrders(marketId?: string, options?: RequestOptions): Promise { + async fetchOpenOrders(marketId?: string): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(marketId, options); + const args: any[] = []; + if (marketId !== undefined) args.push(marketId); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchOpenOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -575,10 +581,11 @@ export abstract class Exchange { } } - async fetchMyTrades(params?: any, options?: RequestOptions): Promise { + async fetchMyTrades(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMyTrades`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -596,10 +603,11 @@ export abstract class Exchange { } } - async fetchClosedOrders(params?: any, options?: RequestOptions): Promise { + async fetchClosedOrders(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchClosedOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -617,10 +625,11 @@ export abstract class Exchange { } } - async fetchAllOrders(params?: any, options?: RequestOptions): Promise { + async fetchAllOrders(params?: any): Promise { await this.initPromise; try { - const args = buildArgsWithOptionalOptions(params, options); + const args: any[] = []; + if (params !== undefined) args.push(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchAllOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -638,10 +647,10 @@ export abstract class Exchange { } } - async fetchPositions(options?: RequestOptions): Promise { + async fetchPositions(): Promise { await this.initPromise; try { - const args = options !== undefined ? [options] : []; + const args: any[] = []; const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchPositions`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -659,10 +668,10 @@ export abstract class Exchange { } } - async fetchBalance(options?: RequestOptions): Promise { + async fetchBalance(): Promise { await this.initPromise; try { - const args = options !== undefined ? [options] : []; + const args: any[] = []; const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchBalance`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -721,8 +730,7 @@ export abstract class Exchange { */ async fetchOHLCV( outcomeId: string, - params: any, - options?: RequestOptions + params: any ): Promise { await this.initPromise; try { @@ -738,7 +746,7 @@ export abstract class Exchange { } const requestBody: FetchOHLCVRequest = { - args: withTrailingOptions([outcomeId, paramsDict], options, 3), + args: [outcomeId, paramsDict], credentials: this.getCredentials() }; @@ -765,21 +773,17 @@ export abstract class Exchange { */ async fetchTrades( outcomeId: string, - params: any, - options?: RequestOptions + params: any ): Promise { await this.initPromise; try { - const paramsDict: any = {}; - if (params?.resolution !== undefined) { - paramsDict.resolution = params.resolution; - } - if (params?.limit !== undefined) { + const paramsDict: any = { resolution: params.resolution }; + if (params.limit) { paramsDict.limit = params.limit; } const requestBody: FetchTradesRequest = { - args: withTrailingOptions([outcomeId, paramsDict], options, 3), + args: [outcomeId, paramsDict], credentials: this.getCredentials() }; @@ -817,18 +821,13 @@ export abstract class Exchange { * } * ``` */ - async watchOrderBook( - outcomeId: string, - limit?: number, - options?: RequestOptions - ): Promise { + async watchOrderBook(outcomeId: string, limit?: number): Promise { await this.initPromise; try { const args: any[] = [outcomeId]; if (limit !== undefined) { args.push(limit); } - withTrailingOptions(args, options, 2); const requestBody: any = { args, @@ -872,8 +871,7 @@ export abstract class Exchange { async watchTrades( outcomeId: string, since?: number, - limit?: number, - options?: RequestOptions + limit?: number ): Promise { await this.initPromise; try { @@ -884,7 +882,6 @@ export abstract class Exchange { if (limit !== undefined) { args.push(limit); } - withTrailingOptions(args, options, 3); const requestBody: any = { args, diff --git a/sdks/typescript/pmxt/models.ts b/sdks/typescript/pmxt/models.ts index 4dce320..95db590 100644 --- a/sdks/typescript/pmxt/models.ts +++ b/sdks/typescript/pmxt/models.ts @@ -1,16 +1,9 @@ /** * Data models for PMXT TypeScript SDK. - * + * * These are clean TypeScript interfaces that provide a user-friendly API. */ - /** - * Request options for API calls. - */ -export interface RequestOptions { - mode?: 'raw'; -} - /** * A single tradeable outcome within a market. */ diff --git a/sdks/typescript/tests/client-args.test.ts b/sdks/typescript/tests/client-args.test.ts deleted file mode 100644 index 8b8dff0..0000000 --- a/sdks/typescript/tests/client-args.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, test, expect } from "@jest/globals"; -import { - buildArgsWithOptionalOptions, - withTrailingOptions, -} from "../pmxt/args"; - -describe("client args helpers", () => { - test("buildArgsWithOptionalOptions handles optional options and primary", () => { - expect(buildArgsWithOptionalOptions(undefined, undefined)).toEqual([]); - expect(buildArgsWithOptionalOptions({ q: 1 }, undefined)).toEqual([{ q: 1 }]); - expect(buildArgsWithOptionalOptions(undefined, { mode: "raw" })).toEqual([ - null, - { mode: "raw" }, - ]); - expect(buildArgsWithOptionalOptions({ q: 1 }, { mode: "raw" })).toEqual([ - { q: 1 }, - { mode: "raw" }, - ]); - }); - - test("withTrailingOptions pads missing optional args before options", () => { - expect(withTrailingOptions(["id"], undefined, 2)).toEqual(["id"]); - expect(withTrailingOptions(["id"], { mode: "raw" }, 2)).toEqual([ - "id", - null, - { mode: "raw" }, - ]); - expect(withTrailingOptions(["id", 10], { mode: "raw" }, 2)).toEqual([ - "id", - 10, - { mode: "raw" }, - ]); - expect(withTrailingOptions(["id", 100], { mode: "raw" }, 3)).toEqual([ - "id", - 100, - null, - { mode: "raw" }, - ]); - }); -}); From 9f7d09ec72046a523bf579593329c202257facf4 Mon Sep 17 00:00:00 2001 From: "Samuel EF. Tinnerholm" Date: Sun, 1 Mar 2026 11:49:54 +0200 Subject: [PATCH 11/13] refactor: centralize exchange price helpers and standardize arg building - Add kalshi/price.ts: fromKalshiCents, invertKalshiCents, invertKalshiUnified - Add baozi/price.ts: clampBaoziPrice, normalizeBaoziOutcomes - Add myriad/price.ts: resolveMyriadPrice - Update utils/exchange files to use the helpers instead of inline math - Add args.ts to TypeScript SDK: buildArgsWithOptionalOptions helper - Update client.ts to use the args helper consistently - Improve Python SDK typing for since/until params (Optional[Union[datetime, int, float, str]] instead of Optional[Any]) - Add unit tests for all price helpers and args helper --- core/src/exchanges/baozi/price.test.ts | 39 +++++++++++++++++++++ core/src/exchanges/baozi/price.ts | 16 +++++++++ core/src/exchanges/baozi/utils.ts | 14 +++----- core/src/exchanges/kalshi/fetchOHLCV.ts | 9 ++--- core/src/exchanges/kalshi/fetchOrderBook.ts | 9 ++--- core/src/exchanges/kalshi/fetchTrades.ts | 3 +- core/src/exchanges/kalshi/index.ts | 13 +++---- core/src/exchanges/kalshi/price.test.ts | 26 ++++++++++++++ core/src/exchanges/kalshi/price.ts | 11 ++++++ core/src/exchanges/kalshi/utils.ts | 9 ++--- core/src/exchanges/myriad/index.ts | 7 ++-- core/src/exchanges/myriad/price.test.ts | 20 +++++++++++ core/src/exchanges/myriad/price.ts | 4 +++ sdks/python/pmxt/client.py | 10 +++--- sdks/typescript/pmxt/args.ts | 3 ++ sdks/typescript/pmxt/client.ts | 28 ++++++--------- sdks/typescript/tests/client-args.test.ts | 28 +++++++++++++++ 17 files changed, 195 insertions(+), 54 deletions(-) create mode 100644 core/src/exchanges/baozi/price.test.ts create mode 100644 core/src/exchanges/baozi/price.ts create mode 100644 core/src/exchanges/kalshi/price.test.ts create mode 100644 core/src/exchanges/kalshi/price.ts create mode 100644 core/src/exchanges/myriad/price.test.ts create mode 100644 core/src/exchanges/myriad/price.ts create mode 100644 sdks/typescript/pmxt/args.ts create mode 100644 sdks/typescript/tests/client-args.test.ts diff --git a/core/src/exchanges/baozi/price.test.ts b/core/src/exchanges/baozi/price.test.ts new file mode 100644 index 0000000..b091237 --- /dev/null +++ b/core/src/exchanges/baozi/price.test.ts @@ -0,0 +1,39 @@ +import { describe, test, expect } from "vitest"; +import { clampBaoziPrice, normalizeBaoziOutcomes } from "./price"; +import { MarketOutcome } from "../../types"; + +describe("clampBaoziPrice", () => { + test("clamps values below 0 to 0", () => { + expect(clampBaoziPrice(-0.1)).toBe(0); + }); + + test("clamps values above 1 to 1", () => { + expect(clampBaoziPrice(1.2)).toBe(1); + }); + + test("leaves values in range unchanged", () => { + expect(clampBaoziPrice(0.3)).toBe(0.3); + expect(clampBaoziPrice(0)).toBe(0); + expect(clampBaoziPrice(1)).toBe(1); + }); +}); + +describe("normalizeBaoziOutcomes", () => { + function makeOutcome(price: number): MarketOutcome { + return { outcomeId: "x", marketId: "m", label: "X", price }; + } + + test("normalizes prices to sum to 1", () => { + const outcomes = [makeOutcome(2), makeOutcome(3)]; + normalizeBaoziOutcomes(outcomes); + expect(outcomes[0].price).toBeCloseTo(0.4); + expect(outcomes[1].price).toBeCloseTo(0.6); + }); + + test("does nothing when sum is zero", () => { + const outcomes = [makeOutcome(0), makeOutcome(0)]; + normalizeBaoziOutcomes(outcomes); + expect(outcomes[0].price).toBe(0); + expect(outcomes[1].price).toBe(0); + }); +}); diff --git a/core/src/exchanges/baozi/price.ts b/core/src/exchanges/baozi/price.ts new file mode 100644 index 0000000..41f0022 --- /dev/null +++ b/core/src/exchanges/baozi/price.ts @@ -0,0 +1,16 @@ +import { MarketOutcome } from "../../types"; + +export function clampBaoziPrice(value: number): number { + return Math.min(Math.max(value, 0), 1); +} + +export function normalizeBaoziOutcomes(outcomes: MarketOutcome[]): void { + const sum = outcomes.reduce((acc, item) => acc + item.price, 0); + if (sum <= 0) { + return; + } + + for (const outcome of outcomes) { + outcome.price = outcome.price / sum; + } +} diff --git a/core/src/exchanges/baozi/utils.ts b/core/src/exchanges/baozi/utils.ts index a55acaf..30a78ed 100644 --- a/core/src/exchanges/baozi/utils.ts +++ b/core/src/exchanges/baozi/utils.ts @@ -3,6 +3,7 @@ import bs58 from 'bs58'; import { createHash } from 'crypto'; import { UnifiedMarket, MarketOutcome } from '../../types'; import { addBinaryOutcomes } from '../../utils/market-utils'; +import { clampBaoziPrice, normalizeBaoziOutcomes } from './price'; // --------------------------------------------------------------------------- // Constants @@ -387,13 +388,13 @@ export function mapBooleanToUnified(market: BaoziMarket, pubkey: string): Unifie outcomeId: `${pubkey}-YES`, marketId: pubkey, label: 'Yes', - price: yesPrice, + price: clampBaoziPrice(yesPrice), }, { outcomeId: `${pubkey}-NO`, marketId: pubkey, label: 'No', - price: noPrice, + price: clampBaoziPrice(noPrice), }, ]; @@ -431,17 +432,12 @@ export function mapRaceToUnified(market: BaoziRaceMarket, pubkey: string): Unifi outcomeId: `${pubkey}-${i}`, marketId: pubkey, label: market.outcomeLabels[i] || `Outcome ${i + 1}`, - price: Math.min(Math.max(price, 0), 1), + price: clampBaoziPrice(price), }); } // Normalize prices to sum to 1 - const priceSum = outcomes.reduce((s, o) => s + o.price, 0); - if (priceSum > 0) { - for (const o of outcomes) { - o.price = o.price / priceSum; - } - } + normalizeBaoziOutcomes(outcomes); const um: UnifiedMarket = { marketId: pubkey, diff --git a/core/src/exchanges/kalshi/fetchOHLCV.ts b/core/src/exchanges/kalshi/fetchOHLCV.ts index 4e8cdd3..1a2f88f 100644 --- a/core/src/exchanges/kalshi/fetchOHLCV.ts +++ b/core/src/exchanges/kalshi/fetchOHLCV.ts @@ -3,6 +3,7 @@ import { PriceCandle } from "../../types"; import { mapIntervalToKalshi } from "./utils"; import { validateIdFormat } from "../../utils/validation"; import { kalshiErrorMapper } from "./errors"; +import { fromKalshiCents } from "./price"; export async function fetchOHLCV( id: string, @@ -98,10 +99,10 @@ export async function fetchOHLCV( return { timestamp: c.end_period_ts * 1000, - open: getVal("open") / 100, - high: getVal("high") / 100, - low: getVal("low") / 100, - close: getVal("close") / 100, + open: fromKalshiCents(getVal("open")), + high: fromKalshiCents(getVal("high")), + low: fromKalshiCents(getVal("low")), + close: fromKalshiCents(getVal("close")), volume: c.volume || 0, }; }); diff --git a/core/src/exchanges/kalshi/fetchOrderBook.ts b/core/src/exchanges/kalshi/fetchOrderBook.ts index 2fd5881..25cc24f 100644 --- a/core/src/exchanges/kalshi/fetchOrderBook.ts +++ b/core/src/exchanges/kalshi/fetchOrderBook.ts @@ -3,6 +3,7 @@ import { OrderBook } from "../../types"; import { validateIdFormat } from "../../utils/validation"; import { kalshiErrorMapper } from "./errors"; import { getMarketsUrl } from "./config"; +import { fromKalshiCents, invertKalshiCents } from "./price"; export async function fetchOrderBook( baseUrl: string, @@ -31,12 +32,12 @@ export async function fetchOrderBook( // - Bids: people buying NO (use data.no directly) // - Asks: people selling NO = people buying YES (invert data.yes) bids = (data.no || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0]), size: level[1], })); asks = (data.yes || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, // Invert YES price to get NO ask price + price: invertKalshiCents(level[0]), // Invert YES price to get NO ask price size: level[1], })); } else { @@ -44,12 +45,12 @@ export async function fetchOrderBook( // - Bids: people buying YES (use data.yes directly) // - Asks: people selling YES = people buying NO (invert data.no) bids = (data.yes || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0]), size: level[1], })); asks = (data.no || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, // Invert NO price to get YES ask price + price: invertKalshiCents(level[0]), // Invert NO price to get YES ask price size: level[1], })); } diff --git a/core/src/exchanges/kalshi/fetchTrades.ts b/core/src/exchanges/kalshi/fetchTrades.ts index ef1fd44..335eec8 100644 --- a/core/src/exchanges/kalshi/fetchTrades.ts +++ b/core/src/exchanges/kalshi/fetchTrades.ts @@ -3,6 +3,7 @@ import { HistoryFilterParams, TradesParams } from "../../BaseExchange"; import { Trade } from "../../types"; import { kalshiErrorMapper } from "./errors"; import { getMarketsUrl } from "./config"; +import { fromKalshiCents } from "./price"; export async function fetchTrades( baseUrl: string, @@ -23,7 +24,7 @@ export async function fetchTrades( return trades.map((t: any) => ({ id: t.trade_id, timestamp: new Date(t.created_time).getTime(), - price: t.yes_price / 100, + price: fromKalshiCents(t.yes_price), amount: t.count, side: t.taker_side === "yes" ? "buy" : "sell", })); diff --git a/core/src/exchanges/kalshi/index.ts b/core/src/exchanges/kalshi/index.ts index f6ae1a4..5951e8a 100644 --- a/core/src/exchanges/kalshi/index.ts +++ b/core/src/exchanges/kalshi/index.ts @@ -32,6 +32,7 @@ import { AuthenticationError } from "../../errors"; import { parseOpenApiSpec } from "../../utils/openapi"; import { kalshiApiSpec } from "./api"; import { getKalshiConfig, KalshiApiConfig, KALSHI_PATHS } from "./config"; +import { fromKalshiCents, invertKalshiCents } from "./price"; // Re-export for external use export type { KalshiWebSocketConfig }; @@ -176,20 +177,20 @@ export class KalshiExchange extends PredictionMarketExchange { if (isNoOutcome) { bids = (data.no || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0]), size: level[1], })); asks = (data.yes || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, + price: invertKalshiCents(level[0]), size: level[1], })); } else { bids = (data.yes || []).map((level: number[]) => ({ - price: level[0] / 100, + price: fromKalshiCents(level[0]), size: level[1], })); asks = (data.no || []).map((level: number[]) => ({ - price: 1 - level[0] / 100, + price: invertKalshiCents(level[0]), size: level[1], })); } @@ -219,7 +220,7 @@ export class KalshiExchange extends PredictionMarketExchange { return trades.map((t: any) => ({ id: t.trade_id, timestamp: new Date(t.created_time).getTime(), - price: t.yes_price / 100, + price: fromKalshiCents(t.yes_price), amount: t.count, side: t.taker_side === "yes" ? "buy" : "sell", })); @@ -326,7 +327,7 @@ export class KalshiExchange extends PredictionMarketExchange { return (data.fills || []).map((f: any) => ({ id: f.fill_id, timestamp: new Date(f.created_time).getTime(), - price: f.yes_price / 100, + price: fromKalshiCents(f.yes_price), amount: f.count, side: f.side === "yes" ? ("buy" as const) : ("sell" as const), orderId: f.order_id, diff --git a/core/src/exchanges/kalshi/price.test.ts b/core/src/exchanges/kalshi/price.test.ts new file mode 100644 index 0000000..3e27a5a --- /dev/null +++ b/core/src/exchanges/kalshi/price.test.ts @@ -0,0 +1,26 @@ +import { describe, test, expect } from "vitest"; +import { fromKalshiCents, invertKalshiCents, invertKalshiUnified } from "./price"; + +describe("fromKalshiCents", () => { + test("converts cents to a decimal probability", () => { + expect(fromKalshiCents(55)).toBe(0.55); + expect(fromKalshiCents(0)).toBe(0); + expect(fromKalshiCents(100)).toBe(1); + }); +}); + +describe("invertKalshiCents", () => { + test("returns the complement of a cent value", () => { + expect(invertKalshiCents(45)).toBeCloseTo(0.55); + expect(invertKalshiCents(0)).toBe(1); + expect(invertKalshiCents(100)).toBe(0); + }); +}); + +describe("invertKalshiUnified", () => { + test("returns the complement of a normalized price", () => { + expect(invertKalshiUnified(0.45)).toBeCloseTo(0.55); + expect(invertKalshiUnified(0)).toBe(1); + expect(invertKalshiUnified(1)).toBe(0); + }); +}); diff --git a/core/src/exchanges/kalshi/price.ts b/core/src/exchanges/kalshi/price.ts new file mode 100644 index 0000000..3797172 --- /dev/null +++ b/core/src/exchanges/kalshi/price.ts @@ -0,0 +1,11 @@ +export function fromKalshiCents(priceInCents: number): number { + return priceInCents / 100; +} + +export function invertKalshiCents(priceInCents: number): number { + return 1 - priceInCents / 100; +} + +export function invertKalshiUnified(price: number): number { + return 1 - price; +} diff --git a/core/src/exchanges/kalshi/utils.ts b/core/src/exchanges/kalshi/utils.ts index e0e589c..4c38958 100644 --- a/core/src/exchanges/kalshi/utils.ts +++ b/core/src/exchanges/kalshi/utils.ts @@ -1,5 +1,6 @@ import { UnifiedMarket, MarketOutcome, CandleInterval } from "../../types"; import { addBinaryOutcomes } from "../../utils/market-utils"; +import { fromKalshiCents, invertKalshiUnified } from "./price"; export function mapMarketToUnified( event: any, @@ -10,11 +11,11 @@ export function mapMarketToUnified( // Calculate price let price = 0.5; if (market.last_price) { - price = market.last_price / 100; + price = fromKalshiCents(market.last_price); } else if (market.yes_ask && market.yes_bid) { - price = (market.yes_ask + market.yes_bid) / 200; + price = (fromKalshiCents(market.yes_ask) + fromKalshiCents(market.yes_bid)) / 2; } else if (market.yes_ask) { - price = market.yes_ask / 100; + price = fromKalshiCents(market.yes_ask); } // Extract candidate name @@ -44,7 +45,7 @@ export function mapMarketToUnified( outcomeId: `${market.ticker}-NO`, marketId: market.ticker, label: candidateName ? `Not ${candidateName}` : "No", - price: 1 - price, + price: invertKalshiUnified(price), priceChange24h: -priceChange, // Inverse change for No? simplified assumption }, ]; diff --git a/core/src/exchanges/myriad/index.ts b/core/src/exchanges/myriad/index.ts index b16f912..6b9a82f 100644 --- a/core/src/exchanges/myriad/index.ts +++ b/core/src/exchanges/myriad/index.ts @@ -11,6 +11,7 @@ import { AuthenticationError } from '../../errors'; import { BASE_URL } from './utils'; import { parseOpenApiSpec } from '../../utils/openapi'; import { myriadApiSpec } from './api'; +import { resolveMyriadPrice } from './price'; export class MyriadExchange extends PredictionMarketExchange { override readonly has = { @@ -140,7 +141,7 @@ export class MyriadExchange extends PredictionMarketExchange { return filtered.map((t: any, index: number) => ({ id: `${t.blockNumber || t.timestamp}-${index}`, timestamp: (t.timestamp || 0) * 1000, - price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, + price: resolveMyriadPrice(t), amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); @@ -169,7 +170,7 @@ export class MyriadExchange extends PredictionMarketExchange { return tradeEvents.map((t: any, i: number) => ({ id: `${t.blockNumber || t.timestamp}-${i}`, timestamp: (t.timestamp || 0) * 1000, - price: t.shares > 0 ? Number(t.value) / Number(t.shares) : 0, + price: resolveMyriadPrice(t), amount: Number(t.shares || 0), side: t.action === 'buy' ? 'buy' as const : 'sell' as const, })); @@ -256,7 +257,7 @@ export class MyriadExchange extends PredictionMarketExchange { outcomeLabel: pos.outcomeTitle || `Outcome ${pos.outcomeId}`, size: Number(pos.shares || 0), entryPrice: Number(pos.price || 0), - currentPrice: Number(pos.value || 0) / Math.max(Number(pos.shares || 1), 1), + currentPrice: resolveMyriadPrice(pos), unrealizedPnL: Number(pos.profit || 0), })); } diff --git a/core/src/exchanges/myriad/price.test.ts b/core/src/exchanges/myriad/price.test.ts new file mode 100644 index 0000000..ad1612b --- /dev/null +++ b/core/src/exchanges/myriad/price.test.ts @@ -0,0 +1,20 @@ +import { describe, test, expect } from "vitest"; +import { resolveMyriadPrice } from "./price"; + +describe("resolveMyriadPrice", () => { + test("divides value by shares", () => { + expect(resolveMyriadPrice({ value: 100, shares: 4 })).toBe(25); + }); + + test("treats missing shares as 1", () => { + expect(resolveMyriadPrice({ value: 50 })).toBe(50); + }); + + test("treats zero shares as 1 to avoid division by zero", () => { + expect(resolveMyriadPrice({ value: 80, shares: 0 })).toBe(80); + }); + + test("treats missing value as 0", () => { + expect(resolveMyriadPrice({ shares: 5 })).toBe(0); + }); +}); diff --git a/core/src/exchanges/myriad/price.ts b/core/src/exchanges/myriad/price.ts new file mode 100644 index 0000000..676e440 --- /dev/null +++ b/core/src/exchanges/myriad/price.ts @@ -0,0 +1,4 @@ +export function resolveMyriadPrice(event: any): number { + const shares = Math.max(Number(event.shares || 1), 1); + return Number(event.value || 0) / shares; +} diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index de3244e..6db3848 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -1452,7 +1452,7 @@ def fetch_my_trades( self, outcome_id: Optional[str] = None, market_id: Optional[str] = None, - since: Optional[Any] = None, + since: Optional[Union[datetime, int, float, str]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, ) -> List[UserTrade]: @@ -1489,8 +1489,8 @@ def fetch_my_trades( def fetch_closed_orders( self, market_id: Optional[str] = None, - since: Optional[Any] = None, - until: Optional[Any] = None, + since: Optional[Union[datetime, int, float, str]] = None, + until: Optional[Union[datetime, int, float, str]] = None, limit: Optional[int] = None, ) -> List[Order]: """ @@ -1523,8 +1523,8 @@ def fetch_closed_orders( def fetch_all_orders( self, market_id: Optional[str] = None, - since: Optional[Any] = None, - until: Optional[Any] = None, + since: Optional[Union[datetime, int, float, str]] = None, + until: Optional[Union[datetime, int, float, str]] = None, limit: Optional[int] = None, ) -> List[Order]: """ diff --git a/sdks/typescript/pmxt/args.ts b/sdks/typescript/pmxt/args.ts new file mode 100644 index 0000000..cd56982 --- /dev/null +++ b/sdks/typescript/pmxt/args.ts @@ -0,0 +1,3 @@ +export function buildArgsWithOptionalOptions(primary?: any): any[] { + return primary !== undefined ? [primary] : []; +} diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index df892f6..ffa987d 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -37,6 +37,7 @@ import { } from "./models.js"; import { ServerManager } from "./server-manager.js"; +import { buildArgsWithOptionalOptions } from "./args.js"; // Converter functions function convertMarket(raw: any): UnifiedMarket { @@ -382,8 +383,7 @@ export abstract class Exchange { async fetchMarkets(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarkets`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -404,8 +404,7 @@ export abstract class Exchange { async fetchMarketsPaginated(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarketsPaginated`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -430,8 +429,7 @@ export abstract class Exchange { async fetchEvents(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchEvents`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -452,8 +450,7 @@ export abstract class Exchange { async fetchMarket(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMarket`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -474,8 +471,7 @@ export abstract class Exchange { async fetchEvent(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchEvent`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -562,8 +558,7 @@ export abstract class Exchange { async fetchOpenOrders(marketId?: string): Promise { await this.initPromise; try { - const args: any[] = []; - if (marketId !== undefined) args.push(marketId); + const args = buildArgsWithOptionalOptions(marketId); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchOpenOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -584,8 +579,7 @@ export abstract class Exchange { async fetchMyTrades(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchMyTrades`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -606,8 +600,7 @@ export abstract class Exchange { async fetchClosedOrders(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchClosedOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, @@ -628,8 +621,7 @@ export abstract class Exchange { async fetchAllOrders(params?: any): Promise { await this.initPromise; try { - const args: any[] = []; - if (params !== undefined) args.push(params); + const args = buildArgsWithOptionalOptions(params); const response = await fetch(`${this.config.basePath}/api/${this.exchangeName}/fetchAllOrders`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers }, diff --git a/sdks/typescript/tests/client-args.test.ts b/sdks/typescript/tests/client-args.test.ts new file mode 100644 index 0000000..08df6c0 --- /dev/null +++ b/sdks/typescript/tests/client-args.test.ts @@ -0,0 +1,28 @@ +import { describe, test, expect } from "vitest"; +import { buildArgsWithOptionalOptions } from "../pmxt/args.js"; + +describe("buildArgsWithOptionalOptions", () => { + test("returns empty array when primary is undefined", () => { + expect(buildArgsWithOptionalOptions(undefined)).toEqual([]); + }); + + test("returns empty array when called with no arguments", () => { + expect(buildArgsWithOptionalOptions()).toEqual([]); + }); + + test("wraps a defined primary value in an array", () => { + expect(buildArgsWithOptionalOptions({ limit: 10 })).toEqual([{ limit: 10 }]); + }); + + test("treats null as a defined value", () => { + expect(buildArgsWithOptionalOptions(null)).toEqual([null]); + }); + + test("treats 0 as a defined value", () => { + expect(buildArgsWithOptionalOptions(0)).toEqual([0]); + }); + + test("treats empty string as a defined value", () => { + expect(buildArgsWithOptionalOptions("")).toEqual([""]); + }); +}); From 2e31c9b6dccdb7f89fef148ae945ce80674a226e Mon Sep 17 00:00:00 2001 From: "Samuel EF. Tinnerholm" Date: Sun, 1 Mar 2026 11:50:30 +0200 Subject: [PATCH 12/13] Delete test-load-markets.ts --- examples/test-load-markets.ts | 105 ---------------------------------- 1 file changed, 105 deletions(-) delete mode 100644 examples/test-load-markets.ts diff --git a/examples/test-load-markets.ts b/examples/test-load-markets.ts deleted file mode 100644 index 1f8c8e4..0000000 --- a/examples/test-load-markets.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Polymarket, Kalshi, Limitless, Myriad, Probable, Baozi } from '../core/src/index'; -import * as dotenv from 'dotenv'; -import path from 'path'; - -// Load .env -dotenv.config({ path: path.join(__dirname, '../.env') }); - -const exchanges: any[] = []; - -// Initialize exchanges we have keys for (or just public if keys not needed for fetchMarkets) - -// 1. Polymarket -exchanges.push(new Polymarket({ - privateKey: process.env.POLYMARKET_PRIVATE_KEY -})); - -// 2. Kalshi -exchanges.push(new Kalshi({ - apiKey: process.env.KALSHI_API_KEY, - privateKey: process.env.KALSHI_PRIVATE_KEY -})); - -// 3. Limitless -exchanges.push(new Limitless({ - apiKey: process.env.LIMITLESS_API_KEY, - privateKey: process.env.LIMITLESS_PRIVATE_KEY -})); - -// 4. Myriad -exchanges.push(new Myriad({ - apiKey: process.env.MYRIAD_PROD // Assuming prod key -})); - -// 5. Probable -exchanges.push(new Probable()); - -// 6. Baozi -exchanges.push(new Baozi({ - privateKey: process.env.BAOZI_PRIVATE_KEY -})); - - -async function testLoadMarkets() { - console.log('Testing loadMarkets on all exchanges...'); - - for (const exchange of exchanges) { - console.log(`\n---------------------------------------------------------`); - console.log(`Testing ${exchange.name}...`); - - try { - console.log(`[${exchange.name}] .loadedMarkets before: ${exchange.loadedMarkets}`); - const start = Date.now(); - - // 1. Load Markets - const markets = await exchange.loadMarkets(); - const duration = Date.now() - start; - - const count = Object.keys(markets).length; - const slugCount = Object.keys(exchange.marketsBySlug).length; - - console.log(`[${exchange.name}] Loaded ${count} markets in ${duration}ms`); - console.log(`[${exchange.name}] .loadedMarkets after: ${exchange.loadedMarkets}`); - console.log(`[${exchange.name}] Cached by Slug: ${slugCount}`); - - if (count > 0) { - // Verify structure - const firstId = Object.keys(markets)[0]; - const firstMarket = markets[firstId]; - console.log(`[${exchange.name}] First Market: ${firstMarket.title} (ID: ${firstMarket.marketId})`); - - // 2. Test Cached fetchMarket (by ID) - const cachedStart = Date.now(); - const cachedMarket = await exchange.fetchMarket({ marketId: firstId }); - const cachedDuration = Date.now() - cachedStart; - - if (cachedMarket.marketId === firstId && cachedDuration < 10) { - console.log(`[${exchange.name}] ✅ Cached fetchMarket(ID) working (${cachedDuration}ms)`); - } else { - console.warn(`[${exchange.name}] ⚠️ Cached fetchMarket(ID) took ${cachedDuration}ms (expected <10ms)`); - } - - // 3. Test Cached fetchMarket (by Slug) - if supported - if (firstMarket.slug && slugCount > 0) { - // Note: Some exchanges like Polymarket might not populate slugs consistently or use them for lookup - // But if it's in the cache, it should work. - try { - const slugStart = Date.now(); - const slugMarket = await exchange.fetchMarket({ slug: firstMarket.slug }); - const slugDuration = Date.now() - slugStart; - console.log(`[${exchange.name}] ✅ Cached fetchMarket(Slug) working (${slugDuration}ms)`); - } catch (e) { - console.warn(`[${exchange.name}] ⚠️ fetchMarket(Slug) failed: ${e.message}`); - } - } - } else { - console.warn(`[${exchange.name}] ⚠️ Returns 0 markets. Is the API down or keys invalid?`); - } - - } catch (error: any) { - console.error(`[${exchange.name}] ❌ FAILED: ${error.message}`); - } - } -} - -testLoadMarkets(); From 6bbb84e6dc68f3c6dfd47bf448ba15bf421fb071 Mon Sep 17 00:00:00 2001 From: "Samuel EF. Tinnerholm" Date: Sun, 1 Mar 2026 11:54:06 +0200 Subject: [PATCH 13/13] fix(tests): replace vitest imports with Jest globals in price tests --- core/src/exchanges/baozi/price.test.ts | 1 - core/src/exchanges/kalshi/price.test.ts | 1 - core/src/exchanges/myriad/price.test.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/core/src/exchanges/baozi/price.test.ts b/core/src/exchanges/baozi/price.test.ts index b091237..50940fa 100644 --- a/core/src/exchanges/baozi/price.test.ts +++ b/core/src/exchanges/baozi/price.test.ts @@ -1,4 +1,3 @@ -import { describe, test, expect } from "vitest"; import { clampBaoziPrice, normalizeBaoziOutcomes } from "./price"; import { MarketOutcome } from "../../types"; diff --git a/core/src/exchanges/kalshi/price.test.ts b/core/src/exchanges/kalshi/price.test.ts index 3e27a5a..e24f350 100644 --- a/core/src/exchanges/kalshi/price.test.ts +++ b/core/src/exchanges/kalshi/price.test.ts @@ -1,4 +1,3 @@ -import { describe, test, expect } from "vitest"; import { fromKalshiCents, invertKalshiCents, invertKalshiUnified } from "./price"; describe("fromKalshiCents", () => { diff --git a/core/src/exchanges/myriad/price.test.ts b/core/src/exchanges/myriad/price.test.ts index ad1612b..85b31be 100644 --- a/core/src/exchanges/myriad/price.test.ts +++ b/core/src/exchanges/myriad/price.test.ts @@ -1,4 +1,3 @@ -import { describe, test, expect } from "vitest"; import { resolveMyriadPrice } from "./price"; describe("resolveMyriadPrice", () => {