hedgequantx 2.6.126 → 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.126",
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,32 +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 symbols (up to MAX_SYMBOLS)
82
- const contracts = await selectSymbols(accountService, selectedAccount);
83
- if (!contracts || contracts.length === 0) 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;
84
106
 
85
- // Configure algo (shared config for all symbols)
86
- const config = await configureAlgo(selectedAccount, contracts);
107
+ // Normalize to array
108
+ const contractList = Array.isArray(contracts) ? contracts : [contracts];
109
+
110
+ // Configure algo
111
+ const config = await configureAlgo(selectedAccount, contractList);
87
112
  if (!config) return;
88
113
 
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);
114
+ // Launch appropriate algo
115
+ if (contractList.length === 1) {
116
+ await launchAlgo(accountService, selectedAccount, contractList[0], config);
93
117
  } else {
94
- // Multiple symbols - launch parallel trading
95
- await launchMultiAlgo(accountService, selectedAccount, contracts, config);
118
+ await launchMultiSymbolRithmic(accountService, selectedAccount, contractList, config);
96
119
  }
97
120
  };
98
121
 
99
- // Maximum number of symbols to trade simultaneously
100
- const MAX_SYMBOLS = 5;
122
+ // Maximum symbols for multi-symbol trading
123
+ const MAX_MULTI_SYMBOLS = 5;
101
124
 
102
125
  /**
103
126
  * Symbol selection - sorted with popular indices first
104
- * Allows selecting multiple symbols (up to MAX_SYMBOLS)
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
105
132
  */
