hedgequantx 2.6.127 → 2.6.129

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.6.127",
3
+ "version": "2.6.129",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -34,6 +34,17 @@ const StrategySupervisor = require('../../services/ai/strategy-supervisor');
34
34
 
35
35
 
36
36
 
37
+ /**
38
+ * Check if service supports fast path (Rithmic direct)
39
+ * @param {Object} service - Trading service
40
+ * @returns {boolean}
41
+ */
42
+ const isRithmicFastPath = (service) => {
43
+ return typeof service.fastEntry === 'function' &&
44
+ typeof service.fastExit === 'function' &&
45
+ service.orderConn?.isConnected;
46
+ };
47
+
37
48
  /**
38
49
  * One Account Menu
39
50
  */
@@ -78,21 +89,48 @@ const oneAccountMenu = async (service) => {
78
89
  // Use the service attached to the account (from getAllAccounts), fallback to getServiceForAccount
79
90
  const accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
80
91
 
81
- // Select symbol
82
- const contract = await selectSymbol(accountService, selectedAccount);
83
- if (!contract) return;
92
+ // Check if Rithmic (supports multi-symbol)
93
+ const isRithmicAccount = isRithmicFastPath(accountService);
94
+
95
+ // Ask for multi-symbol mode (Rithmic only)
96
+ let useMultiSymbol = false;
97
+ if (isRithmicAccount) {
98
+ console.log();
99
+ useMultiSymbol = await prompts.confirmPrompt(chalk.cyan(`MULTI-SYMBOL MODE? (up to ${MAX_MULTI_SYMBOLS} symbols)`), false);
100
+ if (useMultiSymbol === null) return;
101
+ }
102
+
103
+ // Select symbol(s)
104
+ const contracts = await selectSymbol(accountService, selectedAccount, useMultiSymbol);
105
+ if (!contracts) return;
106
+
107
+ // Normalize to array
108
+ const contractList = Array.isArray(contracts) ? contracts : [contracts];
84
109
 
85
110
  // Configure algo
86
- const config = await configureAlgo(selectedAccount, contract);
111
+ const config = await configureAlgo(selectedAccount, contractList);
87
112
  if (!config) return;
88
113
 
89
- await launchAlgo(accountService, selectedAccount, contract, config);
114
+ // Launch appropriate algo
115
+ if (contractList.length === 1) {
116
+ await launchAlgo(accountService, selectedAccount, contractList[0], config);
117
+ } else {
118
+ await launchMultiSymbolRithmic(accountService, selectedAccount, contractList, config);
119
+ }
90
120
  };
91
121
 
122
+ // Maximum symbols for multi-symbol trading
123
+ const MAX_MULTI_SYMBOLS = 5;
124
+
92
125
  /**
93
126
  * Symbol selection - sorted with popular indices first
127
+ * Supports selecting multiple symbols (up to MAX_MULTI_SYMBOLS)
128
+ * @param {Object} service - Trading service
129
+ * @param {Object} account - Trading account
130
+ * @param {boolean} allowMultiple - Allow selecting multiple symbols
131
+ * @returns {Object|Array|null} Single contract or array of contracts
94
132
  */
