hedgequantx 2.6.127 → 2.6.128

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.128",
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,363 @@ 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
+ * @param {Object} service - Rithmic trading service
1389
+ * @param {Object} account - Trading account
1390
+ * @param {Array} contracts - Array of contracts to trade
1391
+ * @param {Object} config - Algo configuration
1392
+ */
1393
+ const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
1394
+ const { contracts: qty, dailyTarget, maxRisk, showName, enableAI } = config;
1395
+
1396
+ const accountName = showName
1397
+ ? (account.accountName || account.rithmicAccountId || account.accountId)
1398
+ : 'HQX *****';
1399
+ const rithmicAccountId = account.rithmicAccountId || account.accountId;
1400
+
1401
+ // Build symbols string for UI
1402
+ const symbolNames = contracts.map(c => c.name || c.symbol);
1403
+ const symbolsDisplay = symbolNames.join(', ');
1404
+
1405
+ const ui = new AlgoUI({
1406
+ subtitle: `MULTI-SYMBOL (${contracts.length})`,
1407
+ mode: 'one-account'
1408
+ });
1409
+
1410
+ // Shared stats
1411
+ const stats = {
1412
+ accountName,
1413
+ symbol: symbolsDisplay,
1414
+ qty,
1415
+ target: dailyTarget,
1416
+ risk: maxRisk,
1417
+ propfirm: account.propfirm || 'Unknown',
1418
+ platform: 'RITHMIC',
1419
+ pnl: null,
1420
+ openPnl: null,
1421
+ closedPnl: null,
1422
+ balance: null,
1423
+ position: 0,
1424
+ trades: 0,
1425
+ wins: 0,
1426
+ losses: 0,
1427
+ sessionPnl: 0,
1428
+ latency: 0,
1429
+ connected: false,
1430
+ startTime: Date.now(),
1431
+ aiSupervision: enableAI || false,
1432
+ fastPath: true,
1433
+ // Per-symbol tracking
1434
+ symbolStats: {},
1435
+ };
1436
+
1437
+ // Initialize per-symbol stats
1438
+ contracts.forEach(c => {
1439
+ const name = c.name || c.symbol;
1440
+ stats.symbolStats[name] = { position: 0, trades: 0, wins: 0, losses: 0, pnl: 0 };
1441
+ });
1442
+
1443
+ let running = true;
1444
+ let stopReason = null;
1445
+
1446
+ // ═══════════════════════════════════════════════════════════════════════════
1447
+ // POSITION MANAGERS - One per symbol
1448
+ // ═══════════════════════════════════════════════════════════════════════════
1449
+ const positionManagers = {};
1450
+ const strategies = {};
1451
+
1452
+ contracts.forEach(contract => {
1453
+ const symbolName = contract.name || contract.symbol;
1454
+ const contractId = contract.id || contract.symbol || contract.name;
1455
+ const tickSize = contract.tickSize ?? null;
1456
+ const tickValue = contract.tickValue ?? null;
1457
+
1458
+ // Create strategy instance for this symbol
1459
+ const strategy = Object.create(hftStrategy);
1460
+ if (tickSize !== null && tickValue !== null) {
1461
+ strategy.initialize(contractId, tickSize, tickValue);
1462
+ }
1463
+ strategies[symbolName] = strategy;
1464
+
1465
+ // Create position manager for this symbol
1466
+ const pm = new PositionManager(service, strategy);
1467
+ if (tickSize !== null && tickValue !== null) {
1468
+ pm.setContractInfo(symbolName, { tickSize, tickValue, contractId });
1469
+ }
1470
+ pm.start();
1471
+ positionManagers[symbolName] = pm;
1472
+
1473
+ // Position manager events
1474
+ pm.on('entryFilled', ({ position, fillLatencyMs }) => {
1475
+ const side = position.side === 0 ? 'LONG' : 'SHORT';
1476
+ ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${position.entryPrice} | ${fillLatencyMs}ms`);
1477
+ stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
1478
+ });
1479
+
1480
+ pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
1481
+ const holdSec = (holdDurationMs / 1000).toFixed(1);
1482
+ if (pnlTicks !== null && tickValue !== null) {
1483
+ const pnlDollars = pnlTicks * tickValue;
1484
+ stats.sessionPnl += pnlDollars;
1485
+ stats.symbolStats[symbolName].pnl += pnlDollars;
1486
+ stats.symbolStats[symbolName].trades++;
1487
+ stats.trades++;
1488
+
1489
+ if (pnlDollars >= 0) {
1490
+ stats.wins++;
1491
+ stats.symbolStats[symbolName].wins++;
1492
+ ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} | ${holdSec}s`);
1493
+ } else {
1494
+ stats.losses++;
1495
+ stats.symbolStats[symbolName].losses++;
1496
+ ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} | ${holdSec}s`);
1497
+ }
1498
+ }
1499
+ stats.symbolStats[symbolName].position = 0;
1500
+ ui.render(stats);
1501
+ });
1502
+
1503
+ // Strategy signals
1504
+ strategy.on('signal', async (signal) => {
1505
+ if (!running) return;
1506
+ if (!pm.canEnter(symbolName)) return;
1507
+
1508
+ const { direction } = signal;
1509
+ const orderSide = direction === 'long' ? 0 : 1;
1510
+ const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
1511
+
1512
+ ui.addLog('entry', `[${symbolName}] ${sideStr} signal`);
1513
+
1514
+ const orderData = {
1515
+ accountId: rithmicAccountId,
1516
+ symbol: symbolName,
1517
+ exchange: contract.exchange || 'CME',
1518
+ size: qty,
1519
+ side: orderSide,
1520
+ };
1521
+
1522
+ const entryResult = service.fastEntry(orderData);
1523
+ if (entryResult.success) {
1524
+ pm.registerEntry(entryResult, orderData, { tickSize, tickValue, contractId });
1525
+ } else {
1526
+ ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
1527
+ }
1528
+ });
1529
+ });
1530
+
1531
+ // ═══════════════════════════════════════════════════════════════════════════
1532
+ // MARKET DATA FEED - Single connection, multiple subscriptions
1533
+ // ═══════════════════════════════════════════════════════════════════════════
1534
+ const marketFeed = new RithmicMarketDataFeed(service);
1535
+
1536
+ marketFeed.on('connected', () => {
1537
+ stats.connected = true;
1538
+ ui.addLog('success', 'Market feed connected');
1539
+ ui.render(stats);
1540
+ });
1541
+
1542
+ marketFeed.on('tick', (tick) => {
1543
+ if (!running) return;
1544
+
1545
+ stats.latency = tick.latency || 0;
1546
+
1547
+ // Route tick to correct strategy based on symbol
1548
+ const tickSymbol = tick.symbol;
1549
+
1550
+ // Find matching strategy (ES matches ESH6, NQ matches NQH6, etc.)
1551
+ for (const [symbolName, strategy] of Object.entries(strategies)) {
1552
+ const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
1553
+ if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
1554
+ const tickData = {
1555
+ contractId: tick.contractId || symbolName,
1556
+ price: tick.price || tick.lastPrice || tick.bid,
1557
+ bid: tick.bid,
1558
+ ask: tick.ask,
1559
+ volume: tick.volume || tick.size || 1,
1560
+ timestamp: tick.timestamp || Date.now()
1561
+ };
1562
+ strategy.processTick(tickData);
1563
+
1564
+ // Update price for position manager
1565
+ service.emit('priceUpdate', {
1566
+ symbol: symbolName,
1567
+ price: tickData.price,
1568
+ timestamp: tickData.timestamp,
1569
+ });
1570
+ break;
1571
+ }
1572
+ }
1573
+
1574
+ ui.render(stats);
1575
+ });
1576
+
1577
+ marketFeed.on('error', (err) => {
1578
+ ui.addLog('error', `Feed error: ${err.message}`);
1579
+ });
1580
+
1581
+ // Connect and subscribe
1582
+ ui.render(stats);
1583
+ ui.addLog('info', `MULTI-SYMBOL: ${contracts.length} symbols`);
1584
+
1585
+ try {
1586
+ await marketFeed.connect();
1587
+ await new Promise(r => setTimeout(r, 1000));
1588
+
1589
+ if (marketFeed.isConnected()) {
1590
+ for (const contract of contracts) {
1591
+ const symbolName = contract.name || contract.symbol;
1592
+ const exchange = contract.exchange || 'CME';
1593
+ marketFeed.subscribe(symbolName, exchange);
1594
+ ui.addLog('success', `[${symbolName}] Subscribed`);
1595
+ }
1596
+ } else {
1597
+ ui.addLog('error', 'Failed to connect market feed');
1598
+ }
1599
+ } catch (e) {
1600
+ ui.addLog('error', `Connection error: ${e.message}`);
1601
+ }
1602
+
1603
+ // ═══════════════════════════════════════════════════════════════════════════
1604
+ // P&L UPDATES VIA WEBSOCKET
1605
+ // ═══════════════════════════════════════════════════════════════════════════
1606
+ if (typeof service.on === 'function') {
1607
+ service.on('pnlUpdate', (pnlData) => {
1608
+ if (pnlData.accountId !== rithmicAccountId) return;
1609
+ if (pnlData.closedPositionPnl !== undefined) {
1610
+ stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
1611
+ }
1612
+ if (pnlData.accountBalance !== undefined) {
1613
+ stats.balance = parseFloat(pnlData.accountBalance);
1614
+ }
1615
+ stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
1616
+ });
1617
+ }
1618
+
1619
+ // ═══════════════════════════════════════════════════════════════════════════
1620
+ // EMERGENCY STOP
1621
+ // ═══════════════════════════════════════════════════════════════════════════
1622
+ const emergencyStopAll = async () => {
1623
+ ui.addLog('warning', '████ EMERGENCY STOP ████');
1624
+
1625
+ // Close all positions
1626
+ for (const [symbolName, pm] of Object.entries(positionManagers)) {
1627
+ try {
1628
+ if (pm.hasPosition(symbolName)) {
1629
+ pm.closePosition(symbolName);
1630
+ ui.addLog('info', `[${symbolName}] Closing position...`);
1631
+ }
1632
+ } catch (e) {
1633
+ ui.addLog('error', `[${symbolName}] Close failed: ${e.message}`);
1634
+ }
1635
+ }
1636
+
1637
+ ui.addLog('info', 'All close orders sent');
1638
+ ui.render(stats);
1639
+ };
1640
+
1641
+ // Keyboard handler
1642
+ let emergencyStopInProgress = false;
1643
+
1644
+ const setupKeyHandler = () => {
1645
+ if (!process.stdin.isTTY) return;
1646
+ readline.emitKeypressEvents(process.stdin);
1647
+ process.stdin.setRawMode(true);
1648
+ process.stdin.resume();
1649
+
1650
+ const onKey = async (str, key) => {
1651
+ if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
1652
+ if (emergencyStopInProgress) return;
1653
+ emergencyStopInProgress = true;
1654
+ running = false;
1655
+ stopReason = 'manual';
1656
+ await emergencyStopAll();
1657
+ }
1658
+ };
1659
+ process.stdin.on('keypress', onKey);
1660
+ return () => {
1661
+ process.stdin.removeListener('keypress', onKey);
1662
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1663
+ };
1664
+ };
1665
+
1666
+ const cleanupKeys = setupKeyHandler();
1667
+
1668
+ // UI refresh
1669
+ const refreshInterval = setInterval(() => {
1670
+ if (running) ui.render(stats);
1671
+ }, 1000);
1672
+
1673
+ // Wait for stop
1674
+ await new Promise(resolve => {
1675
+ const check = setInterval(() => {
1676
+ if (!running) {
1677
+ clearInterval(check);
1678
+ resolve();
1679
+ }
1680
+ }, 100);
1681
+ });
1682
+
1683
+ // ═══════════════════════════════════════════════════════════════════════════
1684
+ // CLEANUP
1685
+ // ═══════════════════════════════════════════════════════════════════════════
1686
+ clearInterval(refreshInterval);
1687
+
1688
+ // Stop all position managers
1689
+ for (const pm of Object.values(positionManagers)) {
1690
+ try { pm.stop(); } catch {}
1691
+ }
1692
+
1693
+ // Disconnect market feed
1694
+ try { await marketFeed.disconnect(); } catch {}
1695
+
1696
+ // Cleanup keyboard
1697
+ try { if (cleanupKeys) cleanupKeys(); } catch {}
1698
+ try { ui.cleanup(); } catch {}
1699
+
1700
+ try {
1701
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1702
+ process.stdin.resume();
1703
+ } catch {}
1704
+
1705
+ // ═══════════════════════════════════════════════════════════════════════════
1706
+ // SESSION SUMMARY
1707
+ // ═══════════════════════════════════════════════════════════════════════════
1708
+ const durationMs = Date.now() - stats.startTime;
1709
+ const hours = Math.floor(durationMs / 3600000);
1710
+ const minutes = Math.floor((durationMs % 3600000) / 60000);
1711
+ const seconds = Math.floor((durationMs % 60000) / 1000);
1712
+ const duration = hours > 0
1713
+ ? `${hours}h ${minutes}m ${seconds}s`
1714
+ : minutes > 0
1715
+ ? `${minutes}m ${seconds}s`
1716
+ : `${seconds}s`;
1717
+
1718
+ console.log();
1719
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1720
+ console.log(chalk.cyan(' MULTI-SYMBOL SESSION SUMMARY'));
1721
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1722
+ console.log();
1723
+
1724
+ for (const [symbol, symStats] of Object.entries(stats.symbolStats)) {
1725
+ const winRate = symStats.trades > 0 ? ((symStats.wins / symStats.trades) * 100).toFixed(0) : 0;
1726
+ const pnlColor = symStats.pnl >= 0 ? chalk.green : chalk.red;
1727
+ console.log(chalk.white(` ${symbol}:`));
1728
+ console.log(` Trades: ${symStats.trades} | WR: ${winRate}% | P&L: ${pnlColor('$' + symStats.pnl.toFixed(2))}`);
1729
+ }
1730
+
1731
+ console.log();
1732
+ const totalPnlColor = stats.sessionPnl >= 0 ? chalk.green : chalk.red;
1733
+ console.log(chalk.white(` TOTAL: ${stats.trades} trades | ${stats.wins}W/${stats.losses}L`));
1734
+ console.log(chalk.white(` Session P&L: ${totalPnlColor('$' + stats.sessionPnl.toFixed(2))}`));
1735
+ console.log(chalk.white(` Duration: ${duration}`));
1736
+ console.log();
1737
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1738
+
1739
+ await prompts.waitForEnter();
1740
+ };
1741
+
1286
1742
  module.exports = { oneAccountMenu };