hedgequantx 2.6.161 → 2.6.163

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 (57) hide show
  1. package/package.json +1 -1
  2. package/src/menus/ai-agent-connect.js +181 -0
  3. package/src/menus/ai-agent-models.js +219 -0
  4. package/src/menus/ai-agent-oauth.js +292 -0
  5. package/src/menus/ai-agent-ui.js +141 -0
  6. package/src/menus/ai-agent.js +88 -1489
  7. package/src/pages/algo/copy-engine.js +449 -0
  8. package/src/pages/algo/copy-trading.js +11 -543
  9. package/src/pages/algo/smart-logs-data.js +218 -0
  10. package/src/pages/algo/smart-logs.js +9 -214
  11. package/src/pages/algo/ui-constants.js +144 -0
  12. package/src/pages/algo/ui-summary.js +184 -0
  13. package/src/pages/algo/ui.js +42 -526
  14. package/src/pages/stats-calculations.js +191 -0
  15. package/src/pages/stats-ui.js +381 -0
  16. package/src/pages/stats.js +14 -507
  17. package/src/services/ai/client-analysis.js +194 -0
  18. package/src/services/ai/client-models.js +333 -0
  19. package/src/services/ai/client.js +6 -489
  20. package/src/services/ai/index.js +2 -257
  21. package/src/services/ai/providers/direct-providers.js +323 -0
  22. package/src/services/ai/providers/index.js +8 -472
  23. package/src/services/ai/providers/other-providers.js +104 -0
  24. package/src/services/ai/proxy-install.js +249 -0
  25. package/src/services/ai/proxy-manager.js +29 -411
  26. package/src/services/ai/proxy-remote.js +161 -0
  27. package/src/services/ai/supervisor-optimize.js +215 -0
  28. package/src/services/ai/supervisor-sync.js +178 -0
  29. package/src/services/ai/supervisor.js +50 -515
  30. package/src/services/ai/validation.js +250 -0
  31. package/src/services/hqx-server-events.js +110 -0
  32. package/src/services/hqx-server-handlers.js +217 -0
  33. package/src/services/hqx-server-latency.js +136 -0
  34. package/src/services/hqx-server.js +51 -403
  35. package/src/services/position-constants.js +28 -0
  36. package/src/services/position-exit-logic.js +174 -0
  37. package/src/services/position-manager.js +90 -629
  38. package/src/services/position-momentum.js +206 -0
  39. package/src/services/projectx/accounts.js +142 -0
  40. package/src/services/projectx/index.js +40 -289
  41. package/src/services/projectx/trading.js +180 -0
  42. package/src/services/rithmic/contracts.js +218 -0
  43. package/src/services/rithmic/handlers.js +2 -208
  44. package/src/services/rithmic/index.js +28 -712
  45. package/src/services/rithmic/latency-tracker.js +182 -0
  46. package/src/services/rithmic/market-data-decoders.js +229 -0
  47. package/src/services/rithmic/market-data.js +1 -278
  48. package/src/services/rithmic/orders-fast.js +246 -0
  49. package/src/services/rithmic/orders.js +1 -251
  50. package/src/services/rithmic/proto-decoders.js +403 -0
  51. package/src/services/rithmic/protobuf.js +7 -443
  52. package/src/services/rithmic/specs.js +146 -0
  53. package/src/services/rithmic/trade-history.js +254 -0
  54. package/src/services/strategy/hft-signal-calc.js +147 -0
  55. package/src/services/strategy/hft-tick.js +33 -133
  56. package/src/services/tradovate/index.js +6 -119
  57. package/src/services/tradovate/orders.js +145 -0
@@ -9,109 +9,22 @@ const EventEmitter = require('events');
9
9
  const { RithmicConnection } = require('./connection');
10
10
  const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, REQ } = require('./constants');
11
11
  const { createOrderHandler, createPnLHandler, LatencyTracker } = require('./handlers');
12
- const {
13
- fetchAccounts,
14
- getTradingAccounts,
15
- requestPnLSnapshot,
16
- subscribePnLUpdates,
17
- getPositions,
18
- hashAccountId,
19
- } = require('./accounts');
12
+ const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions } = require('./accounts');
20
13
  const { placeOrder, cancelOrder, cancelAllOrders, getOrders, getOrderHistory, closePosition, flattenAll, emergencyStop, fastEntry, fastExit } = require('./orders');
21
- const { decodeFrontMonthContract } = require('./protobuf');
22
- const { TIMEOUTS, CACHE } = require('../../config/settings');
14
+ const { TIMEOUTS } = require('../../config/settings');
23
15
  const { logger } = require('../../utils/logger');
24
16
 
