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.
- package/dist/BaseExchange.d.ts +46 -0
- package/dist/BaseExchange.js +56 -0
- package/dist/exchanges/kalshi/fetchOrderBook.js +33 -8
- package/dist/exchanges/kalshi/index.d.ts +12 -1
- package/dist/exchanges/kalshi/index.js +35 -7
- package/dist/exchanges/kalshi/websocket.d.ts +39 -0
- package/dist/exchanges/kalshi/websocket.js +316 -0
- package/dist/exchanges/polymarket/index.d.ts +12 -1
- package/dist/exchanges/polymarket/index.js +34 -6
- package/dist/exchanges/polymarket/websocket.d.ts +41 -0
- package/dist/exchanges/polymarket/websocket.js +219 -0
- package/package.json +17 -5
package/dist/BaseExchange.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/BaseExchange.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|