106
- const selectSymbols = async (service, account) => {
133
+ const selectSymbol = async (service, account, allowMultiple = false) => {
107
134
  const spinner = ora({ text: 'LOADING SYMBOLS...', color: 'yellow' }).start();
108
135
 
109
136
  const contractsResult = await service.getContracts();
@@ -137,64 +164,71 @@ const selectSymbols = async (service, account) => {
137
164
 
138
165
  spinner.succeed(`FOUND ${contracts.length} CONTRACTS`);
139
166
 
140
- // Multi-symbol selection loop
167
+ // Display sorted contracts from API (uniform format: NAME - DESCRIPTION)
168
+ const options = contracts.map(c => {
169
+ const name = c.name || c.symbol || c.baseSymbol;
170
+ const desc = c.description || '';
171
+ const label = desc ? `${name} - ${desc}` : name;
172
+ return { label, value: c };
173
+ });
174
+
175
+ options.push({ label: chalk.gray('< BACK'), value: 'back' });
176
+
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
141
184
  const selectedContracts = [];
142
185
 
143
- while (selectedContracts.length < MAX_SYMBOLS) {
186
+ while (selectedContracts.length < MAX_MULTI_SYMBOLS) {
144
187
  console.log();
188
+
189
+ // Show already selected symbols
145
190
  if (selectedContracts.length > 0) {
146
- console.log(chalk.cyan(` SELECTED (${selectedContracts.length}/${MAX_SYMBOLS}):`));
191
+ console.log(chalk.cyan(` SELECTED (${selectedContracts.length}/${MAX_MULTI_SYMBOLS}):`));
147
192
  selectedContracts.forEach((c, i) => {
148
- const name = c.name || c.symbol || c.baseSymbol;
193
+ const name = c.name || c.symbol;
149
194
  console.log(chalk.green(` ${i + 1}. ${name}`));
150
195
  });
151
196
  console.log();
152
197
  }
153
198
 
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);
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);
158
204
  });
159
205
 
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
206
+ // Add DONE option if at least 1 selected
169
207
  if (selectedContracts.length > 0) {
170
- options.unshift({ label: chalk.green(`✓ DONE (trade ${selectedContracts.length} symbol${selectedContracts.length > 1 ? 's' : ''})`), value: 'done' });
208
+ availableOptions.unshift({
209
+ label: chalk.green(`✓ DONE - Start trading ${selectedContracts.length} symbol${selectedContracts.length > 1 ? 's' : ''}`),
210
+ value: 'done'
211
+ });
171
212
  }
172
- options.push({ label: chalk.gray('< BACK'), value: 'back' });
173
213
 
174
214
  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:`);
215
+ ? chalk.yellow(`SELECT SYMBOL (1/${MAX_MULTI_SYMBOLS}):`)
216
+ : chalk.yellow(`ADD SYMBOL (${selectedContracts.length + 1}/${MAX_MULTI_SYMBOLS}) OR DONE:`);
177
217
 
178
- const choice = await prompts.selectOption(promptText, options);
218
+ const choice = await prompts.selectOption(promptText, availableOptions);
179
219
 
180
- if (choice === 'back' || choice === null) {
181
- if (selectedContracts.length === 0) {
182
- return null; // Go back
183
- }
184
- // Remove last selected
185
- selectedContracts.pop();
220
+ if (choice === null || choice === 'back') {
221
+ if (selectedContracts.length === 0) return null;
222
+ selectedContracts.pop(); // Remove last
186
223
  continue;
187
224
  }
188
225
 
189
- if (choice === 'done') {
190
- break; // Done selecting
191
- }
226
+ if (choice === 'done') break;
192
227
 
193
- // Add selected contract
194
228
  selectedContracts.push(choice);
195
229
 
196
- if (selectedContracts.length >= MAX_SYMBOLS) {
197
- console.log(chalk.yellow(` MAX ${MAX_SYMBOLS} SYMBOLS REACHED`));
230
+ if (selectedContracts.length >= MAX_MULTI_SYMBOLS) {
231
+ console.log(chalk.yellow(` Maximum ${MAX_MULTI_SYMBOLS} symbols reached`));
198
232
  break;
199
233
  }
200
234
  }
@@ -202,36 +236,25 @@ const selectSymbols = async (service, account) => {
202
236
  return selectedContracts.length > 0 ? selectedContracts : null;
203
237
  };
204
238
 
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;
211
- };
212
-
213
239
  /**
214
240
  * Configure algo
215
241
  * @param {Object} account - Trading account
216
- * @param {Array|Object} contractsOrContract - Single contract or array of contracts
242
+ * @param {Object|Array} contractOrContracts - Single contract or array of contracts
217
243
  */
218
- const configureAlgo = async (account, contractsOrContract) => {
219
- // Normalize to array
220
- const contractList = Array.isArray(contractsOrContract) ? contractsOrContract : [contractsOrContract];
244
+ const configureAlgo = async (account, contractOrContracts) => {
245
+ const contractList = Array.isArray(contractOrContracts) ? contractOrContracts : [contractOrContracts];
221
246
 
222
247
  console.log();
223
248
  console.log(chalk.cyan(' CONFIGURE ALGO PARAMETERS'));
224
- console.log();
225
249
 
226
250
  // Show selected symbols
227
251
  if (contractList.length > 1) {
228
- console.log(chalk.white(` TRADING ${contractList.length} SYMBOLS:`));
252
+ console.log(chalk.white(` Trading ${contractList.length} symbols:`));
229
253
  contractList.forEach((c, i) => {
230
- const name = c.name || c.symbol || c.baseSymbol;
231
- console.log(chalk.yellow(` ${i + 1}. ${name}`));
254
+ console.log(chalk.yellow(` ${i + 1}. ${c.name || c.symbol}`));
232
255
  });
233
- console.log();
234
256
  }
257
+ console.log();
235
258
 
236
259
  const contracts = await prompts.numberInput('NUMBER OF CONTRACTS:', 1, 1, 10);
237
260
  if (contracts === null) return null;
@@ -282,17 +305,6 @@ const configureAlgo = async (account, contractsOrContract) => {
282
305
  return { contracts, dailyTarget, maxRisk, showName, enableAI };
283
306
  };
284
307
 
285
- /**
286
- * Check if service supports fast path (Rithmic direct)
287
- * @param {Object} service - Trading service
288
- * @returns {boolean}
289
- */
290
- const isRithmicFastPath = (service) => {
291
- return typeof service.fastEntry === 'function' &&
292
- typeof service.fastExit === 'function' &&
293
- service.orderConn?.isConnected;
294
- };
295
-
296
308
  /**
297
309
  * Launch algo trading - HQX Ultra Scalping Strategy
298
310
  * Real-time market data + Strategy signals + Auto order execution
@@ -1369,51 +1381,46 @@ const launchAlgo = async (service, account, contract, config) => {
1369
1381
  };
1370
1382
 
1371
1383
  /**
1372
- * Launch multi-symbol algo trading
1373
- * Runs multiple symbols in parallel on the same account
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
1374
1387
  *
1375
- * @param {Object} service - Trading service
1388
+ * @param {Object} service - Rithmic trading service
1376
1389
  * @param {Object} account - Trading account
1377
1390
  * @param {Array} contracts - Array of contracts to trade
1378
1391
  * @param {Object} config - Algo configuration
1379
1392
  */
1380
- const launchMultiAlgo = async (service, account, contracts, config) => {
1381
- const { contracts: qty, dailyTarget, maxRisk, showName } = config;
1393
+ const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
1394
+ const { contracts: qty, dailyTarget, maxRisk, showName, enableAI } = config;
1382
1395
 
1383
1396
  const accountName = showName
1384
1397
  ? (account.accountName || account.rithmicAccountId || account.accountId)
1385
1398
  : 'HQX *****';
1386
- const connectionType = account.platform || 'ProjectX';
1387
- const useFastPath = isRithmicFastPath(service);
1399
+ const rithmicAccountId = account.rithmicAccountId || account.accountId;
1388
1400
 
1389
- // Build symbol list string
1390
- const symbolNames = contracts.map(c => c.name || c.symbol || c.baseSymbol);
1391
- const symbolsStr = symbolNames.join(', ');
1401
+ // Build symbols string for UI
1402
+ const symbolNames = contracts.map(c => c.name || c.symbol);
1403
+ const symbolsDisplay = symbolNames.join(', ');
1392
1404
 
1393
1405
  const ui = new AlgoUI({
1394
1406
  subtitle: `MULTI-SYMBOL (${contracts.length})`,
1395
1407
  mode: 'one-account'
1396
1408
  });
1397
1409
 
1398
- // Shared stats across all symbols
1410
+ // Shared stats
1399
1411
  const stats = {
1400
1412
  accountName,
1401
- symbol: symbolsStr,
1413
+ symbol: symbolsDisplay,
1402
1414
  qty,
1403
1415
  target: dailyTarget,
1404
1416
  risk: maxRisk,
1405
1417
  propfirm: account.propfirm || 'Unknown',
1406
- platform: connectionType,
1418
+ platform: 'RITHMIC',
1407
1419
  pnl: null,
1408
1420
  openPnl: null,
1409
1421
  closedPnl: null,
1410
1422
  balance: null,
1411
- buyingPower: null,
1412
- margin: null,
1413
- netLiquidation: null,
1414
1423
  position: 0,
1415
- entryPrice: 0,
1416
- lastPrice: 0,
1417
1424
  trades: 0,
1418
1425
  wins: 0,
1419
1426
  losses: 0,
@@ -1421,140 +1428,217 @@ const launchMultiAlgo = async (service, account, contracts, config) => {
1421
1428
  latency: 0,
1422
1429
  connected: false,
1423
1430
  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
1431
+ aiSupervision: enableAI || false,
1432
+ fastPath: true,
1433
+ // Per-symbol tracking
1434
+ symbolStats: {},
1434
1435
  };
1435
1436
 
1436
1437
  // Initialize per-symbol stats
1437
1438
  contracts.forEach(c => {
1438
1439
  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
- };
1440
+ stats.symbolStats[name] = { position: 0, trades: 0, wins: 0, losses: 0, pnl: 0 };
1446
1441
  });
1447
1442
 
1448
1443
  let running = true;
1449
1444
  let stopReason = null;
1450
1445
 
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}`));
1446
+ // ═══════════════════════════════════════════════════════════════════════════
1447
+ // POSITION MANAGERS - One per symbol
1448
+ // ═══════════════════════════════════════════════════════════════════════════
1449
+ const positionManagers = {};
1450
+ const strategies = {};
1458
1451
 
