hedgequantx 2.6.162 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) 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 +6 -3
  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 +235 -0
  67. package/src/services/rithmic/handlers.js +21 -196
  68. package/src/services/rithmic/index.js +60 -291
  69. package/src/services/rithmic/market.js +31 -0
  70. package/src/services/rithmic/orders.js +5 -361
  71. package/src/services/rithmic/protobuf.js +5 -195
  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/index.js +0 -526
  106. package/src/services/ai/proxy-install.js +0 -249
  107. package/src/services/ai/proxy-manager.js +0 -494
  108. package/src/services/ai/proxy-remote.js +0 -161
  109. package/src/services/ai/strategy-supervisor.js +0 -1312
  110. package/src/services/ai/supervisor-data.js +0 -195
  111. package/src/services/ai/supervisor-optimize.js +0 -215
  112. package/src/services/ai/supervisor-sync.js +0 -178
  113. package/src/services/ai/supervisor-utils.js +0 -158
  114. package/src/services/ai/supervisor.js +0 -484
  115. package/src/services/ai/validation.js +0 -250
  116. package/src/services/hqx-server-events.js +0 -110
  117. package/src/services/hqx-server-handlers.js +0 -217
  118. package/src/services/hqx-server-latency.js +0 -136
  119. package/src/services/hqx-server.js +0 -403
  120. package/src/services/position-constants.js +0 -28
  121. package/src/services/position-manager.js +0 -528
  122. package/src/services/position-momentum.js +0 -206
  123. package/src/services/projectx/accounts.js +0 -142
  124. package/src/services/projectx/index.js +0 -443
  125. package/src/services/projectx/market.js +0 -172
  126. package/src/services/projectx/stats.js +0 -110
  127. package/src/services/projectx/trading.js +0 -180
  128. package/src/services/rithmic/latency-tracker.js +0 -182
  129. package/src/services/rithmic/market-data.js +0 -549
  130. package/src/services/rithmic/specs.js +0 -146
  131. package/src/services/rithmic/trade-history.js +0 -254
  132. package/src/services/session-history.js +0 -475
  133. package/src/services/strategy/hft-tick.js +0 -507
  134. package/src/services/strategy/recovery-math.js +0 -402
  135. package/src/services/tradovate/constants.js +0 -109
  136. package/src/services/tradovate/index.js +0 -505
  137. package/src/services/tradovate/market.js +0 -47
  138. package/src/services/tradovate/websocket.js +0 -97
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @fileoverview Latency tracking and adaptive heartbeat
3
+ * @module services/hqx-server/latency
4
+ */
5
+
6
+ /**
7
+ * Latency tracker with adaptive heartbeat
8
+ */
9
+ class LatencyTracker {
10
+ constructor() {
11
+ this.latency = 0;
12
+ this.minLatency = Infinity;
13
+ this.maxLatency = 0;
14
+ this.avgLatency = 0;
15
+ this.latencySamples = [];
16
+ this.adaptiveHeartbeat = 1000;
17
+ }
18
+
19
+ /**
20
+ * Update latency with new sample
21
+ * @param {number} latency - Latency in ms
22
+ */
23
+ update(latency) {
24
+ this.latency = latency;
25
+ this.minLatency = Math.min(this.minLatency, latency);
26
+ this.maxLatency = Math.max(this.maxLatency, latency);
27
+
28
+ // Rolling average (last 100 samples)
29
+ this.latencySamples.push(latency);
30
+ if (this.latencySamples.length > 100) {
31
+ this.latencySamples.shift();
32
+ }
33
+ this.avgLatency = this.latencySamples.reduce((a, b) => a + b, 0) / this.latencySamples.length;
34
+
35
+ this._adaptHeartbeat();
36
+ }
37
+
38
+ /**
39
+ * Adapt heartbeat interval based on connection quality
40
+ * @private
41
+ */
42
+ _adaptHeartbeat() {
43
+ if (this.avgLatency < 10) {
44
+ this.adaptiveHeartbeat = 2000; // <10ms: 2s heartbeat
45
+ } else if (this.avgLatency < 50) {
46
+ this.adaptiveHeartbeat = 1000; // <50ms: 1s heartbeat
47
+ } else if (this.avgLatency < 100) {
48
+ this.adaptiveHeartbeat = 500; // <100ms: 500ms heartbeat
49
+ } else {
50
+ this.adaptiveHeartbeat = 250; // High latency: 250ms heartbeat
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Get latency statistics
56
+ * @returns {Object}
57
+ */
58
+ getStats() {
59
+ return {
60
+ current: this.latency,
61
+ min: this.minLatency === Infinity ? 0 : this.minLatency,
62
+ max: this.maxLatency,
63
+ avg: this.avgLatency,
64
+ samples: this.latencySamples.length,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Reset all statistics
70
+ */
71
+ reset() {
72
+ this.latency = 0;
73
+ this.minLatency = Infinity;
74
+ this.maxLatency = 0;
75
+ this.avgLatency = 0;
76
+ this.latencySamples = [];
77
+ this.adaptiveHeartbeat = 1000;
78
+ }
79
+ }
80
+
81
+ module.exports = { LatencyTracker };
@@ -1,13 +1,22 @@
1
1
  /**
2
2
  * @fileoverview Services module exports
3
3
  * @module services
4
+ *
5
+ * Rithmic-only service hub
4
6
  */
5
7
 
6
- const { ProjectXService } = require('./projectx/index');
8
+ const { RithmicService } = require('./rithmic/index');
9
+ const { HQXServerService } = require('./hqx-server/index');
7
10
  const { storage, connections } = require('./session');
8
11
 
9
12
  module.exports = {
10
- ProjectXService,
13
+ // Platform Service (Rithmic only)
14
+ RithmicService,
15
+
16
+ // HQX Algo Server
17
+ HQXServerService,
18
+
19
+ // Session Management
11
20
  storage,
12
- connections
21
+ connections,
13
22
  };
@@ -109,46 +109,21 @@ const getTradingAccounts = async (service) => {
109
109
  debug(`Account ${acc.accountId} pnlData:`, JSON.stringify(pnlData));
110
110
  debug(` accountPnL map size:`, service.accountPnL.size);
111
111
 
112
- // REAL DATA FROM RITHMIC ONLY - NO DEFAULTS (RULES.md compliant)
113
- // All values are null if not available from API
114
- const accountBalance = pnlData.accountBalance !== undefined ? parseFloat(pnlData.accountBalance) : null;
115
- const openPnL = pnlData.openPositionPnl !== undefined ? parseFloat(pnlData.openPositionPnl) : null;
116
- const closedPnL = pnlData.closedPositionPnl !== undefined ? parseFloat(pnlData.closedPositionPnl) : null;
117
- const dayPnL = pnlData.dayPnl !== undefined ? parseFloat(pnlData.dayPnl) : null;
118
-
119
- // R Trader additional metrics (from ACCOUNT_PNL_UPDATE 451)
120
- const buyingPower = pnlData.availableBuyingPower !== undefined ? parseFloat(pnlData.availableBuyingPower) : null;
121
- const margin = pnlData.marginBalance !== undefined ? parseFloat(pnlData.marginBalance) : null;
122
- const cashOnHand = pnlData.cashOnHand !== undefined ? parseFloat(pnlData.cashOnHand) : null;
123
-
124
- // Calculate P&L like R Trader: openPositionPnl + closedPositionPnl
125
- // This matches what R Trader shows as "Today's P&L"
126
- const totalPnL = (openPnL !== null || closedPnL !== null)
127
- ? (openPnL || 0) + (closedPnL || 0)
128
- : null;
129
-
130
- // Net Liquidation Value = Account Balance + Open P&L (same as R Trader)
131
- const netLiquidation = (accountBalance !== null || openPnL !== null)
132
- ? (accountBalance || 0) + (openPnL || 0)
133
- : null;
112
+ // REAL DATA FROM RITHMIC ONLY - NO DEFAULTS
113
+ const accountBalance = pnlData.accountBalance ? parseFloat(pnlData.accountBalance) : null;
114
+ const openPnL = pnlData.openPositionPnl ? parseFloat(pnlData.openPositionPnl) : null;
115
+ const closedPnL = pnlData.closedPositionPnl ? parseFloat(pnlData.closedPositionPnl) : null;
116
+ const dayPnL = pnlData.dayPnl ? parseFloat(pnlData.dayPnl) : null;
134
117
 
135
118
  return {
136
119
  accountId: hashAccountId(acc.accountId),
137
120
  rithmicAccountId: acc.accountId,
138
121
  accountName: acc.accountId, // Never expose real name - only account ID
139
122
  name: acc.accountId, // Never expose real name - only account ID
140
- // Core metrics (same as R Trader)
141
123
  balance: accountBalance,
142
- profitAndLoss: totalPnL, // Same as R Trader: open + closed
124
+ profitAndLoss: dayPnL !== null ? dayPnL : (openPnL !== null || closedPnL !== null ? (openPnL || 0) + (closedPnL || 0) : null),
143
125
  openPnL: openPnL,
144
- closedPnL: closedPnL,
145
- dayPnL: dayPnL,
146
- // R Trader additional metrics
147
- buyingPower: buyingPower,
148
- margin: margin,
149
- cashOnHand: cashOnHand,
150
- netLiquidation: netLiquidation,
151
- // Meta
126
+ todayPnL: closedPnL,
152
127
  status: 0,
153
128
  platform: 'Rithmic',
154
129
  propfirm: service.propfirm.name,
@@ -1,11 +1,6 @@
1
1
  /**
2
2
  * Rithmic Connection Manager
3
3
  * Handles WebSocket connection and heartbeat
4
- *
5
- * OPTIMIZED FOR ULTRA-LOW LATENCY:
6
- * - TCP_NODELAY enabled (disable Nagle's algorithm)
7
- * - Compression disabled
8
- * - Skip UTF8 validation for binary
9
4
  */
10
5
 
11
6
  const WebSocket = require('ws');
@@ -20,7 +15,6 @@ class RithmicConnection extends EventEmitter {
20
15
  this.config = null;
21
16
  this.state = 'DISCONNECTED';
22
17
  this.heartbeatTimer = null;
23
- this._socket = null; // Direct socket reference for fast access
24
18
  }
25
19
 
26
20
  get isConnected() {
@@ -33,7 +27,6 @@ class RithmicConnection extends EventEmitter {
33
27
 
34
28
  /**
35
29
  * Connect to Rithmic server
36
- * OPTIMIZED: TCP_NODELAY, no compression, skip UTF8 validation
37
30
  */
38
31
  async connect(config) {
39
32
  this.config = config;
@@ -42,24 +35,10 @@ class RithmicConnection extends EventEmitter {
42
35
  await proto.load();
43
36
 
44
37
  return new Promise((resolve, reject) => {
45
- // OPTIMIZATION: Disable compression and UTF8 validation for speed
46
- this.ws = new WebSocket(config.uri, {
47
- rejectUnauthorized: false,
48
- perMessageDeflate: false, // CRITICAL: Disable compression
49
- skipUTF8Validation: true, // Skip validation for binary protobuf
50
- maxPayload: 64 * 1024, // 64KB max (orders are small)
51
- });
38
+ this.ws = new WebSocket(config.uri, { rejectUnauthorized: false });
52
39
 
53
40
  this.ws.on('open', () => {
54
41
  this.state = 'CONNECTED';
55
-
56
- // CRITICAL: Disable Nagle's algorithm for low latency
57
- // This sends packets immediately instead of buffering
58
- if (this.ws._socket) {
59
- this.ws._socket.setNoDelay(true);
60
- this._socket = this.ws._socket; // Cache for fast access
61
- }
62
-
63
42
  this.emit('connected');
64
43
  resolve(true);
65
44
  });
@@ -113,122 +92,6 @@ class RithmicConnection extends EventEmitter {
113
92
  this.ws.send(buffer);
114
93
  }
115
94
 
116
- /**
117
- * Fast send - bypasses some ws overhead for hot path
118
- * Use for time-critical order messages
119
- * @param {Buffer} buffer - Pre-encoded protobuf buffer
120
- * @returns {boolean} true if sent, false if connection not open
121
- */
122
- fastSend(buffer) {
123
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
124
- this.ws.send(buffer);
125
- return true;
126
- }
127
- return false;
128
- }
129
-
130
- /**
131
- * Ultra-fast send - direct socket write with WebSocket framing
132
- * MAXIMUM PERFORMANCE: Bypasses ws library overhead entirely
133
- * Only use for pre-encoded binary protobuf messages
134
- *
135
- * @param {Buffer} payload - Pre-encoded protobuf buffer
136
- * @returns {boolean} true if sent successfully
137
- */
138
- ultraSend(payload) {
139
- // Require cached socket reference
140
- if (!this._socket || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
141
- return false;
142
- }
143
-
144
- try {
145
- // Build WebSocket frame manually for binary message
146
- // This avoids all ws library overhead (callbacks, validation, etc.)
147
- const frame = this._buildBinaryFrame(payload);
148
-
149
- // Direct socket write - bypasses ws entirely
150
- this._socket.write(frame);
151
- return true;
152
- } catch (e) {
153
- // Fallback to standard send on error
154
- this.ws.send(payload);
155
- return true;
156
- }
157
- }
158
-
159
- /**
160
- * Build WebSocket binary frame manually
161
- * Format: [opcode] [length] [payload]
162
- * @private
163
- * @param {Buffer} payload
164
- * @returns {Buffer}
165
- */
166
- _buildBinaryFrame(payload) {
167
- const len = payload.length;
168
- let frame;
169
-
170
- if (len < 126) {
171
- // 2-byte header: FIN + opcode (0x82 = final binary), length
172
- frame = Buffer.allocUnsafe(2 + len);
173
- frame[0] = 0x82; // FIN=1, opcode=2 (binary)
174
- frame[1] = len; // No mask (server->client would need mask, but we're client)
175
- payload.copy(frame, 2);
176
- } else if (len < 65536) {
177
- // 4-byte header for medium messages
178
- frame = Buffer.allocUnsafe(4 + len);
179
- frame[0] = 0x82;
180
- frame[1] = 126;
181
- frame.writeUInt16BE(len, 2);
182
- payload.copy(frame, 4);
183
- } else {
184
- // 10-byte header for large messages (unlikely for orders)
185
- frame = Buffer.allocUnsafe(10 + len);
186
- frame[0] = 0x82;
187
- frame[1] = 127;
188
- frame.writeBigUInt64BE(BigInt(len), 2);
189
- payload.copy(frame, 10);
190
- }
191
-
192
- // Client frames MUST be masked per RFC 6455
193
- // Apply masking key
194
- return this._applyMask(frame, len < 126 ? 2 : (len < 65536 ? 4 : 10));
195
- }
196
-
197
- /**
198
- * Apply WebSocket client masking
199
- * @private
200
- * @param {Buffer} frame - Frame with unmasked payload
201
- * @param {number} headerLen - Length of header before payload
202
- * @returns {Buffer} - New frame with mask applied
203
- */
204
- _applyMask(frame, headerLen) {
205
- const payloadLen = frame.length - headerLen;
206
-
207
- // Generate 4-byte mask key
208
- const mask = Buffer.allocUnsafe(4);
209
- mask[0] = (Math.random() * 256) | 0;
210
- mask[1] = (Math.random() * 256) | 0;
211
- mask[2] = (Math.random() * 256) | 0;
212
- mask[3] = (Math.random() * 256) | 0;
213
-
214
- // Create new frame with mask bit set and mask key inserted
215
- const maskedFrame = Buffer.allocUnsafe(headerLen + 4 + payloadLen);
216
-
217
- // Copy header, set mask bit
218
- frame.copy(maskedFrame, 0, 0, headerLen);
219
- maskedFrame[1] |= 0x80; // Set MASK bit
220
-
221
- // Insert mask key after length
222
- mask.copy(maskedFrame, headerLen);
223
-
224
- // Copy and mask payload
225
- for (let i = 0; i < payloadLen; i++) {
226
- maskedFrame[headerLen + 4 + i] = frame[headerLen + i] ^ mask[i & 3];
227
- }
228
-
229
- return maskedFrame;
230
- }
231
-
232
95
  /**
233
96
  * Login to system
234
97
  */
@@ -335,72 +198,6 @@ class RithmicConnection extends EventEmitter {
335
198
  this.heartbeatTimer = null;
336
199
  }
337
200
  }
338
-
339
- /**
340
- * Warmup connection for minimum latency on first order
341
- * Call after login but before trading starts
342
- *
343
- * OPTIMIZATIONS:
344
- * - Pre-load protobuf types
345
- * - Keep TCP connection "hot" with small pings
346
- * - Configure socket for trading
347
- */
348
- async warmup() {
349
- if (!this._socket) return false;
350
-
351
- try {
352
- // Ensure TCP_NODELAY is set
353
- this._socket.setNoDelay(true);
354
-
355
- // Set socket keep-alive to prevent idle disconnection
356
- // Aggressive keep-alive: probe every 10 seconds
357
- this._socket.setKeepAlive(true, 10000);
358
-
359
- // Pre-allocate socket buffer space
360
- if (this._socket.setRecvBufferSize) {
361
- this._socket.setRecvBufferSize(65536); // 64KB receive buffer
362
- }
363
- if (this._socket.setSendBufferSize) {
364
- this._socket.setSendBufferSize(65536); // 64KB send buffer
365
- }
366
-
367
- // Send a heartbeat to "warm up" the connection
368
- this.send('RequestHeartbeat', { templateId: REQ.HEARTBEAT });
369
-
370
- this.emit('warmedUp');
371
- return true;
372
- } catch (e) {
373
- return false;
374
- }
375
- }
376
-
377
- /**
378
- * Get connection diagnostics
379
- * @returns {Object}
380
- */
381
- getDiagnostics() {
382
- const diag = {
383
- state: this.state,
384
- isConnected: this.isConnected,
385
- hasSocket: !!this._socket,
386
- socketState: null,
387
- };
388
-
389
- if (this._socket) {
390
- diag.socketState = {
391
- readable: this._socket.readable,
392
- writable: this._socket.writable,
393
- bytesRead: this._socket.bytesRead,
394
- bytesWritten: this._socket.bytesWritten,
395
- localAddress: this._socket.localAddress,
396
- localPort: this._socket.localPort,
397
- remoteAddress: this._socket.remoteAddress,
398
- remotePort: this._socket.remotePort,
399
- };
400
- }
401
-
402
- return diag;
403
- }
404
201
  }
405
202
 
406
203
  module.exports = { RithmicConnection };
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @fileoverview Rithmic contract methods
3
+ * @module services/rithmic/contracts
4
+ *
5
+ * NO FAKE DATA - Only real values from Rithmic API
6
+ */
7
+
8
+ const { decodeFrontMonthContract } = require('./protobuf');
9
+ const { TIMEOUTS, CACHE } = require('../../config/settings');
10
+ const { logger } = require('../../utils/logger');
11
+
12
+ const log = logger.scope('Rithmic:Contracts');
13
+
14
+ /**
15
+ * Get all available contracts from Rithmic API
16
+ * @param {RithmicService} service - Service instance
17
+ * @returns {Promise<{success: boolean, contracts: Array, source?: string, error?: string}>}
18
+ */
19
+ const getContracts = async (service) => {
20
+ // Check cache
21
+ if (service._contractsCache && Date.now() - service._contractsCacheTime < CACHE.CONTRACTS_TTL) {
22
+ return { success: true, contracts: service._contractsCache, source: 'cache' };
23
+ }
24
+
25
+ if (!service.credentials) {
26
+ return { success: false, error: 'Not logged in' };
27
+ }
28
+
29
+ try {
30
+ // Connect to TICKER_PLANT if needed
31
+ if (!service.tickerConn) {
32
+ const connected = await service.connectTicker(service.credentials.username, service.credentials.password);
33
+ if (!connected) {
34
+ return { success: false, error: 'Failed to connect to TICKER_PLANT' };
35
+ }
36
+ }
37
+
38
+ service.tickerConn.setMaxListeners(5000);
39
+
40
+ log.debug('Fetching contracts from Rithmic API');
41
+ const contracts = await fetchAllFrontMonths(service);
42
+
43
+ if (!contracts.length) {
44
+ return { success: false, error: 'No tradeable contracts found' };
45
+ }
46
+
47
+ // Cache results
48
+ service._contractsCache = contracts;
49
+ service._contractsCacheTime = Date.now();
50
+
51
+ return { success: true, contracts, source: 'api' };
52
+ } catch (err) {
53
+ log.error('getContracts error', { error: err.message });
54
+ return { success: false, error: err.message };
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Search contracts
60
+ * @param {RithmicService} service - Service instance
61
+ * @param {string} searchText - Search text
62
+ * @returns {Promise<Array>}
63
+ */
64
+ const searchContracts = async (service, searchText) => {
65
+ const result = await getContracts(service);
66
+ if (!searchText || !result.success) return result.contracts || [];
67
+
68
+ const search = searchText.toUpperCase();
69
+ return result.contracts.filter(c =>
70
+ c.symbol.toUpperCase().includes(search) ||
71
+ c.name.toUpperCase().includes(search)
72
+ );
73
+ };
74
+
75
+ /**
76
+ * Fetch all front month contracts from API
77
+ * @param {RithmicService} service - Service instance
78
+ * @returns {Promise<Array>}
79
+ */
80
+ const fetchAllFrontMonths = (service) => {
81
+ if (!service.tickerConn) {
82
+ throw new Error('TICKER_PLANT not connected');
83
+ }
84
+
85
+ return new Promise((resolve) => {
86
+ const contracts = new Map();
87
+ const productsToCheck = new Map();
88
+
89
+ // Handler for ProductCodes responses
90
+ const productHandler = (msg) => {
91
+ if (msg.templateId !== 112) return;
92
+
93
+ const decoded = decodeProductCodes(msg.data);
94
+ if (!decoded.productCode || !decoded.exchange) return;
95
+
96
+ const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
97
+ if (!validExchanges.includes(decoded.exchange)) return;
98
+
99
+ const name = (decoded.productName || '').toLowerCase();
100
+ if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
101
+
102
+ const key = `${decoded.productCode}:${decoded.exchange}`;
103
+ if (!productsToCheck.has(key)) {
104
+ productsToCheck.set(key, {
105
+ productCode: decoded.productCode,
106
+ productName: decoded.productName || decoded.productCode,
107
+ exchange: decoded.exchange,
108
+ });
109
+ }
110
+ };
111
+
112
+ // Handler for FrontMonth responses
113
+ const frontMonthHandler = (msg) => {
114
+ if (msg.templateId !== 114) return;
115
+
116
+ const decoded = decodeFrontMonthContract(msg.data);
117
+ if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
118
+ contracts.set(decoded.userMsg, {
119
+ symbol: decoded.tradingSymbol,
120
+ baseSymbol: decoded.userMsg,
121
+ exchange: decoded.exchange,
122
+ });
123
+ }
124
+ };
125
+
126
+ service.tickerConn.on('message', productHandler);
127
+ service.tickerConn.on('message', frontMonthHandler);
128
+
129
+ // Request all product codes
130
+ service.tickerConn.send('RequestProductCodes', {
131
+ templateId: 111,
132
+ userMsg: ['get-products'],
133
+ });
134
+
135
+ // After timeout, request front months
136
+ setTimeout(() => {
137
+ service.tickerConn.removeListener('message', productHandler);
138
+ log.debug('Collected products', { count: productsToCheck.size });
139
+
140
+ for (const product of productsToCheck.values()) {
141
+ service.tickerConn.send('RequestFrontMonthContract', {
142
+ templateId: 113,
143
+ userMsg: [product.productCode],
144
+ symbol: product.productCode,
145
+ exchange: product.exchange,
146
+ });
147
+ }
148
+
149
+ // Collect results after timeout
150
+ setTimeout(() => {
151
+ service.tickerConn.removeListener('message', frontMonthHandler);
152
+
153
+ const results = [];
154
+ for (const [baseSymbol, contract] of contracts) {
155
+ const productKey = `${baseSymbol}:${contract.exchange}`;
156
+ const product = productsToCheck.get(productKey);
157
+
158
+ // 100% API data - no static symbol info
159
+ results.push({
160
+ symbol: contract.symbol,
161
+ baseSymbol,
162
+ name: product?.productName || baseSymbol,
163
+ exchange: contract.exchange,
164
+ });
165
+ }
166
+
167
+ // Sort alphabetically by base symbol
168
+ results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
169
+
170
+ log.debug('Got contracts from API', { count: results.length });
171
+ resolve(results);
172
+ }, TIMEOUTS.RITHMIC_PRODUCTS);
173
+ }, TIMEOUTS.RITHMIC_CONTRACTS);
174
+ });
175
+ };
176
+
177
+ /**
178
+ * Decode ProductCodes response
179
+ * @param {Buffer} buffer - Protobuf buffer
180
+ * @returns {Object} Decoded product data
181
+ */
182
+ const decodeProductCodes = (buffer) => {
183
+ const result = {};
184
+ let offset = 0;
185
+
186
+ const readVarint = (buf, off) => {
187
+ let value = 0;
188
+ let shift = 0;
189
+ while (off < buf.length) {
190
+ const byte = buf[off++];
191
+ value |= (byte & 0x7F) << shift;
192
+ if (!(byte & 0x80)) break;
193
+ shift += 7;
194
+ }
195
+ return [value, off];
196
+ };
197
+
198
+ const readString = (buf, off) => {
199
+ const [len, newOff] = readVarint(buf, off);
200
+ return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
201
+ };
202
+
203
+ while (offset < buffer.length) {
204
+ try {
205
+ const [tag, tagOff] = readVarint(buffer, offset);
206
+ const wireType = tag & 0x7;
207
+ const fieldNumber = tag >>> 3;
208
+ offset = tagOff;
209
+
210
+ if (wireType === 0) {
211
+ const [, newOff] = readVarint(buffer, offset);
212
+ offset = newOff;
213
+ } else if (wireType === 2) {
214
+ const [val, newOff] = readString(buffer, offset);
215
+ offset = newOff;
216
+ if (fieldNumber === 110101) result.exchange = val;
217
+ if (fieldNumber === 100749) result.productCode = val;
218
+ if (fieldNumber === 100003) result.productName = val;
219
+ } else {
220
+ break;
221
+ }
222
+ } catch {
223
+ break;
224
+ }
225
+ }
226
+
227
+ return result;
228
+ };
229
+
230
+ module.exports = {
231
+ getContracts,
232
+ searchContracts,
233
+ fetchAllFrontMonths,
234
+ decodeProductCodes,
235
+ };