@catalyst-team/poly-sdk 0.4.6 → 0.4.7

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.
Files changed (55) hide show
  1. package/README.md +18 -9
  2. package/README.zh-CN.md +18 -9
  3. package/dist/src/clients/data-api.d.ts +25 -0
  4. package/dist/src/clients/data-api.d.ts.map +1 -1
  5. package/dist/src/clients/data-api.js +57 -0
  6. package/dist/src/clients/data-api.js.map +1 -1
  7. package/dist/src/core/types.d.ts +55 -0
  8. package/dist/src/core/types.d.ts.map +1 -1
  9. package/dist/src/index.d.ts +6 -5
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +4 -2
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/realtime/index.d.ts +18 -0
  14. package/dist/src/realtime/index.d.ts.map +1 -0
  15. package/dist/src/realtime/index.js +14 -0
  16. package/dist/src/realtime/index.js.map +1 -0
  17. package/dist/src/realtime/realtime-data-client.d.ts +274 -0
  18. package/dist/src/realtime/realtime-data-client.d.ts.map +1 -0
  19. package/dist/src/realtime/realtime-data-client.js +771 -0
  20. package/dist/src/realtime/realtime-data-client.js.map +1 -0
  21. package/dist/src/realtime/types.d.ts +485 -0
  22. package/dist/src/realtime/types.d.ts.map +1 -0
  23. package/dist/src/realtime/types.js +36 -0
  24. package/dist/src/realtime/types.js.map +1 -0
  25. package/dist/src/services/arbitrage-service.d.ts.map +1 -1
  26. package/dist/src/services/arbitrage-service.js +2 -1
  27. package/dist/src/services/arbitrage-service.js.map +1 -1
  28. package/dist/src/services/dip-arb-service.d.ts.map +1 -1
  29. package/dist/src/services/dip-arb-service.js +3 -19
  30. package/dist/src/services/dip-arb-service.js.map +1 -1
  31. package/dist/src/services/market-service.d.ts +93 -11
  32. package/dist/src/services/market-service.d.ts.map +1 -1
  33. package/dist/src/services/market-service.js +189 -22
  34. package/dist/src/services/market-service.js.map +1 -1
  35. package/dist/src/services/order-handle.test.d.ts +15 -0
  36. package/dist/src/services/order-handle.test.d.ts.map +1 -0
  37. package/dist/src/services/order-handle.test.js +333 -0
  38. package/dist/src/services/order-handle.test.js.map +1 -0
  39. package/dist/src/services/order-manager.d.ts +162 -6
  40. package/dist/src/services/order-manager.d.ts.map +1 -1
  41. package/dist/src/services/order-manager.js +419 -30
  42. package/dist/src/services/order-manager.js.map +1 -1
  43. package/dist/src/services/realtime-service-v2.d.ts +122 -6
  44. package/dist/src/services/realtime-service-v2.d.ts.map +1 -1
  45. package/dist/src/services/realtime-service-v2.js +475 -70
  46. package/dist/src/services/realtime-service-v2.js.map +1 -1
  47. package/dist/src/services/trading-service.d.ts +129 -1
  48. package/dist/src/services/trading-service.d.ts.map +1 -1
  49. package/dist/src/services/trading-service.js +198 -5
  50. package/dist/src/services/trading-service.js.map +1 -1
  51. package/package.json +1 -2
  52. package/dist/src/services/ctf-detector.d.ts +0 -215
  53. package/dist/src/services/ctf-detector.d.ts.map +0 -1
  54. package/dist/src/services/ctf-detector.js +0 -420
  55. package/dist/src/services/ctf-detector.js.map +0 -1
