hedgequantx 2.4.6 → 2.4.8

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 (56) hide show
  1. package/package.json +2 -6
  2. package/src/lib/data.js +204 -473
  3. package/src/lib/m/s1.js +450 -301
  4. package/dist/lib/api.js +0 -1
  5. package/dist/lib/api.jsc +0 -0
  6. package/dist/lib/api2.js +0 -1
  7. package/dist/lib/api2.jsc +0 -0
  8. package/dist/lib/core.js +0 -1
  9. package/dist/lib/core.jsc +0 -0
  10. package/dist/lib/core2.js +0 -1
  11. package/dist/lib/core2.jsc +0 -0
  12. package/dist/lib/data.js +0 -1
  13. package/dist/lib/data.jsc +0 -0
  14. package/dist/lib/data2.js +0 -1
  15. package/dist/lib/data2.jsc +0 -0
  16. package/dist/lib/decoder.js +0 -1
  17. package/dist/lib/decoder.jsc +0 -0
  18. package/dist/lib/m/mod1.js +0 -1
  19. package/dist/lib/m/mod1.jsc +0 -0
  20. package/dist/lib/m/mod2.js +0 -1
  21. package/dist/lib/m/mod2.jsc +0 -0
  22. package/dist/lib/n/r1.js +0 -1
  23. package/dist/lib/n/r1.jsc +0 -0
  24. package/dist/lib/n/r2.js +0 -1
  25. package/dist/lib/n/r2.jsc +0 -0
  26. package/dist/lib/n/r3.js +0 -1
  27. package/dist/lib/n/r3.jsc +0 -0
  28. package/dist/lib/n/r4.js +0 -1
  29. package/dist/lib/n/r4.jsc +0 -0
  30. package/dist/lib/n/r5.js +0 -1
  31. package/dist/lib/n/r5.jsc +0 -0
  32. package/dist/lib/n/r6.js +0 -1
  33. package/dist/lib/n/r6.jsc +0 -0
  34. package/dist/lib/n/r7.js +0 -1
  35. package/dist/lib/n/r7.jsc +0 -0
  36. package/dist/lib/o/util1.js +0 -1
  37. package/dist/lib/o/util1.jsc +0 -0
  38. package/dist/lib/o/util2.js +0 -1
  39. package/dist/lib/o/util2.jsc +0 -0
  40. package/src/lib/api.js +0 -198
  41. package/src/lib/api2.js +0 -353
  42. package/src/lib/core.js +0 -539
  43. package/src/lib/core2.js +0 -341
  44. package/src/lib/data2.js +0 -492
  45. package/src/lib/decoder.js +0 -599
  46. package/src/lib/m/s2.js +0 -34
  47. package/src/lib/n/r1.js +0 -454
  48. package/src/lib/n/r2.js +0 -514
  49. package/src/lib/n/r3.js +0 -631
  50. package/src/lib/n/r4.js +0 -401
  51. package/src/lib/n/r5.js +0 -335
  52. package/src/lib/n/r6.js +0 -425
  53. package/src/lib/n/r7.js +0 -530
  54. package/src/lib/o/l1.js +0 -44
  55. package/src/lib/o/l2.js +0 -427
  56. package/src/lib/python-bridge.js +0 -206
package/src/lib/data.js CHANGED
@@ -1,555 +1,286 @@
1
1
  /**
2
- * Market Data Feed
3
- * Connects to PropFirm market data via SignalR (ProjectX Gateway API)
4
- * Feeds real-time data to the algo engine
2
+ * =============================================================================
3
+ * MARKET DATA FEED - SignalR Real-Time Data
4
+ * =============================================================================
5
+ * Connects to ProjectX Gateway RTC for real-time market data
6
+ *
7
+ * Events emitted:
8
+ * - tick: Quote/trade updates (price, bid, ask, volume)
9
+ * - quote: Quote updates only
10
+ * - trade: Trade executions only
11
+ * - depth: DOM/Level 2 updates
12
+ * - connected: Connection established
13
+ * - disconnected: Connection lost
14
+ * - error: Connection error
15
+ *
16
+ * SOURCE: Based on ProjectX Gateway RTC API
5
17
  */
6
18
 
19
+ 'use strict';
20
+
7
21
  const EventEmitter = require('events');
8
- const signalR = require('@microsoft/signalr');
22
+ const { HubConnectionBuilder, HttpTransportType, LogLevel } = require('@microsoft/signalr');
9
23
 
