hedgequantx 2.6.161 → 2.6.162

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 (42) 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/proxy-install.js +249 -0
  22. package/src/services/ai/proxy-manager.js +29 -411
  23. package/src/services/ai/proxy-remote.js +161 -0
  24. package/src/services/ai/supervisor-optimize.js +215 -0
  25. package/src/services/ai/supervisor-sync.js +178 -0
  26. package/src/services/ai/supervisor.js +50 -515
  27. package/src/services/ai/validation.js +250 -0
  28. package/src/services/hqx-server-events.js +110 -0
  29. package/src/services/hqx-server-handlers.js +217 -0
  30. package/src/services/hqx-server-latency.js +136 -0
  31. package/src/services/hqx-server.js +51 -403
  32. package/src/services/position-constants.js +28 -0
  33. package/src/services/position-manager.js +105 -554
  34. package/src/services/position-momentum.js +206 -0
  35. package/src/services/projectx/accounts.js +142 -0
  36. package/src/services/projectx/index.js +40 -289
  37. package/src/services/projectx/trading.js +180 -0
  38. package/src/services/rithmic/handlers.js +2 -208
  39. package/src/services/rithmic/index.js +32 -542
  40. package/src/services/rithmic/latency-tracker.js +182 -0
  41. package/src/services/rithmic/specs.js +146 -0
  42. package/src/services/rithmic/trade-history.js +254 -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
14
  const { decodeFrontMonthContract } = require('./protobuf');
22
15
  const { TIMEOUTS, CACHE } = require('../../config/settings');
23
16
  const { logger } = require('../../utils/logger');
24
17
 
25
- const log = logger.scope('Rithmic');
18
+ // Extracted modules
19
+ const { CME_CONTRACT_SPECS, PROPFIRM_CONFIGS, checkMarketHours } = require('./specs');
20
+ const { getTradeHistory, setupOrderFillListener } = require('./trade-history');
26
21
 
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
- };
87
-
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,39 +336,25 @@ 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
339
  async getContracts() {
781
- // Check cache
782
340
  if (this._contractsCache && Date.now() - this._contractsCacheTime < CACHE.CONTRACTS_TTL) {
783
341
  return { success: true, contracts: this._contractsCache, source: 'cache' };
784
342
  }
785
343
 
786
- if (!this.credentials) {
787
- return { success: false, error: 'Not logged in' };
788
- }
344
+ if (!this.credentials) return { success: false, error: 'Not logged in' };
789
345
 
790
346
  try {
791
- // Connect to TICKER_PLANT if needed
792
347
  if (!this.tickerConn) {
793
348
  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
- }
349
+ if (!connected) return { success: false, error: 'Failed to connect to TICKER_PLANT' };
797
350
  }
798
351
 
799
352
  this.tickerConn.setMaxListeners(5000);
800
-
801
353
  log.debug('Fetching contracts from Rithmic API');
802
354
  const contracts = await this._fetchAllFrontMonths();
803
355
 
804
- if (!contracts.length) {
805
- return { success: false, error: 'No tradeable contracts found' };
806
- }
356
+ if (!contracts.length) return { success: false, error: 'No tradeable contracts found' };
807
357
 
808
- // Cache results
809
358
  this._contractsCache = contracts;
810
359
  this._contractsCacheTime = Date.now();
811
360
 
@@ -816,11 +365,6 @@ class RithmicService extends EventEmitter {
816
365
  }
817
366
  }
818
367
 
819
- /**
820
- * Search contracts
821
- * @param {string} searchText - Search text
822
- * @returns {Promise<Array>}
823
- */
824
368
  async searchContracts(searchText) {
825
369
  const result = await this.getContracts();
826
370
  if (!searchText || !result.success) return result.contracts || [];
@@ -832,20 +376,13 @@ class RithmicService extends EventEmitter {
832
376
  );
833
377
  }
834
378
 