1459
- // Launch trading for each symbol
1460
- const symbolPromises = contracts.map(async (contract, index) => {
1452
+ contracts.forEach(contract => {
1461
1453
  const symbolName = contract.name || contract.symbol;
1462
1454
  const contractId = contract.id || contract.symbol || contract.name;
1463
1455
  const tickSize = contract.tickSize ?? null;
1464
1456
  const tickValue = contract.tickValue ?? null;
1465
1457
 
1466
- // Initialize strategy for this symbol
1467
- const strategy = (USE_HFT_STRATEGY && useFastPath) ? hftStrategy : M1;
1458
+ // Create strategy instance for this symbol
1459
+ const strategy = Object.create(hftStrategy);
1468
1460
  if (tickSize !== null && tickValue !== null) {
1469
1461
  strategy.initialize(contractId, tickSize, tickValue);
1470
1462
  }
1463
+ strategies[symbolName] = strategy;
1471
1464
 
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
- });
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 });
1511
1469
  }
1470
+ pm.start();
1471
+ positionManagers[symbolName] = pm;
1512
1472
 
1513
- // Connect market feed for this symbol
1514
- const MarketFeedClass = useFastPath ? RithmicMarketDataFeed : MarketDataFeed;
1515
- const marketFeed = new MarketFeedClass(service);
1516
- marketFeeds.push(marketFeed);
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
+ });
1517
1479
 
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);
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++;
1532
1488
 
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
- }
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`);
1544
1497
  }
1545
-
1546
- ui.render(stats);
1547
- });
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
+ };
1548
1521
 
1549
- } catch (err) {
1550
- ui.addLog('error', `[${symbolName}] Failed to connect: ${err.message}`);
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
+ }
1551
1572
  }
1573
+
1574
+ ui.render(stats);
1575
+ });
1576
+
1577
+ marketFeed.on('error', (err) => {
1578
+ ui.addLog('error', `Feed error: ${err.message}`);
1552
1579
  });
1553
1580
 
1554
- // Wait for all symbols to initialize
1555
- await Promise.all(symbolPromises);
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
+ }
1556
1618
 
1557
- // Keyboard handler for emergency stop
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
1558
1642
  let emergencyStopInProgress = false;
1559
1643
 
1560
1644
  const setupKeyHandler = () => {
@@ -1569,18 +1653,7 @@ const launchMultiAlgo = async (service, account, contracts, config) => {
1569
1653
  emergencyStopInProgress = true;
1570
1654
  running = false;
1571
1655
  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);
1656
+ await emergencyStopAll();
1584
1657
  }
1585
1658
  };
1586
1659
  process.stdin.on('keypress', onKey);
@@ -1592,6 +1665,11 @@ const launchMultiAlgo = async (service, account, contracts, config) => {
1592
1665
 
1593
1666
  const cleanupKeys = setupKeyHandler();
1594
1667
 
1668
+ // UI refresh
1669
+ const refreshInterval = setInterval(() => {
1670
+ if (running) ui.render(stats);
1671
+ }, 1000);
1672
+
1595
1673
  // Wait for stop
1596
1674
  await new Promise(resolve => {
1597
1675
  const check = setInterval(() => {
@@ -1602,40 +1680,44 @@ const launchMultiAlgo = async (service, account, contracts, config) => {
1602
1680
  }, 100);
1603
1681
  });
1604
1682
 
1605
- // Cleanup
1606
- for (const pm of positionManagers) {
1683
+ // ═══════════════════════════════════════════════════════════════════════════
1684
+ // CLEANUP
1685
+ // ═══════════════════════════════════════════════════════════════════════════
1686
+ clearInterval(refreshInterval);
1687
+
1688
+ // Stop all position managers
1689
+ for (const pm of Object.values(positionManagers)) {
1607
1690
  try { pm.stop(); } catch {}
1608
1691
  }
1609
1692
 
1610
- for (const feed of marketFeeds) {
1611
- try { await feed.disconnect(); } catch {}
1612
- }
1693
+ // Disconnect market feed
1694
+ try { await marketFeed.disconnect(); } catch {}
1613
1695
 
1696
+ // Cleanup keyboard
1614
1697
  try { if (cleanupKeys) cleanupKeys(); } catch {}
1615
1698
  try { ui.cleanup(); } catch {}
1616
1699
 
1617
1700
  try {
1618
- if (process.stdin.isTTY) {
1619
- process.stdin.setRawMode(false);
1620
- }
1701
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1621
1702
  process.stdin.resume();
1622
1703
  } catch {}
1623
1704
 
1624
- // Duration
1705
+ // ═══════════════════════════════════════════════════════════════════════════
1706
+ // SESSION SUMMARY
1707
+ // ═══════════════════════════════════════════════════════════════════════════
1625
1708
  const durationMs = Date.now() - stats.startTime;
1626
1709
  const hours = Math.floor(durationMs / 3600000);
1627
1710
  const minutes = Math.floor((durationMs % 3600000) / 60000);
1628
1711
  const seconds = Math.floor((durationMs % 60000) / 1000);
1629
- stats.duration = hours > 0
1712
+ const duration = hours > 0
1630
1713
  ? `${hours}h ${minutes}m ${seconds}s`
1631
1714
  : minutes > 0
1632
1715
  ? `${minutes}m ${seconds}s`
1633
1716
  : `${seconds}s`;
1634
1717
 
1635
- // Summary with per-symbol breakdown
1636
1718
  console.log();
1637
1719
  console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1638
- console.log(chalk.cyan(' MULTI-SYMBOL SESSION SUMMARY'));
1720
+ console.log(chalk.cyan(' MULTI-SYMBOL SESSION SUMMARY'));
1639
1721
  console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1640
1722
  console.log();
1641
1723
 
@@ -1647,8 +1729,10 @@ const launchMultiAlgo = async (service, account, contracts, config) => {
1647
1729
  }
1648
1730
 
1649
1731
  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}`));
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}`));
1652
1736
  console.log();
1653
1737
  console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
1654
1738