pmxt-core 1.0.4 → 1.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.
@@ -71,4 +71,50 @@ export declare abstract class PredictionMarketExchange {
71
71
  * Fetch account balances.
72
72
  */
73
73
  fetchBalance(): Promise<Balance[]>;
74
+ /**
75
+ * Watch orderbook updates in real-time via WebSocket.
76
+ * Returns a promise that resolves with the latest orderbook state.
77
+ * The orderbook is maintained internally with incremental updates.
78
+ *
79
+ * Usage (async iterator pattern):
80
+ * ```typescript
81
+ * while (true) {
82
+ * const orderbook = await exchange.watchOrderBook(outcomeId);
83
+ * console.log(orderbook);
84
+ * }
85
+ * ```
86
+ *
87
+ * @param id - The Outcome ID to watch
88
+ * @param limit - Optional limit for orderbook depth
89
+ * @returns Promise that resolves with the current orderbook state
90
+ */
91
+ watchOrderBook(id: string, limit?: number): Promise<OrderBook>;
92
+ /**
93
+ * Watch trade executions in real-time via WebSocket.
94
+ * Returns a promise that resolves with an array of recent trades.
95
+ *
96
+ * Usage (async iterator pattern):
97
+ * ```typescript
98
+ * while (true) {
99
+ * const trades = await exchange.watchTrades(outcomeId);
100
+ * console.log(trades);
101
+ * }
102
+ * ```
103
+ *
104
+ * @param id - The Outcome ID to watch
105
+ * @param since - Optional timestamp to filter trades from
106
+ * @param limit - Optional limit for number of trades
107
+ * @returns Promise that resolves with recent trades
108
+ */
109
+ watchTrades(id: string, since?: number, limit?: number): Promise<Trade[]>;
110
+ /**
111
+ * Close all WebSocket connections and clean up resources.
112
+ * Should be called when done with real-time data to prevent memory leaks.
113
+ *
114
+ * Usage:
115
+ * ```typescript
116
+ * await exchange.close();
117
+ * ```
118
+ */
119
+ close(): Promise<void>;
74
120
  }
@@ -68,5 +68,61 @@ class PredictionMarketExchange {
68
68
  async fetchBalance() {
69
69
  throw new Error("Method fetchBalance not implemented.");
70
70
  }
71
+ // ----------------------------------------------------------------------------
72
+ // WebSocket Streaming Methods
73
+ // ----------------------------------------------------------------------------
74
+ /**
75
+ * Watch orderbook updates in real-time via WebSocket.
76
+ * Returns a promise that resolves with the latest orderbook state.
77
+ * The orderbook is maintained internally with incremental updates.
78
+ *
79
+ * Usage (async iterator pattern):
80
+ * ```typescript
81
+ * while (true) {
82
+ * const orderbook = await exchange.watchOrderBook(outcomeId);
83
+ * console.log(orderbook);
84
+ * }
85
+ * ```
86
+ *
87
+ * @param id - The Outcome ID to watch
88
+ * @param limit - Optional limit for orderbook depth
89
+ * @returns Promise that resolves with the current orderbook state
90
+ */
91
+ async watchOrderBook(id, limit) {
92
+ throw new Error(`watchOrderBook() is not supported by ${this.name}`);
93
+ }
94
+ /**
95
+ * Watch trade executions in real-time via WebSocket.
96
+ * Returns a promise that resolves with an array of recent trades.
97
+ *
98
+ * Usage (async iterator pattern):
99
+ * ```typescript
100
+ * while (true) {
101
+ * const trades = await exchange.watchTrades(outcomeId);
102
+ * console.log(trades);
103
+ * }
104
+ * ```
105
+ *
106
+ * @param id - The Outcome ID to watch
107
+ * @param since - Optional timestamp to filter trades from
108
+ * @param limit - Optional limit for number of trades
109
+ * @returns Promise that resolves with recent trades
110
+ */
111
+ async watchTrades(id, since, limit) {
112
+ throw new Error(`watchTrades() is not supported by ${this.name}`);
113
+ }
114
+ /**
115
+ * Close all WebSocket connections and clean up resources.
116
+ * Should be called when done with real-time data to prevent memory leaks.
117
+ *
118
+ * Usage:
119
+ * ```typescript
120
+ * await exchange.close();
121
+ * ```
122
+ */
123
+ async close() {
124
+ // Default implementation: no-op
125
+ // Exchanges with WebSocket support should override this
126
+ }
71
127
  }
72
128
  exports.PredictionMarketExchange = PredictionMarketExchange;
