pmxtjs 0.0.1 → 0.1.1

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.
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PredictionMarketExchange = void 0;
4
+ // ----------------------------------------------------------------------------
5
+ // Base Exchange Class
6
+ // ----------------------------------------------------------------------------
7
+ class PredictionMarketExchange {
8
+ /**
9
+ * Fetch historical price data for a specific market or outcome.
10
+ * @param id - The market ID or specific outcome ID/Token ID depending on the exchange
11
+ */
12
+ async getMarketHistory(id, params) {
13
+ throw new Error("Method getMarketHistory not implemented.");
14
+ }
15
+ /**
16
+ * Fetch the current order book (bids/asks) for a specific outcome.
17
+ * Essential for calculating localized spread and depth.
18
+ */
19
+ async getOrderBook(id) {
20
+ throw new Error("Method getOrderBook not implemented.");
21
+ }
22
+ /**
23
+ * Fetch raw trade history.
24
+ * Useful for generating synthetic OHLCV candles if the exchange doesn't provide them natively.
25
+ */
26
+ async getTradeHistory(id, params) {
27
+ throw new Error("Method getTradeHistory not implemented.");
28
+ }
29
+ }
30
+ exports.PredictionMarketExchange = PredictionMarketExchange;
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.KalshiExchange = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const BaseExchange_1 = require("../BaseExchange");
9
+ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.baseUrl = "https://api.elections.kalshi.com/trade-api/v2/events";
13
+ }
14
+ get name() {
15
+ return "Kalshi";
16
+ }
17
+ async fetchMarkets(params) {
18
+ const limit = params?.limit || 50;
19
+ try {
20
+ // Fetch active events with nested markets
21
+ // For small limits, we can optimize by fetching fewer pages
22
+ const allEvents = await this.fetchActiveEvents(limit);
23
+ // Extract ALL markets from all events
24
+ const allMarkets = [];
25
+ for (const event of allEvents) {
26
+ const markets = event.markets || [];
27
+ for (const market of markets) {
28
+ const unifiedMarket = this.mapMarketToUnified(event, market);
29
+ if (unifiedMarket) {
30
+ allMarkets.push(unifiedMarket);
31
+ }
32
+ }
33
+ }
34
+ console.log(`Extracted ${allMarkets.length} markets from ${allEvents.length} events.`);
35
+ // Sort by 24h volume
36
+ if (params?.sort === 'volume') {
37
+ allMarkets.sort((a, b) => b.volume24h - a.volume24h);
38
+ }
39
+ else if (params?.sort === 'liquidity') {
40
+ allMarkets.sort((a, b) => b.liquidity - a.liquidity);
41
+ }
42
+ return allMarkets.slice(0, limit);
43
+ }
44
+ catch (error) {
45
+ console.error("Error fetching Kalshi data:", error);
46
+ return [];
47
+ }
48
+ }
49
+ async fetchActiveEvents(targetMarketCount) {
50
+ let allEvents = [];
51
+ let totalMarketCount = 0;
52
+ let cursor = null;
53
+ let page = 0;
54
+ // Note: Kalshi API uses cursor-based pagination which requires sequential fetching.
55
+ // We cannot parallelize requests for a single list because we need the cursor from page N to fetch page N+1.
56
+ // To optimize, we use the maximum allowed limit (200) and fetch until exhaustion.
57
+ const MAX_PAGES = 1000; // Safety cap against infinite loops
58
+ const BATCH_SIZE = 200; // Max limit per Kalshi API docs
59
+ do {
60
+ try {
61
+ // console.log(`Fetching Kalshi page ${page + 1}...`);
62
+ const queryParams = {
63
+ limit: BATCH_SIZE,
64
+ with_nested_markets: true,
65
+ status: 'open' // Filter to open markets to improve relevance and speed
66
+ };
67
+ if (cursor)
68
+ queryParams.cursor = cursor;
69
+ const response = await axios_1.default.get(this.baseUrl, { params: queryParams });
70
+ const events = response.data.events || [];
71
+ if (events.length === 0)
72
+ break;
73
+ allEvents = allEvents.concat(events);
74
+ // Count markets in this batch for early termination
75
+ if (targetMarketCount) {
76
+ for (const event of events) {
77
+ totalMarketCount += (event.markets || []).length;
78
+ }
79
+ // Early termination: if we have enough markets, stop fetching
80
+ // Add a buffer (2x) to ensure we have enough after filtering/sorting
81
+ if (totalMarketCount >= targetMarketCount * 2) {
82
+ console.log(`Early termination: collected ${totalMarketCount} markets (target: ${targetMarketCount})`);
83
+ break;
84
+ }
85
+ }
86
+ cursor = response.data.cursor;
87
+ page++;
88
+ // Log progress every few pages to avoid spam
89
+ if (page % 5 === 0) {
90
+ console.log(`Fetched ${page} pages (${allEvents.length} events) from Kalshi...`);
91
+ }
92
+ }
93
+ catch (e) {
94
+ console.error(`Error fetching Kalshi page ${page}:`, e);
95
+ break;
96
+ }
97
+ } while (cursor && page < MAX_PAGES);
98
+ console.log(`Finished fetching Kalshi: ${allEvents.length} total events across ${page} pages.`);
99
+ return allEvents;
100
+ }
101
+ mapMarketToUnified(event, market) {
102
+ if (!market)
103
+ return null;
104
+ // Calculate price
105
+ let price = 0.5;
106
+ if (market.last_price) {
107
+ price = market.last_price / 100;
108
+ }
109
+ else if (market.yes_ask && market.yes_bid) {
110
+ price = (market.yes_ask + market.yes_bid) / 200;
111
+ }
112
+ else if (market.yes_ask) {
113
+ price = market.yes_ask / 100;
114
+ }
115
+ // Extract candidate name
116
+ let candidateName = null;
117
+ if (market.subtitle || market.yes_sub_title) {
118
+ candidateName = market.subtitle || market.yes_sub_title;
119
+ }
120
+ // Calculate 24h change
121
+ let priceChange = 0;
122
+ if (market.previous_price_dollars !== undefined && market.last_price_dollars !== undefined) {
123
+ priceChange = market.last_price_dollars - market.previous_price_dollars;
124
+ }
125
+ const outcomes = [
126
+ {
127
+ id: 'yes',
128
+ label: candidateName || 'Yes',
129
+ price: price,
130
+ priceChange24h: priceChange
131
+ },
132
+ {
133
+ id: 'no',
134
+ label: candidateName ? `Not ${candidateName}` : 'No',
135
+ price: 1 - price,
136
+ priceChange24h: -priceChange // Inverse change for No? simplified assumption
137
+ }
138
+ ];
139
+ return {
140
+ id: market.ticker,
141
+ title: event.title,
142
+ description: event.sub_title || market.subtitle || "",
143
+ outcomes: outcomes,
144
+ resolutionDate: new Date(market.expiration_time),
145
+ volume24h: Number(market.volume_24h || market.volume || 0),
146
+ volume: Number(market.volume || 0),
147
+ liquidity: Number(market.liquidity || 0), // Kalshi 'liquidity' might need specific mapping if available, otherwise 0 to avoid conflation
148
+ openInterest: Number(market.open_interest || 0),
149
+ url: `https://kalshi.com/events/${event.event_ticker}`,
150
+ category: event.category,
151
+ tags: event.tags || []
152
+ };
153
+ }
154
+ async searchMarkets(query, params) {
155
+ // We must fetch ALL markets to search them locally since we don't have server-side search
156
+ const fetchLimit = 100000;
157
+ try {
158
+ const markets = await this.fetchMarkets({ ...params, limit: fetchLimit });
159
+ const lowerQuery = query.toLowerCase();
160
+ const filtered = markets.filter(market => market.title.toLowerCase().includes(lowerQuery) ||
161
+ market.description.toLowerCase().includes(lowerQuery));
162
+ const limit = params?.limit || 20;
163
+ return filtered.slice(0, limit);
164
+ }
165
+ catch (error) {
166
+ console.error("Error searching Kalshi data:", error);
167
+ return [];
168
+ }
169
+ }
170
+ /**
171
+ * Fetch specific markets by their event ticker.
172
+ * Useful for looking up a specific event from a URL.
173
+ * @param eventTicker - The event ticker (e.g. "FED-25JAN" or "PRES-2024")
174
+ */
175
+ async getMarketsBySlug(eventTicker) {
176
+ try {
177
+ // Kalshi API expects uppercase tickers, but URLs use lowercase
178
+ const normalizedTicker = eventTicker.toUpperCase();
179
+ const url = `https://api.elections.kalshi.com/trade-api/v2/events/${normalizedTicker}`;
180
+ const response = await axios_1.default.get(url, {
181
+ params: { with_nested_markets: true }
182
+ });
183
+ const event = response.data.event;
184
+ if (!event)
185
+ return [];
186
+ const unifiedMarkets = [];
187
+ const markets = event.markets || [];
188
+ for (const market of markets) {
189
+ const unifiedMarket = this.mapMarketToUnified(event, market);
190
+ if (unifiedMarket) {
191
+ unifiedMarkets.push(unifiedMarket);
192
+ }
193
+ }
194
+ return unifiedMarkets;
195
+ }
196
+ catch (error) {
197
+ if (axios_1.default.isAxiosError(error) && error.response) {
198
+ if (error.response.status === 404) {
199
+ throw new Error(`Kalshi event not found: "${eventTicker}". Check that the event ticker is correct.`);
200
+ }
201
+ const apiError = error.response.data?.error || error.response.data?.message || "Unknown API Error";
202
+ throw new Error(`Kalshi API Error (${error.response.status}): ${apiError}. Event Ticker: ${eventTicker}`);
203
+ }
204
+ console.error(`Unexpected error fetching Kalshi event ${eventTicker}:`, error);
205
+ throw error;
206
+ }
207
+ }
208
+ mapIntervalToKalshi(interval) {
209
+ const mapping = {
210
+ '1m': 1,
211
+ '5m': 1,
212
+ '15m': 1,
213
+ '1h': 60,
214
+ '6h': 60,
215
+ '1d': 1440
216
+ };
217
+ return mapping[interval];
218
+ }
219
+ async getMarketHistory(id, params) {
220
+ try {
221
+ // Kalshi API expects uppercase tickers
222
+ const normalizedId = id.toUpperCase();
223
+ const interval = this.mapIntervalToKalshi(params.resolution);
224
+ // Heuristic for series_ticker
225
+ const parts = normalizedId.split('-');
226
+ if (parts.length < 2) {
227
+ throw new Error(`Invalid Kalshi Ticker format: "${id}". Expected format like "FED-25JAN29-B4.75".`);
228
+ }
229
+ const seriesTicker = parts.slice(0, -1).join('-');
230
+ const url = `https://api.elections.kalshi.com/trade-api/v2/series/${seriesTicker}/markets/${normalizedId}/candlesticks`;
231
+ const queryParams = { period_interval: interval };
232
+ const now = Math.floor(Date.now() / 1000);
233
+ let startTs = now - (24 * 60 * 60);
234
+ let endTs = now;
235
+ if (params.start) {
236
+ startTs = Math.floor(params.start.getTime() / 1000);
237
+ }
238
+ if (params.end) {
239
+ endTs = Math.floor(params.end.getTime() / 1000);
240
+ if (!params.start) {
241
+ startTs = endTs - (24 * 60 * 60);
242
+ }
243
+ }
244
+ queryParams.start_ts = startTs;
245
+ queryParams.end_ts = endTs;
246
+ const response = await axios_1.default.get(url, { params: queryParams });
247
+ const candles = response.data.candlesticks || [];
248
+ const mappedCandles = candles.map((c) => ({
249
+ timestamp: c.end_period_ts * 1000,
250
+ open: (c.price.open || 0) / 100,
251
+ high: (c.price.high || 0) / 100,
252
+ low: (c.price.low || 0) / 100,
253
+ close: (c.price.close || 0) / 100,
254
+ volume: c.volume
255
+ }));
256
+ if (params.limit && mappedCandles.length > params.limit) {
257
+ return mappedCandles.slice(-params.limit);
258
+ }
259
+ return mappedCandles;
260
+ }
261
+ catch (error) {
262
+ if (axios_1.default.isAxiosError(error) && error.response) {
263
+ const apiError = error.response.data?.error || error.response.data?.message || "Unknown API Error";
264
+ throw new Error(`Kalshi History API Error (${error.response.status}): ${apiError}. Used Ticker: ${id}`);
265
+ }
266
+ console.error(`Unexpected error fetching Kalshi history for ${id}:`, error);
267
+ throw error;
268
+ }
269
+ }
270
+ async getOrderBook(id) {
271
+ try {
272
+ const url = `https://api.elections.kalshi.com/trade-api/v2/markets/${id}/orderbook`;
273
+ const response = await axios_1.default.get(url);
274
+ const data = response.data.orderbook;
275
+ // Structure: { yes: [[price, qty], ...], no: [[price, qty], ...] }
276
+ const bids = (data.yes || []).map((level) => ({
277
+ price: level[0] / 100,
278
+ size: level[1]
279
+ }));
280
+ const asks = (data.no || []).map((level) => ({
281
+ price: (100 - level[0]) / 100,
282
+ size: level[1]
283
+ }));
284
+ // Sort bids desc, asks asc
285
+ bids.sort((a, b) => b.price - a.price);
286
+ asks.sort((a, b) => a.price - b.price);
287
+ return { bids, asks, timestamp: Date.now() };
288
+ }
289
+ catch (error) {
290
+ console.error(`Error fetching Kalshi orderbook for ${id}:`, error);
291
+ return { bids: [], asks: [] };
292
+ }
293
+ }
294
+ async getTradeHistory(id, params) {
295
+ try {
296
+ const url = `https://api.elections.kalshi.com/trade-api/v2/markets/trades`;
297
+ const response = await axios_1.default.get(url, {
298
+ params: {
299
+ ticker: id,
300
+ limit: params.limit || 100
301
+ }
302
+ });
303
+ const trades = response.data.trades || [];
304
+ return trades.map((t) => ({
305
+ id: t.trade_id,
306
+ timestamp: new Date(t.created_time).getTime(),
307
+ price: t.yes_price / 100,
308
+ amount: t.count,
309
+ side: t.taker_side === 'yes' ? 'buy' : 'sell'
310
+ }));
311
+ }
312
+ catch (error) {
313
+ console.error(`Error fetching Kalshi trades for ${id}:`, error);
314
+ return [];
315
+ }
316
+ }
317
+ }
318
+ exports.KalshiExchange = KalshiExchange;
@@ -0,0 +1,420 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PolymarketExchange = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const BaseExchange_1 = require("../BaseExchange");
9
+ class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
10
+ constructor() {
11
+ super(...arguments);
12
+ // Utilizing the Gamma API for rich metadata and list view data
13
+ this.baseUrl = 'https://gamma-api.polymarket.com/events';
14
+ // CLOB API for orderbook, trades, and timeseries
15
+ this.clobUrl = 'https://clob.polymarket.com';
16
+ }
17
+ get name() {
18
+ return 'Polymarket';
19
+ }
20
+ async fetchMarkets(params) {
21
+ const limit = params?.limit || 200; // Higher default for better coverage
22
+ const offset = params?.offset || 0;
23
+ // Map generic sort params to Polymarket Gamma API params
24
+ let queryParams = {
25
+ active: 'true',
26
+ closed: 'false',
27
+ limit: limit,
28
+ offset: offset,
29
+ };
30
+ // Gamma API uses 'order' and 'ascending' for sorting
31
+ if (params?.sort === 'volume') {
32
+ queryParams.order = 'volume';
33
+ queryParams.ascending = 'false';
34
+ }
35
+ else if (params?.sort === 'newest') {
36
+ queryParams.order = 'startDate';
37
+ queryParams.ascending = 'false';
38
+ }
39
+ else if (params?.sort === 'liquidity') {
40
+ // queryParams.order = 'liquidity';
41
+ }
42
+ else {
43
+ // Default: do not send order param to avoid 422
44
+ }
45
+ try {
46
+ // Fetch active events from Gamma
47
+ const response = await axios_1.default.get(this.baseUrl, {
48
+ params: queryParams
49
+ });
50
+ const events = response.data;
51
+ const unifiedMarkets = [];
52
+ for (const event of events) {
53
+ // Each event is a container (e.g. "US Election").
54
+ // It contains specific "markets" (e.g. "Winner", "Pop Vote").
55
+ if (!event.markets)
56
+ continue;
57
+ for (const market of event.markets) {
58
+ const outcomes = [];
59
+ // Polymarket Gamma often returns 'outcomes' and 'outcomePrices' as stringified JSON keys.
60
+ let outcomeLabels = [];
61
+ let outcomePrices = [];
62
+ try {
63
+ outcomeLabels = typeof market.outcomes === 'string' ? JSON.parse(market.outcomes) : (market.outcomes || []);
64
+ outcomePrices = typeof market.outcomePrices === 'string' ? JSON.parse(market.outcomePrices) : (market.outcomePrices || []);
65
+ }
66
+ catch (e) {
67
+ console.warn(`Failed to parse outcomes for market ${market.id}`, e);
68
+ }
69
+ // Extract CLOB token IDs for granular operations
70
+ let clobTokenIds = [];
71
+ try {
72
+ clobTokenIds = typeof market.clobTokenIds === 'string' ? JSON.parse(market.clobTokenIds) : (market.clobTokenIds || []);
73
+ }
74
+ catch (e) {
75
+ console.warn(`Failed to parse clobTokenIds for market ${market.id}`, e);
76
+ }
77
+ // Extract candidate/option name from market question for better outcome labels
78
+ let candidateName = null;
79
+ if (market.question && market.groupItemTitle) {
80
+ candidateName = market.groupItemTitle;
81
+ }
82
+ if (outcomeLabels.length > 0) {
83
+ outcomeLabels.forEach((label, index) => {
84
+ const rawPrice = outcomePrices[index] || "0";
85
+ // For Yes/No markets with specific candidates, use the candidate name
86
+ let outcomeLabel = label;
87
+ if (candidateName && label.toLowerCase() === 'yes') {
88
+ outcomeLabel = candidateName;
89
+ }
90
+ else if (candidateName && label.toLowerCase() === 'no') {
91
+ outcomeLabel = `Not ${candidateName}`;
92
+ }
93
+ // 24h Price Change
94
+ // Polymarket API provides 'oneDayPriceChange' on the market object
95
+ let priceChange = 0;
96
+ if (index === 0 || label.toLowerCase() === 'yes' || (candidateName && label === candidateName)) {
97
+ priceChange = Number(market.oneDayPriceChange || 0);
98
+ }
99
+ outcomes.push({
100
+ id: String(index),
101
+ label: outcomeLabel,
102
+ price: parseFloat(rawPrice) || 0,
103
+ priceChange24h: priceChange,
104
+ metadata: {
105
+ clobTokenId: clobTokenIds[index]
106
+ }
107
+ });
108
+ });
109
+ }
110
+ unifiedMarkets.push({
111
+ id: market.id,
112
+ title: market.question ? `${event.title} - ${market.question}` : event.title,
113
+ description: market.description || event.description,
114
+ outcomes: outcomes,
115
+ resolutionDate: market.endDate ? new Date(market.endDate) : (market.end_date_iso ? new Date(market.end_date_iso) : new Date()),
116
+ volume24h: Number(market.volume24hr || market.volume_24h || 0),
117
+ volume: Number(market.volume || 0),
118
+ liquidity: Number(market.liquidity || market.rewards?.liquidity || 0),
119
+ openInterest: Number(market.openInterest || market.open_interest || 0),
120
+ url: `https://polymarket.com/event/${event.slug}`,
121
+ image: event.image || market.image || `https://polymarket.com/api/og?slug=${event.slug}`,
122
+ category: event.category || event.tags?.[0]?.label,
123
+ tags: event.tags?.map((t) => t.label) || []
124
+ });
125
+ }
126
+ }
127
+ // Client-side Sort capability to ensure contract fulfillment
128
+ // Often API filters are "good effort" or apply to the 'event' but not the 'market'
129
+ if (params?.sort === 'volume') {
130
+ unifiedMarkets.sort((a, b) => b.volume24h - a.volume24h);
131
+ }
132
+ else if (params?.sort === 'newest') {
133
+ // unifiedMarkets.sort((a, b) => b.resolutionDate.getTime() - a.resolutionDate.getTime()); // Not quite 'newest'
134
+ }
135
+ else if (params?.sort === 'liquidity') {
136
+ unifiedMarkets.sort((a, b) => b.liquidity - a.liquidity);
137
+ }
138
+ else {
139
+ // Default volume sort
140
+ unifiedMarkets.sort((a, b) => b.volume24h - a.volume24h);
141
+ }
142
+ // Respect limit strictly after flattening
143
+ return unifiedMarkets.slice(0, limit);
144
+ }
145
+ catch (error) {
146
+ console.error("Error fetching Polymarket data:", error);
147
+ return [];
148
+ }
149
+ }
150
+ async searchMarkets(query, params) {
151
+ // Polymarket Gamma API doesn't support native search
152
+ // Fetch a larger batch and filter client-side
153
+ const searchLimit = 100; // Fetch more markets to search through
154
+ try {
155
+ // Fetch markets with a higher limit
156
+ const markets = await this.fetchMarkets({
157
+ ...params,
158
+ limit: searchLimit
159
+ });
160
+ // Client-side text filtering
161
+ const lowerQuery = query.toLowerCase();
162
+ const filtered = markets.filter(market => market.title.toLowerCase().includes(lowerQuery) ||
163
+ market.description.toLowerCase().includes(lowerQuery));
164
+ // Apply limit to filtered results
165
+ const limit = params?.limit || 20;
166
+ return filtered.slice(0, limit);
167
+ }
168
+ catch (error) {
169
+ console.error("Error searching Polymarket data:", error);
170
+ return [];
171
+ }
172
+ }
173
+ /**
174
+ * Fetch specific markets by their URL slug.
175
+ * Useful for looking up a specific event from a URL.
176
+ * @param slug - The event slug (e.g. "will-fed-cut-rates-in-march")
177
+ */
178
+ async getMarketsBySlug(slug) {
179
+ try {
180
+ const response = await axios_1.default.get(this.baseUrl, {
181
+ params: { slug: slug }
182
+ });
183
+ const events = response.data;
184
+ if (!events || events.length === 0)
185
+ return [];
186
+ // We can reuse the logic from fetchMarkets if we extract it,
187
+ // but for now I'll duplicate the extraction logic to keep it self-contained
188
+ // and avoid safe refactoring risks.
189
+ // Actually, fetchMarkets is built to work with the Gamma response structure.
190
+ // So we can manually map the response using the same logic.
191
+ const unifiedMarkets = [];
192
+ for (const event of events) {
193
+ if (!event.markets)
194
+ continue;
195
+ for (const market of event.markets) {
196
+ const outcomes = [];
197
+ let outcomeLabels = [];
198
+ let outcomePrices = [];
199
+ let clobTokenIds = [];
200
+ try {
201
+ outcomeLabels = typeof market.outcomes === 'string' ? JSON.parse(market.outcomes) : (market.outcomes || []);
202
+ outcomePrices = typeof market.outcomePrices === 'string' ? JSON.parse(market.outcomePrices) : (market.outcomePrices || []);
203
+ clobTokenIds = typeof market.clobTokenIds === 'string' ? JSON.parse(market.clobTokenIds) : (market.clobTokenIds || []);
204
+ }
205
+ catch (e) {
206
+ console.warn(`Parse error for market ${market.id}`, e);
207
+ }
208
+ let candidateName = market.groupItemTitle;
209
+ if (!candidateName && market.question)
210
+ candidateName = market.question;
211
+ if (outcomeLabels.length > 0) {
212
+ outcomeLabels.forEach((label, index) => {
213
+ let outcomeLabel = label;
214
+ // Clean up Yes/No labels if candidate name is available
215
+ if (candidateName && label.toLowerCase() === 'yes')
216
+ outcomeLabel = candidateName;
217
+ else if (candidateName && label.toLowerCase() === 'no')
218
+ outcomeLabel = `Not ${candidateName}`;
219
+ // 24h Price Change Logic
220
+ let priceChange = 0;
221
+ if (index === 0 || label.toLowerCase() === 'yes' || (candidateName && label === candidateName)) {
222
+ priceChange = Number(market.oneDayPriceChange || 0);
223
+ }
224
+ outcomes.push({
225
+ id: String(index),
226
+ label: outcomeLabel,
227
+ price: parseFloat(outcomePrices[index] || "0") || 0,
228
+ priceChange24h: priceChange,
229
+ metadata: {
230
+ clobTokenId: clobTokenIds[index]
231
+ }
232
+ });
233
+ });
234
+ }
235
+ unifiedMarkets.push({
236
+ id: market.id,
237
+ title: event.title,
238
+ description: market.description || event.description,
239
+ outcomes: outcomes,
240
+ resolutionDate: market.endDate ? new Date(market.endDate) : new Date(),
241
+ volume24h: Number(market.volume24hr || market.volume_24h || 0),
242
+ volume: Number(market.volume || 0),
243
+ liquidity: Number(market.liquidity || market.rewards?.liquidity || 0),
244
+ openInterest: Number(market.openInterest || market.open_interest || 0),
245
+ url: `https://polymarket.com/event/${event.slug}`,
246
+ image: event.image || market.image,
247
+ category: event.category || event.tags?.[0]?.label,
248
+ tags: event.tags?.map((t) => t.label) || []
249
+ });
250
+ }
251
+ }
252
+ return unifiedMarkets;
253
+ }
254
+ catch (error) {
255
+ console.error(`Error fetching Polymarket slug ${slug}:`, error);
256
+ return [];
257
+ }
258
+ }
259
+ /**
260
+ * Map our generic CandleInterval to Polymarket's fidelity (in minutes)
261
+ */
262
+ mapIntervalToFidelity(interval) {
263
+ const mapping = {
264
+ '1m': 1,
265
+ '5m': 5,
266
+ '15m': 15,
267
+ '1h': 60,
268
+ '6h': 360,
269
+ '1d': 1440
270
+ };
271
+ return mapping[interval];
272
+ }
273
+ /**
274
+ * Fetch historical price data (OHLCV candles) for a specific token.
275
+ * @param id - The CLOB token ID (e.g., outcome token ID)
276
+ */
277
+ async getMarketHistory(id, params) {
278
+ // ID Validation: Polymarket CLOB requires a Token ID (long numeric string) not a Market ID
279
+ if (id.length < 10 && /^\d+$/.test(id)) {
280
+ throw new Error(`Invalid ID for Polymarket history: "${id}". You provided a Market ID, but Polymarket's CLOB API requires a Token ID. Use outcome.metadata.clobTokenId instead.`);
281
+ }
282
+ try {
283
+ const fidelity = this.mapIntervalToFidelity(params.resolution);
284
+ const nowTs = Math.floor(Date.now() / 1000);
285
+ // 1. Smart Lookback Calculation
286
+ // If start/end not provided, calculate window based on limit * resolution
287
+ let startTs = params.start ? Math.floor(params.start.getTime() / 1000) : 0;
288
+ let endTs = params.end ? Math.floor(params.end.getTime() / 1000) : nowTs;
289
+ if (!params.start) {
290
+ // Default limit is usually 20 in the example, but safety margin is good.
291
+ // If limit is not set, we default to 100 candles.
292
+ const count = params.limit || 100;
293
+ // fidelity is in minutes.
294
+ const durationSeconds = count * fidelity * 60;
295
+ startTs = endTs - durationSeconds;
296
+ }
297
+ const queryParams = {
298
+ market: id,
299
+ fidelity: fidelity,
300
+ startTs: startTs,
301
+ endTs: endTs
302
+ };
303
+ const response = await axios_1.default.get(`${this.clobUrl}/prices-history`, {
304
+ params: queryParams
305
+ });
306
+ const history = response.data.history || [];
307
+ // 2. Align Timestamps (Snap to Grid)
308
+ // Polymarket returns random tick timestamps (e.g. 1:00:21).
309
+ // We want to normalize this to the start of the bucket (1:00:00).
310
+ const resolutionMs = fidelity * 60 * 1000;
311
+ const candles = history.map((item) => {
312
+ const rawMs = item.t * 1000;
313
+ const snappedMs = Math.floor(rawMs / resolutionMs) * resolutionMs;
314
+ return {
315
+ timestamp: snappedMs, // Aligned timestamp
316
+ open: item.p,
317
+ high: item.p,
318
+ low: item.p,
319
+ close: item.p,
320
+ volume: undefined
321
+ };
322
+ });
323
+ // Apply limit if specified
324
+ if (params.limit && candles.length > params.limit) {
325
+ return candles.slice(-params.limit);
326
+ }
327
+ return candles;
328
+ }
329
+ catch (error) {
330
+ if (axios_1.default.isAxiosError(error) && error.response) {
331
+ const apiError = error.response.data?.error || error.response.data?.message || "Unknown API Error";
332
+ throw new Error(`Polymarket History API Error (${error.response.status}): ${apiError}. Used ID: ${id}`);
333
+ }
334
+ console.error(`Unexpected error fetching Polymarket history for ${id}:`, error);
335
+ throw error;
336
+ }
337
+ }
338
+ /**
339
+ * Fetch the current order book for a specific token.
340
+ * @param id - The CLOB token ID
341
+ */
342
+ async getOrderBook(id) {
343
+ try {
344
+ const response = await axios_1.default.get(`${this.clobUrl}/book`, {
345
+ params: { token_id: id }
346
+ });
347
+ const data = response.data;
348
+ // Response format: { bids: [{price: "0.52", size: "100"}], asks: [...] }
349
+ const bids = (data.bids || []).map((level) => ({
350
+ price: parseFloat(level.price),
351
+ size: parseFloat(level.size)
352
+ })).sort((a, b) => b.price - a.price); // Sort Bids Descending (Best/Highest first)
353
+ const asks = (data.asks || []).map((level) => ({
354
+ price: parseFloat(level.price),
355
+ size: parseFloat(level.size)
356
+ })).sort((a, b) => a.price - b.price); // Sort Asks Ascending (Best/Lowest first)
357
+ return {
358
+ bids,
359
+ asks,
360
+ timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now()
361
+ };
362
+ }
363
+ catch (error) {
364
+ console.error(`Error fetching Polymarket orderbook for ${id}:`, error);
365
+ return { bids: [], asks: [] };
366
+ }
367
+ }
368
+ /**
369
+ * Fetch raw trade history for a specific token.
370
+ * @param id - The CLOB token ID
371
+ *
372
+ * NOTE: Polymarket's /trades endpoint currently requires L2 Authentication (API Key).
373
+ * This method will return an empty array if an API key is not provided in headers.
374
+ * Use getMarketHistory for public historical price data instead.
375
+ */
376
+ async getTradeHistory(id, params) {
377
+ // ID Validation
378
+ if (id.length < 10 && /^\d+$/.test(id)) {
379
+ throw new Error(`Invalid ID for Polymarket trades: "${id}". You provided a Market ID, but Polymarket's CLOB API requires a Token ID.`);
380
+ }
381
+ try {
382
+ const queryParams = {
383
+ market: id
384
+ };
385
+ // Add time filters if provided
386
+ if (params.start) {
387
+ queryParams.after = Math.floor(params.start.getTime() / 1000);
388
+ }
389
+ if (params.end) {
390
+ queryParams.before = Math.floor(params.end.getTime() / 1000);
391
+ }
392
+ const response = await axios_1.default.get(`${this.clobUrl}/trades`, {
393
+ params: queryParams
394
+ });
395
+ // Response is an array of trade objects
396
+ const trades = response.data || [];
397
+ const mappedTrades = trades.map((trade) => ({
398
+ id: trade.id || `${trade.timestamp}-${trade.price}`,
399
+ timestamp: trade.timestamp * 1000, // Convert to milliseconds
400
+ price: parseFloat(trade.price),
401
+ amount: parseFloat(trade.size || trade.amount || 0),
402
+ side: trade.side === 'BUY' ? 'buy' : trade.side === 'SELL' ? 'sell' : 'unknown'
403
+ }));
404
+ // Apply limit if specified
405
+ if (params.limit && mappedTrades.length > params.limit) {
406
+ return mappedTrades.slice(-params.limit); // Return most recent N trades
407
+ }
408
+ return mappedTrades;
409
+ }
410
+ catch (error) {
411
+ if (axios_1.default.isAxiosError(error) && error.response) {
412
+ const apiError = error.response.data?.error || error.response.data?.message || "Unknown API Error";
413
+ throw new Error(`Polymarket Trades API Error (${error.response.status}): ${apiError}. Used ID: ${id}`);
414
+ }
415
+ console.error(`Unexpected error fetching Polymarket trades for ${id}:`, error);
416
+ throw error;
417
+ }
418
+ }
419
+ }
420
+ exports.PolymarketExchange = PolymarketExchange;
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./BaseExchange"), exports);
18
+ __exportStar(require("./types"), exports);
19
+ __exportStar(require("./exchanges/Polymarket"), exports);
20
+ __exportStar(require("./exchanges/Kalshi"), exports);
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ // ----------------------------------------------------------------------------
3
+ // Core Data Models
4
+ // ----------------------------------------------------------------------------
5
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,23 +1,26 @@
1
1
  {
2
- "name": "pmxtjs",
3
- "version": "0.0.1",
4
- "description": "The ccxt for Prediction Markets (Node.js bindings)",
5
- "main": "index.js",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/samueltinnerholm/PMXT.git"
9
- },
10
- "keywords": [
11
- "prediction-markets",
12
- "trading",
13
- "polymarket",
14
- "kalshi",
15
- "ccxt"
16
- ],
17
- "author": "PMXT Team",
18
- "license": "MIT",
19
- "bugs": {
20
- "url": "https://github.com/samueltinnerholm/PMXT/issues"
21
- },
22
- "homepage": "https://github.com/samueltinnerholm/PMXT#readme"
2
+ "name": "pmxtjs",
3
+ "version": "0.1.1",
4
+ "description": "pmxt is a unified prediction market data API. The ccxt for prediction markets.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "directories": {
11
+ "example": "examples",
12
+ "test": "test"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "jest -c jest.config.js"
17
+ },
18
+ "keywords": [],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "type": "commonjs",
22
+ "dependencies": {
23
+ "axios": "^1.7.9",
24
+ "tsx": "^4.21.0"
25
+ }
23
26
  }
