hedgequantx 2.6.163 → 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 (146) hide show
  1. package/README.md +15 -88
  2. package/bin/cli.js +0 -11
  3. package/dist/lib/api.jsc +0 -0
  4. package/dist/lib/api2.jsc +0 -0
  5. package/dist/lib/core.jsc +0 -0
  6. package/dist/lib/core2.jsc +0 -0
  7. package/dist/lib/data.js +1 -1
  8. package/dist/lib/data.jsc +0 -0
  9. package/dist/lib/data2.jsc +0 -0
  10. package/dist/lib/decoder.jsc +0 -0
  11. package/dist/lib/m/mod1.jsc +0 -0
  12. package/dist/lib/m/mod2.jsc +0 -0
  13. package/dist/lib/n/r1.jsc +0 -0
  14. package/dist/lib/n/r2.jsc +0 -0
  15. package/dist/lib/n/r3.jsc +0 -0
  16. package/dist/lib/n/r4.jsc +0 -0
  17. package/dist/lib/n/r5.jsc +0 -0
  18. package/dist/lib/n/r6.jsc +0 -0
  19. package/dist/lib/n/r7.jsc +0 -0
  20. package/dist/lib/o/util1.jsc +0 -0
  21. package/dist/lib/o/util2.jsc +0 -0
  22. package/package.json +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 +116 -99
  67. package/src/services/rithmic/handlers.js +21 -196
  68. package/src/services/rithmic/index.js +63 -120
  69. package/src/services/rithmic/market.js +31 -0
  70. package/src/services/rithmic/orders.js +5 -111
  71. package/src/services/rithmic/protobuf.js +384 -138
  72. package/src/services/session.js +22 -173
  73. package/src/ui/box.js +10 -18
  74. package/src/ui/index.js +1 -3
  75. package/src/ui/menu.js +1 -1
  76. package/src/utils/prompts.js +2 -2
  77. package/dist/lib/m/s1.js +0 -1
  78. package/src/menus/ai-agent-connect.js +0 -181
  79. package/src/menus/ai-agent-models.js +0 -219
  80. package/src/menus/ai-agent-oauth.js +0 -292
  81. package/src/menus/ai-agent-ui.js +0 -141
  82. package/src/menus/ai-agent.js +0 -484
  83. package/src/pages/algo/algo-config.js +0 -195
  84. package/src/pages/algo/algo-multi.js +0 -801
  85. package/src/pages/algo/algo-utils.js +0 -58
  86. package/src/pages/algo/copy-engine.js +0 -449
  87. package/src/pages/algo/custom-strategy.js +0 -459
  88. package/src/pages/algo/logger.js +0 -245
  89. package/src/pages/algo/smart-logs-data.js +0 -218
  90. package/src/pages/algo/smart-logs.js +0 -387
  91. package/src/pages/algo/ui-constants.js +0 -144
  92. package/src/pages/algo/ui-summary.js +0 -184
  93. package/src/pages/stats-calculations.js +0 -191
  94. package/src/pages/stats-ui.js +0 -381
  95. package/src/pages/stats.js +0 -339
  96. package/src/services/ai/client-analysis.js +0 -194
  97. package/src/services/ai/client-models.js +0 -333
  98. package/src/services/ai/client.js +0 -343
  99. package/src/services/ai/index.js +0 -384
  100. package/src/services/ai/oauth-anthropic.js +0 -265
  101. package/src/services/ai/oauth-gemini.js +0 -223
  102. package/src/services/ai/oauth-iflow.js +0 -269
  103. package/src/services/ai/oauth-openai.js +0 -233
  104. package/src/services/ai/oauth-qwen.js +0 -279
  105. package/src/services/ai/providers/direct-providers.js +0 -323
  106. package/src/services/ai/providers/index.js +0 -62
  107. package/src/services/ai/providers/other-providers.js +0 -104
  108. package/src/services/ai/proxy-install.js +0 -249
  109. package/src/services/ai/proxy-manager.js +0 -494
  110. package/src/services/ai/proxy-remote.js +0 -161
  111. package/src/services/ai/strategy-supervisor.js +0 -1312
  112. package/src/services/ai/supervisor-data.js +0 -195
  113. package/src/services/ai/supervisor-optimize.js +0 -215
  114. package/src/services/ai/supervisor-sync.js +0 -178
  115. package/src/services/ai/supervisor-utils.js +0 -158
  116. package/src/services/ai/supervisor.js +0 -484
  117. package/src/services/ai/validation.js +0 -250
  118. package/src/services/hqx-server-events.js +0 -110
  119. package/src/services/hqx-server-handlers.js +0 -217
  120. package/src/services/hqx-server-latency.js +0 -136
  121. package/src/services/hqx-server.js +0 -403
  122. package/src/services/position-constants.js +0 -28
  123. package/src/services/position-exit-logic.js +0 -174
  124. package/src/services/position-manager.js +0 -438
  125. package/src/services/position-momentum.js +0 -206
  126. package/src/services/projectx/accounts.js +0 -142
  127. package/src/services/projectx/index.js +0 -443
  128. package/src/services/projectx/market.js +0 -172
  129. package/src/services/projectx/stats.js +0 -110
  130. package/src/services/projectx/trading.js +0 -180
  131. package/src/services/rithmic/latency-tracker.js +0 -182
  132. package/src/services/rithmic/market-data-decoders.js +0 -229
  133. package/src/services/rithmic/market-data.js +0 -272
  134. package/src/services/rithmic/orders-fast.js +0 -246
  135. package/src/services/rithmic/proto-decoders.js +0 -403
  136. package/src/services/rithmic/specs.js +0 -146
  137. package/src/services/rithmic/trade-history.js +0 -254
  138. package/src/services/session-history.js +0 -475
  139. package/src/services/strategy/hft-signal-calc.js +0 -147
  140. package/src/services/strategy/hft-tick.js +0 -407
  141. package/src/services/strategy/recovery-math.js +0 -402
  142. package/src/services/tradovate/constants.js +0 -109
  143. package/src/services/tradovate/index.js +0 -392
  144. package/src/services/tradovate/market.js +0 -47
  145. package/src/services/tradovate/orders.js +0 -145
  146. package/src/services/tradovate/websocket.js +0 -97
