Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions core/src/exchanges/gemini-titan/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,24 @@ export class GeminiFetcher implements IExchangeFetcher<GeminiRawEvent, GeminiRaw
const maxResults = params.limit ?? 250000;

while (allEvents.length < maxResults) {
const queryParams: Record<string, string> = {
const queryParams: Record<string, string | string[]> = {
limit: String(Math.min(pageSize, maxResults - allEvents.length)),
offset: String(offset),
};

if (params.status && params.status !== 'all') {
queryParams.status = params.status === 'active' ? 'active' : params.status;
} else if (!params.status) {
const status = params.status as string | string[] | undefined;
if (Array.isArray(status)) {
const statuses = status.filter(s => s !== 'all');
if (statuses.length > 0) queryParams.status = statuses;
} else if (status && status !== 'all') {
queryParams.status = status === 'active' ? 'active' : status;
} else if (!status) {
queryParams.status = 'active';
}

if (params.category) {
queryParams.category = params.category;
const category = params.category as string | string[] | undefined;
if (category) {
queryParams.category = category;
}

if (params.query) {
Expand Down Expand Up @@ -171,12 +176,18 @@ export class GeminiFetcher implements IExchangeFetcher<GeminiRawEvent, GeminiRaw

// -- HTTP helpers ----------------------------------------------------------

private async get<T>(path: string, params?: Record<string, string>): Promise<T> {
private async get<T>(path: string, params?: Record<string, string | string[]>): Promise<T> {
try {
const url = new URL(path, this.baseUrl);
if (params) {
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
if (Array.isArray(value)) {
for (const item of value) {
url.searchParams.append(key, item);
}
} else {
url.searchParams.set(key, value);
}
}
}
const response = await this.ctx.http.get(url.toString());
Expand Down
4 changes: 3 additions & 1 deletion core/src/exchanges/gemini-titan/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,9 @@ export class GeminiNormalizer implements IExchangeNormalizer<GeminiRawEvent, Gem
size,
entryPrice,
currentPrice,
unrealizedPnL: (currentPrice - entryPrice) * size,
unrealizedPnL: raw.unrealizedPnl != null
? parseFloat(raw.unrealizedPnl)
: (currentPrice - entryPrice) * size,
};
}
}
3 changes: 3 additions & 0 deletions core/src/exchanges/gemini-titan/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ export interface GeminiRawPosition {
isAboveAutoStartThreshold?: boolean;
isLive?: boolean;
realizedPl?: string;
marketValue?: string;
unrealizedPnl?: string;
unrealizedPct?: number;
}

export interface GeminiRawActiveOrdersResponse {
Expand Down
10 changes: 8 additions & 2 deletions core/src/exchanges/hyperliquid/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export interface HyperliquidRawOutcome {
name: string; // e.g. "BTC > $100K @ 2026-05-09 06:00 UTC"
description: string; // pipe-delimited contract spec
sideSpecs: HyperliquidRawSideSpec[];
quoteToken: string; // settlement currency, e.g. "USDC"
}

export interface HyperliquidRawQuestion {
Expand Down Expand Up @@ -106,14 +105,19 @@ export interface HyperliquidRawOpenOrder {
export interface HyperliquidRawPosition {
coin: string;
entryPx: string | null;
leverage: { type: string; value: number };
leverage: { type: string; value: number; rawUsd?: string };
liquidationPx: string | null;
marginUsed: string;
maxTradeSzs: [string, string];
positionValue: string;
returnOnEquity: string;
szi: string;
unrealizedPnl: string;
cumFunding?: {
allTime?: string;
sinceChange?: string;
sinceOpen?: string;
};
}

export interface HyperliquidRawUserState {
Expand All @@ -133,6 +137,8 @@ export interface HyperliquidRawUserState {
totalNtlPos: string;
totalRawUsd: string;
};
crossMaintenanceMarginUsed?: string;
time?: number;
withdrawable: string;
}

Expand Down
10 changes: 10 additions & 0 deletions core/src/exchanges/kalshi/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export interface KalshiRawMarket {
last_price_dollars?: string;
yes_ask_dollars?: string;
yes_bid_dollars?: string;
no_ask_dollars?: string;
no_bid_dollars?: string;
response_price_units?: string;
market_type?: string;
mve_collection_ticker?: string;
mve_selected_legs?: Array<{
event_ticker: string;
market_ticker: string;
side: string;
}>;
yes_ask_size_fp?: string;
yes_bid_size_fp?: string;
rules_primary?: string;
Expand Down
33 changes: 26 additions & 7 deletions core/src/exchanges/kalshi/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@ const KALSHI_PROMOTED_MARKET_KEYS = [
'volume_24h_fp', 'volume_24h', 'volume', 'volume_fp',
'liquidity_dollars', 'liquidity', 'open_interest_fp', 'open_interest',
'status', 'last_price_dollars', 'previous_price_dollars',
'yes_ask_dollars', 'yes_bid_dollars', 'last_price', 'yes_ask', 'yes_bid',
'yes_ask_dollars', 'yes_bid_dollars', 'no_ask_dollars', 'no_bid_dollars',
'response_price_units', 'last_price', 'yes_ask', 'yes_bid',
] as const;

function parseKalshiPrice(value: string | number | undefined, responseUnits?: string): number | undefined {
if (value == null) return undefined;
const parsed = typeof value === 'number' ? value : parseFloat(value);
if (!Number.isFinite(parsed)) return undefined;
const normalizedUnits = responseUnits?.toLowerCase();
return normalizedUnits === 'cent' || normalizedUnits === 'cents' ? parsed / 100 : parsed;
}

export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, KalshiRawEvent> {

normalizeMarket(raw: KalshiRawEvent): UnifiedMarket | null {
Expand All @@ -47,13 +56,17 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal

// Kalshi API v2 migrated from cent integers to FixedPointDollars strings.
// Prefer the _dollars fields; fall back to deprecated cent fields.
const yesBid = parseKalshiPrice(market.yes_bid_dollars, market.response_price_units);
const yesAsk = parseKalshiPrice(market.yes_ask_dollars, market.response_price_units);
const noBid = parseKalshiPrice(market.no_bid_dollars, market.response_price_units);
const noAsk = parseKalshiPrice(market.no_ask_dollars, market.response_price_units);
let price = 0;
if (market.last_price_dollars != null) {
price = parseFloat(market.last_price_dollars);
} else if (market.yes_ask_dollars != null && market.yes_bid_dollars != null) {
price = (parseFloat(market.yes_ask_dollars) + parseFloat(market.yes_bid_dollars)) / 2;
} else if (market.yes_ask_dollars != null) {
price = parseFloat(market.yes_ask_dollars);
price = parseKalshiPrice(market.last_price_dollars, market.response_price_units) ?? 0;
} else if (yesAsk != null && yesBid != null) {
price = (yesAsk + yesBid) / 2;
} else if (yesAsk != null) {
price = yesAsk;
} else if (market.last_price) {
price = fromKalshiCents(market.last_price);
} else if (market.yes_ask && market.yes_bid) {
Expand All @@ -69,20 +82,26 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal
priceChange = parseFloat(market.last_price_dollars) - parseFloat(market.previous_price_dollars);
}

const noPrice = noAsk != null && noBid != null
? (noAsk + noBid) / 2
: noAsk ?? noBid ?? invertKalshiUnified(price);

const outcomes: MarketOutcome[] = [
{
outcomeId: market.ticker,
marketId: market.ticker,
label: candidateName || 'Yes',
price,
priceChange24h: priceChange,
metadata: { bid: yesBid, ask: yesAsk },
},
{
outcomeId: `${market.ticker}-NO`,
marketId: market.ticker,
label: candidateName ? `Not ${candidateName}` : 'No',
price: invertKalshiUnified(price),
price: noPrice,
priceChange24h: -priceChange,
metadata: { bid: noBid, ask: noAsk },
},
];

Expand Down
3 changes: 2 additions & 1 deletion core/src/exchanges/limitless/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export interface LimitlessRawMarket {
buy?: { market?: number[]; limit?: number[] };
sell?: { market?: number[]; limit?: number[] };
};
expirationTimestamp?: string;
expirationDate?: string;
expirationTimestamp?: number | string;
volumeFormatted?: number;
volume?: number;
logo?: string | null;
Expand Down
22 changes: 20 additions & 2 deletions core/src/exchanges/limitless/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const DEFAULT_LIMITLESS_API_URL = 'https://api.limitless.exchange';
const LIMITLESS_PROMOTED_MARKET_KEYS = [
'slug', 'title', 'question', 'description',
'tokens', 'prices',
'expirationTimestamp',
'expirationDate', 'expirationTimestamp',
'volumeFormatted', 'volume',
'logo',
'categories', 'tags',
Expand Down Expand Up @@ -111,7 +111,7 @@ export function mapMarketToUnified(market: any, context: LimitlessMarketContext
description: market.description || resolvedContext.eventDescription,
slug: market.slug,
outcomes: outcomes,
resolutionDate: market.expirationTimestamp ? new Date(market.expirationTimestamp) : new Date(),
resolutionDate: parseLimitlessExpiration(market),
volume24h: Number(market.volumeFormatted || 0),
volume: Number(market.volume || 0),
liquidity: 0, // Not directly in the flat market list
Expand All @@ -131,6 +131,24 @@ export function mapMarketToUnified(market: any, context: LimitlessMarketContext
return um;
}

function parseLimitlessExpiration(market: any): Date | undefined {
if (typeof market.expirationDate === 'string' && market.expirationDate.trim()) {
return new Date(market.expirationDate);
}
const rawTimestamp = market.expirationTimestamp;
if (typeof rawTimestamp === 'number' && Number.isFinite(rawTimestamp)) {
return new Date(rawTimestamp < 1e12 ? rawTimestamp * 1000 : rawTimestamp);
}
if (typeof rawTimestamp === 'string' && rawTimestamp.trim()) {
const numeric = Number(rawTimestamp);
if (Number.isFinite(numeric)) {
return new Date(numeric < 1e12 ? numeric * 1000 : numeric);
}
return new Date(rawTimestamp);
}
return new Date();
}

function getText(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
Expand Down
11 changes: 11 additions & 0 deletions core/src/exchanges/myriad/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,25 @@ export interface MyriadRawMarket {
id: number;
networkId: number;
title?: string;
shortName?: string;
description?: string;
slug?: string;
imageUrl?: string;
expiresAt?: string;
publishedAt?: string;
volume24h?: number;
volume?: number;
volumeNotional?: number;
volumeNotional24h?: number;
liquidity?: number;
eventId?: number;
outcomeIndex?: number;
tradingModel?: string;
negRisk?: boolean;
executionMode?: string;
oracle?: Record<string, unknown>;
topics?: string[];
tags?: string[];
outcomes?: MyriadRawOutcome[];
[key: string]: unknown;
}
Expand All @@ -42,6 +52,7 @@ export interface MyriadRawQuestion {
export interface MyriadRawTradeEvent {
action?: string;
blockNumber?: number;
txId?: string;
timestamp?: number;
value?: number;
shares?: number;
Expand Down
4 changes: 2 additions & 2 deletions core/src/exchanges/myriad/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export class MyriadNormalizer implements IExchangeNormalizer<MyriadRawMarket, My

normalizeTrade(raw: MyriadRawTradeEvent, index: number): Trade {
return {
id: `${raw.blockNumber || raw.timestamp}-${index}`,
id: raw.txId ?? `${raw.blockNumber || raw.timestamp}-${index}`,
timestamp: (raw.timestamp || 0) * 1000,
price: resolveMyriadPrice(raw),
amount: Number(raw.shares || 0),
Expand All @@ -246,7 +246,7 @@ export class MyriadNormalizer implements IExchangeNormalizer<MyriadRawMarket, My

normalizeUserTrade(raw: MyriadRawTradeEvent, index: number): UserTrade {
return {
id: `${raw.blockNumber || raw.timestamp}-${index}`,
id: raw.txId ?? `${raw.blockNumber || raw.timestamp}-${index}`,
timestamp: (raw.timestamp || 0) * 1000,
price: resolveMyriadPrice(raw),
amount: Number(raw.shares || 0),
Expand Down
2 changes: 2 additions & 0 deletions core/src/exchanges/polymarket/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export interface PolymarketRawOrderBook {
bids?: PolymarketRawOrderBookLevel[];
asks?: PolymarketRawOrderBookLevel[];
timestamp?: string | number;
neg_risk?: boolean;
last_trade_price?: string;
}

export interface PolymarketRawTrade {
Expand Down
6 changes: 6 additions & 0 deletions core/src/exchanges/polymarket/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ export class PolymarketNormalizer implements IExchangeNormalizer<PolymarketRawEv
bids,
asks,
timestamp: raw.timestamp ? (typeof raw.timestamp === 'string' ? (isFinite(Number(raw.timestamp)) ? Number(raw.timestamp) : new Date(raw.timestamp).getTime()) : Number(raw.timestamp)) : Date.now(),
isNegRisk: raw.neg_risk ?? false,
lastTradePrice: raw.last_trade_price != null ? parseFloat(raw.last_trade_price) : undefined,
sourceMetadata: buildSourceMetadata(
raw as unknown as Record<string, unknown>,
['asset_id', 'bids', 'asks', 'timestamp', 'neg_risk', 'last_trade_price'],
),
};
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/exchanges/probable/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class ProbableAuth {
credential,
});
} else {
const baseUrl = process.env.PROBABLE_BASE_URL || 'https://api.probable.markets/public/api/v1';
const baseUrl = process.env.PROBABLE_BASE_URL || 'https://market-api.probable.markets/public/api/v1';
this.clobClient = createClobClient({
chainId,
baseUrl,
Expand Down
4 changes: 2 additions & 2 deletions core/src/exchanges/probable/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface QueuedPromise<T> {
export interface ProbableWebSocketConfig {
/** WebSocket URL (default: wss://ws.probable.markets/public/api/v1) */
wsUrl?: string;
/** Base URL for the CLOB client (default: https://api.probable.markets/public/api/v1) */
/** Base URL for the CLOB client (default: https://market-api.probable.markets/public/api/v1) */
baseUrl?: string;
/** Chain ID (default: 56 for BSC mainnet) */
chainId?: number;
Expand Down Expand Up @@ -40,7 +40,7 @@ export class ProbableWebSocket {

const chainId = this.config.chainId || parseInt(process.env.PROBABLE_CHAIN_ID || String(PROBABLE_CHAIN_ID), 10);
const wsUrl = this.config.wsUrl || process.env.PROBABLE_WS_URL || 'wss://ws.probable.markets/public/api/v1';
const baseUrl = this.config.baseUrl || process.env.PROBABLE_BASE_URL || 'https://api.probable.markets/public/api/v1';
const baseUrl = this.config.baseUrl || process.env.PROBABLE_BASE_URL || 'https://market-api.probable.markets/public/api/v1';

// Dynamically import @prob/clob using eval to bypass TS compilation to require()
// which forces native import() usage, resolving ESM/CJS issues
Expand Down
10 changes: 10 additions & 0 deletions core/src/server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3187,6 +3187,16 @@ components:
datetime:
type: string
description: ISO 8601 datetime string of the snapshot (CCXT-compatible).
isNegRisk:
type: boolean
description: Whether the venue marks this snapshot as a negative-risk market.
lastTradePrice:
type: number
description: Last traded price from venues that include it with the book snapshot.
sourceMetadata:
type: object
additionalProperties: {}
description: Venue-specific metadata preserved from the raw order book snapshot.
required:
- bids
- asks
Expand Down
6 changes: 6 additions & 0 deletions core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ export interface OrderBook {
timestamp?: number;
/** ISO 8601 datetime string of the snapshot (CCXT-compatible). */
datetime?: string;
/** Whether the venue marks this snapshot as a negative-risk market. */
isNegRisk?: boolean;
/** Last traded price from venues that include it with the book snapshot. */
lastTradePrice?: number;
/** Venue-specific metadata preserved from the raw order book snapshot. */
sourceMetadata?: Record<string, unknown>;
}

export interface Trade {
Expand Down
Loading
Loading