95
- const selectSymbol = async (service, account) => {
133
+ const selectSymbol = async (service, account, allowMultiple = false) => {
96
134
  const spinner = ora({ text: 'LOADING SYMBOLS...', color: 'yellow' }).start();
97
135
 
98
136
  const contractsResult = await service.getContracts();
@@ -136,16 +174,86 @@ const selectSymbol = async (service, account) => {
136
174
 
137
175
  options.push({ label: chalk.gray('< BACK'), value: 'back' });
138
176
 
139
- const contract = await prompts.selectOption(chalk.yellow('SELECT SYMBOL:'), options);
140
- return contract === 'back' || contract === null ? null : contract;
177
+ // Single symbol mode
178
+ if (!allowMultiple) {
179
+ const contract = await prompts.selectOption(chalk.yellow('SELECT SYMBOL:'), options);
180
+ return contract === 'back' || contract === null ? null : contract;
181
+ }
182
+
183
+ // Multi-symbol selection mode
184
+ const selectedContracts = [];
185
+
186
+ while (selectedContracts.length < MAX_MULTI_SYMBOLS) {
187
+ console.log();
188
+
189
+ // Show already selected symbols
190
+ if (selectedContracts.length > 0) {
191
+ console.log(chalk.cyan(` SELECTED (${selectedContracts.length}/${MAX_MULTI_SYMBOLS}):`));
192
+ selectedContracts.forEach((c, i) => {
193
+ const name = c.name || c.symbol;
194
+ console.log(chalk.green(` ${i + 1}. ${name}`));
195
+ });
196
+ console.log();
197
+ }
198
+
199
+ // Filter out already selected
200
+ const availableOptions = options.filter(opt => {
201
+ if (opt.value === 'back') return true;
202
+ const optId = opt.value.id || opt.value.symbol || opt.value.name;
203
+ return !selectedContracts.some(sc => (sc.id || sc.symbol || sc.name) === optId);
204
+ });
205
+
206
+ // Add DONE option if at least 1 selected
207
+ if (selectedContracts.length > 0) {
208
+ availableOptions.unshift({
209
+ label: chalk.green(`✓ DONE - Start trading ${selectedContracts.length} symbol${selectedContracts.length > 1 ? 's' : ''}`),
210
+ value: 'done'
211
+ });
212
+ }
213
+
214
+ const promptText = selectedContracts.length === 0
215
+ ? chalk.yellow(`SELECT SYMBOL (1/${MAX_MULTI_SYMBOLS}):`)
216
+ : chalk.yellow(`ADD SYMBOL (${selectedContracts.length + 1}/${MAX_MULTI_SYMBOLS}) OR DONE:`);
217
+
218
+ const choice = await prompts.selectOption(promptText, availableOptions);
219
+
220
+ if (choice === null || choice === 'back') {
221
+ if (selectedContracts.length === 0) return null;
222
+ selectedContracts.pop(); // Remove last
223
+ continue;
224
+ }
225
+
226
+ if (choice === 'done') break;
227
+
228
+ selectedContracts.push(choice);
229
+
230
+ if (selectedContracts.length >= MAX_MULTI_SYMBOLS) {
231
+ console.log(chalk.yellow(` Maximum ${MAX_MULTI_SYMBOLS} symbols reached`));
232
+ break;
233
+ }
234
+ }
235
+
236
+ return selectedContracts.length > 0 ? selectedContracts : null;
141
237
  };
142
238
 
143
239
  /**
144
240
  * Configure algo
241
+ * @param {Object} account - Trading account
242
+ * @param {Object|Array} contractOrContracts - Single contract or array of contracts
145
243
  */
146
- const configureAlgo = async (account, contract) => {
244
+ const configureAlgo = async (account, contractOrContracts) => {
245
+ const contractList = Array.isArray(contractOrContracts) ? contractOrContracts : [contractOrContracts];
246
+
147
247
  console.log();
148
248
  console.log(chalk.cyan(' CONFIGURE ALGO PARAMETERS'));
249
+
250
+ // Show selected symbols
251
+ if (contractList.length > 1) {
252
+ console.log(chalk.white(` Trading ${contractList.length} symbols:`));
253
+ contractList.forEach((c, i) => {
254
+ console.log(chalk.yellow(` ${i + 1}. ${c.name || c.symbol}`));
255
+ });
256
+ }
149
257
  console.log();
150
258
 
151
259
  const contracts = await prompts.numberInput('NUMBER OF CONTRACTS:', 1, 1, 10);
@@ -197,17 +305,6 @@ const configureAlgo = async (account, contract) => {
197
305
  return { contracts, dailyTarget, maxRisk, showName, enableAI };
198
306
  };
199
307
 
200
- /**
201
- * Check if service supports fast path (Rithmic direct)
202
- * @param {Object} service - Trading service
203
- * @returns {boolean}
204
- */
205
- const isRithmicFastPath = (service) => {
206
- return typeof service.fastEntry === 'function' &&
207
- typeof service.fastExit === 'function' &&
208
- service.orderConn?.isConnected;
209
- };
210
-
211
308
  /**
212
309
  * Launch algo trading - HQX Ultra Scalping Strategy
213
310
  * Real-time market data + Strategy signals + Auto order execution
@@ -1283,4 +1380,550 @@ const launchAlgo = async (service, account, contract, config) => {
1283
1380
  await prompts.waitForEnter();
1284
1381
  };
1285
1382
 
1383
+ /**
1384
+ * Launch multi-symbol algo trading (Rithmic only)
1385
+ * Uses single market feed connection with multiple subscriptions
1386
+ * Each symbol has its own PositionManager and strategy instance
1387
+ *
1388
+ * Same logic as launchAlgo but for multiple symbols
1389
+ *
1390
+ * @param {Object} service - Rithmic trading service
1391
+ * @param {Object} account - Trading account
1392
+ * @param {Array} contracts - Array of contracts to trade
1393
+ * @param {Object} config - Algo configuration
1394
+ */
1395
+ const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
1396
+ const { contracts: qty, dailyTarget, maxRisk, showName, enableAI } = config;
1397
+
1398
+ const accountName = showName
1399
+ ? (account.accountName || account.rithmicAccountId || account.accountId)
1400
+ : 'HQX *****';
1401
+ const rithmicAccountId = account.rithmicAccountId || account.accountId;
1402
+
1403
+ // Build symbols string for UI
1404
+ const symbolNames = contracts.map(c => c.name || c.symbol);
1405
+ const symbolsDisplay = symbolNames.join(', ');
1406
+
1407
+ const ui = new AlgoUI({
1408
+ subtitle: `MULTI-SYMBOL (${contracts.length})`,
1409
+ mode: 'one-account'
1410
+ });
1411
+
1412
+ // Shared stats (same structure as launchAlgo)
1413
+ const stats = {
1414
+ accountName,
1415
+ symbol: symbolsDisplay,
1416
+ qty,
1417
+ target: dailyTarget,
1418
+ risk: maxRisk,
1419
+ propfirm: account.propfirm || 'Unknown',
1420
+ platform: 'RITHMIC',
1421
+ pnl: null,
1422
+ openPnl: null,
1423
+ closedPnl: null,
1424
+ balance: null,
1425
+ buyingPower: null,
1426
+ margin: null,
1427
+ netLiquidation: null,
1428
+ position: 0,
1429
+ entryPrice: 0,
1430
+ lastPrice: 0,
1431
+ trades: 0,
1432
+ wins: 0,
1433
+ losses: 0,
1434
+ sessionPnl: 0,
1435
+ latency: 0,
1436
+ connected: false,
1437
+ startTime: Date.now(),
1438
+ aiSupervision: enableAI || false,
1439
+ aiMode: null,
1440
+ agentCount: 0,
1441
+ fastPath: true,
1442
+ avgEntryLatency: 0,
1443
+ avgFillLatency: 0,
1444
+ entryLatencies: [],
1445
+ // Per-symbol tracking
1446
+ symbolStats: {},
1447
+ };
1448
+
1449
+ // Initialize per-symbol stats
1450
+ contracts.forEach(c => {
1451
+ const name = c.name || c.symbol;
1452
+ stats.symbolStats[name] = {
1453
+ position: 0,
1454
+ trades: 0,
1455
+ wins: 0,
1456
+ losses: 0,
1457
+ pnl: 0,
1458
+ tickCount: 0,
1459
+ };
1460
+ });
1461
+
1462
+ let running = true;
1463
+ let stopReason = null;
1464
+ let tickCount = 0;
1465
+
1466
+ // Store contract info for later use
1467
+ const contractInfoMap = {};
1468
+ contracts.forEach(c => {
1469
+ const name = c.name || c.symbol;
1470
+ contractInfoMap[name] = {
1471
+ tickSize: c.tickSize ?? null,
1472
+ tickValue: c.tickValue ?? null,
1473
+ contractId: c.id || c.symbol || c.name,
1474
+ exchange: c.exchange || 'CME',
1475
+ };
1476
+ });
1477
+
1478
+ // ═══════════════════════════════════════════════════════════════════════════
1479
+ // POSITION MANAGERS & STRATEGIES - One per symbol
1480
+ // ═══════════════════════════════════════════════════════════════════════════
1481
+ const positionManagers = {};
1482
+ const strategies = {};
1483
+ const pendingOrders = {}; // Track pending orders per symbol
1484
+
1485
+ contracts.forEach(contract => {
1486
+ const symbolName = contract.name || contract.symbol;
1487
+ const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
1488
+
1489
+ // Create strategy instance for this symbol
1490
+ const strategy = Object.create(hftStrategy);
1491
+ if (tickSize !== null && tickValue !== null) {
1492
+ strategy.initialize(contractId, tickSize, tickValue);
1493
+ }
1494
+ strategies[symbolName] = strategy;
1495
+ pendingOrders[symbolName] = false;
1496
+
1497
+ // Create position manager for this symbol
1498
+ const pm = new PositionManager(service, strategy);
1499
+ if (tickSize !== null && tickValue !== null) {
1500
+ pm.setContractInfo(symbolName, { tickSize, tickValue, contractId });
1501
+ }
1502
+ pm.start();
1503
+ positionManagers[symbolName] = pm;
1504
+
1505
+ // ═══════════════════════════════════════════════════════════════════════
1506
+ // POSITION MANAGER EVENTS (same as launchAlgo)
1507
+ // ═══════════════════════════════════════════════════════════════════════
1508
+ pm.on('entryFilled', ({ position, fillLatencyMs }) => {
1509
+ stats.entryLatencies.push(fillLatencyMs);
1510
+ stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
1511
+ const side = position.side === 0 ? 'LONG' : 'SHORT';
1512
+ ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${position.entryPrice} | ${fillLatencyMs}ms`);
1513
+ stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
1514
+ ui.render(stats);
1515
+ });
1516
+
1517
+ pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
1518
+ const holdSec = (holdDurationMs / 1000).toFixed(1);
1519
+ if (pnlTicks !== null && tickValue !== null) {
1520
+ const pnlDollars = pnlTicks * tickValue;
1521
+ stats.sessionPnl += pnlDollars;
1522
+ stats.symbolStats[symbolName].pnl += pnlDollars;
1523
+ stats.symbolStats[symbolName].trades++;
1524
+ stats.trades++;
1525
+
1526
+ // Record trade for Recovery Math
1527
+ recoveryMath.recordTrade({
1528
+ pnl: pnlDollars,
1529
+ ticks: pnlTicks,
1530
+ side: pnlTicks >= 0 ? 'win' : 'loss',
1531
+ duration: holdDurationMs,
1532
+ });
1533
+
1534
+ // Update Recovery Mode state
1535
+ const recovery = recoveryMath.updateSessionPnL(
1536
+ stats.sessionPnl,
1537
+ FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
1538
+ FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
1539
+ );
1540
+
1541
+ if (recovery.justActivated) {
1542
+ stats.recoveryMode = true;
1543
+ ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}%`);
1544
+ } else if (recovery.justDeactivated) {
1545
+ stats.recoveryMode = false;
1546
+ ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
1547
+ }
1548
+
1549
+ if (pnlDollars >= 0) {
1550
+ stats.wins++;
1551
+ stats.symbolStats[symbolName].wins++;
1552
+ ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
1553
+ } else {
1554
+ stats.losses++;
1555
+ stats.symbolStats[symbolName].losses++;
1556
+ ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
1557
+ }
1558
+ } else if (pnlTicks !== null) {
1559
+ // Log with ticks only
1560
+ stats.trades++;
1561
+ stats.symbolStats[symbolName].trades++;
1562
+ if (pnlTicks >= 0) {
1563
+ stats.wins++;
1564
+ stats.symbolStats[symbolName].wins++;
1565
+ ui.addLog('win', `[${symbolName}] +${pnlTicks} ticks | ${holdSec}s`);
1566
+ } else {
1567
+ stats.losses++;
1568
+ stats.symbolStats[symbolName].losses++;
1569
+ ui.addLog('loss', `[${symbolName}] ${pnlTicks} ticks | ${holdSec}s`);
1570
+ }
1571
+ }
1572
+ stats.symbolStats[symbolName].position = 0;
1573
+ pendingOrders[symbolName] = false;
1574
+ ui.render(stats);
1575
+ });
1576
+
1577
+ pm.on('holdComplete', () => {
1578
+ ui.addLog('ready', `[${symbolName}] Hold complete - monitoring exit`);
1579
+ });
1580
+
1581
+ pm.on('breakevenActivated', ({ breakevenPrice, pnlTicks }) => {
1582
+ ui.addLog('be', `[${symbolName}] BE @ ${breakevenPrice} | +${pnlTicks} ticks`);
1583
+ });
1584
+
1585
+ // ═══════════════════════════════════════════════════════════════════════
1586
+ // STRATEGY SIGNALS
1587
+ // ═══════════════════════════════════════════════════════════════════════
1588
+ strategy.on('signal', async (signal) => {
1589
+ if (!running) return;
1590
+ if (pendingOrders[symbolName]) return;
1591
+ if (!pm.canEnter(symbolName)) return;
1592
+
1593
+ const { direction, confidence } = signal;
1594
+ const orderSide = direction === 'long' ? 0 : 1;
1595
+ const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
1596
+
1597
+ // Calculate risk amount
1598
+ const kelly = Math.min(0.25, confidence || 0.15);
1599
+ const riskAmount = Math.round(maxRisk * kelly);
1600
+ const riskPct = Math.round((riskAmount / maxRisk) * 100);
1601
+
1602
+ pendingOrders[symbolName] = true;
1603
+ ui.addLog('entry', `[${symbolName}] ${sideStr} ${qty}x | risk: $${riskAmount} (${riskPct}%)`);
1604
+
1605
+ const orderData = {
1606
+ accountId: rithmicAccountId,
1607
+ symbol: symbolName,
1608
+ exchange: contractInfoMap[symbolName].exchange,
1609
+ size: qty,
1610
+ side: orderSide,
1611
+ };
1612
+
1613
+ try {
1614
+ const entryResult = service.fastEntry(orderData);
1615
+ if (entryResult.success) {
1616
+ pm.registerEntry(entryResult, orderData, contractInfoMap[symbolName]);
1617
+
1618
+ // Update avg entry latency
1619
+ stats.avgEntryLatency = stats.entryLatencies.length > 0
1620
+ ? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
1621
+ : entryResult.latencyMs;
1622
+ } else {
1623
+ ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
1624
+ pendingOrders[symbolName] = false;
1625
+ }
1626
+ } catch (e) {
1627
+ ui.addLog('error', `[${symbolName}] Order error: ${e.message}`);
1628
+ pendingOrders[symbolName] = false;
1629
+ }
1630
+ });
1631
+ });
1632
+
1633
+ // ═══════════════════════════════════════════════════════════════════════════
1634
+ // MARKET DATA FEED - Single connection, multiple subscriptions
1635
+ // ═══════════════════════════════════════════════════════════════════════════
1636
+ const marketFeed = new RithmicMarketDataFeed(service);
1637
+
1638
+ marketFeed.on('connected', () => {
1639
+ stats.connected = true;
1640
+ algoLogger.dataConnected(ui, 'RTC');
1641
+ algoLogger.algoOperational(ui, 'RITHMIC');
1642
+ });
1643
+
1644
+ marketFeed.on('tick', (tick) => {
1645
+ if (!running) return;
1646
+
1647
+ tickCount++;
1648
+ stats.latency = tick.latency || 0;
1649
+
1650
+ // Route tick to correct strategy based on symbol
1651
+ const tickSymbol = tick.symbol;
1652
+
1653
+ // Find matching strategy (ES matches ESH6, NQ matches NQH6, etc.)
1654
+ for (const [symbolName, strategy] of Object.entries(strategies)) {
1655
+ const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
1656
+ if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
1657
+ stats.symbolStats[symbolName].tickCount++;
1658
+
1659
+ // Log first tick per symbol
1660
+ if (stats.symbolStats[symbolName].tickCount === 1) {
1661
+ algoLogger.info(ui, 'FIRST TICK', `[${symbolName}] price=${tick.price} bid=${tick.bid} ask=${tick.ask}`);
1662
+ } else if (stats.symbolStats[symbolName].tickCount === 100) {
1663
+ algoLogger.info(ui, 'DATA FLOWING', `[${symbolName}] 100 ticks received`);
1664
+ }
1665
+
1666
+ const tickData = {
1667
+ contractId: tick.contractId || symbolName,
1668
+ price: tick.price || tick.lastPrice || tick.bid,
1669
+ bid: tick.bid,
1670
+ ask: tick.ask,
1671
+ volume: tick.volume || tick.size || 1,
1672
+ side: tick.lastTradeSide || tick.side || 'unknown',
1673
+ timestamp: tick.timestamp || Date.now()
1674
+ };
1675
+
1676
+ // Update last price
1677
+ stats.lastPrice = tickData.price;
1678
+
1679
+ // Feed tick to strategy
1680
+ strategy.processTick(tickData);
1681
+
1682
+ // Update price for position manager
1683
+ service.emit('priceUpdate', {
1684
+ symbol: symbolName,
1685
+ price: tickData.price,
1686
+ timestamp: tickData.timestamp,
1687
+ });
1688
+
1689
+ // Get momentum data from strategy for position manager
1690
+ const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(symbolName);
1691
+ if (modelValues && positionManagers[symbolName] && typeof positionManagers[symbolName].updateMomentum === 'function') {
1692
+ positionManagers[symbolName].updateMomentum(symbolName, {
1693
+ ofi: modelValues.ofi || 0,
1694
+ zscore: modelValues.zscore || 0,
1695
+ delta: modelValues.delta || 0,
1696
+ timestamp: tickData.timestamp,
1697
+ });
1698
+ }
1699
+
1700
+ break;
1701
+ }
1702
+ }
1703
+
1704
+ ui.render(stats);
1705
+ });
1706
+
1707
+ marketFeed.on('error', (err) => {
1708
+ algoLogger.error(ui, 'MARKET ERROR', err.message);
1709
+ });
1710
+
1711
+ marketFeed.on('disconnected', (err) => {
1712
+ stats.connected = false;
1713
+ algoLogger.dataDisconnected(ui, 'WEBSOCKET', err?.message);
1714
+ });
1715
+
1716
+ // ═══════════════════════════════════════════════════════════════════════════
1717
+ // STARTUP LOGS (same as launchAlgo)
1718
+ // ═══════════════════════════════════════════════════════════════════════════
1719
+ const market = checkMarketHours();
1720
+ const sessionName = market.session || 'AMERICAN';
1721
+ const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
1722
+
1723
+ algoLogger.connectingToEngine(ui, account.accountId);
1724
+ algoLogger.engineStarting(ui, 'RITHMIC', dailyTarget, maxRisk);
1725
+ algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
1726
+ algoLogger.info(ui, 'FAST PATH', `Rithmic direct | ${contracts.length} symbols | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms`);
1727
+
1728
+ ui.render(stats);
1729
+
1730
+ // Connect and subscribe
1731
+ try {
1732
+ algoLogger.info(ui, 'CONNECTING', `RITHMIC TICKER | ${contracts.length} symbols`);
1733
+ await marketFeed.connect();
1734
+ await new Promise(r => setTimeout(r, 1000));
1735
+
1736
+ if (marketFeed.isConnected()) {
1737
+ for (const contract of contracts) {
1738
+ const symbolName = contract.name || contract.symbol;
1739
+ const exchange = contract.exchange || 'CME';
1740
+ marketFeed.subscribe(symbolName, exchange);
1741
+ algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} (${exchange})`);
1742
+ }
1743
+ } else {
1744
+ algoLogger.error(ui, 'CONNECTION', 'Failed to connect market feed');
1745
+ }
1746
+ } catch (e) {
1747
+ algoLogger.error(ui, 'CONNECTION ERROR', e.message);
1748
+ }
1749
+
1750
+ // ═══════════════════════════════════════════════════════════════════════════
1751
+ // REAL-TIME P&L VIA WEBSOCKET (same as launchAlgo)
1752
+ // ═══════════════════════════════════════════════════════════════════════════
1753
+ if (typeof service.on === 'function') {
1754
+ // Account-level P&L updates
1755
+ service.on('pnlUpdate', (pnlData) => {
1756
+ if (pnlData.accountId !== rithmicAccountId) return;
1757
+
1758
+ if (pnlData.closedPositionPnl !== undefined) {
1759
+ stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
1760
+ }
1761
+ if (pnlData.accountBalance !== undefined) {
1762
+ stats.balance = parseFloat(pnlData.accountBalance);
1763
+ }
1764
+ if (pnlData.availableBuyingPower !== undefined) {
1765
+ stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
1766
+ }
1767
+ if (pnlData.marginBalance !== undefined) {
1768
+ stats.margin = parseFloat(pnlData.marginBalance);
1769
+ }
1770
+ if (pnlData.netLiquidation !== undefined) {
1771
+ stats.netLiquidation = parseFloat(pnlData.netLiquidation);
1772
+ } else if (stats.balance !== null) {
1773
+ stats.netLiquidation = stats.balance + (stats.openPnl || 0);
1774
+ }
1775
+
1776
+ stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
1777
+ ui.render(stats);
1778
+ });
1779
+
1780
+ // Position-level updates (for Open P&L)
1781
+ service.on('positionUpdate', (pos) => {
1782
+ if (!pos || pos.accountId !== rithmicAccountId) return;
1783
+
1784
+ const posSymbol = pos.symbol;
1785
+ for (const symbolName of Object.keys(stats.symbolStats)) {
1786
+ const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
1787
+ if (posSymbol === baseSymbol || posSymbol === symbolName || symbolName.startsWith(posSymbol)) {
1788
+ stats.symbolStats[symbolName].position = pos.quantity || 0;
1789
+
1790
+ // Update Open P&L from position
1791
+ if (pos.openPnl !== undefined && pos.openPnl !== null) {
1792
+ stats.openPnl = pos.openPnl;
1793
+ stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
1794
+
1795
+ if (stats.balance !== null) {
1796
+ stats.netLiquidation = stats.balance + stats.openPnl;
1797
+ }
1798
+ }
1799
+ break;
1800
+ }
1801
+ }
1802
+ ui.render(stats);
1803
+ });
1804
+ }
1805
+
1806
+ // ═══════════════════════════════════════════════════════════════════════════
1807
+ // EMERGENCY STOP
1808
+ // ═══════════════════════════════════════════════════════════════════════════
1809
+ const emergencyStopAll = async () => {
1810
+ ui.addLog('warning', '████ EMERGENCY STOP ████');
1811
+
1812
+ // Close all positions
1813
+ for (const [symbolName, pm] of Object.entries(positionManagers)) {
1814
+ try {
1815
+ if (pm.hasPosition(symbolName)) {
1816
+ pm.closePosition(symbolName);
1817
+ ui.addLog('info', `[${symbolName}] Closing position...`);
1818
+ }
1819
+ } catch (e) {
1820
+ ui.addLog('error', `[${symbolName}] Close failed: ${e.message}`);
1821
+ }
1822
+ }
1823
+
1824
+ ui.addLog('info', 'All close orders sent');
1825
+ ui.render(stats);
1826
+ };
1827
+
1828
+ // Keyboard handler
1829
+ let emergencyStopInProgress = false;
1830
+
1831
+ const setupKeyHandler = () => {
1832
+ if (!process.stdin.isTTY) return;
1833
+ readline.emitKeypressEvents(process.stdin);
1834
+ process.stdin.setRawMode(true);
1835
+ process.stdin.resume();
1836
+
1837
+ const onKey = async (str, key) => {
1838
+ if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
1839
+ if (emergencyStopInProgress) return;
1840
+ emergencyStopInProgress = true;
1841
+ running = false;
1842
+ stopReason = 'manual';
1843
+ await emergencyStopAll();
1844
+ }
1845
+ };
1846
+ process.stdin.on('keypress', onKey);
1847
+ return () => {
1848
+ process.stdin.removeListener('keypress', onKey);
1849
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1850
+ };
1851
+ };
1852
+
1853
+ const cleanupKeys = setupKeyHandler();
1854
+
1855
+ // UI refresh
1856
+ const refreshInterval = setInterval(() => {
1857
+ if (running) ui.render(stats);
1858
+ }, 1000);
1859
+
1860
+ // Wait for stop
1861
+ await new Promise(resolve => {
1862
+ const check = setInterval(() => {
1863
+ if (!running) {
1864
+ clearInterval(check);
1865
+ resolve();
1866
+ }
1867
+ }, 100);
1868
+ });
1869
+
1870
+ // ═══════════════════════════════════════════════════════════════════════════
1871
+ // CLEANUP
1872
+ // ═══════════════════════════════════════════════════════════════════════════
1873
+ clearInterval(refreshInterval);
1874
+
1875
+ // Stop all position managers
1876
+ for (const pm of Object.values(positionManagers)) {
1877
+ try { pm.stop(); } catch {}
1878
+ }
1879
+
1880
+ // Disconnect market feed
1881
+ try { await marketFeed.disconnect(); } catch {}
1882
+
1883
+ // Cleanup keyboard
1884
+ try { if (cleanupKeys) cleanupKeys(); } catch {}
1885
+ try { ui.cleanup(); } catch {}
1886
+
1887
+ try {
1888
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1889
+ process.stdin.resume();
1890
+ } catch {}
1891
+
1892
+ // ═══════════════════════════════════════════════════════════════════════════
1893
+ // SESSION SUMMARY
1894
+ // ═══════════════════════════════════════════════════════════════════════════
1895
+ const durationMs = Date.now() - stats.startTime;
1896
+ const hours = Math.floor(durationMs / 3600000);
1897
+ const minutes = Math.floor((durationMs % 3600000) / 60000);
1898
+ const seconds = Math.floor((durationMs % 60000) / 1000);
1899
+ const duration = hours > 0
1900
+ ? `${hours}h ${minutes}m ${seconds}s`
1901
+ : minutes > 0
1902
+ ? `${minutes}m ${seconds}s`
1903
+ : `${seconds}s`;
1904
+
1905
+ console.log();
1906
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1907
+ console.log(chalk.cyan(' MULTI-SYMBOL SESSION SUMMARY'));
1908
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1909
+ console.log();
1910
+
1911
+ for (const [symbol, symStats] of Object.entries(stats.symbolStats)) {
1912
+ const winRate = symStats.trades > 0 ? ((symStats.wins / symStats.trades) * 100).toFixed(0) : 0;
1913
+ const pnlColor = symStats.pnl >= 0 ? chalk.green : chalk.red;
1914
+ console.log(chalk.white(` ${symbol}:`));
1915
+ console.log(` Trades: ${symStats.trades} | WR: ${winRate}% | P&L: ${pnlColor('$' + symStats.pnl.toFixed(2))}`);
1916
+ }
1917
+
1918
+ console.log();
1919
+ const totalPnlColor = stats.sessionPnl >= 0 ? chalk.green : chalk.red;
1920
+ console.log(chalk.white(` TOTAL: ${stats.trades} trades | ${stats.wins}W/${stats.losses}L`));
1921
+ console.log(chalk.white(` Session P&L: ${totalPnlColor('$' + stats.sessionPnl.toFixed(2))}`));
1922
+ console.log(chalk.white(` Duration: ${duration}`));
1923
+ console.log();
1924
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1925
+
1926
+ await prompts.waitForEnter();
1927
+ };
1928
+
1286
1929
  module.exports = { oneAccountMenu };