pmxtjs 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API_REFERENCE.md +88 -0
- package/coverage/clover.xml +334 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/pmxt/BaseExchange.ts.html +256 -0
- package/coverage/lcov-report/pmxt/exchanges/Kalshi.ts.html +1132 -0
- package/coverage/lcov-report/pmxt/exchanges/Polymarket.ts.html +1456 -0
- package/coverage/lcov-report/pmxt/exchanges/index.html +131 -0
- package/coverage/lcov-report/pmxt/index.html +116 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/BaseExchange.ts.html +256 -0
- package/coverage/lcov-report/src/exchanges/Kalshi.ts.html +1132 -0
- package/coverage/lcov-report/src/exchanges/Polymarket.ts.html +1456 -0
- package/coverage/lcov-report/src/exchanges/index.html +131 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov.info +766 -0
- package/examples/get_event_prices.ts +37 -0
- package/examples/historical_prices.ts +117 -0
- package/examples/orderbook.ts +102 -0
- package/examples/recent_trades.ts +29 -0
- package/examples/search_events.ts +68 -0
- package/examples/search_market.ts +29 -0
- package/jest.config.js +11 -0
- package/package.json +21 -21
- package/pmxt-0.1.0.tgz +0 -0
- package/src/BaseExchange.ts +57 -0
- package/src/exchanges/Kalshi.ts +349 -0
- package/src/exchanges/Polymarket.ts +457 -0
- package/src/index.ts +5 -0
- package/src/types.ts +61 -0
- package/test/exchanges/kalshi/ApiErrors.test.ts +132 -0
- package/test/exchanges/kalshi/EmptyResponse.test.ts +44 -0
- package/test/exchanges/kalshi/FetchAndNormalizeMarkets.test.ts +56 -0
- package/test/exchanges/kalshi/LiveApi.integration.test.ts +40 -0
- package/test/exchanges/kalshi/MarketHistory.test.ts +185 -0
- package/test/exchanges/kalshi/OrderBook.test.ts +149 -0
- package/test/exchanges/kalshi/SearchMarkets.test.ts +174 -0
- package/test/exchanges/kalshi/VolumeFallback.test.ts +44 -0
- package/test/exchanges/polymarket/DataValidation.test.ts +271 -0
- package/test/exchanges/polymarket/ErrorHandling.test.ts +34 -0
- package/test/exchanges/polymarket/FetchAndNormalizeMarkets.test.ts +68 -0
- package/test/exchanges/polymarket/GetMarketsBySlug.test.ts +268 -0
- package/test/exchanges/polymarket/LiveApi.integration.test.ts +44 -0
- package/test/exchanges/polymarket/MarketHistory.test.ts +207 -0
- package/test/exchanges/polymarket/OrderBook.test.ts +167 -0
- package/test/exchanges/polymarket/RequestParameters.test.ts +39 -0
- package/test/exchanges/polymarket/SearchMarkets.test.ts +176 -0
- package/test/exchanges/polymarket/TradeHistory.test.ts +248 -0
- package/tsconfig.json +12 -0
- package/Cargo.toml +0 -15
- package/README.md +0 -47
- package/src/lib.rs +0 -6
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { PredictionMarketExchange, MarketFilterParams, HistoryFilterParams } from '../BaseExchange';
|
|
3
|
+
import { UnifiedMarket, MarketOutcome, PriceCandle, CandleInterval, OrderBook, Trade } from '../types';
|
|
4
|
+
|
|
5
|
+
export class KalshiExchange extends PredictionMarketExchange {
|
|
6
|
+
get name(): string {
|
|
7
|
+
return "Kalshi";
|
|
8
|
+
}
|
|
9
|
+
private baseUrl = "https://api.elections.kalshi.com/trade-api/v2/events";
|
|
10
|
+
|
|
11
|
+
async fetchMarkets(params?: MarketFilterParams): Promise<UnifiedMarket[]> {
|
|
12
|
+
const limit = params?.limit || 50;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Fetch active events with nested markets
|
|
16
|
+
// For small limits, we can optimize by fetching fewer pages
|
|
17
|
+
const allEvents = await this.fetchActiveEvents(limit);
|
|
18
|
+
|
|
19
|
+
// Extract ALL markets from all events
|
|
20
|
+
const allMarkets: UnifiedMarket[] = [];
|
|
21
|
+
|
|
22
|
+
for (const event of allEvents) {
|
|
23
|
+
const markets = event.markets || [];
|
|
24
|
+
for (const market of markets) {
|
|
25
|
+
const unifiedMarket = this.mapMarketToUnified(event, market);
|
|
26
|
+
if (unifiedMarket) {
|
|
27
|
+
allMarkets.push(unifiedMarket);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`Extracted ${allMarkets.length} markets from ${allEvents.length} events.`);
|
|
33
|
+
|
|
34
|
+
// Sort by 24h volume
|
|
35
|
+
if (params?.sort === 'volume') {
|
|
36
|
+
allMarkets.sort((a, b) => b.volume24h - a.volume24h);
|
|
37
|
+
} else if (params?.sort === 'liquidity') {
|
|
38
|
+
allMarkets.sort((a, b) => b.liquidity - a.liquidity);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return allMarkets.slice(0, limit);
|
|
42
|
+
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Error fetching Kalshi data:", error);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async fetchActiveEvents(targetMarketCount?: number): Promise<any[]> {
|
|
50
|
+
let allEvents: any[] = [];
|
|
51
|
+
let totalMarketCount = 0;
|
|
52
|
+
let cursor = null;
|
|
53
|
+
let page = 0;
|
|
54
|
+
|
|
55
|
+
// Note: Kalshi API uses cursor-based pagination which requires sequential fetching.
|
|
56
|
+
// We cannot parallelize requests for a single list because we need the cursor from page N to fetch page N+1.
|
|
57
|
+
// To optimize, we use the maximum allowed limit (200) and fetch until exhaustion.
|
|
58
|
+
|
|
59
|
+
const MAX_PAGES = 1000; // Safety cap against infinite loops
|
|
60
|
+
const BATCH_SIZE = 200; // Max limit per Kalshi API docs
|
|
61
|
+
|
|
62
|
+
do {
|
|
63
|
+
try {
|
|
64
|
+
// console.log(`Fetching Kalshi page ${page + 1}...`);
|
|
65
|
+
const queryParams: any = {
|
|
66
|
+
limit: BATCH_SIZE,
|
|
67
|
+
with_nested_markets: true,
|
|
68
|
+
status: 'open' // Filter to open markets to improve relevance and speed
|
|
69
|
+
};
|
|
70
|
+
if (cursor) queryParams.cursor = cursor;
|
|
71
|
+
|
|
72
|
+
const response = await axios.get(this.baseUrl, { params: queryParams });
|
|
73
|
+
const events = response.data.events || [];
|
|
74
|
+
|
|
75
|
+
if (events.length === 0) break;
|
|
76
|
+
|
|
77
|
+
allEvents = allEvents.concat(events);
|
|
78
|
+
|
|
79
|
+
// Count markets in this batch for early termination
|
|
80
|
+
if (targetMarketCount) {
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
totalMarketCount += (event.markets || []).length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Early termination: if we have enough markets, stop fetching
|
|
86
|
+
// Add a buffer (2x) to ensure we have enough after filtering/sorting
|
|
87
|
+
if (totalMarketCount >= targetMarketCount * 2) {
|
|
88
|
+
console.log(`Early termination: collected ${totalMarketCount} markets (target: ${targetMarketCount})`);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cursor = response.data.cursor;
|
|
94
|
+
page++;
|
|
95
|
+
|
|
96
|
+
// Log progress every few pages to avoid spam
|
|
97
|
+
if (page % 5 === 0) {
|
|
98
|
+
console.log(`Fetched ${page} pages (${allEvents.length} events) from Kalshi...`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(`Error fetching Kalshi page ${page}:`, e);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
} while (cursor && page < MAX_PAGES);
|
|
106
|
+
|
|
107
|
+
console.log(`Finished fetching Kalshi: ${allEvents.length} total events across ${page} pages.`);
|
|
108
|
+
return allEvents;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private mapMarketToUnified(event: any, market: any): UnifiedMarket | null {
|
|
112
|
+
if (!market) return null;
|
|
113
|
+
|
|
114
|
+
// Calculate price
|
|
115
|
+
let price = 0.5;
|
|
116
|
+
if (market.last_price) {
|
|
117
|
+
price = market.last_price / 100;
|
|
118
|
+
} else if (market.yes_ask && market.yes_bid) {
|
|
119
|
+
price = (market.yes_ask + market.yes_bid) / 200;
|
|
120
|
+
} else if (market.yes_ask) {
|
|
121
|
+
price = market.yes_ask / 100;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Extract candidate name
|
|
125
|
+
let candidateName: string | null = null;
|
|
126
|
+
if (market.subtitle || market.yes_sub_title) {
|
|
127
|
+
candidateName = market.subtitle || market.yes_sub_title;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Calculate 24h change
|
|
131
|
+
let priceChange = 0;
|
|
132
|
+
if (market.previous_price_dollars !== undefined && market.last_price_dollars !== undefined) {
|
|
133
|
+
priceChange = market.last_price_dollars - market.previous_price_dollars;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const outcomes: MarketOutcome[] = [
|
|
137
|
+
{
|
|
138
|
+
id: 'yes',
|
|
139
|
+
label: candidateName || 'Yes',
|
|
140
|
+
price: price,
|
|
141
|
+
priceChange24h: priceChange
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'no',
|
|
145
|
+
label: candidateName ? `Not ${candidateName}` : 'No',
|
|
146
|
+
price: 1 - price,
|
|
147
|
+
priceChange24h: -priceChange // Inverse change for No? simplified assumption
|
|
148
|
+
}
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
id: market.ticker,
|
|
153
|
+
title: event.title,
|
|
154
|
+
description: event.sub_title || market.subtitle || "",
|
|
155
|
+
outcomes: outcomes,
|
|
156
|
+
resolutionDate: new Date(market.expiration_time),
|
|
157
|
+
volume24h: Number(market.volume_24h || market.volume || 0),
|
|
158
|
+
volume: Number(market.volume || 0),
|
|
159
|
+
liquidity: Number(market.liquidity || 0), // Kalshi 'liquidity' might need specific mapping if available, otherwise 0 to avoid conflation
|
|
160
|
+
openInterest: Number(market.open_interest || 0),
|
|
161
|
+
url: `https://kalshi.com/events/${event.event_ticker}`,
|
|
162
|
+
category: event.category,
|
|
163
|
+
tags: event.tags || []
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async searchMarkets(query: string, params?: MarketFilterParams): Promise<UnifiedMarket[]> {
|
|
168
|
+
// We must fetch ALL markets to search them locally since we don't have server-side search
|
|
169
|
+
const fetchLimit = 100000;
|
|
170
|
+
try {
|
|
171
|
+
const markets = await this.fetchMarkets({ ...params, limit: fetchLimit });
|
|
172
|
+
const lowerQuery = query.toLowerCase();
|
|
173
|
+
const filtered = markets.filter(market =>
|
|
174
|
+
market.title.toLowerCase().includes(lowerQuery) ||
|
|
175
|
+
market.description.toLowerCase().includes(lowerQuery)
|
|
176
|
+
);
|
|
177
|
+
const limit = params?.limit || 20;
|
|
178
|
+
return filtered.slice(0, limit);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error("Error searching Kalshi data:", error);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Fetch specific markets by their event ticker.
|
|
187
|
+
* Useful for looking up a specific event from a URL.
|
|
188
|
+
* @param eventTicker - The event ticker (e.g. "FED-25JAN" or "PRES-2024")
|
|
189
|
+
*/
|
|
190
|
+
async getMarketsBySlug(eventTicker: string): Promise<UnifiedMarket[]> {
|
|
191
|
+
try {
|
|
192
|
+
// Kalshi API expects uppercase tickers, but URLs use lowercase
|
|
193
|
+
const normalizedTicker = eventTicker.toUpperCase();
|
|
194
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/events/${normalizedTicker}`;
|
|
195
|
+
const response = await axios.get(url, {
|
|
196
|
+
params: { with_nested_markets: true }
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const event = response.data.event;
|
|
200
|
+
if (!event) return [];
|
|
201
|
+
|
|
202
|
+
const unifiedMarkets: UnifiedMarket[] = [];
|
|
203
|
+
const markets = event.markets || [];
|
|
204
|
+
|
|
205
|
+
for (const market of markets) {
|
|
206
|
+
const unifiedMarket = this.mapMarketToUnified(event, market);
|
|
207
|
+
if (unifiedMarket) {
|
|
208
|
+
unifiedMarkets.push(unifiedMarket);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return unifiedMarkets;
|
|
213
|
+
|
|
214
|
+
} catch (error: any) {
|
|
215
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
216
|
+
if (error.response.status === 404) {
|
|
217
|
+
throw new Error(`Kalshi event not found: "${eventTicker}". Check that the event ticker is correct.`);
|
|
218
|
+
}
|
|
219
|
+
const apiError = error.response.data?.error || error.response.data?.message || "Unknown API Error";
|
|
220
|
+
throw new Error(`Kalshi API Error (${error.response.status}): ${apiError}. Event Ticker: ${eventTicker}`);
|
|
221
|
+
}
|
|
222
|
+
console.error(`Unexpected error fetching Kalshi event ${eventTicker}:`, error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private mapIntervalToKalshi(interval: CandleInterval): number {
|
|
228
|
+
const mapping: Record<CandleInterval, number> = {
|
|
229
|
+
'1m': 1,
|
|
230
|
+
'5m': 1,
|
|
231
|
+
'15m': 1,
|
|
232
|
+
'1h': 60,
|
|
233
|
+
'6h': 60,
|
|
234
|
+
'1d': 1440
|
|
235
|
+
};
|
|
236
|
+
return mapping[interval];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async getMarketHistory(id: string, params: HistoryFilterParams): Promise<PriceCandle[]> {
|
|
240
|
+
try {
|
|
241
|
+
// Kalshi API expects uppercase tickers
|
|
242
|
+
const normalizedId = id.toUpperCase();
|
|
243
|
+
const interval = this.mapIntervalToKalshi(params.resolution);
|
|
244
|
+
|
|
245
|
+
// Heuristic for series_ticker
|
|
246
|
+
const parts = normalizedId.split('-');
|
|
247
|
+
if (parts.length < 2) {
|
|
248
|
+
throw new Error(`Invalid Kalshi Ticker format: "${id}". Expected format like "FED-25JAN29-B4.75".`);
|
|
249
|
+
}
|
|
250
|
+
const seriesTicker = parts.slice(0, -1).join('-');
|
|
251
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/series/${seriesTicker}/markets/${normalizedId}/candlesticks`;
|
|
252
|
+
|
|
253
|
+
const queryParams: any = { period_interval: interval };
|
|
254
|
+
|
|
255
|
+
const now = Math.floor(Date.now() / 1000);
|
|
256
|
+
let startTs = now - (24 * 60 * 60);
|
|
257
|
+
let endTs = now;
|
|
258
|
+
|
|
259
|
+
if (params.start) {
|
|
260
|
+
startTs = Math.floor(params.start.getTime() / 1000);
|
|
261
|
+
}
|
|
262
|
+
if (params.end) {
|
|
263
|
+
endTs = Math.floor(params.end.getTime() / 1000);
|
|
264
|
+
if (!params.start) {
|
|
265
|
+
startTs = endTs - (24 * 60 * 60);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
queryParams.start_ts = startTs;
|
|
270
|
+
queryParams.end_ts = endTs;
|
|
271
|
+
|
|
272
|
+
const response = await axios.get(url, { params: queryParams });
|
|
273
|
+
const candles = response.data.candlesticks || [];
|
|
274
|
+
|
|
275
|
+
const mappedCandles: PriceCandle[] = candles.map((c: any) => ({
|
|
276
|
+
timestamp: c.end_period_ts * 1000,
|
|
277
|
+
open: (c.price.open || 0) / 100,
|
|
278
|
+
high: (c.price.high || 0) / 100,
|
|
279
|
+
low: (c.price.low || 0) / 100,
|
|
280
|
+
close: (c.price.close || 0) / 100,
|
|
281
|
+
volume: c.volume
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
if (params.limit && mappedCandles.length > params.limit) {
|
|
285
|
+
return mappedCandles.slice(-params.limit);
|
|
286
|
+
}
|
|
287
|
+
return mappedCandles;
|
|
288
|
+
} catch (error: any) {
|
|
289
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
290
|
+
const apiError = error.response.data?.error || error.response.data?.message || "Unknown API Error";
|
|
291
|
+
throw new Error(`Kalshi History API Error (${error.response.status}): ${apiError}. Used Ticker: ${id}`);
|
|
292
|
+
}
|
|
293
|
+
console.error(`Unexpected error fetching Kalshi history for ${id}:`, error);
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async getOrderBook(id: string): Promise<OrderBook> {
|
|
299
|
+
try {
|
|
300
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/markets/${id}/orderbook`;
|
|
301
|
+
const response = await axios.get(url);
|
|
302
|
+
const data = response.data.orderbook;
|
|
303
|
+
|
|
304
|
+
// Structure: { yes: [[price, qty], ...], no: [[price, qty], ...] }
|
|
305
|
+
const bids = (data.yes || []).map((level: number[]) => ({
|
|
306
|
+
price: level[0] / 100,
|
|
307
|
+
size: level[1]
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
const asks = (data.no || []).map((level: number[]) => ({
|
|
311
|
+
price: (100 - level[0]) / 100,
|
|
312
|
+
size: level[1]
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
// Sort bids desc, asks asc
|
|
316
|
+
bids.sort((a: any, b: any) => b.price - a.price);
|
|
317
|
+
asks.sort((a: any, b: any) => a.price - b.price);
|
|
318
|
+
|
|
319
|
+
return { bids, asks, timestamp: Date.now() };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error(`Error fetching Kalshi orderbook for ${id}:`, error);
|
|
322
|
+
return { bids: [], asks: [] };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async getTradeHistory(id: string, params: HistoryFilterParams): Promise<Trade[]> {
|
|
327
|
+
try {
|
|
328
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/markets/trades`;
|
|
329
|
+
const response = await axios.get(url, {
|
|
330
|
+
params: {
|
|
331
|
+
ticker: id,
|
|
332
|
+
limit: params.limit || 100
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const trades = response.data.trades || [];
|
|
336
|
+
|
|
337
|
+
return trades.map((t: any) => ({
|
|
338
|
+
id: t.trade_id,
|
|
339
|
+
timestamp: new Date(t.created_time).getTime(),
|
|
340
|
+
price: t.yes_price / 100,
|
|
341
|
+
amount: t.count,
|
|
342
|
+
side: t.taker_side === 'yes' ? 'buy' : 'sell'
|
|
343
|
+
}));
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error(`Error fetching Kalshi trades for ${id}:`, error);
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|