Skip to content

Commit fc615f2

Browse files
authored
fix(core): capture response drift fields (#960)
* fix(core): capture response drift fields * fix(core): support gemini array event filters * chore(core): sync order book OpenAPI schema * docs: sync order book API references
1 parent 75a43b2 commit fc615f2

19 files changed

Lines changed: 150 additions & 26 deletions

File tree

core/src/exchanges/gemini-titan/fetcher.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,24 @@ export class GeminiFetcher implements IExchangeFetcher<GeminiRawEvent, GeminiRaw
4545
const maxResults = params.limit ?? 250000;
4646

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

53-
if (params.status && params.status !== 'all') {
54-
queryParams.status = params.status === 'active' ? 'active' : params.status;
55-
} else if (!params.status) {
53+
const status = params.status as string | string[] | undefined;
54+
if (Array.isArray(status)) {
55+
const statuses = status.filter(s => s !== 'all');
56+
if (statuses.length > 0) queryParams.status = statuses;
57+
} else if (status && status !== 'all') {
58+
queryParams.status = status === 'active' ? 'active' : status;
59+
} else if (!status) {
5660
queryParams.status = 'active';
5761
}
5862

59-
if (params.category) {
60-
queryParams.category = params.category;
63+
const category = params.category as string | string[] | undefined;
64+
if (category) {
65+
queryParams.category = category;
6166
}
6267

6368
if (params.query) {
@@ -192,12 +197,18 @@ export class GeminiFetcher implements IExchangeFetcher<GeminiRawEvent, GeminiRaw
192197

193198
// -- HTTP helpers ----------------------------------------------------------
194199

195-
private async get<T>(path: string, params?: Record<string, string>): Promise<T> {
200+
private async get<T>(path: string, params?: Record<string, string | string[]>): Promise<T> {
196201
try {
197202
const url = new URL(path, this.baseUrl);
198203
if (params) {
199204
for (const [key, value] of Object.entries(params)) {
200-
url.searchParams.set(key, value);
205+
if (Array.isArray(value)) {
206+
for (const item of value) {
207+
url.searchParams.append(key, item);
208+
}
209+
} else {
210+
url.searchParams.set(key, value);
211+
}
201212
}
202213
}
203214
const response = await this.ctx.http.get(url.toString());

core/src/exchanges/gemini-titan/normalizer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,9 @@ export class GeminiNormalizer implements IExchangeNormalizer<GeminiRawEvent, Gem
333333
size,
334334
entryPrice,
335335
currentPrice,
336-
unrealizedPnL: (currentPrice - entryPrice) * size,
336+
unrealizedPnL: raw.unrealizedPnl != null
337+
? parseFloat(raw.unrealizedPnl)
338+
: (currentPrice - entryPrice) * size,
337339
};
338340
}
339341
}

core/src/exchanges/gemini-titan/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ export interface GeminiRawPosition {
147147
isAboveAutoStartThreshold?: boolean;
148148
isLive?: boolean;
149149
realizedPl?: string;
150+
marketValue?: string;
151+
unrealizedPnl?: string;
152+
unrealizedPct?: number;
150153
}
151154

152155
export interface GeminiRawActiveOrdersResponse {

core/src/exchanges/hyperliquid/fetcher.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export interface HyperliquidRawOutcome {
1717
name: string; // e.g. "BTC > $100K @ 2026-05-09 06:00 UTC"
1818
description: string; // pipe-delimited contract spec
1919
sideSpecs: HyperliquidRawSideSpec[];
20-
quoteToken: string; // settlement currency, e.g. "USDC"
2120
}
2221

2322
export interface HyperliquidRawQuestion {
@@ -106,14 +105,19 @@ export interface HyperliquidRawOpenOrder {
106105
export interface HyperliquidRawPosition {
107106
coin: string;
108107
entryPx: string | null;
109-
leverage: { type: string; value: number };
108+
leverage: { type: string; value: number; rawUsd?: string };
110109
liquidationPx: string | null;
111110
marginUsed: string;
112111
maxTradeSzs: [string, string];
113112
positionValue: string;
114113
returnOnEquity: string;
115114
szi: string;
116115
unrealizedPnl: string;
116+
cumFunding?: {
117+
allTime?: string;
118+
sinceChange?: string;
119+
sinceOpen?: string;
120+
};
117121
}
118122

119123
export interface HyperliquidRawUserState {
@@ -133,6 +137,8 @@ export interface HyperliquidRawUserState {
133137
totalNtlPos: string;
134138
totalRawUsd: string;
135139
};
140+
crossMaintenanceMarginUsed?: string;
141+
time?: number;
136142
withdrawable: string;
137143
}
138144

core/src/exchanges/kalshi/fetcher.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ export interface KalshiRawMarket {
2323
last_price_dollars?: string;
2424
yes_ask_dollars?: string;
2525
yes_bid_dollars?: string;
26+
no_ask_dollars?: string;
27+
no_bid_dollars?: string;
28+
response_price_units?: string;
29+
market_type?: string;
30+
mve_collection_ticker?: string;
31+
mve_selected_legs?: Array<{
32+
event_ticker: string;
33+
market_ticker: string;
34+
side: string;
35+
}>;
2636
yes_ask_size_fp?: string;
2737
yes_bid_size_fp?: string;
2838
rules_primary?: string;

core/src/exchanges/kalshi/normalizer.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,18 @@ const KALSHI_PROMOTED_MARKET_KEYS = [
2121
'volume_24h_fp', 'volume_24h', 'volume', 'volume_fp',
2222
'liquidity_dollars', 'liquidity', 'open_interest_fp', 'open_interest',
2323
'status', 'last_price_dollars', 'previous_price_dollars',
24-
'yes_ask_dollars', 'yes_bid_dollars', 'last_price', 'yes_ask', 'yes_bid',
24+
'yes_ask_dollars', 'yes_bid_dollars', 'no_ask_dollars', 'no_bid_dollars',
25+
'response_price_units', 'last_price', 'yes_ask', 'yes_bid',
2526
] as const;
2627

28+
function parseKalshiPrice(value: string | number | undefined, responseUnits?: string): number | undefined {
29+
if (value == null) return undefined;
30+
const parsed = typeof value === 'number' ? value : parseFloat(value);
31+
if (!Number.isFinite(parsed)) return undefined;
32+
const normalizedUnits = responseUnits?.toLowerCase();
33+
return normalizedUnits === 'cent' || normalizedUnits === 'cents' ? parsed / 100 : parsed;
34+
}
35+
2736
export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, KalshiRawEvent> {
2837

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

4857
// Kalshi API v2 migrated from cent integers to FixedPointDollars strings.
4958
// Prefer the _dollars fields; fall back to deprecated cent fields.
59+
const yesBid = parseKalshiPrice(market.yes_bid_dollars, market.response_price_units);
60+
const yesAsk = parseKalshiPrice(market.yes_ask_dollars, market.response_price_units);
61+
const noBid = parseKalshiPrice(market.no_bid_dollars, market.response_price_units);
62+
const noAsk = parseKalshiPrice(market.no_ask_dollars, market.response_price_units);
5063
let price = 0;
5164
if (market.last_price_dollars != null) {
52-
price = parseFloat(market.last_price_dollars);
53-
} else if (market.yes_ask_dollars != null && market.yes_bid_dollars != null) {
54-
price = (parseFloat(market.yes_ask_dollars) + parseFloat(market.yes_bid_dollars)) / 2;
55-
} else if (market.yes_ask_dollars != null) {
56-
price = parseFloat(market.yes_ask_dollars);
65+
price = parseKalshiPrice(market.last_price_dollars, market.response_price_units) ?? 0;
66+
} else if (yesAsk != null && yesBid != null) {
67+
price = (yesAsk + yesBid) / 2;
68+
} else if (yesAsk != null) {
69+
price = yesAsk;
5770
} else if (market.last_price) {
5871
price = fromKalshiCents(market.last_price);
5972
} else if (market.yes_ask && market.yes_bid) {
@@ -69,20 +82,26 @@ export class KalshiNormalizer implements IExchangeNormalizer<KalshiRawEvent, Kal
6982
priceChange = parseFloat(market.last_price_dollars) - parseFloat(market.previous_price_dollars);
7083
}
7184

85+
const noPrice = noAsk != null && noBid != null
86+
? (noAsk + noBid) / 2
87+
: noAsk ?? noBid ?? invertKalshiUnified(price);
88+
7289
const outcomes: MarketOutcome[] = [
7390
{
7491
outcomeId: market.ticker,
7592
marketId: market.ticker,
7693
label: candidateName || 'Yes',
7794
price,
7895
priceChange24h: priceChange,
96+
metadata: { bid: yesBid, ask: yesAsk },
7997
},
8098
{
8199
outcomeId: `${market.ticker}-NO`,
82100
marketId: market.ticker,
83101
label: candidateName ? `Not ${candidateName}` : 'No',
84-
price: invertKalshiUnified(price),
102+
price: noPrice,
85103
priceChange24h: -priceChange,
104+
metadata: { bid: noBid, ask: noAsk },
86105
},
87106
];
88107

core/src/exchanges/limitless/fetcher.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export interface LimitlessRawMarket {
2020
buy?: { market?: number[]; limit?: number[] };
2121
sell?: { market?: number[]; limit?: number[] };
2222
};
23-
expirationTimestamp?: string;
23+
expirationDate?: string;
24+
expirationTimestamp?: number | string;
2425
volumeFormatted?: number;
2526
volume?: number;
2627
logo?: string | null;

core/src/exchanges/limitless/utils.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const DEFAULT_LIMITLESS_API_URL = 'https://api.limitless.exchange';
1010
const LIMITLESS_PROMOTED_MARKET_KEYS = [
1111
'slug', 'title', 'question', 'description',
1212
'tokens', 'prices',
13-
'expirationTimestamp',
13+
'expirationDate', 'expirationTimestamp',
1414
'volumeFormatted', 'volume',
1515
'logo',
1616
'categories', 'tags',
@@ -111,7 +111,7 @@ export function mapMarketToUnified(market: any, context: LimitlessMarketContext
111111
description: market.description || resolvedContext.eventDescription,
112112
slug: market.slug,
113113
outcomes: outcomes,
114-
resolutionDate: market.expirationTimestamp ? new Date(market.expirationTimestamp) : new Date(),
114+
resolutionDate: parseLimitlessExpiration(market),
115115
volume24h: Number(market.volumeFormatted || 0),
116116
volume: Number(market.volume || 0),
117117
liquidity: 0, // Not directly in the flat market list
@@ -131,6 +131,24 @@ export function mapMarketToUnified(market: any, context: LimitlessMarketContext
131131
return um;
132132
}
133133

134+
function parseLimitlessExpiration(market: any): Date | undefined {
135+
if (typeof market.expirationDate === 'string' && market.expirationDate.trim()) {
136+
return new Date(market.expirationDate);
137+
}
138+
const rawTimestamp = market.expirationTimestamp;
139+
if (typeof rawTimestamp === 'number' && Number.isFinite(rawTimestamp)) {
140+
return new Date(rawTimestamp < 1e12 ? rawTimestamp * 1000 : rawTimestamp);
141+
}
142+
if (typeof rawTimestamp === 'string' && rawTimestamp.trim()) {
143+
const numeric = Number(rawTimestamp);
144+
if (Number.isFinite(numeric)) {
145+
return new Date(numeric < 1e12 ? numeric * 1000 : numeric);
146+
}
147+
return new Date(rawTimestamp);
148+
}
149+
return new Date();
150+
}
151+
134152
function getText(value: unknown): string | undefined {
135153
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
136154
}

core/src/exchanges/myriad/fetcher.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,25 @@ export interface MyriadRawMarket {
1111
id: number;
1212
networkId: number;
1313
title?: string;
14+
shortName?: string;
1415
description?: string;
1516
slug?: string;
1617
imageUrl?: string;
1718
expiresAt?: string;
19+
publishedAt?: string;
1820
volume24h?: number;
1921
volume?: number;
22+
volumeNotional?: number;
23+
volumeNotional24h?: number;
2024
liquidity?: number;
2125
eventId?: number;
26+
outcomeIndex?: number;
27+
tradingModel?: string;
28+
negRisk?: boolean;
29+
executionMode?: string;
30+
oracle?: Record<string, unknown>;
2231
topics?: string[];
32+
tags?: string[];
2333
outcomes?: MyriadRawOutcome[];
2434
[key: string]: unknown;
2535
}
@@ -43,6 +53,7 @@ export interface MyriadRawQuestion {
4353
export interface MyriadRawTradeEvent {
4454
action?: string;
4555
blockNumber?: number;
56+
txId?: string;
4657
timestamp?: number;
4758
value?: number;
4859
shares?: number;

core/src/exchanges/myriad/normalizer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export class MyriadNormalizer implements IExchangeNormalizer<MyriadRawMarket, My
236236

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

247247
normalizeUserTrade(raw: MyriadRawTradeEvent, index: number): UserTrade {
248248
return {
249-
id: `${raw.blockNumber || raw.timestamp}-${index}`,
249+
id: raw.txId ?? `${raw.blockNumber || raw.timestamp}-${index}`,
250250
timestamp: (raw.timestamp || 0) * 1000,
251251
price: resolveMyriadPrice(raw),
252252
amount: Number(raw.shares || 0),

0 commit comments

Comments
 (0)