835
- /**
836
- * Fetch all front month contracts from API
837
- * @private
838
- */
839
379
  async _fetchAllFrontMonths() {
840
- if (!this.tickerConn) {
841
- throw new Error('TICKER_PLANT not connected');
842
- }
380
+ if (!this.tickerConn) throw new Error('TICKER_PLANT not connected');
843
381
 
844
382
  return new Promise((resolve) => {
845
383
  const contracts = new Map();
846
384
  const productsToCheck = new Map();
847
385
 
848
- // Handler for ProductCodes responses
849
386
  const productHandler = (msg) => {
850
387
  if (msg.templateId !== 112) return;
851
388
 
@@ -868,7 +405,6 @@ class RithmicService extends EventEmitter {
868
405
  }
869
406
  };
870
407
 
871
- // Handler for FrontMonth responses
872
408
  const frontMonthHandler = (msg) => {
873
409
  if (msg.templateId !== 114) return;
874
410
 
@@ -885,13 +421,11 @@ class RithmicService extends EventEmitter {
885
421
  this.tickerConn.on('message', productHandler);
886
422
  this.tickerConn.on('message', frontMonthHandler);
887
423
 
888
- // Request all product codes
889
424
  this.tickerConn.send('RequestProductCodes', {
890
425
  templateId: 111,
891
426
  userMsg: ['get-products'],
892
427
  });
893
428
 
894
- // After timeout, request front months
895
429
  setTimeout(() => {
896
430
  this.tickerConn.removeListener('message', productHandler);
897
431
  log.debug('Collected products', { count: productsToCheck.size });
@@ -905,7 +439,6 @@ class RithmicService extends EventEmitter {
905
439
  });
906
440
  }
907
441
 
908
- // Collect results after timeout
909
442
  setTimeout(() => {
910
443
  this.tickerConn.removeListener('message', frontMonthHandler);
911
444
 
@@ -913,27 +446,21 @@ class RithmicService extends EventEmitter {
913
446
  for (const [baseSymbol, contract] of contracts) {
914
447
  const productKey = `${baseSymbol}:${contract.exchange}`;
915
448
  const product = productsToCheck.get(productKey);
916
-
917
- // API data + CME contract specs (technical constants)
918
449
  const specs = CME_CONTRACT_SPECS[baseSymbol] || null;
919
- // Use CME spec name first, then API productName, then baseSymbol as fallback
920
450
  const productName = specs?.name || product?.productName || baseSymbol;
921
451
 
922
452
  results.push({
923
453
  symbol: contract.symbol,
924
454
  baseSymbol,
925
- name: contract.symbol, // Use trading symbol as name
926
- description: productName, // Product name as description
455
+ name: contract.symbol,
456
+ description: productName,
927
457
  exchange: contract.exchange,
928
- // CME contract specifications (official exchange constants)
929
458
  tickSize: specs?.tickSize ?? null,
930
459
  tickValue: specs?.tickValue ?? null,
931
460
  });
932
461
  }
933
462
 
934
- // Sort alphabetically by base symbol
935
463
  results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
936
-
937
464
  log.debug('Got contracts from API', { count: results.length });
938
465
  resolve(results);
939
466
  }, TIMEOUTS.RITHMIC_PRODUCTS);
@@ -941,17 +468,12 @@ class RithmicService extends EventEmitter {
941
468
  });
942
469
  }
943
470
 
944
- /**
945
- * Decode ProductCodes response
946
- * @private
947
- */
948
471
  _decodeProductCodes(buffer) {
949
472
  const result = {};
950
473
  let offset = 0;
951
474
 
952
475
  const readVarint = (buf, off) => {
953
- let value = 0;
954
- let shift = 0;
476
+ let value = 0, shift = 0;
955
477
  while (off < buf.length) {
956
478
  const byte = buf[off++];
957
479
  value |= (byte & 0x7F) << shift;
@@ -985,52 +507,20 @@ class RithmicService extends EventEmitter {
985
507
  } else {
986
508
  break;
987
509
  }
988
- } catch {
989
- break;
990
- }
510
+ } catch { break; }
991
511
  }
992
512
 
993
513
  return result;
994
514
  }
995
515
 
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
- }
1018
-
1019
516
  // ==================== CLEANUP ====================
1020
517
 
1021
- /**
1022
- * Disconnect all connections
1023
- */
1024
518
  async disconnect() {
1025
519
  const connections = [this.orderConn, this.pnlConn, this.tickerConn];
1026
520
 
1027
521
  for (const conn of connections) {
1028
522
  if (conn) {
1029
- try {
1030
- await conn.disconnect();
1031
- } catch (err) {
1032
- log.warn('Disconnect error', { error: err.message });
1033
- }
523
+ try { await conn.disconnect(); } catch (err) { log.warn('Disconnect error', { error: err.message }); }
1034
524
  }
1035
525
  }
1036
526