@@ -7,19 +7,44 @@ exports.fetchOrderBook = fetchOrderBook;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  async function fetchOrderBook(id) {
9
9
  try {
10
+ // Check if this is a NO outcome request
11
+ const isNoOutcome = id.endsWith('-NO');
10
12
  const ticker = id.replace(/-NO$/, '');
11
13
  const url = `https://api.elections.kalshi.com/trade-api/v2/markets/${ticker}/orderbook`;
12
14
  const response = await axios_1.default.get(url);
13
15
  const data = response.data.orderbook;
14
16
  // Structure: { yes: [[price, qty], ...], no: [[price, qty], ...] }
15
- const bids = (data.yes || []).map((level) => ({
16
- price: level[0] / 100,
17
- size: level[1]
18
- }));
19
- const asks = (data.no || []).map((level) => ({
20
- price: (100 - level[0]) / 100,
21
- size: level[1]
22
- }));
17
+ // Kalshi returns bids at their actual prices (not inverted)
18
+ // - yes: bids for buying YES at price X
19
+ // - no: bids for buying NO at price X
20
+ let bids;
21
+ let asks;
22
+ if (isNoOutcome) {
23
+ // NO outcome order book:
24
+ // - Bids: people buying NO (use data.no directly)
25
+ // - Asks: people selling NO = people buying YES (invert data.yes)
26
+ bids = (data.no || []).map((level) => ({
27
+ price: level[0] / 100,
28
+ size: level[1]
29
+ }));
30
+ asks = (data.yes || []).map((level) => ({
31
+ price: 1 - (level[0] / 100), // Invert YES price to get NO ask price
32
+ size: level[1]
33
+ }));
34
+ }
35
+ else {
36
+ // YES outcome order book:
37
+ // - Bids: people buying YES (use data.yes directly)
38
+ // - Asks: people selling YES = people buying NO (invert data.no)
39
+ bids = (data.yes || []).map((level) => ({
40
+ price: level[0] / 100,
41
+ size: level[1]
42
+ }));
43
+ asks = (data.no || []).map((level) => ({
44
+ price: 1 - (level[0] / 100), // Invert NO price to get YES ask price
45
+ size: level[1]
46
+ }));
47
+ }
23
48
  // Sort bids desc, asks asc
24
49
  bids.sort((a, b) => b.price - a.price);
25
50
  asks.sort((a, b) => a.price - b.price);
@@ -1,8 +1,15 @@
1
1
  import { PredictionMarketExchange, MarketFilterParams, HistoryFilterParams, ExchangeCredentials } from '../../BaseExchange';
2
2
  import { UnifiedMarket, PriceCandle, OrderBook, Trade, Balance, Order, Position, CreateOrderParams } from '../../types';
3
+ import { KalshiWebSocketConfig } from './websocket';
4
+ export { KalshiWebSocketConfig };
5
+ export interface KalshiExchangeOptions {
6
+ credentials?: ExchangeCredentials;
7
+ websocket?: KalshiWebSocketConfig;
8
+ }
3
9
  export declare class KalshiExchange extends PredictionMarketExchange {
4
10
  private auth?;
5
- constructor(credentials?: ExchangeCredentials);
11
+ private wsConfig?;
12
+ constructor(options?: ExchangeCredentials | KalshiExchangeOptions);
6
13
  get name(): string;
7
14
  private ensureAuth;
8
15
  fetchMarkets(params?: MarketFilterParams): Promise<UnifiedMarket[]>;
@@ -18,4 +25,8 @@ export declare class KalshiExchange extends PredictionMarketExchange {
18
25
  fetchOpenOrders(marketId?: string): Promise<Order[]>;
19
26
  fetchPositions(): Promise<Position[]>;
20
27
  private mapKalshiOrderStatus;
28
+ private ws?;
29
+ watchOrderBook(id: string, limit?: number): Promise<OrderBook>;
30
+ watchTrades(id: string, since?: number, limit?: number): Promise<Trade[]>;
31
+ close(): Promise<void>;
21
32
  }
@@ -13,9 +13,23 @@ const fetchOHLCV_1 = require("./fetchOHLCV");
13
13
  const fetchOrderBook_1 = require("./fetchOrderBook");
14
14
  const fetchTrades_1 = require("./fetchTrades");
15
15
  const auth_1 = require("./auth");
16
+ const websocket_1 = require("./websocket");
16
17
  class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
17
- constructor(credentials) {
18
+ constructor(options) {
19
+ // Support both old signature (credentials only) and new signature (options object)
20
+ let credentials;
21
+ let wsConfig;
22
+ if (options && 'credentials' in options) {
23
+ // New signature: KalshiExchangeOptions
24
+ credentials = options.credentials;
25
+ wsConfig = options.websocket;
26
+ }
27
+ else {
28
+ // Old signature: ExchangeCredentials directly
29
+ credentials = options;
30
+ }
18
31
  super(credentials);
32
+ this.wsConfig = wsConfig;
19
33
  if (credentials?.apiKey && credentials?.privateKey) {
20
34
  this.auth = new auth_1.KalshiAuth(credentials);
21
35
  }
@@ -84,7 +98,6 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
84
98
  }];
85
99
  }
86
100
  catch (error) {
87
- console.error("Kalshi fetchBalance error:", error?.response?.data || error.message);
88
101
  throw error;
89
102
  }
90
103
  }
@@ -135,7 +148,6 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
135
148
  };
136
149
  }
137
150
  catch (error) {
138
- console.error("Kalshi createOrder error:", error?.response?.data || error.message);
139
151
  throw error;
140
152
  }
141
153
  }
@@ -161,7 +173,6 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
161
173
  };
162
174
  }
163
175
  catch (error) {
164
- console.error("Kalshi cancelOrder error:", error?.response?.data || error.message);
165
176
  throw error;
166
177
  }
167
178
  }
@@ -188,7 +199,6 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
188
199
  };
189
200
  }
190
201
  catch (error) {
191
- console.error("Kalshi fetchOrder error:", error?.response?.data || error.message);
192
202
  throw error;
193
203
  }
194
204
  }
@@ -221,7 +231,6 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
221
231
  }));
222
232
  }