@@ -0,0 +1,771 @@
1
+ /**
2
+ * RealTimeDataClient
3
+ *
4
+ * Custom WebSocket client for Polymarket real-time data.
5
+ * Replaces @polymarket/real-time-data-client with proper:
6
+ * - RFC 6455 ping/pong mechanism
7
+ * - Exponential backoff reconnection
8
+ * - Full subscription management
9
+ *
10
+ * WebSocket Protocol (from official docs):
11
+ * - URL: wss://ws-subscriptions-clob.polymarket.com/ws/market
12
+ * - Initial subscription: { type: "MARKET", assets_ids: ["token_id_1", ...] }
13
+ * - Dynamic subscribe: { operation: "subscribe", assets_ids: ["token_id_1"] }
14
+ * - Dynamic unsubscribe: { operation: "unsubscribe", assets_ids: ["token_id_1"] }
15
+ *
16
+ * Event types: book, price_change, last_trade_price, tick_size_change, best_bid_ask
17
+ *
18
+ * @see https://docs.polymarket.com/developers/CLOB/websocket/wss-overview
19
+ * @see https://docs.polymarket.com/developers/CLOB/websocket/market-channel
20
+ */
21
+ import WebSocket from 'ws';
22
+ import { ConnectionStatus, WS_ENDPOINTS, } from './types.js';
23
+ // Default to market channel
24
+ const DEFAULT_URL = WS_ENDPOINTS.MARKET;
25
+ const DEFAULT_PING_INTERVAL = 30_000; // 30 seconds
26
+ const DEFAULT_PONG_TIMEOUT = 10_000; // 10 seconds
27
+ const DEFAULT_RECONNECT_DELAY = 1_000; // 1 second
28
+ const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
29
+ export class RealTimeDataClient {
30
+ ws = null;
31
+ config;
32
+ status = ConnectionStatus.DISCONNECTED;
33
+ reconnectAttempts = 0;
34
+ reconnectTimer = null;
35
+ pingTimer = null;
36
+ pongTimer = null;
37
+ pongReceived = true;
38
+ intentionalDisconnect = false;
39
+ constructor(config = {}) {
40
+ // Determine URL: explicit URL > channel-based URL > default
41
+ let url = config.url;
42
+ if (!url) {
43
+ if (config.channel === 'USER') {
44
+ url = WS_ENDPOINTS.USER;
45
+ }
46
+ else {
47
+ url = WS_ENDPOINTS.MARKET;
48
+ }
49
+ }
50
+ this.config = {
51
+ url,
52
+ autoReconnect: config.autoReconnect ?? true,
53
+ pingInterval: config.pingInterval ?? DEFAULT_PING_INTERVAL,
54
+ reconnectDelay: config.reconnectDelay ?? DEFAULT_RECONNECT_DELAY,
55
+ maxReconnectAttempts: config.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS,
56
+ pongTimeout: config.pongTimeout ?? DEFAULT_PONG_TIMEOUT,
57
+ debug: config.debug ?? false,
58
+ onConnect: config.onConnect,
59
+ onMessage: config.onMessage,
60
+ onStatusChange: config.onStatusChange,
61
+ };
62
+ }
63
+ /**
64
+ * Connect to the WebSocket server
65
+ */
66
+ connect() {
67
+ if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
68
+ this.log('Already connected or connecting');
69
+ return;
70
+ }
71
+ this.intentionalDisconnect = false;
72
+ this.setStatus(ConnectionStatus.CONNECTING);
73
+ this.log(`Connecting to ${this.config.url}`);
74
+ this.ws = new WebSocket(this.config.url);
75
+ this.ws.on('open', this.handleOpen.bind(this));
76
+ this.ws.on('message', this.handleMessage.bind(this));
77
+ this.ws.on('close', this.handleClose.bind(this));
78
+ this.ws.on('error', this.handleError.bind(this));
79
+ this.ws.on('pong', this.handlePong.bind(this));
80
+ }
81
+ /**
82
+ * Disconnect from the WebSocket server
83
+ */
84
+ disconnect() {
85
+ this.log('Disconnecting...');
86
+ this.intentionalDisconnect = true;
87
+ this.cleanup();
88
+ if (this.ws) {
89
+ this.ws.close(1000, 'Client disconnect');
90
+ this.ws = null;
91
+ }
92
+ this.setStatus(ConnectionStatus.DISCONNECTED);
93
+ }
94
+ /**
95
+ * Subscribe to market data
96
+ *
97
+ * For initial subscription (when first connecting or adding new tokens):
98
+ * { type: "MARKET", assets_ids: ["token_id_1", "token_id_2"] }
99
+ *
100
+ * For adding tokens to existing subscription:
101
+ * { operation: "subscribe", assets_ids: ["token_id_3"] }
102
+ *
103
+ * @deprecated Use subscribeMarket() for the new API format
104
+ */
105
+ subscribe(msg) {
106
+ // Convert old format to new format for backwards compatibility
107
+ const assetsIds = [];
108
+ for (const sub of msg.subscriptions) {
109
+ if (sub.filters) {
110
+ try {
111
+ const parsed = JSON.parse(sub.filters);
112
+ if (Array.isArray(parsed)) {
113
+ assetsIds.push(...parsed);
114
+ }
115
+ }
116
+ catch {
117
+ // Ignore parse errors
118
+ }
119
+ }
120
+ }
121
+ if (assetsIds.length > 0) {
122
+ this.subscribeMarket(assetsIds);
123
+ }
124
+ }
125
+ /**
126
+ * Unsubscribe from market data
127
+ *
128
+ * Format: { operation: "unsubscribe", assets_ids: ["token_id_1"] }
129
+ *
130
+ * @deprecated Use unsubscribeMarket() for the new API format
131
+ */
132
+ unsubscribe(msg) {
133
+ // Convert old format to new format for backwards compatibility
134
+ const assetsIds = [];
135
+ for (const sub of msg.subscriptions) {
136
+ if (sub.filters) {
137
+ try {
138
+ const parsed = JSON.parse(sub.filters);
139
+ if (Array.isArray(parsed)) {
140
+ assetsIds.push(...parsed);
141
+ }
142
+ }
143
+ catch {
144
+ // Ignore parse errors
145
+ }
146
+ }
147
+ }
148
+ if (assetsIds.length > 0) {
149
+ this.unsubscribeMarket(assetsIds);
150
+ }
151
+ }
152
+ /**
153
+ * Subscribe to market data (new API)
154
+ *
155
+ * @param assetsIds - Array of token IDs to subscribe to
156
+ * @param isInitial - If true, sends initial subscription format { type: "MARKET", ... }
157
+ * If false, sends dynamic format { operation: "subscribe", ... }
158
+ */
159
+ subscribeMarket(assetsIds, isInitial = true) {
160
+ if (isInitial) {
161
+ // Initial subscription format
162
+ const msg = {
163
+ type: 'MARKET',
164
+ assets_ids: assetsIds,
165
+ };
166
+ this.send(JSON.stringify(msg));
167
+ }
168
+ else {
169
+ // Dynamic subscription format
170
+ const msg = {
171
+ operation: 'subscribe',
172
+ assets_ids: assetsIds,
173
+ };
174
+ this.send(JSON.stringify(msg));
175
+ }
176
+ }
177
+ /**
178
+ * Unsubscribe from market data (new API)
179
+ *
180
+ * @param assetsIds - Array of token IDs to unsubscribe from
181
+ */
182
+ unsubscribeMarket(assetsIds) {
183
+ const msg = {
184
+ operation: 'unsubscribe',
185
+ assets_ids: assetsIds,
186
+ };
187
+ this.send(JSON.stringify(msg));
188
+ }
189
+ /**
190
+ * Subscribe to user channel (requires authentication)
191
+ *
192
+ * User channel provides personal order and trade events.
193
+ * Note: This requires connecting to the USER WebSocket endpoint.
194
+ *
195
+ * Format: { type: "USER", auth: { apiKey, secret, passphrase }, markets?: [...] }
196
+ *
197
+ * @param auth - CLOB API credentials
198
+ * @param markets - Optional array of condition IDs to filter events
199
+ */
200
+ subscribeUser(auth, markets) {
201
+ const msg = {
202
+ type: 'USER',
203
+ auth,
204
+ };
205
+ if (markets && markets.length > 0) {
206
+ msg.markets = markets;
207
+ }
208
+ this.send(JSON.stringify(msg));
209
+ }
210
+ /**
211
+ * Subscribe to Binance crypto prices
212
+ *
213
+ * Provides real-time price updates from Binance exchange.
214
+ *
215
+ * **Usage:**
216
+ * ```typescript
217
+ * const client = new RealTimeDataClient({ url: WS_ENDPOINTS.LIVE_DATA });
218
+ * client.connect();
219
+ * client.subscribeCryptoPrices(['btcusdt', 'ethusdt', 'solusdt', 'xrpusdt']);
220
+ * ```
221
+ *
222
+ * **Symbol Format:** Lowercase concatenated pairs (e.g., 'btcusdt', 'ethusdt', 'solusdt', 'xrpusdt')
223
+ *
224
+ * **Subscription Message Format:**
225
+ * ```json
226
+ * {
227
+ * "action": "subscribe",
228
+ * "subscriptions": [{
229
+ * "topic": "crypto_prices",
230
+ * "type": "*",
231
+ * "filters": "{\"symbol\":\"btcusdt\"}"
232
+ * }]
233
+ * }
234
+ * ```
235
+ *
236
+ * **Response Message Format:**
237
+ * ```json
238
+ * {
239
+ * "topic": "crypto_prices",
240
+ * "type": "update",
241
+ * "timestamp": 1769846473135,
242
+ * "payload": {
243
+ * "symbol": "ethusdt",
244
+ * "timestamp": 1769846473000,
245
+ * "value": 2681.42,
246
+ * "full_accuracy_value": "2681.42000000"
247
+ * }
248
+ * }
249
+ * ```
250
+ *
251
+ * **Important Notes:**
252
+ * - Each symbol requires a separate subscription message
253
+ * - Use `type: "*"` (not `"update"`)
254
+ * - Filters must be JSON-stringified: `JSON.stringify({ symbol: "btcusdt" })`
255
+ * - This implementation matches the official @polymarket/real-time-data-client
256
+ *
257
+ * @param symbols - Array of Binance symbols in lowercase (e.g., ['btcusdt', 'ethusdt'])
258
+ * @see https://docs.polymarket.com/developers/RTDS/RTDS-crypto-prices
259
+ * @see https://github.com/Polymarket/real-time-data-client
260
+ */
261
+ subscribeCryptoPrices(symbols) {
262
+ if (symbols.length === 0)
263
+ return;
264
+ for (const symbol of symbols) {
265
+ const msg = {
266
+ action: 'subscribe',
267
+ subscriptions: [
268
+ {
269
+ topic: 'crypto_prices',
270
+ type: '*',
271
+ filters: JSON.stringify({ symbol }),
272
+ },
273
+ ],
274
+ };
275
+ this.send(JSON.stringify(msg));
276
+ }
277
+ }
278
+ /**
279
+ * Unsubscribe from Binance crypto prices
280
+ *
281
+ * @param symbols - Array of Binance symbols to unsubscribe from
282
+ */
283
+ unsubscribeCryptoPrices(symbols) {
284
+ if (symbols.length === 0)
285
+ return;
286
+ for (const symbol of symbols) {
287
+ const msg = {
288
+ action: 'unsubscribe',
289
+ subscriptions: [
290
+ {
291
+ topic: 'crypto_prices',
292
+ type: '*',
293
+ filters: JSON.stringify({ symbol }),
294
+ },
295
+ ],
296
+ };
297
+ this.send(JSON.stringify(msg));
298
+ }
299
+ }
300
+ /**
301
+ * Subscribe to Chainlink crypto prices
302
+ *
303
+ * Provides real-time price updates from Chainlink oracles (official settlement price source).
304
+ *
305
+ * **Usage:**
306
+ * ```typescript
307
+ * const client = new RealTimeDataClient({ url: WS_ENDPOINTS.LIVE_DATA });
308
+ * client.connect();
309
+ * client.subscribeCryptoChainlinkPrices(['btc/usd', 'eth/usd', 'sol/usd', 'xrp/usd']);
310
+ * ```
311
+ *
312
+ * **Symbol Format:** Lowercase slash-separated pairs (e.g., 'btc/usd', 'eth/usd', 'sol/usd', 'xrp/usd')
313
+ *
314
+ * **Subscription Message Format:**
315
+ * ```json
316
+ * {
317
+ * "action": "subscribe",
318
+ * "subscriptions": [{
319
+ * "topic": "crypto_prices_chainlink",
320
+ * "type": "*",
321
+ * "filters": "{\"symbol\":\"btc/usd\"}"
322
+ * }]
323
+ * }
324
+ * ```
325
+ *
326
+ * **Response Message Format:**
327
+ * ```json
328
+ * {
329
+ * "topic": "crypto_prices_chainlink",
330
+ * "type": "update",
331
+ * "timestamp": 1769833333076,
332
+ * "payload": {
333
+ * "symbol": "btc/usd",
334
+ * "timestamp": 1769833332000,
335
+ * "value": 83915.04025109926,
336
+ * "full_accuracy_value": "83915040251099250000000"
337
+ * }
338
+ * }
339
+ * ```
340
+ *
341
+ * **Important Notes:**
342
+ * - Each symbol requires a separate subscription message
343
+ * - Use `type: "*"` for all message types
344
+ * - Filters must be JSON-stringified: `JSON.stringify({ symbol: "btc/usd" })`
345
+ * - Chainlink prices are used as the official settlement source for 15m crypto markets
346
+ * - Higher precision available via `full_accuracy_value` field
347
+ *
348
+ * @param symbols - Array of Chainlink symbols in lowercase (e.g., ['btc/usd', 'eth/usd'])
349
+ * @see https://docs.polymarket.com/developers/RTDS/RTDS-crypto-prices
350
+ * @see https://data.chain.link/streams - Official Chainlink data feeds
351
+ */
352
+ subscribeCryptoChainlinkPrices(symbols) {
353
+ if (symbols.length === 0)
354
+ return;
355
+ for (const symbol of symbols) {
356
+ const msg = {
357
+ action: 'subscribe',
358
+ subscriptions: [
359
+ {
360
+ topic: 'crypto_prices_chainlink',
361
+ type: '*',
362
+ filters: JSON.stringify({ symbol }),
363
+ },
364
+ ],
365
+ };
366
+ this.send(JSON.stringify(msg));
367
+ }
368
+ }
369
+ /**
370
+ * Unsubscribe from Chainlink crypto prices
371
+ *
372
+ * @param symbols - Array of Chainlink symbols to unsubscribe from
373
+ */
374
+ unsubscribeCryptoChainlinkPrices(symbols) {
375
+ if (symbols.length === 0)
376
+ return;
377
+ for (const symbol of symbols) {
378
+ const msg = {
379
+ action: 'unsubscribe',
380
+ subscriptions: [
381
+ {
382
+ topic: 'crypto_prices_chainlink',
383
+ type: '*',
384
+ filters: JSON.stringify({ symbol }),
385
+ },
386
+ ],
387
+ };
388
+ this.send(JSON.stringify(msg));
389
+ }
390
+ }
391
+ /**
392
+ * Check if connected
393
+ */
394
+ isConnected() {
395
+ return this.status === ConnectionStatus.CONNECTED;
396
+ }
397
+ /**
398
+ * Get current connection status
399
+ */
400
+ getStatus() {
401
+ return this.status;
402
+ }
403
+ // ============================================================================
404
+ // Private Methods
405
+ // ============================================================================
406
+ send(data) {
407
+ if (this.ws?.readyState === WebSocket.OPEN) {
408
+ this.ws.send(data);
409
+ this.log(`Sent: ${data.slice(0, 200)}${data.length > 200 ? '...' : ''}`);
410
+ }
411
+ else {
412
+ this.log('Cannot send: WebSocket not open');
413
+ }
414
+ }
415
+ handleOpen() {
416
+ this.log('WebSocket connected');
417
+ this.reconnectAttempts = 0;
418
+ this.pongReceived = true;
419
+ this.setStatus(ConnectionStatus.CONNECTED);
420
+ this.startPing();
421
+ this.config.onConnect?.(this);
422
+ }
423
+ handleMessage(data) {
424
+ try {
425
+ const raw = data.toString();
426
+ this.log(`Raw message: ${raw.slice(0, 200)}${raw.length > 200 ? '...' : ''}`);
427
+ const parsed = JSON.parse(raw);
428
+ // Handle different message formats from Polymarket CLOB WebSocket
429
+ const messages = this.parseMessages(parsed);
430
+ for (const message of messages) {
431
+ this.config.onMessage?.(this, message);
432
+ }
433
+ }
434
+ catch (err) {
435
+ this.log(`Message parse error: ${err}`);
436
+ }
437
+ }
438
+ /**
439
+ * Parse incoming WebSocket messages into our Message format
440
+ *
441
+ * ## Market Channel Events (topic: 'clob_market')
442
+ * @see https://docs.polymarket.com/developers/CLOB/websocket/market-channel
443
+ *
444
+ * | Event Type | Format | Trigger |
445
+ * |-------------------|-----------------------------------------------------------|----------------------------------|
446
+ * | book | Array: [{ market, asset_id, bids, asks, timestamp, hash }]| Initial subscribe or trade |
447
+ * | price_change | { market, price_changes: [{ asset_id, price, size, ... }]}| Order placed or cancelled |
448
+ * | last_trade_price | { market, asset_id, price, side, size, fee_rate_bps, ... }| Trade execution |
449
+ * | tick_size_change | { market, asset_id, old_tick_size, new_tick_size, ... } | Price > 0.96 or < 0.04 |
450
+ * | best_bid_ask | { market, asset_id, best_bid, best_ask, spread, ... } | Best price change (feature-flag) |
451
+ * | new_market | { id, question, market, slug, assets_ids, outcomes, ... } | Market creation (feature-flag) |
452
+ * | market_resolved | { ..., winning_asset_id, winning_outcome } | Market resolved (feature-flag) |
453
+ *
454
+ * ## User Channel Events (topic: 'clob_user')
455
+ * @see https://docs.polymarket.com/developers/CLOB/websocket/user-channel
456
+ *
457
+ * | Event Type | Format | Trigger |
458
+ * |------------|-----------------------------------------------------------------|--------------------------------|
459
+ * | trade | { event_type: 'trade', status, side, price, size, maker_orders }| Order matched/mined/confirmed |
460
+ * | order | { event_type: 'order', type, side, price, original_size, ... } | Order placed/updated/cancelled |
461
+ */
462
+ parseMessages(raw) {
463
+ const messages = [];
464
+ // ========================================================================
465
+ // Array messages (book snapshots from market channel)
466
+ // ========================================================================
467
+ if (Array.isArray(raw)) {
468
+ for (const item of raw) {
469
+ if (typeof item === 'object' && item !== null) {
470
+ const book = item;
471
+ // book event: Orderbook snapshot with bids/asks
472
+ if ('bids' in book || 'asks' in book) {
473
+ const timestamp = this.normalizeTimestamp(book.timestamp);
474
+ messages.push({
475
+ topic: 'clob_market',
476
+ type: 'book',
477
+ timestamp,
478
+ payload: book,
479
+ });
480
+ }
481
+ }
482
+ }
483
+ return messages;
484
+ }
485
+ // ========================================================================
486
+ // Object messages
487
+ // ========================================================================
488
+ if (typeof raw === 'object' && raw !== null) {
489
+ const obj = raw;
490
+ const timestamp = this.normalizeTimestamp(obj.timestamp) || Date.now();
491
+ // ----------------------------------------------------------------------
492
+ // User Channel Events (check event_type field first)
493
+ // ----------------------------------------------------------------------
494
+ // trade event: Trade status updates (MATCHED, MINED, CONFIRMED, RETRYING, FAILED)
495
+ if (obj.event_type === 'trade' || ('status' in obj && 'maker_orders' in obj)) {
496
+ messages.push({
497
+ topic: 'clob_user',
498
+ type: 'trade',
499
+ timestamp,
500
+ payload: obj,
501
+ });
502
+ return messages;
503
+ }
504
+ // order event: Order placed (PLACEMENT), updated (UPDATE), or cancelled (CANCELLATION)
505
+ if (obj.event_type === 'order' || ('original_size' in obj && 'size_matched' in obj)) {
506
+ messages.push({
507
+ topic: 'clob_user',
508
+ type: 'order',
509
+ timestamp,
510
+ payload: obj,
511
+ });
512
+ return messages;
513
+ }
514
+ // ----------------------------------------------------------------------
515
+ // Market Channel Events
516
+ // ----------------------------------------------------------------------
517
+ // price_change event: Order placed or cancelled (price level changes)
518
+ if ('price_changes' in obj && Array.isArray(obj.price_changes)) {
519
+ for (const change of obj.price_changes) {
520
+ messages.push({
521
+ topic: 'clob_market',
522
+ type: 'price_change',
523
+ timestamp,
524
+ payload: {
525
+ market: obj.market,
526
+ ...change,
527
+ },
528
+ });
529
+ }
530
+ return messages;
531
+ }
532
+ // last_trade_price event: Trade execution (maker/taker matched)
533
+ if ('fee_rate_bps' in obj || ('price' in obj && 'side' in obj && 'size' in obj && !('price_changes' in obj) && !('original_size' in obj))) {
534
+ messages.push({
535
+ topic: 'clob_market',
536
+ type: 'last_trade_price',
537
+ timestamp,
538
+ payload: obj,
539
+ });
540
+ return messages;
541
+ }
542
+ // tick_size_change event: Tick size adjustment (price > 0.96 or < 0.04)
543
+ if ('old_tick_size' in obj || 'new_tick_size' in obj) {
544
+ messages.push({
545
+ topic: 'clob_market',
546
+ type: 'tick_size_change',
547
+ timestamp,
548
+ payload: obj,
549
+ });
550
+ return messages;
551
+ }
552
+ // best_bid_ask event: Best prices changed (feature-flagged)
553
+ if ('best_bid' in obj && 'best_ask' in obj && 'spread' in obj && !('price_changes' in obj)) {
554
+ messages.push({
555
+ topic: 'clob_market',
556
+ type: 'best_bid_ask',
557
+ timestamp,
558
+ payload: obj,
559
+ });
560
+ return messages;
561
+ }
562
+ // market_resolved event: Market resolution (feature-flagged)
563
+ // Must check before new_market since it extends new_market fields
564
+ if ('winning_asset_id' in obj || 'winning_outcome' in obj) {
565
+ messages.push({
566
+ topic: 'clob_market',
567
+ type: 'market_resolved',
568
+ timestamp,
569
+ payload: obj,
570
+ });
571
+ return messages;
572
+ }
573
+ // new_market event: Market creation (feature-flagged)
574
+ if ('question' in obj && 'slug' in obj && 'assets_ids' in obj && 'outcomes' in obj) {
575
+ messages.push({
576
+ topic: 'clob_market',
577
+ type: 'new_market',
578
+ timestamp,
579
+ payload: obj,
580
+ });
581
+ return messages;
582
+ }
583
+ // book event: Single orderbook snapshot (fallback for non-array format)
584
+ if ('bids' in obj || 'asks' in obj) {
585
+ messages.push({
586
+ topic: 'clob_market',
587
+ type: 'book',
588
+ timestamp,
589
+ payload: obj,
590
+ });
591
+ return messages;
592
+ }
593
+ // ----------------------------------------------------------------------
594
+ // Crypto Price Events (topic: crypto_prices or crypto_prices_chainlink)
595
+ // @see https://docs.polymarket.com/developers/RTDS/RTDS-crypto-prices
596
+ // ----------------------------------------------------------------------
597
+ // crypto_prices or crypto_prices_chainlink: Real-time price updates
598
+ if (obj.topic === 'crypto_prices' || obj.topic === 'crypto_prices_chainlink') {
599
+ const payload = obj.payload;
600
+ const priceTimestamp = this.normalizeTimestamp(payload?.timestamp) || timestamp;
601
+ messages.push({
602
+ topic: obj.topic,
603
+ type: obj.type || 'update',
604
+ timestamp: priceTimestamp,
605
+ payload: payload || obj,
606
+ });
607
+ return messages;
608
+ }
609
+ // Unknown message - log for debugging
610
+ this.log(`Unknown message format: ${JSON.stringify(obj).slice(0, 100)}`);
611
+ }
612
+ return messages;
613
+ }
614
+ /**
615
+ * Normalize timestamp to milliseconds
616
+ * Polymarket sends timestamps in seconds, need to convert to milliseconds
617
+ */
618
+ normalizeTimestamp(ts) {
619
+ if (typeof ts === 'string') {
620
+ const parsed = parseInt(ts, 10);
621
+ if (isNaN(parsed))
622
+ return Date.now();
623
+ // If timestamp is in seconds (< 1e12), convert to milliseconds
624
+ return parsed < 1e12 ? parsed * 1000 : parsed;
625
+ }
626
+ if (typeof ts === 'number') {
627
+ // If timestamp is in seconds (< 1e12), convert to milliseconds
628
+ return ts < 1e12 ? ts * 1000 : ts;
629
+ }
630
+ return Date.now();
631
+ }
632
+ handleClose(code, reason) {
633
+ this.log(`WebSocket closed: ${code} - ${reason.toString()}`);
634
+ this.cleanup();
635
+ if (this.intentionalDisconnect) {
636
+ this.setStatus(ConnectionStatus.DISCONNECTED);
637
+ return;
638
+ }
639
+ if (this.config.autoReconnect) {
640
+ this.handleReconnect();
641
+ }
642
+ else {
643
+ this.setStatus(ConnectionStatus.DISCONNECTED);
644
+ }
645
+ }
646
+ handleError(err) {
647
+ this.log(`WebSocket error: ${err.message}`);
648
+ // Don't set status here - the 'close' event will follow
649
+ }
650
+ handlePong() {
651
+ this.log('Received pong');
652
+ this.pongReceived = true;
653
+ this.clearPongTimeout();
654
+ }
655
+ // ============================================================================
656
+ // Ping/Pong Mechanism
657
+ // ============================================================================
658
+ /**
659
+ * Start periodic ping to keep connection alive
660
+ *
661
+ * Uses RFC 6455 WebSocket ping frames, which the server MUST respond to
662
+ * with pong frames. If no pong is received within pongTimeout, we
663
+ * consider the connection dead and reconnect.
664
+ */
665
+ startPing() {
666
+ this.stopPing();
667
+ this.pingTimer = setInterval(() => {
668
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
669
+ this.log('Ping skipped: WebSocket not open');
670
+ return;
671
+ }
672
+ if (!this.pongReceived) {
673
+ this.log('Pong not received for previous ping - connection may be dead');
674
+ this.handleDeadConnection();
675
+ return;
676
+ }
677
+ this.pongReceived = false;
678
+ this.ws.ping();
679
+ this.log('Sent ping');
680
+ // Set timeout for pong response
681
+ this.setPongTimeout();
682
+ }, this.config.pingInterval);
683
+ }
684
+ stopPing() {
685
+ if (this.pingTimer) {
686
+ clearInterval(this.pingTimer);
687
+ this.pingTimer = null;
688
+ }
689
+ this.clearPongTimeout();
690
+ }
691
+ setPongTimeout() {
692
+ this.clearPongTimeout();
693
+ this.pongTimer = setTimeout(() => {
694
+ if (!this.pongReceived) {
695
+ this.log('Pong timeout - connection dead');
696
+ this.handleDeadConnection();
697
+ }
698
+ }, this.config.pongTimeout);
699
+ }
700
+ clearPongTimeout() {
701
+ if (this.pongTimer) {
702
+ clearTimeout(this.pongTimer);
703
+ this.pongTimer = null;
704
+ }
705
+ }
706
+ handleDeadConnection() {
707
+ this.log('Handling dead connection');
708
+ this.cleanup();
709
+ if (this.ws) {
710
+ this.ws.terminate(); // Force close
711
+ this.ws = null;
712
+ }
713
+ if (this.config.autoReconnect && !this.intentionalDisconnect) {
714
+ this.handleReconnect();
715
+ }
716
+ else {
717
+ this.setStatus(ConnectionStatus.DISCONNECTED);
718
+ }
719
+ }
720
+ // ============================================================================
721
+ // Reconnection Logic
722
+ // ============================================================================
723
+ /**
724
+ * Handle reconnection with exponential backoff
725
+ * Delays: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at maxReconnectAttempts)
726
+ */
727
+ handleReconnect() {
728
+ if (this.intentionalDisconnect) {
729
+ return;
730
+ }
731
+ if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
732
+ this.log(`Max reconnect attempts (${this.config.maxReconnectAttempts}) reached`);
733
+ this.setStatus(ConnectionStatus.DISCONNECTED);
734
+ return;
735
+ }
736
+ const delay = this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts);
737
+ this.reconnectAttempts++;
738
+ this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`);
739
+ this.setStatus(ConnectionStatus.RECONNECTING);
740
+ this.reconnectTimer = setTimeout(() => {
741
+ this.reconnectTimer = null;
742
+ this.connect();
743
+ }, delay);
744
+ }
745
+ cancelReconnect() {
746
+ if (this.reconnectTimer) {
747
+ clearTimeout(this.reconnectTimer);
748
+ this.reconnectTimer = null;
749
+ }
750
+ }
751
+ // ============================================================================
752
+ // Utility Methods
753
+ // ============================================================================
754
+ cleanup() {
755
+ this.stopPing();
756
+ this.cancelReconnect();
757
+ }
758
+ setStatus(status) {
759
+ if (this.status !== status) {
760
+ this.status = status;
761
+ this.log(`Status changed: ${status}`);
762
+ this.config.onStatusChange?.(status);
763
+ }
764
+ }
765
+ log(message) {
766
+ if (this.config.debug) {
767
+ console.log(`[RealTimeDataClient] ${message}`);
768
+ }
769
+ }
770
+ }
771
+ //# sourceMappingURL=realtime-data-client.js.map