hedgequantx 2.6.163 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +15 -88
  2. package/bin/cli.js +0 -11
  3. package/dist/lib/api.jsc +0 -0
  4. package/dist/lib/api2.jsc +0 -0
  5. package/dist/lib/core.jsc +0 -0
  6. package/dist/lib/core2.jsc +0 -0
  7. package/dist/lib/data.js +1 -1
  8. package/dist/lib/data.jsc +0 -0
  9. package/dist/lib/data2.jsc +0 -0
  10. package/dist/lib/decoder.jsc +0 -0
  11. package/dist/lib/m/mod1.jsc +0 -0
  12. package/dist/lib/m/mod2.jsc +0 -0
  13. package/dist/lib/n/r1.jsc +0 -0
  14. package/dist/lib/n/r2.jsc +0 -0
  15. package/dist/lib/n/r3.jsc +0 -0
  16. package/dist/lib/n/r4.jsc +0 -0
  17. package/dist/lib/n/r5.jsc +0 -0
  18. package/dist/lib/n/r6.jsc +0 -0
  19. package/dist/lib/n/r7.jsc +0 -0
  20. package/dist/lib/o/util1.jsc +0 -0
  21. package/dist/lib/o/util2.jsc +0 -0
  22. package/package.json +8 -5
  23. package/src/app.js +40 -162
  24. package/src/config/constants.js +31 -33
  25. package/src/config/propfirms.js +13 -217
  26. package/src/config/settings.js +0 -43
  27. package/src/lib/api.js +198 -0
  28. package/src/lib/api2.js +353 -0
  29. package/src/lib/core.js +539 -0
  30. package/src/lib/core2.js +341 -0
  31. package/src/lib/data.js +555 -0
  32. package/src/lib/data2.js +492 -0
  33. package/src/lib/decoder.js +599 -0
  34. package/src/lib/m/s1.js +804 -0
  35. package/src/lib/m/s2.js +34 -0
  36. package/src/lib/n/r1.js +454 -0
  37. package/src/lib/n/r2.js +514 -0
  38. package/src/lib/n/r3.js +631 -0
  39. package/src/lib/n/r4.js +401 -0
  40. package/src/lib/n/r5.js +335 -0
  41. package/src/lib/n/r6.js +425 -0
  42. package/src/lib/n/r7.js +530 -0
  43. package/src/lib/o/l1.js +44 -0
  44. package/src/lib/o/l2.js +427 -0
  45. package/src/lib/python-bridge.js +206 -0
  46. package/src/menus/connect.js +14 -176
  47. package/src/menus/dashboard.js +65 -110
  48. package/src/pages/accounts.js +18 -18
  49. package/src/pages/algo/copy-trading.js +210 -240
  50. package/src/pages/algo/index.js +41 -104
  51. package/src/pages/algo/one-account.js +386 -33
  52. package/src/pages/algo/ui.js +312 -151
  53. package/src/pages/orders.js +3 -3
  54. package/src/pages/positions.js +3 -3
  55. package/src/pages/stats/chart.js +74 -0
  56. package/src/pages/stats/display.js +228 -0
  57. package/src/pages/stats/index.js +236 -0
  58. package/src/pages/stats/metrics.js +213 -0
  59. package/src/pages/user.js +6 -6
  60. package/src/services/hqx-server/constants.js +55 -0
  61. package/src/services/hqx-server/index.js +401 -0
  62. package/src/services/hqx-server/latency.js +81 -0
  63. package/src/services/index.js +12 -3
  64. package/src/services/rithmic/accounts.js +7 -32
  65. package/src/services/rithmic/connection.js +1 -204
  66. package/src/services/rithmic/contracts.js +116 -99
  67. package/src/services/rithmic/handlers.js +21 -196
  68. package/src/services/rithmic/index.js +63 -120
  69. package/src/services/rithmic/market.js +31 -0
  70. package/src/services/rithmic/orders.js +5 -111
  71. package/src/services/rithmic/protobuf.js +384 -138
  72. package/src/services/session.js +22 -173
  73. package/src/ui/box.js +10 -18
  74. package/src/ui/index.js +1 -3
  75. package/src/ui/menu.js +1 -1
  76. package/src/utils/prompts.js +2 -2
  77. package/dist/lib/m/s1.js +0 -1
  78. package/src/menus/ai-agent-connect.js +0 -181
  79. package/src/menus/ai-agent-models.js +0 -219
  80. package/src/menus/ai-agent-oauth.js +0 -292
  81. package/src/menus/ai-agent-ui.js +0 -141
  82. package/src/menus/ai-agent.js +0 -484
  83. package/src/pages/algo/algo-config.js +0 -195
  84. package/src/pages/algo/algo-multi.js +0 -801
  85. package/src/pages/algo/algo-utils.js +0 -58
  86. package/src/pages/algo/copy-engine.js +0 -449
  87. package/src/pages/algo/custom-strategy.js +0 -459
  88. package/src/pages/algo/logger.js +0 -245
  89. package/src/pages/algo/smart-logs-data.js +0 -218
  90. package/src/pages/algo/smart-logs.js +0 -387
  91. package/src/pages/algo/ui-constants.js +0 -144
  92. package/src/pages/algo/ui-summary.js +0 -184
  93. package/src/pages/stats-calculations.js +0 -191
  94. package/src/pages/stats-ui.js +0 -381
  95. package/src/pages/stats.js +0 -339
  96. package/src/services/ai/client-analysis.js +0 -194
  97. package/src/services/ai/client-models.js +0 -333
  98. package/src/services/ai/client.js +0 -343
  99. package/src/services/ai/index.js +0 -384
  100. package/src/services/ai/oauth-anthropic.js +0 -265
  101. package/src/services/ai/oauth-gemini.js +0 -223
  102. package/src/services/ai/oauth-iflow.js +0 -269
  103. package/src/services/ai/oauth-openai.js +0 -233
  104. package/src/services/ai/oauth-qwen.js +0 -279
  105. package/src/services/ai/providers/direct-providers.js +0 -323
  106. package/src/services/ai/providers/index.js +0 -62
  107. package/src/services/ai/providers/other-providers.js +0 -104
  108. package/src/services/ai/proxy-install.js +0 -249
  109. package/src/services/ai/proxy-manager.js +0 -494
  110. package/src/services/ai/proxy-remote.js +0 -161
  111. package/src/services/ai/strategy-supervisor.js +0 -1312
  112. package/src/services/ai/supervisor-data.js +0 -195
  113. package/src/services/ai/supervisor-optimize.js +0 -215
  114. package/src/services/ai/supervisor-sync.js +0 -178
  115. package/src/services/ai/supervisor-utils.js +0 -158
  116. package/src/services/ai/supervisor.js +0 -484
  117. package/src/services/ai/validation.js +0 -250
  118. package/src/services/hqx-server-events.js +0 -110
  119. package/src/services/hqx-server-handlers.js +0 -217
  120. package/src/services/hqx-server-latency.js +0 -136
  121. package/src/services/hqx-server.js +0 -403
  122. package/src/services/position-constants.js +0 -28
  123. package/src/services/position-exit-logic.js +0 -174
  124. package/src/services/position-manager.js +0 -438
  125. package/src/services/position-momentum.js +0 -206
  126. package/src/services/projectx/accounts.js +0 -142
  127. package/src/services/projectx/index.js +0 -443
  128. package/src/services/projectx/market.js +0 -172
  129. package/src/services/projectx/stats.js +0 -110
  130. package/src/services/projectx/trading.js +0 -180
  131. package/src/services/rithmic/latency-tracker.js +0 -182
  132. package/src/services/rithmic/market-data-decoders.js +0 -229
  133. package/src/services/rithmic/market-data.js +0 -272
  134. package/src/services/rithmic/orders-fast.js +0 -246
  135. package/src/services/rithmic/proto-decoders.js +0 -403
  136. package/src/services/rithmic/specs.js +0 -146
  137. package/src/services/rithmic/trade-history.js +0 -254
  138. package/src/services/session-history.js +0 -475
  139. package/src/services/strategy/hft-signal-calc.js +0 -147
  140. package/src/services/strategy/hft-tick.js +0 -407
  141. package/src/services/strategy/recovery-math.js +0 -402
  142. package/src/services/tradovate/constants.js +0 -109
  143. package/src/services/tradovate/index.js +0 -392
  144. package/src/services/tradovate/market.js +0 -47
  145. package/src/services/tradovate/orders.js +0 -145
  146. package/src/services/tradovate/websocket.js +0 -97