223
233
  catch (error) {
224
- console.error("Kalshi fetchOpenOrders error:", error?.response?.data || error.message);
225
234
  return [];
226
235
  }
227
236
  }
@@ -250,7 +259,6 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
250
259
  });
251
260
  }
252
261
  catch (error) {
253
- console.error("Kalshi fetchPositions error:", error?.response?.data || error.message);
254
262
  return [];
255
263
  }
256
264
  }
@@ -269,5 +277,25 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
269
277
  return 'open';
270
278
  }
271
279
  }
280
+ async watchOrderBook(id, limit) {
281
+ const auth = this.ensureAuth();
282
+ if (!this.ws) {
283
+ this.ws = new websocket_1.KalshiWebSocket(auth, this.wsConfig);
284
+ }
285
+ return this.ws.watchOrderBook(id);
286
+ }
287
+ async watchTrades(id, since, limit) {
288
+ const auth = this.ensureAuth();
289
+ if (!this.ws) {
290
+ this.ws = new websocket_1.KalshiWebSocket(auth, this.wsConfig);
291
+ }
292
+ return this.ws.watchTrades(id);
293
+ }
294
+ async close() {
295
+ if (this.ws) {
296
+ await this.ws.close();
297
+ this.ws = undefined;
298
+ }
299
+ }
272
300
  }
273
301
  exports.KalshiExchange = KalshiExchange;