25
- const log = logger.scope('Rithmic');
26
-
27
- /**
28
- * CME Contract Specifications - Official exchange tick sizes, values, and names
29
- * These are technical constants defined by the exchange, not market data.
30
- * Source: CME Group contract specifications
31
- */
32
- const CME_CONTRACT_SPECS = {
33
- // E-mini Index Futures (CME)
34
- ES: { tickSize: 0.25, tickValue: 12.50, name: 'E-mini S&P 500' },
35
- NQ: { tickSize: 0.25, tickValue: 5.00, name: 'E-mini NASDAQ-100' },
36
- RTY: { tickSize: 0.10, tickValue: 5.00, name: 'E-mini Russell 2000' },
37
- YM: { tickSize: 1.00, tickValue: 5.00, name: 'E-mini Dow' },
38
-
39
- // Micro Index Futures (CME)
40
- MES: { tickSize: 0.25, tickValue: 1.25, name: 'Micro E-mini S&P 500' },
41
- MNQ: { tickSize: 0.25, tickValue: 0.50, name: 'Micro E-mini NASDAQ-100' },
42
- M2K: { tickSize: 0.10, tickValue: 0.50, name: 'Micro E-mini Russell 2000' },
43
- MYM: { tickSize: 1.00, tickValue: 0.50, name: 'Micro E-mini Dow' },
44
-
45
- // Energy Futures (NYMEX)
46
- CL: { tickSize: 0.01, tickValue: 10.00, name: 'Crude Oil' },
47
- QM: { tickSize: 0.025, tickValue: 12.50, name: 'E-mini Crude Oil' },
48
- MCL: { tickSize: 0.01, tickValue: 1.00, name: 'Micro Crude Oil' },
49
- NG: { tickSize: 0.001, tickValue: 10.00, name: 'Natural Gas' },
50
- QG: { tickSize: 0.005, tickValue: 12.50, name: 'E-mini Natural Gas' },
51
-
52
- // Metal Futures (COMEX)
53
- GC: { tickSize: 0.10, tickValue: 10.00, name: 'Gold' },
54
- MGC: { tickSize: 0.10, tickValue: 1.00, name: 'Micro Gold' },
55
- SI: { tickSize: 0.005, tickValue: 25.00, name: 'Silver' },
56
- SIL: { tickSize: 0.005, tickValue: 2.50, name: '1000oz Silver' },
57
- HG: { tickSize: 0.0005, tickValue: 12.50, name: 'Copper' },
58
- MHG: { tickSize: 0.0005, tickValue: 1.25, name: 'Micro Copper' },
59
-
60
- // Treasury Futures (CBOT)
61
- ZB: { tickSize: 0.03125, tickValue: 31.25, name: '30-Year T-Bond' },
62
- ZN: { tickSize: 0.015625, tickValue: 15.625, name: '10-Year T-Note' },
63
- ZF: { tickSize: 0.0078125, tickValue: 7.8125, name: '5-Year T-Note' },
64
- ZT: { tickSize: 0.0078125, tickValue: 15.625, name: '2-Year T-Note' },
65
-
66
- // Agricultural Futures (CBOT)
67
- ZC: { tickSize: 0.25, tickValue: 12.50, name: 'Corn' },
68
- ZS: { tickSize: 0.25, tickValue: 12.50, name: 'Soybeans' },
69
- ZW: { tickSize: 0.25, tickValue: 12.50, name: 'Wheat' },
70
- ZL: { tickSize: 0.01, tickValue: 6.00, name: 'Soybean Oil' },
71
- ZM: { tickSize: 0.10, tickValue: 10.00, name: 'Soybean Meal' },
72
-
73
- // Currency Futures (CME)
74
- '6E': { tickSize: 0.00005, tickValue: 6.25, name: 'Euro FX' },
75
- '6J': { tickSize: 0.0000005, tickValue: 6.25, name: 'Japanese Yen' },
76
- '6B': { tickSize: 0.0001, tickValue: 6.25, name: 'British Pound' },
77
- '6A': { tickSize: 0.0001, tickValue: 10.00, name: 'Australian Dollar' },
78
- '6C': { tickSize: 0.00005, tickValue: 5.00, name: 'Canadian Dollar' },
79
- '6M': { tickSize: 0.0001, tickValue: 5.00, name: 'Mexican Peso' },
80
-
81
- // Nikkei (CME)
82
- NKD: { tickSize: 5.0, tickValue: 25.00, name: 'Nikkei 225' },
83
-
84
- // VIX Futures (CFE)
85
- VX: { tickSize: 0.05, tickValue: 50.00, name: 'VIX Futures' },
86
- };
17
+ // Extracted modules
18
+ const { PROPFIRM_CONFIGS, checkMarketHours } = require('./specs');
19
+ const { getTradeHistory, setupOrderFillListener } = require('./trade-history');
20
+ const { getContracts, searchContracts } = require('./contracts');
87
21
 
