hedgequantx 2.9.136 → 2.9.138

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.9.136",
3
+ "version": "2.9.138",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Multi-Symbol Executor - Trade up to 5 symbols in parallel
3
+ * Single TICKER_PLANT connection, multiple strategy instances
4
+ */
5
+ const readline = require('readline');
6
+ const { AlgoUI } = require('./ui');
7
+ const { loadStrategy } = require('../../lib/m');
8
+ const { MarketDataFeed } = require('../../lib/data');
9
+ const smartLogs = require('../../lib/smart-logs');
10
+ const { sessionLogger } = require('../../services/session-logger');
11
+
12
+ /**
13
+ * Execute algo strategy on multiple symbols
14
+ * @param {Object} params - Execution parameters
15
+ * @param {Object} params.service - Rithmic trading service
16
+ * @param {Object} params.account - Account object
17
+ * @param {Array} params.contracts - Array of contract objects (max 5)
18
+ * @param {Object} params.config - Algo config (contracts, target, risk, showName)
19
+ * @param {Object} params.strategy - Strategy info object with id, name
20
+ * @param {Object} params.options - Optional: supervisionConfig, startSpinner
21
+ */
22
+ const executeMultiSymbol = async ({ service, account, contracts, config, strategy: strategyInfo, options = {} }) => {
23
+ const { contractsPerSymbol, dailyTarget, maxRisk, showName } = config;
24
+ const { startSpinner } = options;
25
+
26
+ const strategyId = strategyInfo?.id || 'hqx-2b';
27
+ const strategyName = strategyInfo?.name || 'HQX-2B';
28
+ const strategyModule = loadStrategy(strategyId);
29
+ const StrategyClass = strategyModule.M1;
30
+
31
+ const accountName = showName
32
+ ? (account.accountName || account.rithmicAccountId || account.accountId)
33
+ : 'HQX *****';
34
+
35
+ // Create strategy instance and stats for each symbol
36
+ const symbolData = new Map();
37
+
38
+ for (const contract of contracts) {
39
+ const symbolCode = contract.symbol || contract.baseSymbol || contract.id;
40
+ const tickSize = contract.tickSize || 0.25;
41
+
42
+ const strategy = new StrategyClass({ tickSize });
43
+ strategy.initialize(symbolCode, tickSize);
44
+
45
+ symbolData.set(symbolCode, {
46
+ contract,
47
+ strategy,
48
+ tickSize,
49
+ symbolCode,
50
+ symbolName: contract.name || contract.baseSymbol || symbolCode,
51
+ stats: {
52
+ pnl: 0,
53
+ trades: 0,
54
+ wins: 0,
55
+ losses: 0,
56
+ tickCount: 0,
57
+ lastPrice: null,
58
+ position: 0
59
+ },
60
+ pendingOrder: false,
61
+ startingPnL: null
62
+ });
63
+
64
+ // Forward signals
65
+ strategy.on('signal', async (signal) => {
66
+ await handleSignal(symbolCode, signal);
67
+ });
68
+
69
+ strategy.on('log', (log) => {
70
+ const prefix = `[${symbolCode}] `;
71
+ ui.addLog(log.type === 'debug' ? 'debug' : 'analysis', prefix + log.message);
72
+ });
73
+ }
74
+
75
+ // Aggregated stats
76
+ const globalStats = {
77
+ accountName,
78
+ symbols: contracts.map(c => c.baseSymbol || c.symbol).join(', '),
79
+ symbolCount: contracts.length,
80
+ qty: contractsPerSymbol,
81
+ target: dailyTarget,
82
+ risk: maxRisk,
83
+ pnl: 0,
84
+ trades: 0,
85
+ wins: 0,
86
+ losses: 0,
87
+ latency: 0,
88
+ connected: false,
89
+ startTime: Date.now()
90
+ };
91
+
92
+ let running = true;
93
+ let stopReason = null;
94
+
95
+ // UI setup
96
+ const ui = new AlgoUI({
97
+ subtitle: `${strategyName} - ${contracts.length} Symbols`,
98
+ mode: 'multi-symbol'
99
+ });
100
+
101
+ smartLogs.setStrategy(strategyId);
102
+
103
+ // Start session logger
104
+ const logFile = sessionLogger.start({
105
+ strategy: strategyId,
106
+ account: accountName,
107
+ symbol: globalStats.symbols,
108
+ contracts: contractsPerSymbol,
109
+ target: dailyTarget,
110
+ risk: maxRisk,
111
+ multiSymbol: true,
112
+ symbolCount: contracts.length
113
+ });
114
+
115
+ sessionLogger.log('CONFIG', `Multi-symbol mode: ${contracts.length} symbols`);
116
+ for (const contract of contracts) {
117
+ sessionLogger.log('CONFIG', `Symbol: ${contract.symbol} exchange=${contract.exchange} tickSize=${contract.tickSize}`);
118
+ }
119
+
120
+ if (startSpinner) startSpinner.succeed('Multi-symbol algo initialized');
121
+
122
+ ui.addLog('system', `Strategy: ${strategyName} | Symbols: ${contracts.length}`);
123
+ ui.addLog('system', `Account: ${accountName}`);
124
+ for (const contract of contracts) {
125
+ ui.addLog('system', ` ${contract.symbol} - ${contract.name || contract.baseSymbol}`);
126
+ }
127
+ ui.addLog('risk', `Target: $${dailyTarget} | Risk: $${maxRisk} | Qty: ${contractsPerSymbol}/symbol`);
128
+ ui.addLog('system', 'Connecting to market data...');
129
+
130
+ // Signal handler
131
+ const handleSignal = async (symbolCode, signal) => {
132
+ const data = symbolData.get(symbolCode);
133
+ if (!data) return;
134
+
135
+ const dir = signal.direction?.toUpperCase() || 'UNKNOWN';
136
+ ui.addLog('signal', `[${symbolCode}] ${dir} @ ${signal.entry?.toFixed(2)} | Conf: ${((signal.confidence || 0) * 100).toFixed(0)}%`);
137
+ sessionLogger.signal(dir, signal.entry, signal.confidence, `[${symbolCode}]`);
138
+
139
+ if (!running || data.pendingOrder || data.stats.position !== 0) {
140
+ ui.addLog('risk', `[${symbolCode}] Signal blocked - ${!running ? 'stopped' : data.pendingOrder ? 'order pending' : 'position open'}`);
141
+ return;
142
+ }
143
+
144
+ // Place order
145
+ data.pendingOrder = true;
146
+ try {
147
+ const orderSide = signal.direction === 'long' ? 0 : 1;
148
+ const orderResult = await service.placeOrder({
149
+ accountId: account.rithmicAccountId || account.accountId,
150
+ symbol: symbolCode,
151
+ exchange: data.contract.exchange || 'CME',
152
+ type: 2,
153
+ side: orderSide,
154
+ size: contractsPerSymbol
155
+ });
156
+
157
+ if (orderResult.success) {
158
+ data.stats.position = signal.direction === 'long' ? contractsPerSymbol : -contractsPerSymbol;
159
+ data.stats.trades++;
160
+ globalStats.trades++;
161
+
162
+ ui.addLog('fill_' + (signal.direction === 'long' ? 'buy' : 'sell'),
163
+ `[${symbolCode}] ${dir} ${contractsPerSymbol}x @ ${signal.entry?.toFixed(2)}`);
164
+ sessionLogger.trade('ENTRY', dir, signal.entry, contractsPerSymbol, `[${symbolCode}]`);
165
+
166
+ // Bracket orders
167
+ if (signal.stopLoss && signal.takeProfit) {
168
+ await service.placeOrder({
169
+ accountId: account.rithmicAccountId || account.accountId,
170
+ symbol: symbolCode, exchange: data.contract.exchange || 'CME',
171
+ type: 4, side: signal.direction === 'long' ? 1 : 0,
172
+ size: contractsPerSymbol, price: signal.stopLoss
173
+ });
174
+ await service.placeOrder({
175
+ accountId: account.rithmicAccountId || account.accountId,
176
+ symbol: symbolCode, exchange: data.contract.exchange || 'CME',
177
+ type: 1, side: signal.direction === 'long' ? 1 : 0,
178
+ size: contractsPerSymbol, price: signal.takeProfit
179
+ });
180
+ ui.addLog('trade', `[${symbolCode}] SL: ${signal.stopLoss.toFixed(2)} | TP: ${signal.takeProfit.toFixed(2)}`);
181
+ }
182
+ } else {
183
+ ui.addLog('error', `[${symbolCode}] Order failed: ${orderResult.error}`);
184
+ sessionLogger.error(`[${symbolCode}] Order failed`, orderResult.error);
185
+ }
186
+ } catch (e) {
187
+ ui.addLog('error', `[${symbolCode}] Order error: ${e.message}`);
188
+ }
189
+ data.pendingOrder = false;
190
+ };
191
+
192
+ // Market data feed (single connection for all symbols)
193
+ const marketFeed = new MarketDataFeed();
194
+
195
+ let lastLogSecond = 0;
196
+
197
+ marketFeed.on('tick', (tick) => {
198
+ const symbolCode = tick.symbol || tick.contractId;
199
+ const data = symbolData.get(symbolCode);
200
+ if (!data) return;
201
+
202
+ data.stats.tickCount++;
203
+ const price = Number(tick.price) || Number(tick.tradePrice) || null;
204
+ const volume = Number(tick.volume) || Number(tick.size) || 1;
205
+
206
+ if (data.stats.tickCount === 1) {
207
+ ui.addLog('connected', `[${symbolCode}] First tick @ ${price?.toFixed(2) || 'N/A'}`);
208
+ }
209
+
210
+ data.stats.lastPrice = price;
211
+
212
+ // Process tick through strategy
213
+ if (price && price > 0) {
214
+ data.strategy.processTick({
215
+ contractId: symbolCode,
216
+ price,
217
+ bid: Number(tick.bid) || null,
218
+ ask: Number(tick.ask) || null,
219
+ volume,
220
+ side: tick.side || 'unknown',
221
+ timestamp: tick.timestamp || Date.now()
222
+ });
223
+ }
224
+
225
+ // Latency
226
+ if (tick.ssboe && tick.usecs !== undefined) {
227
+ const tickTimeMs = (tick.ssboe * 1000) + Math.floor(tick.usecs / 1000);
228
+ const latency = Date.now() - tickTimeMs;
229
+ if (latency >= 0 && latency < 5000) globalStats.latency = latency;
230
+ }
231
+ });
232
+
233
+ // Log aggregated stats periodically
234
+ const logInterval = setInterval(() => {
235
+ const now = Math.floor(Date.now() / 1000);
236
+ if (now - lastLogSecond >= 60) {
237
+ lastLogSecond = now;
238
+ let totalTicks = 0;
239
+ let totalBars = 0;
240
+ for (const [sym, data] of symbolData) {
241
+ totalTicks += data.stats.tickCount;
242
+ const state = data.strategy.getAnalysisState?.(sym, data.stats.lastPrice);
243
+ totalBars += state?.barsProcessed || 0;
244
+ }
245
+ ui.addLog('debug', `Total: ${totalTicks} ticks | ${totalBars} bars | ${contracts.length} symbols`);
246
+
247
+ // Log per-symbol state
248
+ for (const [sym, data] of symbolData) {
249
+ const state = data.strategy.getAnalysisState?.(sym, data.stats.lastPrice);
250
+ if (state?.ready) {
251
+ ui.addLog('analysis', `[${sym}] Zones: ${state.activeZones} | Swings: ${state.swingsDetected}`);
252
+ }
253
+ }
254
+ }
255
+ }, 5000);
256
+
257
+ marketFeed.on('connected', () => { globalStats.connected = true; ui.addLog('connected', 'Market data connected'); });
258
+ marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
259
+ marketFeed.on('disconnected', () => { globalStats.connected = false; ui.addLog('error', 'Market disconnected'); });
260
+
261
+ // Connect and subscribe to all symbols
262
+ try {
263
+ const rithmicCredentials = service.getRithmicCredentials?.();
264
+ if (!rithmicCredentials) throw new Error('Rithmic credentials not available');
265
+
266
+ if (service.disconnectTicker) await service.disconnectTicker();
267
+ await marketFeed.connect(rithmicCredentials);
268
+
269
+ for (const contract of contracts) {
270
+ const symbolCode = contract.symbol || contract.baseSymbol;
271
+ await marketFeed.subscribe(symbolCode, contract.exchange || 'CME');
272
+ ui.addLog('system', `Subscribed: ${symbolCode}`);
273
+ }
274
+ } catch (e) {
275
+ ui.addLog('error', `Failed to connect: ${e.message}`);
276
+ }
277
+
278
+ // P&L polling
279
+ const pollPnL = async () => {
280
+ try {
281
+ const accountResult = await service.getTradingAccounts();
282
+ if (accountResult.success && accountResult.accounts) {
283
+ const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
284
+ if (acc && acc.profitAndLoss !== undefined) {
285
+ // For multi-symbol, we track total P&L
286
+ globalStats.pnl = acc.profitAndLoss;
287
+ }
288
+ }
289
+
290
+ // Check positions per symbol
291
+ const posResult = await service.getPositions(account.accountId);
292
+ if (posResult.success && posResult.positions) {
293
+ for (const [sym, data] of symbolData) {
294
+ const pos = posResult.positions.find(p => (p.contractId || p.symbol || '').includes(sym));
295
+ data.stats.position = pos?.quantity || 0;
296
+ }
297
+ }
298
+
299
+ // Risk checks
300
+ if (globalStats.pnl >= dailyTarget) {
301
+ stopReason = 'target'; running = false;
302
+ ui.addLog('fill_win', `TARGET REACHED! +$${globalStats.pnl.toFixed(2)}`);
303
+ } else if (globalStats.pnl <= -maxRisk) {
304
+ stopReason = 'risk'; running = false;
305
+ ui.addLog('fill_loss', `MAX RISK! -$${Math.abs(globalStats.pnl).toFixed(2)}`);
306
+ }
307
+ } catch (e) { /* silent */ }
308
+ };
309
+
310
+ const refreshInterval = setInterval(() => { if (running) ui.render(globalStats); }, 100);
311
+ const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
312
+ pollPnL();
313
+
314
+ // Key handler
315
+ const setupKeyHandler = () => {
316
+ if (!process.stdin.isTTY) return null;
317
+ readline.emitKeypressEvents(process.stdin);
318
+ process.stdin.setRawMode(true);
319
+ process.stdin.resume();
320
+ const onKey = (str, key) => {
321
+ const keyName = key?.name?.toLowerCase();
322
+ if (keyName === 'x' || (key?.ctrl && keyName === 'c')) {
323
+ running = false;
324
+ stopReason = 'manual';
325
+ }
326
+ };
327
+ process.stdin.on('keypress', onKey);
328
+ return () => {
329
+ process.stdin.removeListener('keypress', onKey);
330
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
331
+ };
332
+ };
333
+ const cleanupKeys = setupKeyHandler();
334
+
335
+ // Wait for stop
336
+ await new Promise(resolve => {
337
+ const check = setInterval(() => { if (!running) { clearInterval(check); resolve(); } }, 100);
338
+ });
339
+
340
+ // Cleanup
341
+ clearInterval(refreshInterval);
342
+ clearInterval(pnlInterval);
343
+ clearInterval(logInterval);
344
+ await marketFeed.disconnect();
345
+ if (cleanupKeys) cleanupKeys();
346
+ ui.cleanup();
347
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
348
+ process.stdin.resume();
349
+
350
+ // Summary
351
+ const durationMs = Date.now() - globalStats.startTime;
352
+ const h = Math.floor(durationMs / 3600000);
353
+ const m = Math.floor((durationMs % 3600000) / 60000);
354
+ const s = Math.floor((durationMs % 60000) / 1000);
355
+ globalStats.duration = h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`;
356
+
357
+ sessionLogger.end(globalStats, stopReason?.toUpperCase() || 'MANUAL');
358
+
359
+ console.log('\n');
360
+ console.log(' Multi-Symbol Session Summary');
361
+ console.log(' ────────────────────────────');
362
+ console.log(` Symbols: ${globalStats.symbols}`);
363
+ console.log(` Duration: ${globalStats.duration}`);
364
+ console.log(` Trades: ${globalStats.trades} | P&L: $${globalStats.pnl.toFixed(2)}`);
365
+ console.log(` Stop: ${stopReason || 'manual'}`);
366
+ console.log();
367
+
368
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
369
+ await new Promise(resolve => {
370
+ rl.question(' Press Enter to return to menu...', () => { rl.close(); resolve(); });
371
+ });
372
+ };
373
+
374
+ module.exports = { executeMultiSymbol };
@@ -11,12 +11,27 @@ const { prompts } = require('../../utils');
11
11
  const { getContractDescription } = require('../../config');
12
12
  const { checkMarketHours } = require('../../services/rithmic/market');
13
13
  const { executeAlgo } = require('./algo-executor');
14
+ const { executeMultiSymbol } = require('./multi-symbol-executor');
14
15
  const { getActiveAgentCount, getSupervisionConfig, getActiveAgents } = require('../ai-agents');
15
16
  const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../../services/ai-supervision');
16
17
  const { getAvailableStrategies } = require('../../lib/m');
17
18
  const { getLastOneAccountConfig, saveOneAccountConfig } = require('../../services/algo-config');
18
19
 
20
+ // Popular symbols for sorting
21
+ const POPULAR_PREFIXES = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
19
22
 
23
+ const sortContracts = (contracts) => {
24
+ return contracts.sort((a, b) => {
25
+ const baseA = a.baseSymbol || a.symbol || '';
26
+ const baseB = b.baseSymbol || b.symbol || '';
27
+ const idxA = POPULAR_PREFIXES.findIndex(p => baseA === p || baseA.startsWith(p));
28
+ const idxB = POPULAR_PREFIXES.findIndex(p => baseB === p || baseB.startsWith(p));
29
+ if (idxA !== -1 && idxB !== -1) return idxA - idxB;
30
+ if (idxA !== -1) return -1;
31
+ if (idxB !== -1) return 1;
32
+ return baseA.localeCompare(baseB);
33
+ });
34
+ };
20
35
 
21
36
  /**
22
37
  * One Account Menu
@@ -135,6 +150,9 @@ const oneAccountMenu = async (service) => {
135
150
  }
136
151
 
137
152
  // If no saved config used, go through normal selection
153
+ let isMultiSymbol = false;
154
+ let multiContracts = [];
155
+
138
156
  if (!selectedAccount) {
139
157
  // Select account - display RAW API fields
140
158
  const options = activeAccounts.map(acc => {
@@ -156,32 +174,52 @@ const oneAccountMenu = async (service) => {
156
174
  // Use the service attached to the account (from getAllAccounts), fallback to getServiceForAccount
157
175
  accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
158
176
 
159
- // Select symbol
160
- contract = await selectSymbol(accountService, selectedAccount);
161
- if (!contract) return;
177
+ // Ask for trading mode
178
+ const modeOptions = [
179
+ { label: 'Single Symbol', value: 'single' },
180
+ { label: 'Multi-Symbol (up to 5)', value: 'multi' },
181
+ { label: chalk.gray('< Back'), value: 'back' }
182
+ ];
183
+ const mode = await prompts.selectOption('Trading Mode:', modeOptions);
184
+ if (mode === 'back' || !mode) return;
185
+
186
+ isMultiSymbol = mode === 'multi';
187
+
188
+ if (isMultiSymbol) {
189
+ // Multi-symbol selection
190
+ multiContracts = await selectMultipleSymbols(accountService, selectedAccount);
191
+ if (!multiContracts || multiContracts.length === 0) return;
192
+ contract = multiContracts[0]; // For strategy selection display
193
+ } else {
194
+ // Single symbol selection
195
+ contract = await selectSymbol(accountService, selectedAccount);
196
+ if (!contract) return;
197
+ }
162
198
 
163
199
  // Select strategy
164
200
  strategy = await selectStrategy();
165
201
  if (!strategy) return;
166
202
 
167
203
  // Configure algo
168
- config = await configureAlgo(selectedAccount, contract, strategy);
204
+ config = await configureAlgo(selectedAccount, contract, strategy, isMultiSymbol);
169
205
  if (!config) return;
170
206
 
171
- // Save config for next time (include baseSymbol for stable matching across contract rolls)
172
- saveOneAccountConfig({
173
- accountId: selectedAccount.accountId || selectedAccount.rithmicAccountId,
174
- accountName: selectedAccount.accountName || selectedAccount.rithmicAccountId || selectedAccount.accountId,
175
- propfirm: selectedAccount.propfirm || selectedAccount.platform || 'Unknown',
176
- symbol: contract.symbol,
177
- baseSymbol: contract.baseSymbol,
178
- strategyId: strategy.id,
179
- strategyName: strategy.name,
180
- contracts: config.contracts,
181
- dailyTarget: config.dailyTarget,
182
- maxRisk: config.maxRisk,
183
- showName: config.showName
184
- });
207
+ // Save config for next time (only for single symbol mode)
208
+ if (!isMultiSymbol) {
209
+ saveOneAccountConfig({
210
+ accountId: selectedAccount.accountId || selectedAccount.rithmicAccountId,
211
+ accountName: selectedAccount.accountName || selectedAccount.rithmicAccountId || selectedAccount.accountId,
212
+ propfirm: selectedAccount.propfirm || selectedAccount.platform || 'Unknown',
213
+ symbol: contract.symbol,
214
+ baseSymbol: contract.baseSymbol,
215
+ strategyId: strategy.id,
216
+ strategyName: strategy.name,
217
+ contracts: config.contracts,
218
+ dailyTarget: config.dailyTarget,
219
+ maxRisk: config.maxRisk,
220
+ showName: config.showName
221
+ });
222
+ }
185
223
  }
186
224
 
187
225
  // Check for AI Supervision BEFORE asking to start
@@ -235,20 +273,33 @@ const oneAccountMenu = async (service) => {
235
273
 
236
274
  const startSpinner = ora({ text: 'Initializing algo trading...', color: 'cyan' }).start();
237
275
 
238
- await executeAlgo({
239
- service: accountService,
240
- account: selectedAccount,
241
- contract,
242
- config,
243
- strategy,
244
- options: { supervisionConfig, startSpinner }
245
- });
276
+ if (isMultiSymbol && multiContracts.length > 0) {
277
+ // Multi-symbol execution
278
+ await executeMultiSymbol({
279
+ service: accountService,
280
+ account: selectedAccount,
281
+ contracts: multiContracts,
282
+ config,
283
+ strategy,
284
+ options: { supervisionConfig, startSpinner }
285
+ });
286
+ } else {
287
+ // Single symbol execution
288
+ await executeAlgo({
289
+ service: accountService,
290
+ account: selectedAccount,
291
+ contract,
292
+ config,
293
+ strategy,
294
+ options: { supervisionConfig, startSpinner }
295
+ });
296
+ }
246
297
  };
247
298
 
248
299
  /**
249
- * Symbol selection - sorted with popular indices first
300
+ * Multi-symbol selection - select up to 5 symbols
250
301
  */
251
- const selectSymbol = async (service, account) => {
302
+ const selectMultipleSymbols = async (service, account) => {
252
303
  const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
253
304
 
254
305
  // Ensure we have a logged-in service
@@ -267,29 +318,94 @@ const selectSymbol = async (service, account) => {
267
318
  return null;
268
319
  }
269
320
 
270
- let contracts = contractsResult.contracts;
321
+ const contracts = sortContracts(contractsResult.contracts);
322
+ spinner.succeed(`Found ${contracts.length} contracts`);
271
323
 
272
- // Sort: Popular indices first (ES, NQ, MES, MNQ, RTY, YM, etc.)
273
- const popularPrefixes = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
324
+ console.log();
325
+ console.log(chalk.cyan(' Select up to 5 symbols (one at a time)'));
326
+ console.log(chalk.gray(' Select "Done" when finished'));
327
+ console.log();
274
328
 
275
- contracts.sort((a, b) => {
276
- const baseA = a.baseSymbol || a.symbol || '';
277
- const baseB = b.baseSymbol || b.symbol || '';
329
+ const selectedContracts = [];
330
+ const maxSymbols = 5;
331
+
332
+ while (selectedContracts.length < maxSymbols) {
333
+ const remaining = maxSymbols - selectedContracts.length;
334
+ const selectedSymbols = selectedContracts.map(c => c.symbol);
278
335
 
279
- // Check if baseSymbol matches popular prefixes
280
- const idxA = popularPrefixes.findIndex(p => baseA === p || baseA.startsWith(p));
281
- const idxB = popularPrefixes.findIndex(p => baseB === p || baseB.startsWith(p));
336
+ // Filter out already selected
337
+ const availableContracts = contracts.filter(c => !selectedSymbols.includes(c.symbol));
282
338
 
283
- // Both are popular - sort by popularity order
284
- if (idxA !== -1 && idxB !== -1) return idxA - idxB;
285
- // Only A is popular - A first
286
- if (idxA !== -1) return -1;
287
- // Only B is popular - B first
288
- if (idxB !== -1) return 1;
289
- // Neither - alphabetical by baseSymbol
290
- return baseA.localeCompare(baseB);
291
- });
339
+ const options = availableContracts.map(c => {
340
+ const desc = getContractDescription(c.baseSymbol || c.name);
341
+ const isMicro = desc.toLowerCase().includes('micro');
342
+ const label = isMicro
343
+ ? `${c.symbol} - ${chalk.cyan(desc)} (${c.exchange})`
344
+ : `${c.symbol} - ${desc} (${c.exchange})`;
345
+ return { label, value: c };
346
+ });
347
+
348
+ // Add done/back options
349
+ if (selectedContracts.length > 0) {
350
+ options.unshift({ label: chalk.green(`✓ Done (${selectedContracts.length} selected)`), value: 'done' });
351
+ }
352
+ options.push({ label: chalk.gray('< Cancel'), value: 'back' });
353
+
354
+ const promptText = selectedContracts.length === 0
355
+ ? `Select Symbol 1/${maxSymbols}:`
356
+ : `Select Symbol ${selectedContracts.length + 1}/${maxSymbols} (${remaining} remaining):`;
357
+
358
+ const selection = await prompts.selectOption(chalk.yellow(promptText), options);
359
+
360
+ if (selection === 'back' || selection === null) {
361
+ return null;
362
+ }
363
+ if (selection === 'done') {
364
+ break;
365
+ }
366
+
367
+ selectedContracts.push(selection);
368
+ console.log(chalk.green(` ✓ Added: ${selection.symbol}`));
369
+ }
370
+
371
+ if (selectedContracts.length === 0) {
372
+ return null;
373
+ }
374
+
375
+ // Display summary
376
+ console.log();
377
+ console.log(chalk.cyan(` Selected ${selectedContracts.length} symbol(s):`));
378
+ for (const c of selectedContracts) {
379
+ console.log(chalk.white(` - ${c.symbol} (${c.baseSymbol || c.name})`));
380
+ }
381
+ console.log();
382
+
383
+ return selectedContracts;
384
+ };
385
+
386
+ /**
387
+ * Symbol selection - sorted with popular indices first
388
+ */
389
+ const selectSymbol = async (service, account) => {
390
+ const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
292
391
 
392
+ // Ensure we have a logged-in service
393
+ if (!service.loginInfo && service.credentials) {
394
+ spinner.text = 'Reconnecting to broker...';
395
+ const loginResult = await service.login(service.credentials.username, service.credentials.password);
396
+ if (!loginResult.success) {
397
+ spinner.fail(`Login failed: ${loginResult.error}`);
398
+ return null;
399
+ }
400
+ }
401
+
402
+ const contractsResult = await service.getContracts();
403
+ if (!contractsResult.success || !contractsResult.contracts?.length) {
404
+ spinner.fail(`Failed to load contracts: ${contractsResult.error || 'No contracts'}`);
405
+ return null;
406
+ }
407
+
408
+ const contracts = sortContracts(contractsResult.contracts);
293
409
  spinner.succeed(`Found ${contracts.length} contracts`);
294
410
 
295
411
  // Display sorted contracts with full description
@@ -339,13 +455,17 @@ const selectStrategy = async () => {
339
455
  /**
340
456
  * Configure algo
341
457
  */
342
- const configureAlgo = async (account, contract, strategy) => {
458
+ const configureAlgo = async (account, contract, strategy, isMultiSymbol = false) => {
343
459
  console.log();
344
460
  console.log(chalk.cyan(' Configure Algo Parameters'));
345
461
  console.log(chalk.gray(` Strategy: ${strategy.name}`));
462
+ if (isMultiSymbol) {
463
+ console.log(chalk.gray(` Mode: Multi-Symbol`));
464
+ }
346
465
  console.log();
347
466
 
348
- const contracts = await prompts.numberInput('Number of contracts:', 1, 1, 10);
467
+ const contractsLabel = isMultiSymbol ? 'Contracts per symbol:' : 'Number of contracts:';
468
+ const contracts = await prompts.numberInput(contractsLabel, 1, 1, 10);
349
469
  if (contracts === null) return null;
350
470
 
351
471
  const dailyTarget = await prompts.numberInput('Daily target ($):', 1000, 1, 10000);
@@ -357,6 +477,11 @@ const configureAlgo = async (account, contract, strategy) => {
357
477
  const showName = await prompts.confirmPrompt('Show account name?', false);
358
478
  if (showName === null) return null;
359
479
 
480
+ // Return different config shape for multi-symbol
481
+ if (isMultiSymbol) {
482
+ return { contractsPerSymbol: contracts, dailyTarget, maxRisk, showName };
483
+ }
484
+
360
485
  return { contracts, dailyTarget, maxRisk, showName };
361
486
  };
362
487
 
@@ -185,17 +185,21 @@ class AlgoUI {
185
185
 
186
186
  this._line(chalk.cyan(GT));
187
187
 
188
- // Row 1: Account | Symbol
188
+ // Row 1: Account | Symbol(s)
189
189
  const accountName = String(stats.accountName || 'N/A').substring(0, 40);
190
- const symbol = String(stats.symbol || 'N/A').substring(0, 35);
190
+ const isMultiSymbol = stats.symbolCount && stats.symbolCount > 1;
191
+ const symbolDisplay = isMultiSymbol
192
+ ? `${stats.symbolCount} symbols`
193
+ : String(stats.symbol || stats.symbols || 'N/A').substring(0, 35);
191
194
  const r1c1 = buildCell('Account', accountName, chalk.cyan, colL);
192
- const r1c2 = buildCell('Symbol', symbol, chalk.yellow, colR);
195
+ const r1c2 = buildCell(isMultiSymbol ? 'Symbols' : 'Symbol', symbolDisplay, chalk.yellow, colR);
193
196
  row(r1c1.padded, r1c2.padded);
194
197
 
195
198
  this._line(chalk.cyan(GM));
196
199
 
197
200
  // Row 2: Qty | P&L
198
- const r2c1 = buildCell('Qty', (stats.qty || '1').toString(), chalk.cyan, colL);
201
+ const qtyLabel = isMultiSymbol ? 'Qty/Symbol' : 'Qty';
202
+ const r2c1 = buildCell(qtyLabel, (stats.qty || '1').toString(), chalk.cyan, colL);
199
203
  const r2c2 = buildCell('P&L', pnlStr, pnlColor, colR);
200
204
  row(r2c1.padded, r2c2.padded);
201
205