package/Cargo.toml DELETED
@@ -1,15 +0,0 @@
1
- [package]
2
- name = "pmxt-node"
3
- version = "0.0.1"
4
- edition = "2021"
5
-
6
- [lib]
7
- crate-type = ["cdylib"]
8
-
9
- [dependencies]
10
- napi = { version = "2.12", features = ["async"] }
11
- napi-derive = "2.12"
12
- pmxt-core = { path = "../../crates/core" }
13
-
14
- [build-dependencies]
15
- napi-build = "2.0"
package/README.md DELETED
@@ -1,47 +0,0 @@
1
- # PMXT: The ccxt for Prediction Markets
2
-
3
- PMXT is a unified endpoint for trading on prediction markets (Polymarket, Kalshi, etc.), written in Rust for performance with native bindings for Python and Node.js.
4
-
5
- ## Architecture
6
-
7
- This project is organized as a Cargo Workspace:
8
-
9
- - **`crates/core`**: Defines the shared `Exchange` trait and data models (`Market`, `Order`, `Ticker`).
10
- - **`crates/polymarket`**: The implementation for Polymarket.
11
- - **`bindings/python`**: The native Python extension (using PyO3).
12
-
13
- ## Development
14
-
15
- ### Prerequisites
16
-
17
- - Rust (cargo)
18
- - Python 3.9+
19
- - `maturin` (for building Python bindings) -> `pip install maturin`
20
-
21
- ### Building Python Bindings
22
-
23
- To build and install the python library into your current environment:
24
-
25
- ```bash
26
- cd bindings/python
27
- maturin develop
28
- ```
29
-
30
- ### Usage (Python)
31
-
32
- ```python
33
- import pmxt
34
- import asyncio
35
-
36
- async def main():
37
- exchange = pmxt.Polymarket(api_key="your_key")
38
- markets = await exchange.load_markets()
39
- print(markets)
40
-
41
- asyncio.run(main())
42
- ```
43
-
44
- ## Contributing
45
-
46
- 1. Implement `Exchange` trait for a new market in `crates/new_market`.
47
- 2. Register it in `bindings/python/src/lib.rs`.
package/src/lib.rs DELETED
@@ -1,6 +0,0 @@
1
- use napi_derive::napi;
2
-
3
- #[napi]
4
- pub fn info() -> String {
5
- "PMXT Node.js Bindings".to_string()
6
- }