@@ -0,0 +1,39 @@
1
+ import { OrderBook, Trade } from '../../types';
2
+ import { KalshiAuth } from './auth';
3
+ export interface KalshiWebSocketConfig {
4
+ /** WebSocket URL (default: wss://api.elections.kalshi.com/trade-api/ws/v2) */
5
+ wsUrl?: string;
6
+ /** Reconnection interval in milliseconds (default: 5000) */
7
+ reconnectIntervalMs?: number;
8
+ }
9
+ /**
10
+ * Kalshi WebSocket implementation for real-time order book and trade streaming.
11
+ * Follows CCXT Pro-style async iterator pattern.
12
+ */
13
+ export declare class KalshiWebSocket {
14
+ private ws?;
15
+ private auth;
16
+ private config;
17
+ private orderBookResolvers;
18
+ private tradeResolvers;
19
+ private orderBooks;
20
+ private subscribedTickers;
21
+ private messageIdCounter;
22
+ private isConnecting;
23
+ private isConnected;
24
+ private reconnectTimer?;
25
+ constructor(auth: KalshiAuth, config?: KalshiWebSocketConfig);
26
+ private connect;
27
+ private scheduleReconnect;
28
+ private subscribeToOrderbook;
29
+ private subscribeToTrades;
30
+ private handleMessage;
31
+ private handleOrderbookSnapshot;
32
+ private handleOrderbookDelta;
33
+ private applyDelta;
34
+ private handleTrade;
35
+ private resolveOrderBook;
36
+ watchOrderBook(ticker: string): Promise<OrderBook>;
37
+ watchTrades(ticker: string): Promise<Trade[]>;
38
+ close(): Promise<void>;
39
+ }
@@ -0,0 +1,316 @@
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.KalshiWebSocket = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ /**
9
+ * Kalshi WebSocket implementation for real-time order book and trade streaming.
10
+ * Follows CCXT Pro-style async iterator pattern.
11
+ */
12
+ class KalshiWebSocket {
13
+ constructor(auth, config = {}) {
14
+ this.orderBookResolvers = new Map();
15
+ this.tradeResolvers = new Map();
16
+ this.orderBooks = new Map();
17
+ this.subscribedTickers = new Set();
18
+ this.messageIdCounter = 1;
19
+ this.isConnecting = false;
20
+ this.isConnected = false;
21
+ this.auth = auth;
22
+ this.config = {
23
+ wsUrl: config.wsUrl || 'wss://api.elections.kalshi.com/trade-api/ws/v2',
24
+ reconnectIntervalMs: config.reconnectIntervalMs || 5000
25
+ };
26
+ }
27
+ async connect() {
28
+ if (this.isConnected || this.isConnecting) {
29
+ return;
30
+ }
31
+ this.isConnecting = true;
32
+ return new Promise((resolve, reject) => {
33
+ try {
34
+ // Extract path from URL for signature
35
+ const url = new URL(this.config.wsUrl);
36
+ const path = url.pathname;
37
+ console.log(`Kalshi WS: Connecting to ${this.config.wsUrl} (using path ${path} for signature)`);
38
+ // Get authentication headers
39
+ const headers = this.auth.getHeaders('GET', path);
40
+ this.ws = new ws_1.default(this.config.wsUrl, { headers });
41
+ this.ws.on('open', () => {
42
+ this.isConnected = true;
43
+ this.isConnecting = false;
44
+ console.log('Kalshi WebSocket connected');
45
+ // Resubscribe to all tickers if reconnecting
46
+ if (this.subscribedTickers.size > 0) {
47
+ this.subscribeToOrderbook(Array.from(this.subscribedTickers));
48
+ }
49
+ resolve();
50
+ });
51
+ this.ws.on('message', (data) => {
52
+ try {
53
+ const message = JSON.parse(data.toString());
54
+ this.handleMessage(message);
55
+ }
56
+ catch (error) {
57
+ console.error('Error parsing Kalshi WebSocket message:', error);
58
+ }
59
+ });
60
+ this.ws.on('error', (error) => {
61
+ console.error('Kalshi WebSocket error:', error);
62
+ this.isConnecting = false;
63
+ reject(error);
64
+ });
65
+ this.ws.on('close', () => {
66
+ console.log('Kalshi WebSocket closed');
67
+ this.isConnected = false;
68
+ this.isConnecting = false;
69
+ this.scheduleReconnect();
70
+ });
71
+ }
72
+ catch (error) {
73
+ this.isConnecting = false;
74
+ reject(error);
75
+ }
76
+ });
77
+ }
78
+ scheduleReconnect() {
79
+ if (this.reconnectTimer) {
80
+ clearTimeout(this.reconnectTimer);
81
+ }
82
+ this.reconnectTimer = setTimeout(() => {
83
+ console.log('Attempting to reconnect Kalshi WebSocket...');
84
+ this.connect().catch(console.error);
85
+ }, this.config.reconnectIntervalMs);
86
+ }
87
+ subscribeToOrderbook(marketTickers) {
88
+ if (!this.ws || !this.isConnected) {
89
+ return;
90
+ }
91
+ const subscription = {
92
+ id: this.messageIdCounter++,
93
+ cmd: 'subscribe',
94
+ params: {
95
+ channels: ['orderbook_delta'],
96
+ market_tickers: marketTickers
97
+ }
98
+ };
99
+ this.ws.send(JSON.stringify(subscription));
100
+ }
101
+ subscribeToTrades(marketTickers) {
102
+ if (!this.ws || !this.isConnected) {
103
+ return;
104
+ }
105
+ const subscription = {
106
+ id: this.messageIdCounter++,
107
+ cmd: 'subscribe',
108
+ params: {
109
+ channels: ['trade'],
110
+ market_tickers: marketTickers
111
+ }
112
+ };
113
+ this.ws.send(JSON.stringify(subscription));
114
+ }
115
+ handleMessage(message) {
116
+ const msgType = message.type;
117
+ // Kalshi V2 uses 'data' field for payloads
118
+ const data = message.data || message.msg;
119
+ if (!data && msgType !== 'subscribed' && msgType !== 'pong') {
120
+ return;
121
+ }
122
+ // Add message-level timestamp as a fallback for handlers
123
+ if (data && typeof data === 'object' && !data.ts && !data.created_time) {
124
+ data.message_ts = message.ts || message.time;
125
+ }
126
+ switch (msgType) {
127
+ case 'orderbook_snapshot':
128
+ this.handleOrderbookSnapshot(data);
129
+ break;
130
+ case 'orderbook_delta':
131
+ case 'orderbook_update': // Some versions use update
132
+ this.handleOrderbookDelta(data);
133
+ break;
134
+ case 'trade':
135
+ this.handleTrade(data);
136
+ break;
137
+ case 'error':
138
+ console.error('Kalshi WebSocket error:', message.msg || message.error || message.data);
139
+ break;
140
+ case 'subscribed':
141
+ console.log('Kalshi subscription confirmed:', JSON.stringify(message));
142
+ break;
143
+ case 'pong':
144
+ // Ignore keep-alive
145
+ break;
146
+ default:
147
+ // Ignore unknown message types
148
+ break;
149
+ }
150
+ }
151
+ handleOrderbookSnapshot(data) {
152
+ const ticker = data.market_ticker;
153
+ // Kalshi orderbook structure:
154
+ // yes: [{ price: number (cents), quantity: number }, ...]
155
+ // no: [{ price: number (cents), quantity: number }, ...]
156
+ const bids = (data.yes || []).map((level) => {
157
+ const price = (level.price || level[0]) / 100;
158
+ const size = (level.quantity !== undefined ? level.quantity : (level.size !== undefined ? level.size : level[1])) || 0;
159
+ return { price, size };
160
+ }).sort((a, b) => b.price - a.price);
161
+ const asks = (data.no || []).map((level) => {
162
+ const price = (100 - (level.price || level[0])) / 100;
163
+ const size = (level.quantity !== undefined ? level.quantity : (level.size !== undefined ? level.size : level[1])) || 0;
164
+ return { price, size };
165
+ }).sort((a, b) => a.price - b.price);
166
+ const orderBook = {
167
+ bids,
168
+ asks,
169
+ timestamp: Date.now()
170
+ };
171
+ this.orderBooks.set(ticker, orderBook);
172
+ this.resolveOrderBook(ticker, orderBook);
173
+ }
174
+ handleOrderbookDelta(data) {
175
+ const ticker = data.market_ticker;
176
+ const existing = this.orderBooks.get(ticker);
177
+ if (!existing) {
178
+ // No snapshot yet, skip delta
179
+ return;
180
+ }
181
+ // Apply delta updates
182
+ // Kalshi sends: { price: number, delta: number, side: 'yes' | 'no' }
183
+ const price = (data.price) / 100;
184
+ const delta = data.delta !== undefined ? data.delta : (data.quantity !== undefined ? data.quantity : 0);
185
+ const side = data.side;
186
+ if (side === 'yes') {
187
+ this.applyDelta(existing.bids, price, delta, 'desc');
188
+ }
189
+ else {
190
+ const yesPrice = (100 - data.price) / 100;
191
+ this.applyDelta(existing.asks, yesPrice, delta, 'asc');
192
+ }
193
+ existing.timestamp = Date.now();
194
+ this.resolveOrderBook(ticker, existing);
195
+ }
196
+ applyDelta(levels, price, delta, sortOrder) {
197
+ const existingIndex = levels.findIndex(l => Math.abs(l.price - price) < 0.001);
198
+ if (delta === 0) {
199
+ // Remove level
200
+ if (existingIndex !== -1) {
201
+ levels.splice(existingIndex, 1);
202
+ }
203
+ }
204
+ else {
205
+ // Update or add level
206
+ if (existingIndex !== -1) {
207
+ levels[existingIndex].size += delta;
208
+ if (levels[existingIndex].size <= 0) {
209
+ levels.splice(existingIndex, 1);
210
+ }
211
+ }
212
+ else {
213
+ levels.push({ price, size: delta });
214
+ // Re-sort
215
+ if (sortOrder === 'desc') {
216
+ levels.sort((a, b) => b.price - a.price);
217
+ }
218
+ else {
219
+ levels.sort((a, b) => a.price - b.price);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ handleTrade(data) {
225
+ const ticker = data.market_ticker;
226
+ // Kalshi trade structure:
227
+ // { trade_id, market_ticker, yes_price, no_price, count, created_time, taker_side }
228
+ // The timestamp could be in created_time, created_at, or ts.
229
+ let timestamp = Date.now();
230
+ const rawTime = data.created_time || data.created_at || data.ts || data.time || data.message_ts;
231
+ if (rawTime) {
232
+ const parsed = new Date(rawTime).getTime();
233
+ if (!isNaN(parsed)) {
234
+ timestamp = parsed;
235
+ // If the timestamp is too small, it might be in seconds
236
+ if (timestamp < 10000000000) {
237
+ timestamp *= 1000;
238
+ }
239
+ }
240
+ else if (typeof rawTime === 'number') {
241
+ // If it's already a number but new Date() failed (maybe it's a large timestamp)
242
+ timestamp = rawTime;
243
+ if (timestamp < 10000000000) {
244
+ timestamp *= 1000;
245
+ }
246
+ }
247
+ }
248
+ const trade = {
249
+ id: data.trade_id || `${timestamp}-${Math.random()}`,
250
+ timestamp,
251
+ price: (data.yes_price || data.price) ? (data.yes_price || data.price) / 100 : 0.5,
252
+ amount: data.count || data.size || 0,
253
+ side: data.taker_side === 'yes' || data.side === 'buy' ? 'buy' : data.taker_side === 'no' || data.side === 'sell' ? 'sell' : 'unknown'
254
+ };
255
+ const resolvers = this.tradeResolvers.get(ticker);
256
+ if (resolvers && resolvers.length > 0) {
257
+ resolvers.forEach(r => r.resolve([trade]));
258
+ this.tradeResolvers.set(ticker, []);
259
+ }
260
+ }
261
+ resolveOrderBook(ticker, orderBook) {
262
+ const resolvers = this.orderBookResolvers.get(ticker);
263
+ if (resolvers && resolvers.length > 0) {
264
+ resolvers.forEach(r => r.resolve(orderBook));
265
+ this.orderBookResolvers.set(ticker, []);
266
+ }
267
+ }
268
+ async watchOrderBook(ticker) {
269
+ // Ensure connection
270
+ if (!this.isConnected) {
271
+ await this.connect();
272
+ }
273
+ // Subscribe if not already subscribed
274
+ if (!this.subscribedTickers.has(ticker)) {
275
+ this.subscribedTickers.add(ticker);
276
+ this.subscribeToOrderbook([ticker]);
277
+ }
278
+ // Return a promise that resolves on the next orderbook update
279
+ return new Promise((resolve, reject) => {
280
+ if (!this.orderBookResolvers.has(ticker)) {
281
+ this.orderBookResolvers.set(ticker, []);
282
+ }
283
+ this.orderBookResolvers.get(ticker).push({ resolve, reject });
284
+ });
285
+ }
286
+ async watchTrades(ticker) {
287
+ // Ensure connection
288
+ if (!this.isConnected) {
289
+ await this.connect();
290
+ }
291
+ // Subscribe if not already subscribed
292
+ if (!this.subscribedTickers.has(ticker)) {
293
+ this.subscribedTickers.add(ticker);
294
+ this.subscribeToTrades([ticker]);
295
+ }
296
+ // Return a promise that resolves on the next trade
297
+ return new Promise((resolve, reject) => {
298
+ if (!this.tradeResolvers.has(ticker)) {
299
+ this.tradeResolvers.set(ticker, []);
300
+ }
301
+ this.tradeResolvers.get(ticker).push({ resolve, reject });
302
+ });
303
+ }
304
+ async close() {
305
+ if (this.reconnectTimer) {
306
+ clearTimeout(this.reconnectTimer);
307
+ }
308
+ if (this.ws) {
309
+ this.ws.close();
310
+ this.ws = undefined;
311
+ }
312
+ this.isConnected = false;
313
+ this.isConnecting = false;
314
+ }
315
+ }
316
+ exports.KalshiWebSocket = KalshiWebSocket;
@@ -1,8 +1,15 @@
1
1
  import { PredictionMarketExchange, MarketFilterParams, HistoryFilterParams, ExchangeCredentials } from '../../BaseExchange';
2
2
  import { UnifiedMarket, PriceCandle, OrderBook, Trade, Order, Position, Balance, CreateOrderParams } from '../../types';
3
+ import { PolymarketWebSocketConfig } from './websocket';
4
+ export { PolymarketWebSocketConfig };
5
+ export interface PolymarketExchangeOptions {
6
+ credentials?: ExchangeCredentials;
7
+ websocket?: PolymarketWebSocketConfig;
8
+ }
3
9
  export declare class PolymarketExchange extends PredictionMarketExchange {
4
10
  private auth?;
5
- constructor(credentials?: ExchangeCredentials);
11
+ private wsConfig?;
12
+ constructor(options?: ExchangeCredentials | PolymarketExchangeOptions);
6
13
  get name(): string;
7
14
  fetchMarkets(params?: MarketFilterParams): Promise<UnifiedMarket[]>;
8
15
  searchMarkets(query: string, params?: MarketFilterParams): Promise<UnifiedMarket[]>;
@@ -20,4 +27,8 @@ export declare class PolymarketExchange extends PredictionMarketExchange {
20
27
  fetchOpenOrders(marketId?: string): Promise<Order[]>;
21
28
  fetchPositions(): Promise<Position[]>;
22
29
  fetchBalance(): Promise<Balance[]>;
30
+ private ws?;
31
+ watchOrderBook(id: string, limit?: number): Promise<OrderBook>;
32
+ watchTrades(id: string, since?: number, limit?: number): Promise<Trade[]>;
33
+ close(): Promise<void>;
23
34
  }
@@ -11,9 +11,23 @@ const fetchTrades_1 = require("./fetchTrades");
11
11
  const fetchPositions_1 = require("./fetchPositions");
12
12
  const auth_1 = require("./auth");
13
13
  const clob_client_1 = require("@polymarket/clob-client");
14
+ const websocket_1 = require("./websocket");
14
15
  class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
15
- constructor(credentials) {
16
+ constructor(options) {
17
+ // Support both old signature (credentials only) and new signature (options object)
18
+ let credentials;
19
+ let wsConfig;
20
+ if (options && 'credentials' in options) {
21
+ // New signature: PolymarketExchangeOptions
22
+ credentials = options.credentials;
23
+ wsConfig = options.websocket;
24
+ }
25
+ else {
26
+ // Old signature: ExchangeCredentials directly
27
+ credentials = options;
28
+ }
16
29
  super(credentials);
30
+ this.wsConfig = wsConfig;
17
31
  // Initialize auth if credentials are provided
18
32
  if (credentials?.privateKey) {
19
33
  this.auth = new auth_1.PolymarketAuth(credentials);
@@ -93,7 +107,6 @@ class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
93
107
  };
94
108
  }
95
109
  catch (error) {
96
- console.error("Polymarket createOrder error:", error);
97
110
  throw error;
98
111
  }
99
112
  }
@@ -116,7 +129,6 @@ class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
116
129
  };
117
130
  }
118
131
  catch (error) {
119
- console.error("Polymarket cancelOrder error:", error);
120
132
  throw error;
121
133
  }
122
134
  }
@@ -140,7 +152,6 @@ class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
140
152
  };
141
153
  }