88
- /** PropFirm configurations */
89
- const PROPFIRM_CONFIGS = {
90
- apex: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
91
- apex_rithmic: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
92
- topstep_r: { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, gateway: RITHMIC_ENDPOINTS.CHICAGO },
93
- bulenox_r: { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, gateway: RITHMIC_ENDPOINTS.CHICAGO },
94
- earn2trade: { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, gateway: RITHMIC_ENDPOINTS.CHICAGO },
95
- mescapital: { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, gateway: RITHMIC_ENDPOINTS.CHICAGO },
96
- tradefundrr: { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, gateway: RITHMIC_ENDPOINTS.CHICAGO },
97
- thetradingpit: { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, gateway: RITHMIC_ENDPOINTS.CHICAGO },
98
- fundedfutures: { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, gateway: RITHMIC_ENDPOINTS.CHICAGO },
99
- propshop: { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
100
- '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
101
- daytraders: { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, gateway: RITHMIC_ENDPOINTS.CHICAGO },
102
- '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, gateway: RITHMIC_ENDPOINTS.CHICAGO },
103
- lucidtrading: { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
104
- thrivetrading: { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
105
- legendstrading: { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
106
- };
22
+ const log = logger.scope('Rithmic');
107
23
 
108
24
  /**
109
25
  * Rithmic Service for prop firm trading
110
26
  */
111
27
  class RithmicService extends EventEmitter {
112
- /**
113
- * @param {string} propfirmKey - PropFirm identifier
114
- */
115
28
  constructor(propfirmKey) {
116
29
  super();
117
30
  this.propfirmKey = propfirmKey;
@@ -132,10 +45,10 @@ class RithmicService extends EventEmitter {
132
45
  this.accountPnL = new Map();
133
46
  this.positions = new Map();
134
47
  this.orders = [];
135
- this.completedTrades = []; // Store filled orders for trade history
48
+ this.completedTrades = [];
136
49
  this.user = null;
137
50
  this.credentials = null;
138
- this.tradeRoutes = new Map(); // exchange -> tradeRoute (from API)
51
+ this.tradeRoutes = new Map();
139
52
 
140
53
  // Cache
141
54
  this._contractsCache = null;
@@ -144,12 +57,6 @@ class RithmicService extends EventEmitter {
144
57
 
145
58
  // ==================== AUTH ====================
146
59
 
147
- /**
148
- * Login to Rithmic
149
- * @param {string} username - Username
150
- * @param {string} password - Password
151
- * @returns {Promise<{success: boolean, user?: Object, accounts?: Array, error?: string}>}
152
- */
153
60
  async login(username, password) {
154
61
  try {
155
62
  this.orderConn = new RithmicConnection();
@@ -176,7 +83,7 @@ class RithmicService extends EventEmitter {
176
83
  this.loginInfo = data;
177
84
  this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
178
85
 
179
- // Fetch trade routes first (needed for orders)
86
+ // Fetch trade routes
180
87
  try {
181
88
  await this._fetchTradeRoutes();
182
89
  log.debug('Fetched trade routes', { count: this.tradeRoutes.size });
@@ -192,7 +99,7 @@ class RithmicService extends EventEmitter {
192
99
  log.warn('Failed to fetch accounts', { error: err.message });
193
100
  }
194
101
 
195
- // Subscribe to order updates for each account (required to receive fill notifications)
102
+ // Subscribe to order updates
196
103
  try {
197
104
  for (const acc of this.accounts) {
198
105
  this.orderConn.send('RequestSubscribeForOrderUpdates', {
@@ -202,85 +109,13 @@ class RithmicService extends EventEmitter {
202
109
  ibId: acc.ibId || data.ibId,
203
110
  accountId: acc.accountId,
204
111
  });
205
- log.debug('Subscribed to order updates for account', { accountId: acc.accountId });
206
112
  }
207
113
  } catch (err) {
208
114
  log.warn('Failed to subscribe to order updates', { error: err.message });
209
115
  }
210
116
 
211
- // Track open entries for P&L calculation
212
- this._openEntries = new Map(); // key: accountId:symbol, value: {side, qty, price}
213
-
214
- // Listen for filled orders to build trade history
215
- this.on('orderFilled', (fillInfo) => {
216
- const key = `${fillInfo.accountId}:${fillInfo.symbol}`;
217
- const side = fillInfo.transactionType === 1 ? 0 : 1; // 0=BUY, 1=SELL (entry side)
218
- const qty = fillInfo.fillQuantity || fillInfo.totalFillQuantity || 0;
219
- const price = fillInfo.avgFillPrice || fillInfo.lastFillPrice || 0;
220
-
221
- // Check if this is closing an existing position
222
- const openEntry = this._openEntries.get(key);
223
- let pnl = null;
224
- let isClosingTrade = false;
225
-
226
- if (openEntry && openEntry.side !== side) {
227
- // This is a closing trade (opposite side of open position)
228
- isClosingTrade = true;
229
- const closeQty = Math.min(qty, openEntry.qty);
230
-
231
- // Calculate P&L based on direction
232
- // If we bought (side=0), we sell to close - profit if close price > entry price
233
- // If we sold (side=1), we buy to close - profit if entry price > close price
234
- if (openEntry.side === 0) {
235
- // Was long, selling to close
236
- pnl = (price - openEntry.price) * closeQty;
237
- } else {
238
- // Was short, buying to close
239
- pnl = (openEntry.price - price) * closeQty;
240
- }
241
-
242
- // Update or remove open entry
243
- const remainingQty = openEntry.qty - closeQty;
244
- if (remainingQty <= 0) {
245
- this._openEntries.delete(key);
246
- } else {
247
- openEntry.qty = remainingQty;
248
- }
249
-
250
- // Store the closing trade with P&L
251
- this.completedTrades.push({
252
- id: fillInfo.orderId || fillInfo.orderTag,
253
- orderTag: fillInfo.orderTag,
254
- accountId: fillInfo.accountId,
255
- symbol: fillInfo.symbol,
256
- exchange: fillInfo.exchange,
257
- side: openEntry.side, // Original entry side (for stats: LONG or SHORT)
258
- size: closeQty,
259
- entryPrice: openEntry.price,
260
- exitPrice: price,
261
- price: price,
262
- timestamp: fillInfo.localTimestamp || Date.now(),
263
- creationTimestamp: new Date().toISOString(),
264
- status: 'CLOSED',
265
- profitAndLoss: pnl,
266
- pnl: pnl,
267
- fees: 0,
268
- });
269
- log.debug('Trade closed', { symbol: fillInfo.symbol, pnl, trades: this.completedTrades.length });
270
- } else {
271
- // This is opening a new position or adding to existing
272
- if (openEntry && openEntry.side === side) {
273
- // Adding to position - update average price
274
- const totalQty = openEntry.qty + qty;
275
- openEntry.price = ((openEntry.price * openEntry.qty) + (price * qty)) / totalQty;
276
- openEntry.qty = totalQty;
277
- } else {
278
- // New position
279
- this._openEntries.set(key, { side, qty, price, timestamp: Date.now() });
280
- }
281
- log.debug('Position opened/added', { symbol: fillInfo.symbol, side, qty, price });
282
- }
283
- });
117
+ // Setup order fill listener for P&L tracking
118
+ setupOrderFillListener(this);
284
119
 
285
120
  // Store credentials for reconnection
286
121
  this.credentials = { username, password };
@@ -316,12 +151,6 @@ class RithmicService extends EventEmitter {
316
151
  }
317
152
  }
318
153
 
319
- /**
320
- * Connect to PNL_PLANT for balance data
321
- * @param {string} username - Username
322
- * @param {string} password - Password
323
- * @returns {Promise<boolean>}
324
- */
325
154
  async connectPnL(username, password) {
326
155
  try {
327
156
  this.pnlConn = new RithmicConnection();
@@ -360,12 +189,6 @@ class RithmicService extends EventEmitter {
360
189
  }
361
190
  }
362
191
 
363
- /**
364
- * Connect to TICKER_PLANT for symbol lookup
365
- * @param {string} username - Username
366
- * @param {string} password - Password
367
- * @returns {Promise<boolean>}
368
- */
369
192
  async connectTicker(username, password) {
370
193
  try {
371
194
  this.tickerConn = new RithmicConnection();
@@ -408,31 +231,17 @@ class RithmicService extends EventEmitter {
408
231
 
409
232
  // ==================== TRADE ROUTES ====================
410
233
 
411
- /**
412
- * Fetch trade routes from Rithmic API
413
- * Trade routes are required for order submission
414
- * @private
415
- * @returns {Promise<void>}
416
- */
417
234
  async _fetchTradeRoutes() {
418
- if (!this.orderConn || !this.loginInfo) {
419
- return;
420
- }
235
+ if (!this.orderConn || !this.loginInfo) return;
421
236
 
422
237
  return new Promise((resolve) => {
423
- const timeout = setTimeout(() => {
424
- log.debug('Trade routes fetch timeout');
425
- resolve();
426
- }, 5000);
238
+ const timeout = setTimeout(() => resolve(), 5000);
427
239
 
428
240
  const onTradeRoute = (res) => {
429
241
  if (res.tradeRoute && res.exchange) {
430
- // Store trade route by exchange (from API)
431
242
  this.tradeRoutes.set(res.exchange, res.tradeRoute);
432
- log.debug('Trade route received', { exchange: res.exchange, route: res.tradeRoute });
433
243
  }
434
244
  if (res.rpCode?.[0] === '0' && !res.tradeRoute) {
435
- // End of trade routes
436
245
  clearTimeout(timeout);
437
246
  this.removeListener('tradeRoutes', onTradeRoute);
438
247
  resolve();
@@ -445,7 +254,7 @@ class RithmicService extends EventEmitter {
445
254
  this.orderConn.send('RequestTradeRoutes', {
446
255
  templateId: REQ.TRADE_ROUTES,
447
256
  userMsg: ['HQX'],
448
- subscribeForUpdates: true, // Required by Rithmic API
257
+ subscribeForUpdates: true,
449
258
  });
450
259
  } catch (e) {
451
260
  clearTimeout(timeout);
@@ -455,11 +264,6 @@ class RithmicService extends EventEmitter {
455
264
  });
456
265
  }
457
266
 
458
- /**
459
- * Get trade route for an exchange
460
- * @param {string} exchange - Exchange name (e.g., 'CME', 'NYMEX')
461
- * @returns {string|null} Trade route from API, or null if not available
462
- */
463
267
  getTradeRoute(exchange) {
464
268
  return this.tradeRoutes.get(exchange) || null;
465
269
  }
@@ -477,67 +281,22 @@ class RithmicService extends EventEmitter {
477
281
  async flattenAll(accountId) { return flattenAll(this, accountId); }
478
282
  async emergencyStop(accountId) { return emergencyStop(this, accountId); }
479
283
 
480
- // ==================== FAST SCALPING (Ultra-Low Latency) ====================
284
+ // ==================== FAST SCALPING ====================
481
285
 
482
- /**
483
- * Ultra-fast market order entry - fire-and-forget
484
- * Target latency: < 5ms local processing (network latency separate)
485
- * @param {Object} orderData - { accountId, symbol, exchange, size, side }
486
- * @returns {{ success: boolean, orderTag: string, entryTime: number, latencyMs: number }}
487
- */
488
286
  fastEntry(orderData) { return fastEntry(this, orderData); }
489
-
490
- /**
491
- * Ultra-fast market exit - fire-and-forget
492
- * @param {Object} orderData - { accountId, symbol, exchange, size, side }
493
- * @returns {{ success: boolean, orderTag: string, exitTime: number, latencyMs: number }}
494
- */
495
287
  fastExit(orderData) { return fastExit(this, orderData); }
496
288
 
497
- /**
498
- * Warmup connections for minimum latency
499
- * Call after login but before trading starts
500
- * @returns {Promise<boolean>}
501
- */
502
289
  async warmup() {
503
290
  const results = [];
504
-
505
- if (this.orderConn) {
506
- results.push(await this.orderConn.warmup());
507
- }
508
- if (this.pnlConn) {
509
- results.push(await this.pnlConn.warmup());
510
- }
511
-
512
- log.debug('Connection warmup complete', {
513
- success: results.filter(Boolean).length,
514
- total: results.length,
515
- });
516
-
291
+ if (this.orderConn) results.push(await this.orderConn.warmup());
292
+ if (this.pnlConn) results.push(await this.pnlConn.warmup());
293
+ log.debug('Connection warmup complete', { success: results.filter(Boolean).length, total: results.length });
517
294
  return results.every(Boolean);
518
295
  }
519
296
 
520
- /**
521
- * Get latency statistics from order fills
522
- * @returns {Object} Latency stats: min, max, avg, p50, p99, samples
523
- */
524
- getLatencyStats() {
525
- return LatencyTracker.getStats();
526
- }
527
-
528
- /**
529
- * Get recent latency samples
530
- * @param {number} n - Number of samples to return
531
- * @returns {number[]}
532
- */
533
- getRecentLatencies(n = 10) {
534
- return LatencyTracker.getRecent(n);
535
- }
297
+ getLatencyStats() { return LatencyTracker.getStats(); }
298
+ getRecentLatencies(n = 10) { return LatencyTracker.getRecent(n); }
536
299
 
537
- /**
538
- * Get connection diagnostics
539
- * @returns {Object}
540
- */
541
300
  getDiagnostics() {
542
301
  return {
543
302
  orderConn: this.orderConn?.getDiagnostics() || null,
@@ -554,210 +313,14 @@ class RithmicService extends EventEmitter {
554
313
  async getUser() { return this.user; }
555
314
  async getLifetimeStats() { return { success: true, stats: null }; }
556
315
  async getDailyStats() { return { success: true, stats: [] }; }
557
-
558
- /**
559
- * Get trade history from Rithmic API
560
- * Uses RequestShowOrderHistory to fetch historical fills
561
- * @param {string} accountId - Optional account filter
562
- * @param {number} days - Number of days to fetch (default: 30)
563
- * @returns {Promise<{success: boolean, trades: Array}>}
564
- */
565
- async getTradeHistory(accountId, days = 30) {
566
- if (!this.orderConn || !this.loginInfo) {
567
- return { success: true, trades: this.completedTrades || [] };
568
- }
569
-
570
- return new Promise((resolve) => {
571
- const trades = [];
572
- const historyOrders = [];
573
- let resolved = false;
574
-
575
- // Timeout after 5 seconds
576
- const timeout = setTimeout(() => {
577
- if (!resolved) {
578
- resolved = true;
579
- cleanup();
580
- // Combine API history with session trades
581
- const allTrades = [...this._processHistoryToTrades(historyOrders), ...this.completedTrades];
582
- resolve({ success: true, trades: allTrades });
583
- }
584
- }, 5000);
585
-
586
- // Listen for order history snapshots (RithmicOrderNotification with is_snapshot=true)
587
- const onOrderNotification = (data) => {
588
- try {
589
- // Check if this is a historical order (snapshot)
590
- if (data.isSnapshot || data.status === 'complete' || data.status === 'Complete') {
591
- const order = {
592
- orderId: data.orderId || data.orderTag,
593
- accountId: data.accountId,
594
- symbol: data.symbol,
595
- exchange: data.exchange,
596
- side: data.transactionType === 1 ? 0 : 1, // 1=BUY->0, 2=SELL->1
597
- quantity: data.quantity || data.totalFillQuantity || 0,
598
- fillPrice: data.avgFillPrice || data.lastFillPrice || 0,
599
- timestamp: data.ssboe ? data.ssboe * 1000 : Date.now(),
600
- status: data.status,
601
- };
602
-
603
- if (order.quantity > 0 && order.fillPrice > 0) {
604
- historyOrders.push(order);
605
- }
606
- }
607
- } catch (e) {
608
- // Ignore parse errors
609
- }
610
- };
611
-
612
- // Listen for history complete response
613
- const onHistoryComplete = () => {
614
- if (!resolved) {
615
- resolved = true;
616
- cleanup();
617
- const allTrades = [...this._processHistoryToTrades(historyOrders), ...this.completedTrades];
618
- resolve({ success: true, trades: allTrades });
619
- }
620
- };
621
-
622
- const cleanup = () => {
623
- clearTimeout(timeout);
624
- this.removeListener('orderNotification', onOrderNotification);
625
- this.removeListener('orderHistoryComplete', onHistoryComplete);
626
- };
627
-
628
- this.on('orderNotification', onOrderNotification);
629
- this.on('orderHistoryComplete', onHistoryComplete);
630
-
631
- // Request order history for each account
632
- try {
633
- const accounts = accountId
634
- ? this.accounts.filter(a => a.accountId === accountId)
635
- : this.accounts;
636
-
637
- for (const acc of accounts) {
638
- this.orderConn.send('RequestShowOrderHistory', {
639
- templateId: 324, // REQ.SHOW_ORDER_HISTORY
640
- userMsg: ['HQX-HISTORY'],
641
- fcmId: acc.fcmId || this.loginInfo.fcmId,
642
- ibId: acc.ibId || this.loginInfo.ibId,
643
- accountId: acc.accountId,
644
- });
645
- }
646
- } catch (e) {
647
- if (!resolved) {
648
- resolved = true;
649
- cleanup();
650
- resolve({ success: false, error: e.message, trades: this.completedTrades || [] });
651
- }
652
- }
653
- });
654
- }
655
-
656
- /**
657
- * Process historical orders into trades with P&L
658
- * Matches entries and exits to calculate P&L
659
- * @private
660
- */
661
- _processHistoryToTrades(orders) {
662
- const trades = [];
663
- const openPositions = new Map(); // key: accountId:symbol
664
-
665
- // Sort by timestamp (oldest first)
666
- const sorted = [...orders].sort((a, b) => a.timestamp - b.timestamp);
667
-
668
- for (const order of sorted) {
669
- const key = `${order.accountId}:${order.symbol}`;
670
- const open = openPositions.get(key);
671
-
672
- if (open && open.side !== order.side) {
673
- // Closing trade - calculate P&L
674
- const closeQty = Math.min(order.quantity, open.quantity);
675
- let pnl;
676
-
677
- if (open.side === 0) {
678
- // Was long (bought), now selling
679
- pnl = (order.fillPrice - open.price) * closeQty;
680
- } else {
681
- // Was short (sold), now buying
682
- pnl = (open.price - order.fillPrice) * closeQty;
683
- }
684
-
685
- // Estimate tick value (futures typically $12.50-$50 per tick)
686
- // For indices like ES/NQ, multiply by contract multiplier
687
- const tickMultiplier = this._getTickMultiplier(order.symbol);
688
- pnl = pnl * tickMultiplier;
689
-
690
- trades.push({
691
- id: order.orderId,
692
- accountId: order.accountId,
693
- symbol: order.symbol,
694
- exchange: order.exchange,
695
- side: open.side,
696
- size: closeQty,
697
- entryPrice: open.price,
698
- exitPrice: order.fillPrice,
699
- price: order.fillPrice,
700
- timestamp: order.timestamp,
701
- creationTimestamp: new Date(order.timestamp).toISOString(),
702
- status: 'CLOSED',
703
- profitAndLoss: pnl,
704
- pnl: pnl,
705
- fees: 0,
706
- });
707
-
708
- // Update or remove open position
709
- const remaining = open.quantity - closeQty;
710
- if (remaining <= 0) {
711
- openPositions.delete(key);
712
- } else {
713
- open.quantity = remaining;
714
- }
715
- } else if (open && open.side === order.side) {
716
- // Adding to position
717
- const totalQty = open.quantity + order.quantity;
718
- open.price = ((open.price * open.quantity) + (order.fillPrice * order.quantity)) / totalQty;
719
- open.quantity = totalQty;
720
- } else {
721
- // Opening new position
722
- openPositions.set(key, {
723
- side: order.side,
724
- quantity: order.quantity,
725
- price: order.fillPrice,
726
- timestamp: order.timestamp,
727
- });
728
- }
729
- }
730
-
731
- return trades;
732
- }
733
-
734
- /**
735
- * Get tick multiplier for P&L calculation
736
- * @private
737
- */
738
- _getTickMultiplier(symbol) {
739
- const sym = (symbol || '').toUpperCase();
740
- if (sym.startsWith('ES')) return 50; // E-mini S&P 500: $50 per point
741
- if (sym.startsWith('NQ')) return 20; // E-mini Nasdaq: $20 per point
742
- if (sym.startsWith('YM')) return 5; // E-mini Dow: $5 per point
743
- if (sym.startsWith('RTY')) return 50; // E-mini Russell: $50 per point
744
- if (sym.startsWith('MES')) return 5; // Micro E-mini S&P: $5 per point
745
- if (sym.startsWith('MNQ')) return 2; // Micro E-mini Nasdaq: $2 per point
746
- if (sym.startsWith('GC')) return 100; // Gold: $100 per point
747
- if (sym.startsWith('SI')) return 5000; // Silver: $5000 per point
748
- if (sym.startsWith('CL')) return 1000; // Crude Oil: $1000 per point
749
- if (sym.startsWith('NG')) return 10000; // Natural Gas: $10000 per point
750
- if (sym.startsWith('ZB') || sym.startsWith('ZN')) return 1000; // Bonds
751
- if (sym.startsWith('6E')) return 125000; // Euro FX
752
- if (sym.startsWith('6J')) return 12500000; // Japanese Yen
753
- return 1; // Default
754
- }
316
+ async getTradeHistory(accountId, days = 30) { return getTradeHistory(this, accountId, days); }
755
317
 
756
318
  async getMarketStatus() {
757
- const status = this.checkMarketHours();
319
+ const status = checkMarketHours();
758
320
  return { success: true, isOpen: status.isOpen, message: status.message };
759
321
  }
760
322
 
323
+ checkMarketHours() { return checkMarketHours(); }
761
324
  getToken() { return this.loginInfo ? 'connected' : null; }
762
325
  getPropfirm() { return this.propfirmKey || 'apex'; }
763
326
 
@@ -773,264 +336,17 @@ class RithmicService extends EventEmitter {
773
336
 
774
337
  // ==================== CONTRACTS ====================
775
338
 
776
- /**
777
- * Get all available contracts from Rithmic API
778
- * @returns {Promise<{success: boolean, contracts: Array, source?: string, error?: string}>}
779
- */
780
- async getContracts() {
781
- // Check cache
782
- if (this._contractsCache && Date.now() - this._contractsCacheTime < CACHE.CONTRACTS_TTL) {
783
- return { success: true, contracts: this._contractsCache, source: 'cache' };
784
- }
785
-
786
- if (!this.credentials) {
787
- return { success: false, error: 'Not logged in' };
788
- }
789
-
790
- try {
791
- // Connect to TICKER_PLANT if needed
792
- if (!this.tickerConn) {
793
- const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
794
- if (!connected) {
795
- return { success: false, error: 'Failed to connect to TICKER_PLANT' };
796
- }
797
- }
798
-
799
- this.tickerConn.setMaxListeners(5000);
800
-
801
- log.debug('Fetching contracts from Rithmic API');
802
- const contracts = await this._fetchAllFrontMonths();
803
-
804
- if (!contracts.length) {
805
- return { success: false, error: 'No tradeable contracts found' };
806
- }
807
-
808
- // Cache results
809
- this._contractsCache = contracts;
810
- this._contractsCacheTime = Date.now();
811
-
812
- return { success: true, contracts, source: 'api' };
813
- } catch (err) {
814
- log.error('getContracts error', { error: err.message });
815
- return { success: false, error: err.message };
816
- }
817
- }
818
-
819
- /**
820
- * Search contracts
821
- * @param {string} searchText - Search text
822
- * @returns {Promise<Array>}
823
- */
824
- async searchContracts(searchText) {
825
- const result = await this.getContracts();
826
- if (!searchText || !result.success) return result.contracts || [];
827
-
828
- const search = searchText.toUpperCase();
829
- return result.contracts.filter(c =>
830
- c.symbol.toUpperCase().includes(search) ||
831
- c.name.toUpperCase().includes(search)
832
- );
833
- }
834
-
835
- /**
836
- * Fetch all front month contracts from API
837
- * @private
838
- */
839
- async _fetchAllFrontMonths() {
840
- if (!this.tickerConn) {
841
- throw new Error('TICKER_PLANT not connected');
842
- }
843
-
844
- return new Promise((resolve) => {
845
- const contracts = new Map();
846
- const productsToCheck = new Map();
847
-
848
- // Handler for ProductCodes responses
849
- const productHandler = (msg) => {
850
- if (msg.templateId !== 112) return;
851
-
852
- const decoded = this._decodeProductCodes(msg.data);
853
- if (!decoded.productCode || !decoded.exchange) return;
854
-
855
- const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
856
- if (!validExchanges.includes(decoded.exchange)) return;
857
-
858
- const name = (decoded.productName || '').toLowerCase();
859
- if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
860
-
861
- const key = `${decoded.productCode}:${decoded.exchange}`;
862
- if (!productsToCheck.has(key)) {
863
- productsToCheck.set(key, {
864
- productCode: decoded.productCode,
865
- productName: decoded.productName || decoded.productCode,
866
- exchange: decoded.exchange,
867
- });
868
- }
869
- };
870
-
871
- // Handler for FrontMonth responses
872
- const frontMonthHandler = (msg) => {
873
- if (msg.templateId !== 114) return;
874
-
875
- const decoded = decodeFrontMonthContract(msg.data);
876
- if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
877
- contracts.set(decoded.userMsg, {
878
- symbol: decoded.tradingSymbol,
879
- baseSymbol: decoded.userMsg,
880
- exchange: decoded.exchange,
881
- });
882
- }
883
- };
884
-
885
- this.tickerConn.on('message', productHandler);
886
- this.tickerConn.on('message', frontMonthHandler);
887
-
888
- // Request all product codes
889
- this.tickerConn.send('RequestProductCodes', {
890
- templateId: 111,
891
- userMsg: ['get-products'],
892
- });
893
-
894
- // After timeout, request front months
895
- setTimeout(() => {
896
- this.tickerConn.removeListener('message', productHandler);
897
- log.debug('Collected products', { count: productsToCheck.size });
898
-
899
- for (const product of productsToCheck.values()) {
900
- this.tickerConn.send('RequestFrontMonthContract', {
901
- templateId: 113,
902
- userMsg: [product.productCode],
903
- symbol: product.productCode,
904
- exchange: product.exchange,
905
- });
906
- }
907
-
908
- // Collect results after timeout
909
- setTimeout(() => {
910
- this.tickerConn.removeListener('message', frontMonthHandler);
911
-
912
- const results = [];
913
- for (const [baseSymbol, contract] of contracts) {
914
- const productKey = `${baseSymbol}:${contract.exchange}`;
915
- const product = productsToCheck.get(productKey);
916
-
917
- // API data + CME contract specs (technical constants)
918
- const specs = CME_CONTRACT_SPECS[baseSymbol] || null;
919
- // Use CME spec name first, then API productName, then baseSymbol as fallback
920
- const productName = specs?.name || product?.productName || baseSymbol;
921
-
922
- results.push({
923
- symbol: contract.symbol,
924
- baseSymbol,
925
- name: contract.symbol, // Use trading symbol as name
926
- description: productName, // Product name as description
927
- exchange: contract.exchange,
928
- // CME contract specifications (official exchange constants)
929
- tickSize: specs?.tickSize ?? null,
930
- tickValue: specs?.tickValue ?? null,
931
- });
932
- }
933
-
934
- // Sort alphabetically by base symbol
935
- results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
936
-
937
- log.debug('Got contracts from API', { count: results.length });
938
- resolve(results);
939
- }, TIMEOUTS.RITHMIC_PRODUCTS);
940
- }, TIMEOUTS.RITHMIC_CONTRACTS);
941
- });
942
- }
943
-
944
- /**
945
- * Decode ProductCodes response
946
- * @private
947
- */
948
- _decodeProductCodes(buffer) {
949
- const result = {};
950
- let offset = 0;
951
-
952
- const readVarint = (buf, off) => {
953
- let value = 0;
954
- let shift = 0;
955
- while (off < buf.length) {
956
- const byte = buf[off++];
957
- value |= (byte & 0x7F) << shift;
958
- if (!(byte & 0x80)) break;
959
- shift += 7;
960
- }
961
- return [value, off];
962
- };
963
-
964
- const readString = (buf, off) => {
965
- const [len, newOff] = readVarint(buf, off);
966
- return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
967
- };
968
-
969
- while (offset < buffer.length) {
970
- try {
971
- const [tag, tagOff] = readVarint(buffer, offset);
972
- const wireType = tag & 0x7;
973
- const fieldNumber = tag >>> 3;
974
- offset = tagOff;
975
-
976
- if (wireType === 0) {
977
- const [, newOff] = readVarint(buffer, offset);
978
- offset = newOff;
979
- } else if (wireType === 2) {
980
- const [val, newOff] = readString(buffer, offset);
981
- offset = newOff;
982
- if (fieldNumber === 110101) result.exchange = val;
983
- if (fieldNumber === 100749) result.productCode = val;
984
- if (fieldNumber === 100003) result.productName = val;
985
- } else {
986
- break;
987
- }
988
- } catch {
989
- break;
990
- }
991
- }
992
-
993
- return result;
994
- }
995
-
996
- // ==================== MARKET HOURS ====================
997
-
998
- checkMarketHours() {
999
- const now = new Date();
1000
- const utcDay = now.getUTCDay();
1001
- const utcHour = now.getUTCHours();
1002
-
1003
- const isDST = now.getTimezoneOffset() < Math.max(
1004
- new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
1005
- new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
1006
- );
1007
- const ctOffset = isDST ? 5 : 6;
1008
- const ctHour = (utcHour - ctOffset + 24) % 24;
1009
- const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
1010
-
1011
- if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
1012
- if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
1013
- if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
1014
- if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
1015
-
1016
- return { isOpen: true, message: 'Market is open' };
1017
- }
339
+ async getContracts() { return getContracts(this); }
340
+ async searchContracts(searchText) { return searchContracts(this, searchText); }
1018
341
 
1019
342
  // ==================== CLEANUP ====================
1020
343
 
1021
- /**
1022
- * Disconnect all connections
1023
- */
1024
344
  async disconnect() {
1025
345
  const connections = [this.orderConn, this.pnlConn, this.tickerConn];
1026
346
 
1027
347
  for (const conn of connections) {
1028
348
  if (conn) {
1029
- try {
1030
- await conn.disconnect();
1031
- } catch (err) {
1032
- log.warn('Disconnect error', { error: err.message });
1033
- }
349
+ try { await conn.disconnect(); } catch (err) { log.warn('Disconnect error', { error: err.message }); }
1034
350
  }
1035
351
  }
1036
352