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
|
@@ -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
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
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 (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
*
|
|
300
|
+
* Multi-symbol selection - select up to 5 symbols
|
|
250
301
|
*/
|
|
251
|
-
const
|
|
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
|
-
|
|
321
|
+
const contracts = sortContracts(contractsResult.contracts);
|
|
322
|
+
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
271
323
|
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
//
|
|
280
|
-
const
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
|
package/src/pages/algo/ui.js
CHANGED
|
@@ -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
|
|
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',
|
|
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
|
|
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
|
|