142
154
  catch (error) {
143
- console.error("Polymarket fetchOrder error:", error);
144
155
  throw error;
145
156
  }
146
157
  }
@@ -166,7 +177,7 @@ class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
166
177
  }));
167
178
  }
168
179
  catch (error) {
169
- console.error("Polymarket fetchOpenOrders error:", error);
180
+ console.error('Error fetching Polymarket open orders:', error);
170
181
  return [];
171
182
  }
172
183
  }
@@ -208,9 +219,26 @@ class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
208
219
  }];
209
220
  }
210
221
  catch (error) {
211
- console.error("Polymarket fetchBalance error:", error);
212
222
  throw error;
213
223
  }
214
224
  }
225
+ async watchOrderBook(id, limit) {
226
+ if (!this.ws) {
227
+ this.ws = new websocket_1.PolymarketWebSocket(this.wsConfig);
228
+ }
229
+ return this.ws.watchOrderBook(id);
230
+ }
231
+ async watchTrades(id, since, limit) {
232
+ if (!this.ws) {
233
+ this.ws = new websocket_1.PolymarketWebSocket(this.wsConfig);
234
+ }
235
+ return this.ws.watchTrades(id);
236
+ }
237
+ async close() {
238
+ if (this.ws) {
239
+ this.ws.close();
240
+ this.ws = undefined;
241
+ }
242
+ }
215
243
  }