@@ -1,801 +0,0 @@
1
- /**
2
- * Multi-Symbol Algo Trading (Rithmic)
3
- * Handles trading multiple symbols simultaneously
4
- */
5
-
6
- 'use strict';
7
-
8
- const chalk = require('chalk');
9
- const readline = require('readline');
10
-
11
- const { AlgoUI, renderMultiSymbolSummary } = require('./ui');
12
- const { prompts } = require('../../utils');
13
- const { FAST_SCALPING } = require('../../config/settings');
14
- const { PositionManager } = require('../../services/position-manager');
15
- const { hftStrategy } = require('../../services/strategy/hft-tick');
16
- const { RithmicMarketDataFeed } = require('../../services/rithmic/market-data');
17
- const { algoLogger } = require('./logger');
18
- const { recoveryMath } = require('../../services/strategy/recovery-math');
19
- const aiService = require('../../services/ai');
20
- const StrategySupervisor = require('../../services/ai/strategy-supervisor');
21
- const { formatPrice } = require('./algo-utils');
22
-
23
- /**
24
- * Launch multi-symbol algo trading (Rithmic only)
25
- * Uses single market feed connection with multiple subscriptions
26
- * Each symbol has its own PositionManager and strategy instance
27
- *
28
- * Same logic as launchAlgo but for multiple symbols
29
- *
30
- * @param {Object} service - Rithmic trading service
31
- * @param {Object} account - Trading account
32
- * @param {Array} contracts - Array of contracts to trade
33
- * @param {Object} config - Algo configuration
34
- * @param {Function} [CustomStrategyClass] - Optional custom strategy class (if not provided, uses hftStrategy)
35
- */
36
- const launchMultiSymbolRithmic = async (service, account, contracts, config, CustomStrategyClass = null) => {
37
- const { dailyTarget, maxRisk, showName, enableAI } = config;
38
-
39
- const accountName = showName
40
- ? (account.accountName || account.rithmicAccountId || account.accountId)
41
- : 'HQX *****';
42
- const rithmicAccountId = account.rithmicAccountId || account.accountId;
43
-
44
- // Build symbols string for UI (clean names without X1 suffix, symbol only)
45
- const symbolsDisplay = contracts.map(c => {
46
- let name = c.name || c.symbol;
47
- // Remove X1, X2 suffix (Rithmic internal suffixes)
48
- name = name.replace(/X\d+$/, '');
49
- return name;
50
- }).join(', ');
51
-
52
- const ui = new AlgoUI({
53
- subtitle: `MULTI-SYMBOL (${contracts.length})`,
54
- mode: 'one-account'
55
- });
56
-
57
- // Calculate total qty across all symbols
58
- const totalQty = contracts.reduce((sum, c) => sum + (c.qty || 1), 0);
59
-
60
- // Baseline P&L captured at session start (to show only THIS session's P&L)
61
- let baselineClosedPnl = null; // Will be set on first pnlUpdate
62
-
63
- // Shared stats (same structure as launchAlgo)
64
- const stats = {
65
- accountName,
66
- symbol: symbolsDisplay,
67
- qty: totalQty,
68
- target: dailyTarget,
69
- risk: maxRisk,
70
- propfirm: account.propfirm || 'Unknown',
71
- platform: 'RITHMIC',
72
- pnl: null,
73
- openPnl: 0, // Start at 0 for session
74
- closedPnl: 0, // Start at 0 for session (will show delta from baseline)
75
- balance: null,
76
- buyingPower: null,
77
- margin: null,
78
- netLiquidation: null,
79
- position: 0,
80
- entryPrice: 0,
81
- lastPrice: 0,
82
- trades: 0,
83
- wins: 0,
84
- losses: 0,
85
- sessionPnl: 0,
86
- latency: 0,
87
- connected: false,
88
- startTime: Date.now(),
89
- aiSupervision: enableAI || false,
90
- aiMode: null,
91
- agentCount: 0,
92
- fastPath: true,
93
- avgEntryLatency: 0,
94
- avgFillLatency: 0,
95
- entryLatencies: [],
96
- // Per-symbol tracking
97
- symbolStats: {},
98
- };
99
-
100
- // Initialize per-symbol stats
101
- contracts.forEach(c => {
102
- const name = c.name || c.symbol;
103
- stats.symbolStats[name] = {
104
- position: 0,
105
- trades: 0,
106
- wins: 0,
107
- losses: 0,
108
- pnl: 0,
109
- openPnl: 0, // Per-symbol unrealized P&L
110
- tickCount: 0,
111
- };
112
- });
113
-
114
- let running = true;
115
- let stopReason = null;
116
- let tickCount = 0;
117
-
118
- // Store contract info for later use (including qty per symbol)
119
- const contractInfoMap = {};
120
- contracts.forEach(c => {
121
- const name = c.name || c.symbol;
122
- contractInfoMap[name] = {
123
- tickSize: c.tickSize ?? null,
124
- tickValue: c.tickValue ?? null,
125
- contractId: c.id || c.symbol || c.name,
126
- exchange: c.exchange || 'CME',
127
- qty: c.qty || 1, // Per-symbol quantity
128
- };
129
- });
130
-
131
- // ═══════════════════════════════════════════════════════════════════════════
132
- // AI SUPERVISOR INITIALIZATION (same as single-symbol)
133
- // ═══════════════════════════════════════════════════════════════════════════
134
- let aiAgentCount = 0;
135
- if (enableAI) {
136
- const aiAgents = aiService.getAgents();
137
- aiAgentCount = aiAgents.length;
138
- stats.agentCount = aiAgentCount;
139
- if (aiAgents.length > 0) {
140
- // Use first symbol's strategy for AI supervisor
141
- const firstContract = contracts[0];
142
- const firstSymbolName = firstContract.name || firstContract.symbol;
143
- // Strategy will be created below, so we init supervisor after strategies
144
- }
145
- }
146
-
147
- // ═══════════════════════════════════════════════════════════════════════════
148
- // POSITION MANAGERS & STRATEGIES - One per symbol
149
- // ═══════════════════════════════════════════════════════════════════════════
150
- const positionManagers = {};
151
- const strategies = {};
152
- const pendingOrders = {}; // Track pending orders per symbol
153
- // No disabled symbols - all symbols can trade until Target/Risk reached
154
-
155
- contracts.forEach(contract => {
156
- const symbolName = contract.name || contract.symbol;
157
- const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
158
-
159
- // Create strategy instance for this symbol
160
- // Use custom strategy class if provided, otherwise use default hftStrategy
161
- let strategy;
162
- if (CustomStrategyClass) {
163
- strategy = new CustomStrategyClass();
164
- } else {
165
- strategy = Object.create(hftStrategy);
166
- }
167
- if (tickSize !== null && tickValue !== null) {
168
- strategy.initialize(contractId, tickSize, tickValue);
169
- }
170
- strategies[symbolName] = strategy;
171
- pendingOrders[symbolName] = false;
172
-
173
- // Create position manager for this symbol
174
- const pm = new PositionManager(service, strategy);
175
- if (tickSize !== null && tickValue !== null) {
176
- pm.setContractInfo(symbolName, { tickSize, tickValue, contractId });
177
- }
178
- pm.start();
179
- positionManagers[symbolName] = pm;
180
-
181
- // ═══════════════════════════════════════════════════════════════════════
182
- // POSITION MANAGER EVENTS (same as launchAlgo)
183
- // ═══════════════════════════════════════════════════════════════════════
184
- pm.on('entryFilled', ({ position, fillLatencyMs }) => {
185
- stats.entryLatencies.push(fillLatencyMs);
186
- stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
187
- const side = position.side === 0 ? 'LONG' : 'SHORT';
188
- const priceStr = formatPrice(position.entryPrice, tickSize || 0.25);
189
- ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${priceStr} | ${fillLatencyMs}ms`);
190
- stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
191
- ui.render(stats);
192
- });
193
-
194
- pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
195
- const holdSec = (holdDurationMs / 1000).toFixed(1);
196
- if (pnlTicks !== null && tickValue !== null) {
197
- const pnlDollars = pnlTicks * tickValue;
198
- stats.sessionPnl += pnlDollars;
199
- stats.symbolStats[symbolName].pnl += pnlDollars;
200
- stats.symbolStats[symbolName].trades++;
201
- stats.trades++;
202
-
203
- // Record trade for Recovery Math
204
- recoveryMath.recordTrade({
205
- pnl: pnlDollars,
206
- ticks: pnlTicks,
207
- side: pnlTicks >= 0 ? 'win' : 'loss',
208
- duration: holdDurationMs,
209
- });
210
-
211
- // Update Recovery Mode state
212
- const recovery = recoveryMath.updateSessionPnL(
213
- stats.sessionPnl,
214
- FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
215
- FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
216
- );
217
-
218
- if (recovery.justActivated) {
219
- stats.recoveryMode = true;
220
- ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}%`);
221
- } else if (recovery.justDeactivated) {
222
- stats.recoveryMode = false;
223
- ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
224
- }
225
-
226
- if (pnlDollars >= 0) {
227
- stats.wins++;
228
- stats.symbolStats[symbolName].wins++;
229
- const priceStr = formatPrice(exitPrice, tickSize || 0.25);
230
- ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} @ ${priceStr} | ${holdSec}s`);
231
- } else {
232
- stats.losses++;
233
- stats.symbolStats[symbolName].losses++;
234
- const priceStr = formatPrice(exitPrice, tickSize || 0.25);
235
- ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} @ ${priceStr} | ${holdSec}s`);
236
- // Symbol can trade again - no disable, continue until Target/Risk reached
237
- }
238
- } else if (pnlTicks !== null) {
239
- // Log with ticks only
240
- stats.trades++;
241
- stats.symbolStats[symbolName].trades++;
242
- if (pnlTicks >= 0) {
243
- stats.wins++;
244
- stats.symbolStats[symbolName].wins++;
245
- ui.addLog('win', `[${symbolName}] +${pnlTicks} ticks | ${holdSec}s`);
246
- } else {
247
- stats.losses++;
248
- stats.symbolStats[symbolName].losses++;
249
- ui.addLog('loss', `[${symbolName}] ${pnlTicks} ticks | ${holdSec}s`);
250
- // Symbol can trade again - no disable, continue until Target/Risk reached
251
- }
252
- }
253
- stats.symbolStats[symbolName].position = 0;
254
- stats.symbolStats[symbolName].openPnl = 0; // Reset open P&L when position closed
255
- // Recalculate total Open P&L
256
- stats.openPnl = Object.values(stats.symbolStats).reduce((sum, s) => sum + (s.openPnl || 0), 0);
257
- pendingOrders[symbolName] = false;
258
- ui.render(stats);
259
- });
260
-
261
- pm.on('holdComplete', () => {
262
- ui.addLog('ready', `[${symbolName}] Hold complete - monitoring exit`);
263
- });
264
-
265
- pm.on('breakevenActivated', ({ breakevenPrice, pnlTicks }) => {
266
- const priceStr = formatPrice(breakevenPrice, tickSize || 0.25);
267
- ui.addLog('be', `[${symbolName}] BE @ ${priceStr} | +${pnlTicks} ticks`);
268
- });
269
-
270
- // ═══════════════════════════════════════════════════════════════════════
271
- // STRATEGY SIGNALS
272
- // ═══════════════════════════════════════════════════════════════════════
273
- strategy.on('signal', async (signal) => {
274
- if (!running) return;
275
- if (pendingOrders[symbolName]) return;
276
- if (!pm.canEnter(symbolName)) return;
277
-
278
- const { direction, confidence } = signal;
279
- const orderSide = direction === 'long' ? 0 : 1;
280
- const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
281
-
282
- // Use per-symbol quantity
283
- const symbolQty = contractInfoMap[symbolName].qty;
284
-
285
- // Calculate risk amount
286
- const kelly = Math.min(0.25, confidence || 0.15);
287
- const riskAmount = Math.round(maxRisk * kelly);
288
- const riskPct = Math.round((riskAmount / maxRisk) * 100);
289
-
290
- pendingOrders[symbolName] = true;
291
- ui.addLog('entry', `[${symbolName}] ${sideStr} ${symbolQty}x | risk: $${riskAmount} (${riskPct}%)`);
292
-
293
- const orderData = {
294
- accountId: rithmicAccountId,
295
- symbol: symbolName,
296
- exchange: contractInfoMap[symbolName].exchange,
297
- size: symbolQty,
298
- side: orderSide,
299
- };
300
-
301
- try {
302
- const entryResult = service.fastEntry(orderData);
303
- if (entryResult.success) {
304
- pm.registerEntry(entryResult, orderData, contractInfoMap[symbolName]);
305
-
306
- // Update avg entry latency
307
- stats.avgEntryLatency = stats.entryLatencies.length > 0
308
- ? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
309
- : entryResult.latencyMs;
310
- } else {
311
- ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
312
- pendingOrders[symbolName] = false;
313
- }
314
- } catch (e) {
315
- ui.addLog('error', `[${symbolName}] Order error: ${e.message}`);
316
- pendingOrders[symbolName] = false;
317
- }
318
- });
319
- });
320
-
321
- // ═══════════════════════════════════════════════════════════════════════════
322
- // AI SUPERVISOR - Initialize after strategies are created
323
- // ═══════════════════════════════════════════════════════════════════════════
324
- if (enableAI && aiAgentCount > 0) {
325
- const aiAgents = aiService.getAgents();
326
- const firstSymbol = Object.keys(strategies)[0];
327
- const firstStrategy = strategies[firstSymbol];
328
- const supervisorResult = StrategySupervisor.initialize(firstStrategy, aiAgents, service, rithmicAccountId);
329
- stats.aiSupervision = supervisorResult.success;
330
- stats.aiMode = supervisorResult.mode;
331
-
332
- if (stats.aiSupervision) {
333
- algoLogger.info(ui, 'AI SUPERVISION', `${aiAgentCount} agent(s) - ${stats.aiMode} mode - LEARNING ACTIVE`);
334
- }
335
- }
336
-
337
- // ═══════════════════════════════════════════════════════════════════════════
338
- // MARKET DATA FEED - Single connection, multiple subscriptions
339
- // ═══════════════════════════════════════════════════════════════════════════
340
- const marketFeed = new RithmicMarketDataFeed(service);
341
-
342
- marketFeed.on('connected', () => {
343
- stats.connected = true;
344
- algoLogger.dataConnected(ui, 'RTC');
345
- algoLogger.algoOperational(ui, 'RITHMIC');
346
- });
347
-
348
- // Smart logs state tracking
349
- let lastHeartbeat = Date.now();
350
- let tps = 0;
351
-
352
- marketFeed.on('tick', (tick) => {
353
- if (!running) return;
354
-
355
- tickCount++;
356
- tps++;
357
- stats.latency = tick.latency || 0;
358
-
359
- // Route tick to correct strategy based on symbol
360
- const tickSymbol = tick.symbol;
361
-
362
- // Find matching strategy (ES matches ESH6, NQ matches NQH6, etc.)
363
- for (const [symbolName, strategy] of Object.entries(strategies)) {
364
- const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
365
- if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
366
- stats.symbolStats[symbolName].tickCount++;
367
-
368
- // Log first tick per symbol
369
- if (stats.symbolStats[symbolName].tickCount === 1) {
370
- algoLogger.info(ui, 'FIRST TICK', `[${symbolName}] price=${parseFloat(Number(tick.price).toFixed(6))} bid=${parseFloat(Number(tick.bid).toFixed(6))} ask=${parseFloat(Number(tick.ask).toFixed(6))}`);
371
- } else if (stats.symbolStats[symbolName].tickCount === 100) {
372
- algoLogger.info(ui, 'DATA FLOWING', `[${symbolName}] 100 ticks received`);
373
- }
374
-
375
- const tickData = {
376
- contractId: tick.contractId || symbolName,
377
- price: tick.price || tick.lastPrice || tick.bid,
378
- bid: tick.bid,
379
- ask: tick.ask,
380
- volume: tick.volume || tick.size || 1,
381
- side: tick.lastTradeSide || tick.side || 'unknown',
382
- timestamp: tick.timestamp || Date.now()
383
- };
384
-
385
- // Update last price
386
- stats.lastPrice = tickData.price;
387
-
388
- // Feed tick to strategy
389
- strategy.processTick(tickData);
390
-
391
- // Update price for position manager
392
- service.emit('priceUpdate', {
393
- symbol: symbolName,
394
- price: tickData.price,
395
- timestamp: tickData.timestamp,
396
- });
397
-
398
- // Get momentum data from strategy for position manager
399
- const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(symbolName);
400
- if (modelValues && positionManagers[symbolName] && typeof positionManagers[symbolName].updateMomentum === 'function') {
401
- positionManagers[symbolName].updateMomentum(symbolName, {
402
- ofi: modelValues.ofi || 0,
403
- zscore: modelValues.zscore || 0,
404
- delta: modelValues.delta || 0,
405
- timestamp: tickData.timestamp,
406
- });
407
- }
408
-
409
- break;
410
- }
411
- }
412
-
413
- // ═══════════════════════════════════════════════════════════════════════════
414
- // SMART LOGS - Same as single-symbol mode
415
- // ═══════════════════════════════════════════════════════════════════════════
416
- const now = Date.now();
417
- if (now - lastHeartbeat > 1000) {
418
- // Get model values from first active symbol's strategy
419
- const firstSymbol = Object.keys(strategies)[0];
420
- const firstStrategy = strategies[firstSymbol];
421
- const modelValues = firstStrategy?.getModelValues?.() || firstStrategy?.getModelValues?.(firstSymbol) || null;
422
-
423
- if (modelValues && modelValues.ofi !== undefined) {
424
- const ofi = modelValues.ofi || 0;
425
- const delta = modelValues.delta || 0;
426
- const zscore = modelValues.zscore || 0;
427
- const mom = modelValues.momentum || 0;
428
-
429
- // Check if any symbol has an open position
430
- const totalPosition = Object.values(stats.symbolStats).reduce((sum, s) => sum + Math.abs(s.position || 0), 0);
431
-
432
- if (totalPosition === 0) {
433
- // Not in position - show market analysis (varied messages)
434
- const smartLogs = require('./smart-logs');
435
- const stateLog = smartLogs.getMarketStateLog(ofi, zscore, mom, delta);
436
- if (stateLog.details) {
437
- ui.addLog('analysis', `${stateLog.message} - ${stateLog.details}`);
438
- } else {
439
- ui.addLog('info', stateLog.message);
440
- }
441
- }
442
- // When IN POSITION: Don't spam logs every second
443
- } else {
444
- // Waiting for data - log every 5 seconds only
445
- if (now - lastHeartbeat > 5000) {
446
- const smartLogs = require('./smart-logs');
447
- const scanLog = smartLogs.getScanningLog(true);
448
- ui.addLog('info', `${scanLog.message} ${tps} ticks/s`);
449
- }
450
- }
451
- lastHeartbeat = now;
452
- tps = 0;
453
- }
454
-
455
- ui.render(stats);
456
- });
457
-
458
- marketFeed.on('error', (err) => {
459
- algoLogger.error(ui, 'MARKET ERROR', err.message);
460
- });
461
-
462
- marketFeed.on('disconnected', (err) => {
463
- stats.connected = false;
464
- algoLogger.dataDisconnected(ui, 'WEBSOCKET', err?.message);
465
- });
466
-
467
- // ═══════════════════════════════════════════════════════════════════════════
468
- // STARTUP LOGS (same as launchAlgo)
469
- // ═══════════════════════════════════════════════════════════════════════════
470
- const market = checkMarketHours();
471
- const sessionName = market.session || 'AMERICAN';
472
- const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
473
-
474
- algoLogger.connectingToEngine(ui, account.accountId);
475
- algoLogger.engineStarting(ui, 'RITHMIC', dailyTarget, maxRisk);
476
- algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
477
- algoLogger.info(ui, 'FAST PATH', `Rithmic direct | ${contracts.length} symbols | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms`);
478
-
479
- ui.render(stats);
480
-
481
- // Connect and subscribe
482
- try {
483
- algoLogger.info(ui, 'CONNECTING', `RITHMIC TICKER | ${contracts.length} symbols`);
484
- await marketFeed.connect();
485
- await new Promise(r => setTimeout(r, 1000));
486
-
487
- if (marketFeed.isConnected()) {
488
- for (const contract of contracts) {
489
- const symbolName = contract.name || contract.symbol;
490
- const exchange = contract.exchange || 'CME';
491
- marketFeed.subscribe(symbolName, exchange);
492
- algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} (${exchange})`);
493
- }
494
- } else {
495
- algoLogger.error(ui, 'CONNECTION', 'Failed to connect market feed');
496
- }
497
- } catch (e) {
498
- algoLogger.error(ui, 'CONNECTION ERROR', e.message);
499
- }
500
-
501
- // ═══════════════════════════════════════════════════════════════════════════
502
- // TARGET/RISK CHECK - Stop algo when limits reached
503
- // Uses SESSION P&L (trades from this HQX session only) + Open P&L
504
- // Risk triggers if: closedPnl hits risk OR openPnl hits risk OR total hits risk
505
- // ═══════════════════════════════════════════════════════════════════════════
506
- const checkTargetRisk = () => {
507
- if (!running) return;
508
-
509
- const closedPnl = stats.closedPnl || 0; // Session closed P&L
510
- const openPnl = stats.openPnl || 0; // Current open P&L
511
- const totalPnl = closedPnl + openPnl; // Total session P&L
512
-
513
- // Daily target reached - STOP with profit
514
- if (totalPnl >= dailyTarget) {
515
- stopReason = 'target';
516
- running = false;
517
- algoLogger.info(ui, 'TARGET REACHED', `+$${totalPnl.toFixed(2)} >= $${dailyTarget}`);
518
- ui.addLog('success', `████ DAILY TARGET REACHED: +$${totalPnl.toFixed(2)} ████`);
519
- emergencyStopAll();
520
- return;
521
- }
522
-
523
- // Max risk reached - STOP to protect capital
524
- // Trigger if: closed P&L hits risk OR open P&L hits risk OR total hits risk
525
- if (closedPnl <= -maxRisk) {
526
- stopReason = 'risk';
527
- running = false;
528
- algoLogger.info(ui, 'CLOSED P&L RISK', `Closed: -$${Math.abs(closedPnl).toFixed(2)} <= -$${maxRisk}`);
529
- ui.addLog('error', `████ MAX RISK (CLOSED): -$${Math.abs(closedPnl).toFixed(2)} ████`);
530
- emergencyStopAll();
531
- } else if (openPnl <= -maxRisk) {
532
- stopReason = 'risk';
533
- running = false;
534
- algoLogger.info(ui, 'OPEN P&L RISK', `Open: -$${Math.abs(openPnl).toFixed(2)} <= -$${maxRisk}`);
535
- ui.addLog('error', `████ MAX RISK (OPEN): -$${Math.abs(openPnl).toFixed(2)} ████`);
536
- emergencyStopAll();
537
- } else if (totalPnl <= -maxRisk) {
538
- stopReason = 'risk';
539
- running = false;
540
- algoLogger.info(ui, 'TOTAL P&L RISK', `Total: -$${Math.abs(totalPnl).toFixed(2)} <= -$${maxRisk}`);
541
- ui.addLog('error', `████ MAX RISK (TOTAL): -$${Math.abs(totalPnl).toFixed(2)} ████`);
542
- emergencyStopAll();
543
- }
544
- };
545
-
546
- // ═══════════════════════════════════════════════════════════════════════════
547
- // REAL-TIME P&L VIA WEBSOCKET (same as launchAlgo)
548
- // ═══════════════════════════════════════════════════════════════════════════
549
- if (typeof service.on === 'function') {
550
- // Account-level P&L updates
551
- service.on('pnlUpdate', (pnlData) => {
552
- if (pnlData.accountId !== rithmicAccountId) return;
553
-
554
- if (pnlData.closedPositionPnl !== undefined) {
555
- const accountClosedPnl = parseFloat(pnlData.closedPositionPnl);
556
-
557
- // Capture baseline on first update (P&L at session start)
558
- if (baselineClosedPnl === null) {
559
- baselineClosedPnl = accountClosedPnl;
560
- // Baseline captured silently - no need to show to user
561
- }
562
-
563
- // stats.closedPnl shows ONLY this session's closed P&L (delta from baseline)
564
- stats.closedPnl = accountClosedPnl - baselineClosedPnl;
565
- }
566
- if (pnlData.accountBalance !== undefined) {
567
- stats.balance = parseFloat(pnlData.accountBalance);
568
- }
569
- if (pnlData.availableBuyingPower !== undefined) {
570
- stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
571
- }
572
- if (pnlData.marginBalance !== undefined) {
573
- stats.margin = parseFloat(pnlData.marginBalance);
574
- }
575
- if (pnlData.netLiquidation !== undefined) {
576
- stats.netLiquidation = parseFloat(pnlData.netLiquidation);
577
- } else if (stats.balance !== null) {
578
- stats.netLiquidation = stats.balance + (stats.openPnl || 0);
579
- }
580
-
581
- stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
582
-
583
- // Check target/risk on every P&L update
584
- checkTargetRisk();
585
-
586
- ui.render(stats);
587
- });
588
-
589
- // Position-level updates (for Open P&L per symbol)
590
- service.on('positionUpdate', (pos) => {
591
- if (!pos || pos.accountId !== rithmicAccountId) return;
592
-
593
- const posSymbol = pos.symbol;
594
- for (const symbolName of Object.keys(stats.symbolStats)) {
595
- const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
596
- if (posSymbol === baseSymbol || posSymbol === symbolName || symbolName.startsWith(posSymbol)) {
597
- stats.symbolStats[symbolName].position = pos.quantity || 0;
598
-
599
- // Update Open P&L for this symbol
600
- if (pos.openPnl !== undefined && pos.openPnl !== null) {
601
- stats.symbolStats[symbolName].openPnl = pos.openPnl;
602
-
603
- // Calculate total Open P&L from all symbols
604
- stats.openPnl = Object.values(stats.symbolStats).reduce((sum, s) => sum + (s.openPnl || 0), 0);
605
- stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
606
-
607
- if (stats.balance !== null) {
608
- stats.netLiquidation = stats.balance + stats.openPnl;
609
- }
610
-
611
- // Check target/risk on Open P&L changes too
612
- checkTargetRisk();
613
- }
614
- break;
615
- }
616
- }
617
- ui.render(stats);
618
- });
619
- }
620
-
621
- // ═══════════════════════════════════════════════════════════════════════════
622
- // EMERGENCY STOP - Force close ALL positions
623
- // ═══════════════════════════════════════════════════════════════════════════
624
- const emergencyStopAll = async () => {
625
- ui.addLog('warning', '████ EMERGENCY STOP INITIATED ████');
626
- ui.render(stats);
627
-
628
- const TIMEOUT_MS = 5000;
629
- const withTimeout = (promise, ms) => Promise.race([
630
- promise,
631
- new Promise((_, reject) => setTimeout(() => reject(new Error('TIMEOUT')), ms))
632
- ]);
633
-
634
- try {
635
- // CRITICAL: Use ONLY Rithmic API to get REAL positions from exchange
636
- // DO NOT use local stats which can be corrupted
637
-
638
- // Step 1: Cancel all pending orders first
639
- ui.addLog('info', 'Cancelling all orders...');
640
- ui.render(stats);
641
- try {
642
- if (service && typeof service.cancelAllOrders === 'function') {
643
- await withTimeout(service.cancelAllOrders(rithmicAccountId), TIMEOUT_MS);
644
- ui.addLog('success', 'All orders cancelled');
645
- }
646
- } catch (e) {
647
- ui.addLog('warning', `Cancel orders: ${e.message}`);
648
- }
649
-
650
- // Step 2: Use Rithmic flattenAll - reads REAL positions from exchange
651
- ui.addLog('info', 'Flattening all positions via Rithmic API...');
652
- ui.render(stats);
653
-
654
- try {
655
- if (service && typeof service.flattenAll === 'function') {
656
- const result = await withTimeout(service.flattenAll(rithmicAccountId), TIMEOUT_MS);
657
- if (result && result.results) {
658
- for (const r of result.results) {
659
- if (r.success) {
660
- ui.addLog('success', `[${r.symbol}] FLATTENED`);
661
- } else {
662
- ui.addLog('error', `[${r.symbol}] ${r.error || 'Failed'}`);
663
- }
664
- }
665
- }
666
- ui.addLog('success', 'Flatten all complete');
667
- } else if (service && typeof service.emergencyStop === 'function') {
668
- // Fallback to emergencyStop
669
- await withTimeout(service.emergencyStop(rithmicAccountId), TIMEOUT_MS);
670
- ui.addLog('success', 'Emergency stop complete');
671
- }
672
- } catch (e) {
673
- ui.addLog('warning', `Flatten: ${e.message}`);
674
- }
675
-
676
- // Step 3: Reset local state
677
- for (const [symbolName, symStats] of Object.entries(stats.symbolStats)) {
678
- symStats.position = 0;
679
- symStats.openPnl = 0;
680
- // Also reset position manager
681
- const pm = positionManagers[symbolName];
682
- if (pm) {
683
- try { pm.reset?.(); } catch {}
684
- }
685
- }
686
- stats.openPnl = 0;
687
-
688
- ui.addLog('success', '████ EMERGENCY STOP COMPLETE ████');
689
-
690
- } catch (e) {
691
- ui.addLog('error', `Emergency stop error: ${e.message}`);
692
- }
693
-
694
- ui.render(stats);
695
- };
696
-
697
- // Keyboard handler
698
- let emergencyStopInProgress = false;
699
-
700
- const setupKeyHandler = () => {
701
- if (!process.stdin.isTTY) return;
702
- readline.emitKeypressEvents(process.stdin);
703
- process.stdin.setRawMode(true);
704
- process.stdin.resume();
705
-
706
- const onKey = async (str, key) => {
707
- if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
708
- if (emergencyStopInProgress) return;
709
- emergencyStopInProgress = true;
710
- running = false;
711
- stopReason = 'manual';
712
- await emergencyStopAll();
713
- }
714
- };
715
- process.stdin.on('keypress', onKey);
716
- return () => {
717
- process.stdin.removeListener('keypress', onKey);
718
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
719
- };
720
- };
721
-
722
- const cleanupKeys = setupKeyHandler();
723
-
724
- // UI refresh
725
- const refreshInterval = setInterval(() => {
726
- if (running) ui.render(stats);
727
- }, 1000);
728
-
729
- // Wait for stop
730
- await new Promise(resolve => {
731
- const check = setInterval(() => {
732
- if (!running) {
733
- clearInterval(check);
734
- resolve();
735
- }
736
- }, 100);
737
- });
738
-
739
- // ═══════════════════════════════════════════════════════════════════════════
740
- // CLEANUP
741
- // ═══════════════════════════════════════════════════════════════════════════
742
- clearInterval(refreshInterval);
743
-
744
- // Stop all position managers
745
- for (const pm of Object.values(positionManagers)) {
746
- try { pm.stop(); } catch {}
747
- }
748
-
749
- // Disconnect market feed with timeout
750
- try {
751
- const disconnectPromise = marketFeed.disconnect();
752
- const timeoutPromise = new Promise(resolve => setTimeout(resolve, 2000));
753
- await Promise.race([disconnectPromise, timeoutPromise]);
754
- } catch {}
755
-
756
- // Cleanup keyboard FIRST - restore stdin to normal mode
757
- try { if (cleanupKeys) cleanupKeys(); } catch {}
758
-
759
- // Force stdin back to normal mode (critical for waitForEnter to work)
760
- try {
761
- if (process.stdin.isTTY) {
762
- process.stdin.setRawMode(false);
763
- }
764
- // Remove all listeners to prevent interference
765
- process.stdin.removeAllListeners('keypress');
766
- process.stdin.resume();
767
- } catch {}
768
-
769
- // Small delay to let stdin settle
770
- await new Promise(r => setTimeout(r, 100));
771
-
772
- // Calculate duration before closeLog
773
- const durationMs = Date.now() - stats.startTime;
774
- const hours = Math.floor(durationMs / 3600000);
775
- const minutes = Math.floor((durationMs % 3600000) / 60000);
776
- const seconds = Math.floor((durationMs % 60000) / 1000);
777
- stats.duration = hours > 0
778
- ? `${hours}h ${minutes}m ${seconds}s`
779
- : minutes > 0
780
- ? `${minutes}m ${seconds}s`
781
- : `${seconds}s`;
782
-
783
- // Close log file with session summary
784
- try { ui.closeLog(stats); } catch {}
785
-
786
- // ═══════════════════════════════════════════════════════════════════════════
787
- // SESSION SUMMARY (duration already calculated above)
788
- // ═══════════════════════════════════════════════════════════════════════════
789
- // Clear screen, show cursor, and render summary
790
- process.stdout.write('\x1B[?25h'); // Show cursor
791
- console.clear();
792
-
793
- // Render multi-symbol summary with same style as single-symbol
794
- renderMultiSymbolSummary(stats, stopReason, stats.symbolStats);
795
-
796
- // Wait for user to press Enter before returning to menu
797
- await prompts.waitForEnter();
798
- };
799
-
800
-
801
- module.exports = { launchMultiSymbolRithmic };