hedgequantx 2.6.124 → 2.6.126

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.124",
3
+ "version": "2.6.126",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -78,21 +78,32 @@ const oneAccountMenu = async (service) => {
78
78
  // Use the service attached to the account (from getAllAccounts), fallback to getServiceForAccount
79
79
  const accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
80
80
 
81
- // Select symbol
82
- const contract = await selectSymbol(accountService, selectedAccount);
83
- if (!contract) return;
81
+ // Select symbols (up to MAX_SYMBOLS)
82
+ const contracts = await selectSymbols(accountService, selectedAccount);
83
+ if (!contracts || contracts.length === 0) return;
84
84
 
85
- // Configure algo
86
- const config = await configureAlgo(selectedAccount, contract);
85
+ // Configure algo (shared config for all symbols)
86
+ const config = await configureAlgo(selectedAccount, contracts);
87
87
  if (!config) return;
88
88
 
89
- await launchAlgo(accountService, selectedAccount, contract, config);
89
+ // Launch algo for all selected symbols
90
+ if (contracts.length === 1) {
91
+ // Single symbol - use original launchAlgo
92
+ await launchAlgo(accountService, selectedAccount, contracts[0], config);
93
+ } else {
94
+ // Multiple symbols - launch parallel trading
95
+ await launchMultiAlgo(accountService, selectedAccount, contracts, config);
96
+ }
90
97
  };
91
98
 
99
+ // Maximum number of symbols to trade simultaneously
100
+ const MAX_SYMBOLS = 5;
101
+
92
102
  /**
93
103
  * Symbol selection - sorted with popular indices first
104
+ * Allows selecting multiple symbols (up to MAX_SYMBOLS)
94
105
  */