216
244
  exports.PolymarketExchange = PolymarketExchange;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * Polymarket WebSocket implementation for pmxt.
4
+ *
5
+ * NOTICE: This implementation depends on "@nevuamarkets/poly-websockets",
6
+ * which is licensed under the AGPL-3.0 License.
7
+ *
8
+ * While pmxt is licensed under MIT, using these specific WebSocket methods
9
+ * may subject your application to the terms of the AGPL-3.0.
10
+ *
11
+ * If you must avoid AGPL-3.0, do not use the watchOrderBook() or
12
+ * watchTrades() methods for Polymarket.
13
+ */
14
+ import { OrderBook, Trade } from '../../types';
15
+ export interface PolymarketWebSocketConfig {
16
+ /** Reconnection check interval in milliseconds (default: 5000) */
17
+ reconnectIntervalMs?: number;
18
+ /** Pending subscription flush interval in milliseconds (default: 100) */
19
+ flushIntervalMs?: number;
20
+ }
21
+ /**
22
+ * Wrapper around @nevuamarkets/poly-websockets that provides CCXT Pro-style
23
+ * watchOrderBook() and watchTrades() methods.
24
+ */
25
+ export declare class PolymarketWebSocket {
26
+ private manager;
27
+ private orderBookResolvers;
28
+ private tradeResolvers;
29
+ private orderBooks;
30
+ private config;
31
+ private initializationPromise?;
32
+ constructor(config?: PolymarketWebSocketConfig);
33
+ private ensureInitialized;
34
+ watchOrderBook(id: string): Promise<OrderBook>;
35
+ watchTrades(id: string): Promise<Trade[]>;
36
+ private handleBookSnapshot;
37
+ private handlePriceChange;
38
+ private handleTrade;
39
+ private resolveOrderBook;
40
+ close(): Promise<void>;
41
+ }
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Polymarket WebSocket implementation for pmxt.
5
+ *
6
+ * NOTICE: This implementation depends on "@nevuamarkets/poly-websockets",
7
+ * which is licensed under the AGPL-3.0 License.
8
+ *
9
+ * While pmxt is licensed under MIT, using these specific WebSocket methods
10
+ * may subject your application to the terms of the AGPL-3.0.
11
+ *
12
+ * If you must avoid AGPL-3.0, do not use the watchOrderBook() or
13
+ * watchTrades() methods for Polymarket.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.PolymarketWebSocket = void 0;
50
+ /**
51
+ * Wrapper around @nevuamarkets/poly-websockets that provides CCXT Pro-style
52
+ * watchOrderBook() and watchTrades() methods.
53
+ */
54
+ class PolymarketWebSocket {
55
+ constructor(config = {}) {
56
+ this.orderBookResolvers = new Map();
57
+ this.tradeResolvers = new Map();
58
+ this.orderBooks = new Map();
59
+ this.config = config;
60
+ }
61
+ async ensureInitialized() {
62
+ if (this.initializationPromise)
63
+ return this.initializationPromise;
64
+ this.initializationPromise = (async () => {
65
+ try {
66
+ // Dynamic import to handle AGPL dependency optionally
67
+ const poly = await Promise.resolve().then(() => __importStar(require('@nevuamarkets/poly-websockets')));
68
+ this.manager = new poly.WSSubscriptionManager({
69
+ onBook: async (events) => {
70
+ for (const event of events) {
71
+ this.handleBookSnapshot(event);
72
+ }
73
+ },
74
+ onPriceChange: async (events) => {
75
+ for (const event of events) {
76
+ this.handlePriceChange(event);
77
+ }
78
+ },
79
+ onLastTradePrice: async (events) => {
80
+ for (const event of events) {
81
+ this.handleTrade(event);
82
+ }
83
+ },
84
+ onError: async (error) => {
85
+ console.error('Polymarket WebSocket error:', error.message);
86
+ },
87
+ }, {
88
+ reconnectAndCleanupIntervalMs: this.config.reconnectIntervalMs ?? 5000,
89
+ pendingFlushIntervalMs: this.config.flushIntervalMs ?? 100,
90
+ });
91
+ }
92
+ catch (e) {
93
+ const error = e;
94
+ if (error.message.includes('Cannot find module')) {
95
+ throw new Error('Polymarket WebSocket support requires the "@nevuamarkets/poly-websockets" package (AGPL-3.0).\n' +
96
+ 'To use this feature, please install it: npm install @nevuamarkets/poly-websockets');
97
+ }
98
+ throw e;
99
+ }
100
+ })();
101
+ return this.initializationPromise;
102
+ }
103
+ async watchOrderBook(id) {
104
+ await this.ensureInitialized();
105
+ // Subscribe to the asset if not already subscribed
106
+ const currentAssets = this.manager.getAssetIds();
107
+ if (!currentAssets.includes(id)) {
108
+ await this.manager.addSubscriptions([id]);
109
+ }
110
+ // Return a promise that resolves on the next orderbook update
111
+ return new Promise((resolve, reject) => {
112
+ if (!this.orderBookResolvers.has(id)) {
113
+ this.orderBookResolvers.set(id, []);
114
+ }
115
+ this.orderBookResolvers.get(id).push({ resolve, reject });
116
+ });
117
+ }
118
+ async watchTrades(id) {
119
+ await this.ensureInitialized();
120
+ // Subscribe to the asset if not already subscribed
121
+ const currentAssets = this.manager.getAssetIds();
122
+ if (!currentAssets.includes(id)) {
123
+ await this.manager.addSubscriptions([id]);
124
+ }
125
+ // Return a promise that resolves on the next trade
126
+ return new Promise((resolve, reject) => {
127
+ if (!this.tradeResolvers.has(id)) {
128
+ this.tradeResolvers.set(id, []);
129
+ }
130
+ this.tradeResolvers.get(id).push({ resolve, reject });
131
+ });
132
+ }
133
+ handleBookSnapshot(event) {
134
+ const id = event.asset_id;
135
+ const bids = event.bids.map((b) => ({
136
+ price: parseFloat(b.price),
137
+ size: parseFloat(b.size),
138
+ })).sort((a, b) => b.price - a.price);
139
+ const asks = event.asks.map((a) => ({
140
+ price: parseFloat(a.price),
141
+ size: parseFloat(a.size),
142
+ })).sort((a, b) => a.price - b.price);
143
+ const orderBook = {
144
+ bids,
145
+ asks,
146
+ timestamp: event.timestamp ? (isNaN(Number(event.timestamp)) ? new Date(event.timestamp).getTime() : Number(event.timestamp)) : Date.now(),
147
+ };
148
+ this.orderBooks.set(id, orderBook);
149
+ this.resolveOrderBook(id, orderBook);
150
+ }
151
+ handlePriceChange(event) {
152
+ // Apply deltas to existing orderbook
153
+ for (const change of event.price_changes) {
154
+ const id = change.asset_id;
155
+ const existing = this.orderBooks.get(id);
156
+ if (!existing) {
157
+ // No snapshot yet, skip delta
158
+ continue;
159
+ }
160
+ const price = parseFloat(change.price);
161
+ const size = parseFloat(change.size);
162
+ const side = change.side.toUpperCase();
163
+ const levels = side === 'BUY' ? existing.bids : existing.asks;
164
+ const existingIndex = levels.findIndex((l) => l.price === price);
165
+ if (size === 0) {
166
+ // Remove level
167
+ if (existingIndex !== -1) {
168
+ levels.splice(existingIndex, 1);
169
+ }
170
+ }
171
+ else {
172
+ // Update or add level
173
+ if (existingIndex !== -1) {
174
+ levels[existingIndex].size = size;
175
+ }
176
+ else {
177
+ levels.push({ price, size });
178
+ // Re-sort
179
+ if (side === 'BUY') {
180
+ levels.sort((a, b) => b.price - a.price);
181
+ }
182
+ else {
183
+ levels.sort((a, b) => a.price - b.price);
184
+ }
185
+ }
186
+ }
187
+ existing.timestamp = event.timestamp ? (isNaN(Number(event.timestamp)) ? new Date(event.timestamp).getTime() : Number(event.timestamp)) : Date.now();
188
+ this.resolveOrderBook(id, existing);
189
+ }
190
+ }
191
+ handleTrade(event) {
192
+ const id = event.asset_id;
193
+ const trade = {
194
+ id: `${event.timestamp}-${Math.random()}`,
195
+ timestamp: event.timestamp ? (isNaN(Number(event.timestamp)) ? new Date(event.timestamp).getTime() : Number(event.timestamp)) : Date.now(),
196
+ price: parseFloat(event.price),
197
+ amount: parseFloat(event.size),
198
+ side: event.side.toLowerCase(),
199
+ };
200
+ const resolvers = this.tradeResolvers.get(id);
201
+ if (resolvers && resolvers.length > 0) {
202
+ resolvers.forEach((r) => r.resolve([trade]));
203
+ this.tradeResolvers.set(id, []);
204
+ }
205
+ }
206
+ resolveOrderBook(id, orderBook) {
207
+ const resolvers = this.orderBookResolvers.get(id);
208
+ if (resolvers && resolvers.length > 0) {
209
+ resolvers.forEach((r) => r.resolve(orderBook));
210
+ this.orderBookResolvers.set(id, []);
211
+ }
212
+ }
213
+ async close() {
214
+ if (this.manager) {
215
+ await this.manager.clearState();
216
+ }
217
+ }
218
+ }
219
+ exports.PolymarketWebSocket = PolymarketWebSocket;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxt-core",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "pmxt is a unified prediction market data API. The ccxt for prediction markets.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,8 +29,8 @@
29
29
  "test": "jest -c jest.config.js",