10
- // ProjectX Gateway SignalR endpoints
11
- // Pattern: replace 'api.' with 'rtc.' in gatewayApi URL
12
- const PROPFIRM_RTC_ENDPOINTS = {
13
- topstep: {
14
- market: 'https://rtc.topstepx.com/hubs/market',
15
- user: 'https://rtc.topstepx.com/hubs/user'
16
- },
17
- alpha_futures: {
18
- market: 'https://rtc.alphafutures.projectx.com/hubs/market',
19
- user: 'https://rtc.alphafutures.projectx.com/hubs/user'
20
- },
21
- tickticktrader: {
22
- market: 'https://rtc.tickticktrader.projectx.com/hubs/market',
23
- user: 'https://rtc.tickticktrader.projectx.com/hubs/user'
24
- },
25
- bulenox: {
26
- market: 'https://rtc.bulenox.projectx.com/hubs/market',
27
- user: 'https://rtc.bulenox.projectx.com/hubs/user'
28
- },
29
- tradeday: {
30
- market: 'https://rtc.tradeday.projectx.com/hubs/market',
31
- user: 'https://rtc.tradeday.projectx.com/hubs/user'
32
- },
33
- blusky: {
34
- market: 'https://rtc.blusky.projectx.com/hubs/market',
35
- user: 'https://rtc.blusky.projectx.com/hubs/user'
36
- },
37
- goat_futures: {
38
- market: 'https://rtc.goatfutures.projectx.com/hubs/market',
39
- user: 'https://rtc.goatfutures.projectx.com/hubs/user'
40
- },
41
- futures_desk: {
42
- market: 'https://rtc.thefuturesdesk.projectx.com/hubs/market',
43
- user: 'https://rtc.thefuturesdesk.projectx.com/hubs/user'
44
- },
45
- daytraders: {
46
- market: 'https://rtc.daytraders.projectx.com/hubs/market',
47
- user: 'https://rtc.daytraders.projectx.com/hubs/user'
48
- },
49
- e8_futures: {
50
- market: 'https://rtc.e8futures.projectx.com/hubs/market',
51
- user: 'https://rtc.e8futures.projectx.com/hubs/user'
52
- },
53
- blue_guardian: {
54
- market: 'https://rtc.blueguardianfutures.projectx.com/hubs/market',
55
- user: 'https://rtc.blueguardianfutures.projectx.com/hubs/user'
56
- },
57
- futures_elite: {
58
- market: 'https://rtc.futureselite.projectx.com/hubs/market',
59
- user: 'https://rtc.futureselite.projectx.com/hubs/user'
60
- },
61
- fxify: {
62
- market: 'https://rtc.fxify.projectx.com/hubs/market',
63
- user: 'https://rtc.fxify.projectx.com/hubs/user'
64
- },
65
- hola_prime: {
66
- market: 'https://rtc.holaprime.projectx.com/hubs/market',
67
- user: 'https://rtc.holaprime.projectx.com/hubs/user'
68
- },
69
- top_one_futures: {
70
- market: 'https://rtc.toponefutures.projectx.com/hubs/market',
71
- user: 'https://rtc.toponefutures.projectx.com/hubs/user'
72
- },
73
- funding_futures: {
74
- market: 'https://rtc.fundingfutures.projectx.com/hubs/market',
75
- user: 'https://rtc.fundingfutures.projectx.com/hubs/user'
76
- },
77
- tx3_funding: {
78
- market: 'https://rtc.tx3funding.projectx.com/hubs/market',
79
- user: 'https://rtc.tx3funding.projectx.com/hubs/user'
80
- },
81
- lucid_trading: {
82
- market: 'https://rtc.lucidtrading.projectx.com/hubs/market',
83
- user: 'https://rtc.lucidtrading.projectx.com/hubs/user'
84
- },
85
- tradeify: {
86
- market: 'https://rtc.tradeify.projectx.com/hubs/market',
87
- user: 'https://rtc.tradeify.projectx.com/hubs/user'
88
- }
24
+ // Inline PROPFIRMS config for RTC URLs (standalone module)
25
+ const PROPFIRMS = {
26
+ topstep: { gatewayApi: 'api.topstepx.com' },
27
+ alpha_futures: { gatewayApi: 'api.alphafutures.projectx.com' },
28
+ tickticktrader: { gatewayApi: 'api.tickticktrader.projectx.com' },
29
+ bulenox: { gatewayApi: 'api.bulenox.projectx.com' },
30
+ tradeday: { gatewayApi: 'api.tradeday.projectx.com' },
31
+ blusky: { gatewayApi: 'api.blusky.projectx.com' },
32
+ goat_futures: { gatewayApi: 'api.goatfutures.projectx.com' },
33
+ futures_desk: { gatewayApi: 'api.thefuturesdesk.projectx.com' },
34
+ daytraders: { gatewayApi: 'api.daytraders.projectx.com' },
35
+ e8_futures: { gatewayApi: 'api.e8futures.projectx.com' },
36
+ blue_guardian: { gatewayApi: 'api.blueguardianfutures.projectx.com' },
37
+ futures_elite: { gatewayApi: 'api.futureselite.projectx.com' },
38
+ fxify: { gatewayApi: 'api.fxify.projectx.com' },
39
+ hola_prime: { gatewayApi: 'api.holaprime.projectx.com' },
40
+ top_one_futures: { gatewayApi: 'api.toponefutures.projectx.com' },
41
+ funding_futures: { gatewayApi: 'api.fundingfutures.projectx.com' },
42
+ tx3_funding: { gatewayApi: 'api.tx3funding.projectx.com' },
43
+ lucid_trading: { gatewayApi: 'api.lucidtrading.projectx.com' },
44
+ tradeify: { gatewayApi: 'api.tradeify.projectx.com' }
89
45
  };
90
46
 
91
- // Default fallback
92
- const DEFAULT_ENDPOINTS = PROPFIRM_RTC_ENDPOINTS.topstep;
93
-
94
- // NO STATIC CONTRACT DATA - All contract specs come from API
95
- // Use contract.tickSize and contract.tickValue from API response
47
+ // =============================================================================
48
+ // MARKET DATA FEED CLASS
49
+ // =============================================================================
96
50
 
97
51
  class MarketDataFeed extends EventEmitter {
98
- constructor(config) {
52
+ constructor(options = {}) {
99
53
  super();
100
54
 
101
- this.config = config;
102
- this.marketConnection = null;
103
- this.userConnection = null;
104
- this.isConnected = false;
55
+ this.propfirmKey = (options.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
56
+ this.connection = null;
57
+ this.connected = false;
58
+ this.subscriptions = new Set();
105
59
  this.reconnectAttempts = 0;
106
60
  this.maxReconnectAttempts = 5;
107
- this.subscriptions = new Set();
61
+ }
62
+
63
+ /**
64
+ * Get market hub URL for propfirm
65
+ */
66
+ _getMarketHubUrl(propfirmKey) {
67
+ const propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
108
68
 
109
- // Data buffers
110
- this.dataBuffers = new Map();
111
- this.lastTick = new Map();
112
- this.orderBook = new Map(); // DOM data
69
+ if (propfirm.rtcApi) {
70
+ return `https://${propfirm.rtcApi}/hubs/market`;
71
+ }
72
+
73
+ if (propfirm.gatewayApi) {
74
+ const rtcHost = propfirm.gatewayApi.replace('gateway-api', 'gateway-rtc');
75
+ return `https://${rtcHost}/hubs/market`;
76
+ }
77
+
78
+ return 'https://gateway-rtc-demo.s2f.projectx.com/hubs/market';
113
79
  }
114
80
 
115
81
  /**
116
- * Connect to market data feed via SignalR
82
+ * Connect to market data hub
117
83
  */
118
- async connect(userToken, propfirm, contractId = null) {
84
+ async connect(token, propfirmKey, contractId = null) {
85
+ if (this.connected) return;
86
+
87
+ this.propfirmKey = propfirmKey || this.propfirmKey;
88
+ const hubUrl = this._getMarketHubUrl(this.propfirmKey);
89
+
119
90
  try {
120
- const endpoints = PROPFIRM_RTC_ENDPOINTS[propfirm] || DEFAULT_ENDPOINTS;
121
- // // console.log(`[MARKET] Connecting to ${propfirm} RTC: ${endpoints.market}`);
122
-
123
- // Build URL with access_token query parameter
124
- const urlWithToken = `${endpoints.market}?access_token=${encodeURIComponent(userToken)}`;
125
-
126
- // Build SignalR connection - NO LOGGING to prevent stdout pollution
127
- this.marketConnection = new signalR.HubConnectionBuilder()
128
- .withUrl(urlWithToken, {
91
+ this.connection = new HubConnectionBuilder()
92
+ .withUrl(hubUrl, {
129
93
  skipNegotiation: true,
130
- transport: signalR.HttpTransportType.WebSockets
94
+ transport: HttpTransportType.WebSockets,
95
+ accessTokenFactory: () => token,
96
+ timeout: 30000,
131
97
  })
132
- .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
133
- .configureLogging(signalR.LogLevel.None)
98
+ .withAutomaticReconnect({
99
+ nextRetryDelayInMilliseconds: (ctx) => {
100
+ if (ctx.previousRetryCount >= this.maxReconnectAttempts) return null;
101
+ return Math.min(1000 * Math.pow(2, ctx.previousRetryCount), 30000);
102
+ }
103
+ })
104
+ .configureLogging(LogLevel.Warning)
134
105
  .build();
106
+
107
+ this._setupEventHandlers();
108
+ await this.connection.start();
135
109
 
136
- // Store contractId for immediate subscription
137
- this._pendingContractId = contractId;
138
-
139
- // Setup event handlers BEFORE starting
140
- this._setupMarketEventHandlers();
141
-
142
- // Start connection
143
- // // console.log(`[MARKET] Starting SignalR connection...`);
144
- await this.marketConnection.start();
145
- // // console.log(`[MARKET] Connected! State: ${this.marketConnection.state}`);
146
-
147
- // IMMEDIATELY subscribe if contractId provided - don't wait!
148
- if (contractId && this.marketConnection.state === 'Connected') {
149
- // // console.log(`[MARKET] Immediate subscribe to ${contractId}`);
150
- try {
151
- await this.marketConnection.invoke('SubscribeContractQuotes', contractId);
152
- // // console.log(`[MARKET] Quotes OK`);
153
- await this.marketConnection.invoke('SubscribeContractTrades', contractId);
154
- // // console.log(`[MARKET] Trades OK`);
155
-
156
- const subscriptionKey = `${contractId}:${contractId}`;
157
- this.subscriptions.add(subscriptionKey);
158
- this.dataBuffers.set(subscriptionKey, []);
159
- } catch (subError) {
160
- // // console.log(`[MARKET] Immediate subscribe failed: ${subError.message}`);
161
- }
162
- }
163
-
164
- this.isConnected = true;
110
+ this.connected = true;
165
111
  this.reconnectAttempts = 0;
166
112
  this.emit('connected');
167
-
168
- return true;
169
-
113
+
114
+ if (contractId) {
115
+ await this.subscribe(null, contractId);
116
+ }
170
117
  } catch (error) {
171
- // // console.log(`[MARKET] Connection error: ${error.message}`);
172
118
  this.emit('error', error);
173
119
  throw error;
174
120
  }
175
121
  }
176
122
 
177
123
  /**
178
- * Setup SignalR event handlers for market data
124
+ * Setup SignalR event handlers
179
125
  */
180
- _setupMarketEventHandlers() {
181
- // Quote updates (bid/ask) - args: [contractId, quoteObject]
182
- this.marketConnection.on('GatewayQuote', (...args) => {
183
- this._handleQuote(args);
184
- });
185
-
186
- // Trade updates - args: [contractId, tradesArray]
187
- this.marketConnection.on('GatewayTrade', (...args) => {
188
- this._handleTrade(args);
189
- });
190
-
191
- // DOM/Depth updates - args: [contractId, depthObject]
192
- this.marketConnection.on('GatewayDepth', (...args) => {
193
- this._handleDepth(args);
126
+ _setupEventHandlers() {
127
+ if (!this.connection) return;
128
+
129
+ // Quote updates
130
+ this.connection.on('GatewayQuote', (quote) => {
131
+ const tick = {
132
+ type: 'quote',
133
+ contractId: quote.symbol || quote.symbolId,
134
+ symbol: quote.symbolName || quote.symbol,
135
+ price: quote.lastPrice,
136
+ bid: quote.bestBid,
137
+ ask: quote.bestAsk,
138
+ change: quote.change,
139
+ changePercent: quote.changePercent,
140
+ open: quote.open,
141
+ high: quote.high,
142
+ low: quote.low,
143
+ volume: quote.volume,
144
+ timestamp: quote.timestamp ? new Date(quote.timestamp).getTime() : Date.now()
145
+ };
146
+ this.emit('tick', tick);
147
+ this.emit('quote', tick);
194
148
  });
195
-
196
- // Gateway logout - server is kicking us out
197
- this.marketConnection.on('GatewayLogout', (...args) => {
198
- // // console.log(`[MARKET] GatewayLogout received:`, args);
149
+
150
+ // Trade executions
151
+ this.connection.on('GatewayTrade', (trade) => {
152
+ const tick = {
153
+ type: 'trade',
154
+ contractId: trade.symbolId,
155
+ price: trade.price,
156
+ volume: trade.volume,
157
+ side: trade.type === 0 ? 'buy' : 'sell',
158
+ lastTradeSide: trade.type === 0 ? 'buy' : 'sell',
159
+ timestamp: trade.timestamp ? new Date(trade.timestamp).getTime() : Date.now()
160
+ };
161
+ this.emit('tick', tick);
162
+ this.emit('trade', tick);
199
163
  });
200
-
201
- // Also handle lowercase version
202
- this.marketConnection.on('gatewaylogout', (...args) => {
203
- // // console.log(`[MARKET] gatewaylogout received:`, args);
164
+
165
+ // DOM updates
166
+ this.connection.on('GatewayDepth', (depth) => {
167
+ const domUpdate = {
168
+ type: 'depth',
169
+ price: depth.price,
170
+ volume: depth.volume,
171
+ currentVolume: depth.currentVolume,
172
+ side: depth.type === 0 ? 'bid' : 'ask',
173
+ timestamp: depth.timestamp ? new Date(depth.timestamp).getTime() : Date.now()
174
+ };
175
+ this.emit('depth', domUpdate);
176
+ this.emit('dom', domUpdate);
204
177
  });
205
-
206
- // Connection state changes
207
- this.marketConnection.onreconnecting((error) => {
208
- // // console.log(`[MARKET] Reconnecting... Error: ${error?.message || 'none'}`);
209
- this.isConnected = false;
210
- this.emit('reconnecting', { error: error?.message });
178
+
179
+ // Connection state
180
+ this.connection.onreconnecting((error) => {
181
+ this.connected = false;
182
+ this.emit('reconnecting', error);
211
183
  });
212
-
213
- this.marketConnection.onreconnected((connectionId) => {
214
- // // console.log(`[MARKET] Reconnected! ConnectionId: ${connectionId}`);
215
- this.isConnected = true;
216
- this.emit('reconnected', { connectionId });
217
- // Resubscribe to all symbols
184
+
185
+ this.connection.onreconnected((connectionId) => {
186
+ this.connected = true;
187
+ this.reconnectAttempts = 0;
188
+ this.emit('reconnected', connectionId);
218
189
  this._resubscribeAll();
219
190
  });
220
-
221
- this.marketConnection.onclose((error) => {
222
- // // console.log(`[MARKET] Connection CLOSED! Error: ${error?.message || 'none'}`);
223
- this.isConnected = false;
224
- this.emit('disconnected', { error: error?.message });
191
+
192
+ this.connection.onclose((error) => {
193
+ this.connected = false;
194
+ this.emit('disconnected', error);
225
195
  });
226
196
  }
227
197
 
228
198
  /**
229
- * Subscribe to symbol market data
230
- * Implements retry logic for connection race conditions
199
+ * Subscribe to contract market data
231
200
  */
232
- async subscribe(symbol, contractId, retryCount = 0) {
233
- // // console.log(`[MARKET] Subscribe called - symbol: ${symbol}, contractId: ${contractId}`);
234
-
235
- const subscriptionKey = `${symbol}:${contractId}`;
236
-
237
- if (this.subscriptions.has(subscriptionKey)) {
238
- return true; // Already subscribed
239
- }
240
-
241
- // Check connection state
242
- const state = this.marketConnection?.state;
243
- // // console.log(`[MARKET] Connection state: ${state}`);
244
-
245
- if (state !== 'Connected') {
246
- if (retryCount < 5) {
247
- // // console.log(`[MARKET] Not connected, waiting 500ms and retrying... (${retryCount + 1}/5)`);
248
- await new Promise(resolve => setTimeout(resolve, 500));
249
- return this.subscribe(symbol, contractId, retryCount + 1);
250
- } else {
251
- // // console.log(`[MARKET] ERROR: Connection not available after 5 retries`);
252
- this.emit('error', new Error('Connection not available'));
253
- return false;
254
- }
201
+ async subscribe(symbol, contractId) {
202
+ if (!this.connection || !this.connected) {
203
+ throw new Error('Not connected');
255
204
  }
205
+
206
+ const id = contractId || symbol;
256
207
 
257
208
  try {
258
- // Subscribe to quotes - do it immediately, no delay
259
- // // console.log(`[MARKET] Subscribing to ${contractId}...`);
260
- await this.marketConnection.invoke('SubscribeContractQuotes', contractId);
261
- // // console.log(`[MARKET] Quotes subscribed`);
262
-
263
- // Subscribe to trades
264
- await this.marketConnection.invoke('SubscribeContractTrades', contractId);
265
- // // console.log(`[MARKET] Trades subscribed`);
266
-
267
- this.subscriptions.add(subscriptionKey);
268
- this.dataBuffers.set(subscriptionKey, []);
269
- this.isConnected = true;
270
-
271
- this.emit('subscribed', { symbol, contractId });
272
- return true;
273
-
209
+ await this.connection.invoke('SubscribeContractQuotes', id);
210
+ await this.connection.invoke('SubscribeContractTrades', id);
211
+ await this.connection.invoke('SubscribeContractMarketDepth', id);
212
+ this.subscriptions.add(id);
213
+ this.emit('subscribed', { symbol, contractId: id });
274
214
  } catch (error) {
275
- // // console.log(`[MARKET] Subscribe ERROR: ${error.message}`);
276
-
277
- // If connection was closed, try to reconnect
278
- if (error.message.includes('not in the') && retryCount < 3) {
279
- // // console.log(`[MARKET] Connection lost, will retry on reconnect`);
280
- this.isConnected = false;
281
- }
282
-
283
- this.emit('error', error);
284
- return false;
215
+ this.emit('error', new Error(`Subscribe failed: ${error.message}`));
216
+ throw error;
285
217
  }
286
218
  }
287
219
 
288
220
  /**
289
- * Unsubscribe from symbol market data
221
+ * Unsubscribe from contract
290
222
  */
291
- async unsubscribe(symbol, contractId) {
292
- const subscriptionKey = `${symbol}:${contractId}`;
293
-
294
- if (!this.subscriptions.has(subscriptionKey)) {
295
- return true;
296
- }
297
-
223
+ async unsubscribe(contractId) {
224
+ if (!this.connection || !this.connected) return;
225
+
298
226
  try {
299
- await this.marketConnection.invoke('UnsubscribeContractQuotes', contractId);
300
- await this.marketConnection.invoke('UnsubscribeContractTrades', contractId);
301
- await this.marketConnection.invoke('UnsubscribeContractMarketDepth', contractId);
302
-
303
- this.subscriptions.delete(subscriptionKey);
304
- this.dataBuffers.delete(subscriptionKey);
305
-
306
- this.emit('unsubscribed', { symbol, contractId });
307
- return true;
308
-
227
+ await this.connection.invoke('UnsubscribeContractQuotes', contractId);
228
+ await this.connection.invoke('UnsubscribeContractTrades', contractId);
229
+ await this.connection.invoke('UnsubscribeContractMarketDepth', contractId);
230
+ this.subscriptions.delete(contractId);
231
+ this.emit('unsubscribed', { contractId });
309
232
  } catch (error) {
310
- this.emit('error', error);
311
- return false;
233
+ // Silently handle
312
234
  }
313
235
  }
314
236
 
315
237
  /**
316
- * Resubscribe to all symbols after reconnection
238
+ * Resubscribe after reconnect
317
239
  */
318
240
  async _resubscribeAll() {
319
- for (const subscriptionKey of this.subscriptions) {
320
- const [symbol, contractId] = subscriptionKey.split(':');
321
- this.subscriptions.delete(subscriptionKey); // Remove so subscribe works
322
- await this.subscribe(symbol, contractId);
323
- }
324
- }
325
-
326
- /**
327
- * Handle quote update (bid/ask)
328
- * Format: args[0] = contractId, args[1] = quote object
329
- */
330
- _handleQuote(args) {
331
- // Debug log every 100th quote
332
- if (!this._quoteCount) this._quoteCount = 0;
333
- this._quoteCount++;
334
- if (this._quoteCount % 100 === 1) {
335
- // // console.log(`[MARKET] Quote #${this._quoteCount}:`, JSON.stringify(args).substring(0, 200));
336
- }
337
-
338
- const contractId = args[0];
339
- const quote = args[1] || {};
340
-
341
- const { symbol, lastPrice, bestBid, bestAsk, change, changePercent, volume, timestamp } = quote;
342
-
343
- // Find matching subscription
344
- let subscriptionKey = null;
345
- for (const key of this.subscriptions) {
346
- if (key.includes(contractId)) {
347
- subscriptionKey = key;
348
- break;
349
- }
350
- }
351
-
352
- if (!subscriptionKey) {
353
- subscriptionKey = `${contractId}:${contractId}`;
354
- }
355
-
356
- // Calculate price - use lastPrice, or mid of bid/ask
357
- const bid = parseFloat(bestBid) || 0;
358
- const ask = parseFloat(bestAsk) || 0;
359
- const last = parseFloat(lastPrice) || 0;
360
- const price = last > 0 ? last : (bid > 0 && ask > 0 ? (bid + ask) / 2 : bid || ask);
361
-
362
- const quoteData = {
363
- type: 'quote',
364
- symbol: symbol || contractId,
365
- contractId,
366
- price,
367
- bid,
368
- ask,
369
- spread: ask > 0 && bid > 0 ? ask - bid : 0,
370
- mid: bid > 0 && ask > 0 ? (bid + ask) / 2 : price,
371
- change: parseFloat(change) || 0,
372
- changePercent: parseFloat(changePercent) || 0,
373
- volume: parseInt(volume) || 0,
374
- timestamp: timestamp || Date.now()
375
- };
376
-
377
- // Update last tick - merge with existing data
378
- const lastTick = this.lastTick.get(subscriptionKey) || {};
379
- const updatedTick = { ...lastTick };
380
-
381
- // Only update fields that have valid values
382
- if (price > 0) updatedTick.price = price;
383
- if (bid > 0) updatedTick.bid = bid;
384
- if (ask > 0) updatedTick.ask = ask;
385
- if (quoteData.spread > 0) updatedTick.spread = quoteData.spread;
386
- if (quoteData.mid > 0) updatedTick.mid = quoteData.mid;
387
- if (quoteData.change !== 0) updatedTick.change = quoteData.change;
388
- if (quoteData.changePercent !== 0) updatedTick.changePercent = quoteData.changePercent;
389
- if (quoteData.volume > 0) updatedTick.volume = quoteData.volume;
390
- updatedTick.timestamp = quoteData.timestamp;
391
- updatedTick.type = 'quote';
392
- updatedTick.symbol = quoteData.symbol;
393
- updatedTick.contractId = contractId;
394
-
395
- this.lastTick.set(subscriptionKey, updatedTick);
396
-
397
- this.emit('quote', updatedTick);
398
- this.emit('tick', updatedTick);
399
- }
400
-
401
- /**
402
- * Handle trade update
403
- * Format: args[0] = contractId, args[1] = array of trades
404
- */
405
- _handleTrade(args) {
406
- // Debug log every 50th trade
407
- if (!this._tradeCount) this._tradeCount = 0;
408
- this._tradeCount++;
409
- if (this._tradeCount % 50 === 1) {
410
- // // console.log(`[MARKET] Trade #${this._tradeCount}:`, JSON.stringify(args).substring(0, 200));
411
- }
412
-
413
- const contractId = args[0];
414
- const trades = args[1] || [];
415
-
416
- // Find matching subscription
417
- let subscriptionKey = null;
418
- for (const key of this.subscriptions) {
419
- if (key.includes(contractId)) {
420
- subscriptionKey = key;
421
- break;
422
- }
423
- }
424
-
425
- if (!subscriptionKey) {
426
- subscriptionKey = `${contractId}:${contractId}`;
427
- }
428
-
429
- // Process each trade in the array
430
- for (const trade of trades) {
431
- const { symbolId, price, timestamp, type, volume } = trade;
432
-
433
- const tradeData = {
434
- type: 'trade',
435
- symbol: symbolId || contractId,
436
- contractId,
437
- price: parseFloat(price),
438
- size: parseInt(volume) || 1,
439
- side: type === 0 ? 'buy' : type === 1 ? 'sell' : 'unknown',
440
- volume: parseInt(volume) || 1,
441
- timestamp: timestamp || Date.now()
442
- };
443
-
444
- // Update last tick with trade price
445
- const lastTick = this.lastTick.get(subscriptionKey) || {};
446
- if (tradeData.price > 0) {
447
- this.lastTick.set(subscriptionKey, {
448
- ...lastTick,
449
- price: tradeData.price,
450
- lastTradeVolume: tradeData.volume,
451
- lastTradeTime: tradeData.timestamp,
452
- lastTradeSide: tradeData.side
453
- });
454
- }
455
-
456
- // Add to buffer
457
- const buffer = this.dataBuffers.get(subscriptionKey) || [];
458
- buffer.push(tradeData);
459
-
460
- // Keep buffer size limited (last 1000 trades)
461
- if (buffer.length > 1000) {
462
- buffer.shift();
241
+ for (const contractId of this.subscriptions) {
242
+ try {
243
+ await this.connection.invoke('SubscribeContractQuotes', contractId);
244
+ await this.connection.invoke('SubscribeContractTrades', contractId);
245
+ await this.connection.invoke('SubscribeContractMarketDepth', contractId);
246
+ } catch (error) {
247
+ // Continue
463
248
  }
464
- this.dataBuffers.set(subscriptionKey, buffer);
465
-
466
- this.emit('trade', tradeData);
467
- this.emit('tick', this.lastTick.get(subscriptionKey));
468
249
  }
469
250
  }
470
251
 
471
252
  /**
472
- * Handle depth/DOM update
473
- * Format: args[0] = contractId, args[1] = depth data
474
- */
475
- _handleDepth(args) {
476
- const contractId = args[0];
477
- const depth = args[1] || {};
478
- const { timestamp, type, price, volume, currentVolume } = depth;
479
-
480
- // type: 0 = bid, 1 = ask
481
- const depthData = {
482
- type: 'depth',
483
- contractId,
484
- side: type === 0 ? 'bid' : 'ask',
485
- price: parseFloat(price) || 0,
486
- volume: parseInt(volume) || 0,
487
- currentVolume: parseInt(currentVolume) || 0,
488
- timestamp: timestamp || Date.now()
489
- };
490
-
491
- this.emit('depth', depthData);
492
- }
493
-
494
- /**
495
- * Disconnect from market data feed
253
+ * Disconnect
496
254
  */
497
255
  async disconnect() {
498
- this.subscriptions.clear();
499
-
500
- if (this.marketConnection) {
256
+ if (this.connection) {
501
257
  try {
502
- await this.marketConnection.stop();
503
- } catch (e) {
504
- // Ignore disconnect errors
258
+ for (const contractId of this.subscriptions) {
259
+ await this.unsubscribe(contractId);
260
+ }
261
+ await this.connection.stop();
262
+ } catch (error) {
263
+ // Ignore
505
264
  }
506
- this.marketConnection = null;
265
+ this.connection = null;
266
+ this.connected = false;
267
+ this.subscriptions.clear();
507
268
  }
508
-
509
- this.isConnected = false;
510
- this.emit('disconnected', { code: 1000, reason: 'User requested disconnect' });
511
269
  }
512
270
 
513
271
  /**
514
- * Get last tick for symbol
272
+ * Check connection status
515
273
  */
516
- getLastTick(symbol, contractId) {
517
- const subscriptionKey = `${symbol}:${contractId}`;
518
- return this.lastTick.get(subscriptionKey);
274
+ isConnected() {
275
+ return this.connected && this.connection?.state === 'Connected';
519
276
  }
520
277
 
521
278
  /**
522
- * Get trade buffer for symbol
279
+ * Get active subscriptions
523
280
  */
524
- getTradeBuffer(symbol, contractId) {
525
- const subscriptionKey = `${symbol}:${contractId}`;
526
- return this.dataBuffers.get(subscriptionKey) || [];
527
- }
528
-
529
- // NO STATIC CONTRACT DATA - All specs come from API
530
- // ProjectX: GET /api/Contract/available
531
- // Rithmic: TICKER_PLANT API
532
-
533
- /**
534
- * Check if market is open
535
- */
536
- static isMarketOpen() {
537
- const now = new Date();
538
- const day = now.getUTCDay();
539
- const hour = now.getUTCHours();
540
-
541
- // Futures trade Sunday 6pm - Friday 5pm ET (with daily break 5pm-6pm ET)
542
- // Weekend check
543
- if (day === 6) return false; // Saturday
544
- if (day === 0 && hour < 23) return false; // Sunday before open
545
- if (day === 5 && hour >= 22) return false; // Friday after close
546
- if (hour === 22) return false; // Daily maintenance
547
-
548
- return true;
281
+ getSubscriptions() {
282
+ return Array.from(this.subscriptions);
549
283
  }
550
284
  }
551
285
 
552
- module.exports = {
553
- MarketDataFeed,
554
- PROPFIRM_RTC_ENDPOINTS
555
- };
286
+ module.exports = { MarketDataFeed };