95
- const selectSymbol = async (service, account) => {
106
+ const selectSymbols = async (service, account) => {
96
107
  const spinner = ora({ text: 'LOADING SYMBOLS...', color: 'yellow' }).start();
97
108
 
98
109
  const contractsResult = await service.getContracts();
@@ -126,28 +137,102 @@ const selectSymbol = async (service, account) => {
126
137
 
127
138
  spinner.succeed(`FOUND ${contracts.length} CONTRACTS`);
128
139
 
129
- // Display sorted contracts from API (uniform format: NAME - DESCRIPTION)
130
- const options = contracts.map(c => {
131
- const name = c.name || c.symbol || c.baseSymbol;
132
- const desc = c.description || '';
133
- const label = desc ? `${name} - ${desc}` : name;
134
- return { label, value: c };
135
- });
140
+ // Multi-symbol selection loop
141
+ const selectedContracts = [];
136
142
 
137
- options.push({ label: chalk.gray('< BACK'), value: 'back' });
143
+ while (selectedContracts.length < MAX_SYMBOLS) {
144
+ console.log();
145
+ if (selectedContracts.length > 0) {
146
+ console.log(chalk.cyan(` SELECTED (${selectedContracts.length}/${MAX_SYMBOLS}):`));
147
+ selectedContracts.forEach((c, i) => {
148
+ const name = c.name || c.symbol || c.baseSymbol;
149
+ console.log(chalk.green(` ${i + 1}. ${name}`));
150
+ });
151
+ console.log();
152
+ }
153
+
154
+ // Filter out already selected contracts
155
+ const availableContracts = contracts.filter(c => {
156
+ const cId = c.id || c.symbol || c.name;
157
+ return !selectedContracts.some(sc => (sc.id || sc.symbol || sc.name) === cId);
158
+ });
159
+
160
+ // Build options
161
+ const options = availableContracts.map(c => {
162
+ const name = c.name || c.symbol || c.baseSymbol;
163
+ const desc = c.description || '';
164
+ const label = desc ? `${name} - ${desc}` : name;
165
+ return { label, value: c };
166
+ });
167
+
168
+ // Add done/back options
169
+ if (selectedContracts.length > 0) {
170
+ options.unshift({ label: chalk.green(`✓ DONE (trade ${selectedContracts.length} symbol${selectedContracts.length > 1 ? 's' : ''})`), value: 'done' });
171
+ }
172
+ options.push({ label: chalk.gray('< BACK'), value: 'back' });
173
+
174
+ const promptText = selectedContracts.length === 0
175
+ ? chalk.yellow(`SELECT SYMBOL (1-${MAX_SYMBOLS}):`)
176
+ : chalk.yellow(`ADD SYMBOL (${selectedContracts.length + 1}/${MAX_SYMBOLS}) OR DONE:`);
177
+
178
+ const choice = await prompts.selectOption(promptText, options);
179
+
180
+ if (choice === 'back' || choice === null) {
181
+ if (selectedContracts.length === 0) {
182
+ return null; // Go back
183
+ }
184
+ // Remove last selected
185
+ selectedContracts.pop();
186
+ continue;
187
+ }
188
+
189
+ if (choice === 'done') {
190
+ break; // Done selecting
191
+ }
192
+
193
+ // Add selected contract
194
+ selectedContracts.push(choice);
195
+
196
+ if (selectedContracts.length >= MAX_SYMBOLS) {
197
+ console.log(chalk.yellow(` MAX ${MAX_SYMBOLS} SYMBOLS REACHED`));
198
+ break;
199
+ }
200
+ }
138
201
 
139
- const contract = await prompts.selectOption(chalk.yellow('SELECT SYMBOL:'), options);
140
- return contract === 'back' || contract === null ? null : contract;
202
+ return selectedContracts.length > 0 ? selectedContracts : null;
203
+ };
204
+
205
+ /**
206
+ * Legacy single symbol selection (kept for compatibility)
207
+ */
208
+ const selectSymbol = async (service, account) => {
209
+ const contracts = await selectSymbols(service, account);
210
+ return contracts && contracts.length > 0 ? contracts[0] : null;
141
211
  };
142
212
 
143
213
  /**
144
214
  * Configure algo
215
+ * @param {Object} account - Trading account
216
+ * @param {Array|Object} contractsOrContract - Single contract or array of contracts
145
217
  */
146
- const configureAlgo = async (account, contract) => {
218
+ const configureAlgo = async (account, contractsOrContract) => {
219
+ // Normalize to array
220
+ const contractList = Array.isArray(contractsOrContract) ? contractsOrContract : [contractsOrContract];
221
+
147
222
  console.log();
148
223
  console.log(chalk.cyan(' CONFIGURE ALGO PARAMETERS'));
149
224
  console.log();
150
225
 
226
+ // Show selected symbols
227
+ if (contractList.length > 1) {
228
+ console.log(chalk.white(` TRADING ${contractList.length} SYMBOLS:`));
229
+ contractList.forEach((c, i) => {
230
+ const name = c.name || c.symbol || c.baseSymbol;
231
+ console.log(chalk.yellow(` ${i + 1}. ${name}`));
232
+ });
233
+ console.log();
234
+ }
235
+
151
236
  const contracts = await prompts.numberInput('NUMBER OF CONTRACTS:', 1, 1, 10);
152
237
  if (contracts === null) return null;
153
238
 
@@ -1283,4 +1368,291 @@ const launchAlgo = async (service, account, contract, config) => {
1283
1368
  await prompts.waitForEnter();
1284
1369
  };
1285
1370
 
1371
+ /**
1372
+ * Launch multi-symbol algo trading
1373
+ * Runs multiple symbols in parallel on the same account
1374
+ *
1375
+ * @param {Object} service - Trading service
1376
+ * @param {Object} account - Trading account
1377
+ * @param {Array} contracts - Array of contracts to trade
1378
+ * @param {Object} config - Algo configuration
1379
+ */
1380
+ const launchMultiAlgo = async (service, account, contracts, config) => {
1381
+ const { contracts: qty, dailyTarget, maxRisk, showName } = config;
1382
+
1383
+ const accountName = showName
1384
+ ? (account.accountName || account.rithmicAccountId || account.accountId)
1385
+ : 'HQX *****';
1386
+ const connectionType = account.platform || 'ProjectX';
1387
+ const useFastPath = isRithmicFastPath(service);
1388
+
1389
+ // Build symbol list string
1390
+ const symbolNames = contracts.map(c => c.name || c.symbol || c.baseSymbol);
1391
+ const symbolsStr = symbolNames.join(', ');
1392
+
1393
+ const ui = new AlgoUI({
1394
+ subtitle: `MULTI-SYMBOL (${contracts.length})`,
1395
+ mode: 'one-account'
1396
+ });
1397
+
1398
+ // Shared stats across all symbols
1399
+ const stats = {
1400
+ accountName,
1401
+ symbol: symbolsStr,
1402
+ qty,
1403
+ target: dailyTarget,
1404
+ risk: maxRisk,
1405
+ propfirm: account.propfirm || 'Unknown',
1406
+ platform: connectionType,
1407
+ pnl: null,
1408
+ openPnl: null,
1409
+ closedPnl: null,
1410
+ balance: null,
1411
+ buyingPower: null,
1412
+ margin: null,
1413
+ netLiquidation: null,
1414
+ position: 0,
1415
+ entryPrice: 0,
1416
+ lastPrice: 0,
1417
+ trades: 0,
1418
+ wins: 0,
1419
+ losses: 0,
1420
+ sessionPnl: 0,
1421
+ latency: 0,
1422
+ connected: false,
1423
+ startTime: Date.now(),
1424
+ aiSupervision: config.enableAI || false,
1425
+ aiMode: null,
1426
+ agentCount: 0,
1427
+ fastPath: useFastPath,
1428
+ avgEntryLatency: 0,
1429
+ avgFillLatency: 0,
1430
+ entryLatencies: [],
1431
+ // Multi-symbol specific
1432
+ activeSymbols: contracts.length,
1433
+ symbolStats: {}, // Per-symbol stats
1434
+ };
1435
+
1436
+ // Initialize per-symbol stats
1437
+ contracts.forEach(c => {
1438
+ const name = c.name || c.symbol;
1439
+ stats.symbolStats[name] = {
1440
+ position: 0,
1441
+ trades: 0,
1442
+ wins: 0,
1443
+ losses: 0,
1444
+ pnl: 0,
1445
+ };
1446
+ });
1447
+
1448
+ let running = true;
1449
+ let stopReason = null;
1450
+
1451
+ // Track market feeds and position managers for cleanup
1452
+ const marketFeeds = [];
1453
+ const positionManagers = [];
1454
+
1455
+ ui.render(stats);
1456
+ ui.addLog('info', `MULTI-SYMBOL MODE: ${contracts.length} symbols`);
1457
+ symbolNames.forEach(name => ui.addLog('info', ` → ${name}`));
1458
+
1459
+ // Launch trading for each symbol
1460
+ const symbolPromises = contracts.map(async (contract, index) => {
1461
+ const symbolName = contract.name || contract.symbol;
1462
+ const contractId = contract.id || contract.symbol || contract.name;
1463
+ const tickSize = contract.tickSize ?? null;
1464
+ const tickValue = contract.tickValue ?? null;
1465
+
1466
+ // Initialize strategy for this symbol
1467
+ const strategy = (USE_HFT_STRATEGY && useFastPath) ? hftStrategy : M1;
1468
+ if (tickSize !== null && tickValue !== null) {
1469
+ strategy.initialize(contractId, tickSize, tickValue);
1470
+ }
1471
+
1472
+ // Initialize Position Manager for fast path
1473
+ let positionManager = null;
1474
+ if (useFastPath) {
1475
+ positionManager = new PositionManager(service, strategy);
1476
+ if (tickSize !== null && tickValue !== null) {
1477
+ positionManager.setContractInfo(symbolName, { tickSize, tickValue, contractId });
1478
+ }
1479
+ positionManager.start();
1480
+ positionManagers.push(positionManager);
1481
+
1482
+ // Position manager events
1483
+ positionManager.on('entryFilled', ({ position, fillLatencyMs }) => {
1484
+ stats.entryLatencies.push(fillLatencyMs);
1485
+ stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
1486
+ const side = position.side === 0 ? 'LONG' : 'SHORT';
1487
+ ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${position.entryPrice} | ${fillLatencyMs}ms`);
1488
+ });
1489
+
1490
+ positionManager.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
1491
+ const holdSec = (holdDurationMs / 1000).toFixed(1);
1492
+ if (pnlTicks !== null && tickValue !== null) {
1493
+ const pnlDollars = pnlTicks * tickValue;
1494
+ stats.sessionPnl += pnlDollars;
1495
+ stats.symbolStats[symbolName].pnl += pnlDollars;
1496
+
1497
+ if (pnlDollars >= 0) {
1498
+ stats.wins++;
1499
+ stats.symbolStats[symbolName].wins++;
1500
+ ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
1501
+ } else {
1502
+ stats.losses++;
1503
+ stats.symbolStats[symbolName].losses++;
1504
+ ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
1505
+ }
1506
+ stats.trades++;
1507
+ stats.symbolStats[symbolName].trades++;
1508
+ }
1509
+ ui.render(stats);
1510
+ });
1511
+ }
1512
+
1513
+ // Connect market feed for this symbol
1514
+ const MarketFeedClass = useFastPath ? RithmicMarketDataFeed : MarketDataFeed;
1515
+ const marketFeed = new MarketFeedClass(service);
1516
+ marketFeeds.push(marketFeed);
1517
+
1518
+ try {
1519
+ await marketFeed.connect(contractId, account.accountId);
1520
+ ui.addLog('success', `[${symbolName}] Market feed connected`);
1521
+ stats.connected = true;
1522
+
1523
+ // Handle market data ticks
1524
+ marketFeed.on('tick', (tick) => {
1525
+ if (!running) return;
1526
+
1527
+ stats.latency = tick.latency || 0;
1528
+ stats.lastPrice = tick.price || tick.last || 0;
1529
+
1530
+ // Get strategy signal
1531
+ const signal = strategy.onTick ? strategy.onTick(tick) : strategy.analyze?.(tick);
1532
+
1533
+ if (signal && signal.action && signal.action !== 'hold') {
1534
+ if (useFastPath && positionManager) {
1535
+ // Fast path execution
1536
+ if (signal.action === 'buy' || signal.action === 'long') {
1537
+ positionManager.openPosition(symbolName, contract.exchange || 'CME', 0, qty, account.accountId);
1538
+ } else if (signal.action === 'sell' || signal.action === 'short') {
1539
+ positionManager.openPosition(symbolName, contract.exchange || 'CME', 1, qty, account.accountId);
1540
+ } else if (signal.action === 'close' || signal.action === 'exit') {
1541
+ positionManager.closePosition(symbolName);
1542
+ }
1543
+ }
1544
+ }
1545
+
1546
+ ui.render(stats);
1547
+ });
1548
+
1549
+ } catch (err) {
1550
+ ui.addLog('error', `[${symbolName}] Failed to connect: ${err.message}`);
1551
+ }
1552
+ });
1553
+
1554
+ // Wait for all symbols to initialize
1555
+ await Promise.all(symbolPromises);
1556
+
1557
+ // Keyboard handler for emergency stop
1558
+ let emergencyStopInProgress = false;
1559
+
1560
+ const setupKeyHandler = () => {
1561
+ if (!process.stdin.isTTY) return;
1562
+ readline.emitKeypressEvents(process.stdin);
1563
+ process.stdin.setRawMode(true);
1564
+ process.stdin.resume();
1565
+
1566
+ const onKey = async (str, key) => {
1567
+ if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
1568
+ if (emergencyStopInProgress) return;
1569
+ emergencyStopInProgress = true;
1570
+ running = false;
1571
+ stopReason = 'manual';
1572
+
1573
+ ui.addLog('warning', '████ EMERGENCY STOP - CLOSING ALL POSITIONS ████');
1574
+
1575
+ // Close all positions on all symbols
1576
+ for (const pm of positionManagers) {
1577
+ try {
1578
+ pm.closeAllPositions();
1579
+ } catch (e) {}
1580
+ }
1581
+
1582
+ ui.addLog('info', 'All positions closed');
1583
+ ui.render(stats);
1584
+ }
1585
+ };
1586
+ process.stdin.on('keypress', onKey);
1587
+ return () => {
1588
+ process.stdin.removeListener('keypress', onKey);
1589
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1590
+ };
1591
+ };
1592
+
1593
+ const cleanupKeys = setupKeyHandler();
1594
+
1595
+ // Wait for stop
1596
+ await new Promise(resolve => {
1597
+ const check = setInterval(() => {
1598
+ if (!running) {
1599
+ clearInterval(check);
1600
+ resolve();
1601
+ }
1602
+ }, 100);
1603
+ });
1604
+
1605
+ // Cleanup
1606
+ for (const pm of positionManagers) {
1607
+ try { pm.stop(); } catch {}
1608
+ }
1609
+
1610
+ for (const feed of marketFeeds) {
1611
+ try { await feed.disconnect(); } catch {}
1612
+ }
1613
+
1614
+ try { if (cleanupKeys) cleanupKeys(); } catch {}
1615
+ try { ui.cleanup(); } catch {}
1616
+
1617
+ try {
1618
+ if (process.stdin.isTTY) {
1619
+ process.stdin.setRawMode(false);
1620
+ }
1621
+ process.stdin.resume();
1622
+ } catch {}
1623
+
1624
+ // Duration
1625
+ const durationMs = Date.now() - stats.startTime;
1626
+ const hours = Math.floor(durationMs / 3600000);
1627
+ const minutes = Math.floor((durationMs % 3600000) / 60000);
1628
+ const seconds = Math.floor((durationMs % 60000) / 1000);
1629
+ stats.duration = hours > 0
1630
+ ? `${hours}h ${minutes}m ${seconds}s`
1631
+ : minutes > 0
1632
+ ? `${minutes}m ${seconds}s`
1633
+ : `${seconds}s`;
1634
+
1635
+ // Summary with per-symbol breakdown
1636
+ console.log();
1637
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1638
+ console.log(chalk.cyan(' MULTI-SYMBOL SESSION SUMMARY'));
1639
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1640
+ console.log();
1641
+
1642
+ for (const [symbol, symStats] of Object.entries(stats.symbolStats)) {
1643
+ const winRate = symStats.trades > 0 ? ((symStats.wins / symStats.trades) * 100).toFixed(0) : 0;
1644
+ const pnlColor = symStats.pnl >= 0 ? chalk.green : chalk.red;
1645
+ console.log(chalk.white(` ${symbol}:`));
1646
+ console.log(` Trades: ${symStats.trades} | WR: ${winRate}% | P&L: ${pnlColor('$' + symStats.pnl.toFixed(2))}`);
1647
+ }
1648
+
1649
+ console.log();
1650
+ console.log(chalk.white(` TOTAL: ${stats.trades} trades | ${stats.wins}W/${stats.losses}L | Session P&L: ${stats.sessionPnl >= 0 ? chalk.green('$' + stats.sessionPnl.toFixed(2)) : chalk.red('$' + stats.sessionPnl.toFixed(2))}`));
1651
+ console.log(chalk.white(` Duration: ${stats.duration}`));
1652
+ console.log();
1653
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1654
+
1655
+ await prompts.waitForEnter();
1656
+ };
1657
+
1286
1658
  module.exports = { oneAccountMenu };
@@ -529,8 +529,10 @@ const submitCallback = async (callbackUrl, provider = 'anthropic') => {
529
529
  const config = providerConfig[provider] || providerConfig.anthropic;
530
530
 
531
531
  // Submit to the provider's OAuth callback port directly
532
+ // Pass ALL query parameters from the callback URL, not just code and state
533
+ // Some providers (like Gemini) require additional params like scope, authuser, etc.
532
534
  return new Promise((resolve, reject) => {
533
- const callbackPath = `${config.path}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
535
+ const callbackPath = `${config.path}?${url.searchParams.toString()}`;
534
536
 
535
537
  const req = http.request({
536
538
  hostname: '127.0.0.1',