30
30
  "server": "tsx watch src/server/index.ts",
31
31
  "server:prod": "node dist/server/index.js",
32
- "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=1.0.4,library=urllib3",
33
- "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=1.0.4,supportsES6=true,typescriptThreePlus=true",
32
+ "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=1.1.0,library=urllib3",
33
+ "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=1.1.0,supportsES6=true,typescriptThreePlus=true",
34
34
  "generate:docs": "node ../scripts/generate-api-docs.js",
35
35
  "generate:sdk:all": "npm run generate:sdk:python && npm run generate:sdk:typescript && npm run generate:docs"
36
36
  },
@@ -46,10 +46,13 @@
46
46
  "dotenv": "^17.2.3",
47
47
  "ethers": "^5.8.0",
48
48
  "express": "^5.2.1",
49
+ "isows": "^1.0.6",
49
50
  "jest": "^30.2.0",
50
- "tsx": "^4.21.0"
51
+ "tsx": "^4.21.0",
52
+ "ws": "^8.18.0"
51
53
  },
52
54
  "devDependencies": {
55
+ "@nevuamarkets/poly-websockets": "^1.0.0",
53
56
  "@openapitools/openapi-generator-cli": "^2.27.0",
54
57
  "@types/cors": "^2.8.19",
55
58
  "@types/jest": "^30.0.0",
@@ -59,6 +62,15 @@
59
62
  "identity-obj-proxy": "^3.0.0",
60
63
  "js-yaml": "^3.14.2",
61
64
  "ts-jest": "^29.4.6",
62
- "typescript": "^5.9.3"
65
+ "typescript": "^5.9.3",
66
+ "@types/ws": "^8.5.10"
67
+ },
68
+ "peerDependencies": {
69
+ "@nevuamarkets/poly-websockets": "^1.0.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@nevuamarkets/poly-websockets": {
73
+ "optional": true
74
+ }
63
75
  }
64
76
  }