@@ -0,0 +1,555 @@
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
5
+ */
6
+
7
+ const EventEmitter = require('events');
8
+ const signalR = require('@microsoft/signalr');
9
+
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
+ }
89
+ };
90
+
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
96
+
97
+ class MarketDataFeed extends EventEmitter {
98
+ constructor(config) {
99
+ super();
100
+
101
+ this.config = config;
102
+ this.marketConnection = null;
103
+ this.userConnection = null;
104
+ this.isConnected = false;
105
+ this.reconnectAttempts = 0;
106
+ this.maxReconnectAttempts = 5;
107
+ this.subscriptions = new Set();
108
+
109
+ // Data buffers
110
+ this.dataBuffers = new Map();
111
+ this.lastTick = new Map();
112
+ this.orderBook = new Map(); // DOM data
113
+ }
114
+
115
+ /**
116
+ * Connect to market data feed via SignalR
117
+ */
118
+ async connect(userToken, propfirm, contractId = null) {
119
+ 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, {
129
+ skipNegotiation: true,
130
+ transport: signalR.HttpTransportType.WebSockets
131
+ })
132
+ .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
133
+ .configureLogging(signalR.LogLevel.None)
134
+ .build();
135
+
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;
165
+ this.reconnectAttempts = 0;
166
+ this.emit('connected');
167
+
168
+ return true;
169
+
170
+ } catch (error) {
171
+ // // console.log(`[MARKET] Connection error: ${error.message}`);
172
+ this.emit('error', error);
173
+ throw error;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Setup SignalR event handlers for market data
179
+ */
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);
194
+ });
195
+
196
+ // Gateway logout - server is kicking us out
197
+ this.marketConnection.on('GatewayLogout', (...args) => {
198
+ // // console.log(`[MARKET] GatewayLogout received:`, args);
199
+ });
200
+
201
+ // Also handle lowercase version
202
+ this.marketConnection.on('gatewaylogout', (...args) => {
203
+ // // console.log(`[MARKET] gatewaylogout received:`, args);
204
+ });
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 });
211
+ });
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
218
+ this._resubscribeAll();
219
+ });
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 });
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Subscribe to symbol market data
230
+ * Implements retry logic for connection race conditions
231
+ */
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
+ }
255
+ }
256
+
257
+ 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
+
274
+ } 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;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Unsubscribe from symbol market data
290
+ */
291
+ async unsubscribe(symbol, contractId) {
292
+ const subscriptionKey = `${symbol}:${contractId}`;
293
+
294
+ if (!this.subscriptions.has(subscriptionKey)) {
295
+ return true;
296
+ }
297
+
298
+ 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
+
309
+ } catch (error) {
310
+ this.emit('error', error);
311
+ return false;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Resubscribe to all symbols after reconnection
317
+ */
318
+ 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();
463
+ }
464
+ this.dataBuffers.set(subscriptionKey, buffer);
465
+
466
+ this.emit('trade', tradeData);
467
+ this.emit('tick', this.lastTick.get(subscriptionKey));
468
+ }
469
+ }
470
+
471
+ /**
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
496
+ */
497
+ async disconnect() {
498
+ this.subscriptions.clear();
499
+
500
+ if (this.marketConnection) {
501
+ try {
502
+ await this.marketConnection.stop();
503
+ } catch (e) {
504
+ // Ignore disconnect errors
505
+ }
506
+ this.marketConnection = null;
507
+ }
508
+
509
+ this.isConnected = false;
510
+ this.emit('disconnected', { code: 1000, reason: 'User requested disconnect' });
511
+ }
512
+
513
+ /**
514
+ * Get last tick for symbol
515
+ */
516
+ getLastTick(symbol, contractId) {
517
+ const subscriptionKey = `${symbol}:${contractId}`;
518
+ return this.lastTick.get(subscriptionKey);
519
+ }
520
+
521
+ /**
522
+ * Get trade buffer for symbol
523
+ */
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;
549
+ }
550
+ }
551
+
552
+ module.exports = {
553
+ MarketDataFeed,
554
+ PROPFIRM_RTC_ENDPOINTS
555
+ };