hedgequantx 2.6.127 → 2.6.128
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/pages/algo/one-account.js +476 -20
package/package.json
CHANGED
|
@@ -34,6 +34,17 @@ const StrategySupervisor = require('../../services/ai/strategy-supervisor');
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Check if service supports fast path (Rithmic direct)
|
|
39
|
+
* @param {Object} service - Trading service
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
const isRithmicFastPath = (service) => {
|
|
43
|
+
return typeof service.fastEntry === 'function' &&
|
|
44
|
+
typeof service.fastExit === 'function' &&
|
|
45
|
+
service.orderConn?.isConnected;
|
|
46
|
+
};
|
|
47
|
+
|
|
37
48
|
/**
|
|
38
49
|
* One Account Menu
|
|
39
50
|
*/
|
|
@@ -78,21 +89,48 @@ const oneAccountMenu = async (service) => {
|
|
|
78
89
|
// Use the service attached to the account (from getAllAccounts), fallback to getServiceForAccount
|
|
79
90
|
const accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
|
|
80
91
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
92
|
+
// Check if Rithmic (supports multi-symbol)
|
|
93
|
+
const isRithmicAccount = isRithmicFastPath(accountService);
|
|
94
|
+
|
|
95
|
+
// Ask for multi-symbol mode (Rithmic only)
|
|
96
|
+
let useMultiSymbol = false;
|
|
97
|
+
if (isRithmicAccount) {
|
|
98
|
+
console.log();
|
|
99
|
+
useMultiSymbol = await prompts.confirmPrompt(chalk.cyan(`MULTI-SYMBOL MODE? (up to ${MAX_MULTI_SYMBOLS} symbols)`), false);
|
|
100
|
+
if (useMultiSymbol === null) return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Select symbol(s)
|
|
104
|
+
const contracts = await selectSymbol(accountService, selectedAccount, useMultiSymbol);
|
|
105
|
+
if (!contracts) return;
|
|
106
|
+
|
|
107
|
+
// Normalize to array
|
|
108
|
+
const contractList = Array.isArray(contracts) ? contracts : [contracts];
|
|
84
109
|
|
|
85
110
|
// Configure algo
|
|
86
|
-
const config = await configureAlgo(selectedAccount,
|
|
111
|
+
const config = await configureAlgo(selectedAccount, contractList);
|
|
87
112
|
if (!config) return;
|
|
88
113
|
|
|
89
|
-
|
|
114
|
+
// Launch appropriate algo
|
|
115
|
+
if (contractList.length === 1) {
|
|
116
|
+
await launchAlgo(accountService, selectedAccount, contractList[0], config);
|
|
117
|
+
} else {
|
|
118
|
+
await launchMultiSymbolRithmic(accountService, selectedAccount, contractList, config);
|
|
119
|
+
}
|
|
90
120
|
};
|
|
91
121
|
|
|
122
|
+
// Maximum symbols for multi-symbol trading
|
|
123
|
+
const MAX_MULTI_SYMBOLS = 5;
|
|
124
|
+
|
|
92
125
|
/**
|
|
93
126
|
* Symbol selection - sorted with popular indices first
|
|
127
|
+
* Supports selecting multiple symbols (up to MAX_MULTI_SYMBOLS)
|
|
128
|
+
* @param {Object} service - Trading service
|
|
129
|
+
* @param {Object} account - Trading account
|
|
130
|
+
* @param {boolean} allowMultiple - Allow selecting multiple symbols
|
|
131
|
+
* @returns {Object|Array|null} Single contract or array of contracts
|
|
94
132
|
*/
|
|
95
|
-
const selectSymbol = async (service, account) => {
|
|
133
|
+
const selectSymbol = async (service, account, allowMultiple = false) => {
|
|
96
134
|
const spinner = ora({ text: 'LOADING SYMBOLS...', color: 'yellow' }).start();
|
|
97
135
|
|
|
98
136
|
const contractsResult = await service.getContracts();
|
|
@@ -136,16 +174,86 @@ const selectSymbol = async (service, account) => {
|
|
|
136
174
|
|
|
137
175
|
options.push({ label: chalk.gray('< BACK'), value: 'back' });
|
|
138
176
|
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
// Single symbol mode
|
|
178
|
+
if (!allowMultiple) {
|
|
179
|
+
const contract = await prompts.selectOption(chalk.yellow('SELECT SYMBOL:'), options);
|
|
180
|
+
return contract === 'back' || contract === null ? null : contract;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Multi-symbol selection mode
|
|
184
|
+
const selectedContracts = [];
|
|
185
|
+
|
|
186
|
+
while (selectedContracts.length < MAX_MULTI_SYMBOLS) {
|
|
187
|
+
console.log();
|
|
188
|
+
|
|
189
|
+
// Show already selected symbols
|
|
190
|
+
if (selectedContracts.length > 0) {
|
|
191
|
+
console.log(chalk.cyan(` SELECTED (${selectedContracts.length}/${MAX_MULTI_SYMBOLS}):`));
|
|
192
|
+
selectedContracts.forEach((c, i) => {
|
|
193
|
+
const name = c.name || c.symbol;
|
|
194
|
+
console.log(chalk.green(` ${i + 1}. ${name}`));
|
|
195
|
+
});
|
|
196
|
+
console.log();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Filter out already selected
|
|
200
|
+
const availableOptions = options.filter(opt => {
|
|
201
|
+
if (opt.value === 'back') return true;
|
|
202
|
+
const optId = opt.value.id || opt.value.symbol || opt.value.name;
|
|
203
|
+
return !selectedContracts.some(sc => (sc.id || sc.symbol || sc.name) === optId);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Add DONE option if at least 1 selected
|
|
207
|
+
if (selectedContracts.length > 0) {
|
|
208
|
+
availableOptions.unshift({
|
|
209
|
+
label: chalk.green(`✓ DONE - Start trading ${selectedContracts.length} symbol${selectedContracts.length > 1 ? 's' : ''}`),
|
|
210
|
+
value: 'done'
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const promptText = selectedContracts.length === 0
|
|
215
|
+
? chalk.yellow(`SELECT SYMBOL (1/${MAX_MULTI_SYMBOLS}):`)
|
|
216
|
+
: chalk.yellow(`ADD SYMBOL (${selectedContracts.length + 1}/${MAX_MULTI_SYMBOLS}) OR DONE:`);
|
|
217
|
+
|
|
218
|
+
const choice = await prompts.selectOption(promptText, availableOptions);
|
|
219
|
+
|
|
220
|
+
if (choice === null || choice === 'back') {
|
|
221
|
+
if (selectedContracts.length === 0) return null;
|
|
222
|
+
selectedContracts.pop(); // Remove last
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (choice === 'done') break;
|
|
227
|
+
|
|
228
|
+
selectedContracts.push(choice);
|
|
229
|
+
|
|
230
|
+
if (selectedContracts.length >= MAX_MULTI_SYMBOLS) {
|
|
231
|
+
console.log(chalk.yellow(` Maximum ${MAX_MULTI_SYMBOLS} symbols reached`));
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return selectedContracts.length > 0 ? selectedContracts : null;
|
|
141
237
|
};
|
|
142
238
|
|
|
143
239
|
/**
|
|
144
240
|
* Configure algo
|
|
241
|
+
* @param {Object} account - Trading account
|
|
242
|
+
* @param {Object|Array} contractOrContracts - Single contract or array of contracts
|
|
145
243
|
*/
|
|
146
|
-
const configureAlgo = async (account,
|
|
244
|
+
const configureAlgo = async (account, contractOrContracts) => {
|
|
245
|
+
const contractList = Array.isArray(contractOrContracts) ? contractOrContracts : [contractOrContracts];
|
|
246
|
+
|
|
147
247
|
console.log();
|
|
148
248
|
console.log(chalk.cyan(' CONFIGURE ALGO PARAMETERS'));
|
|
249
|
+
|
|
250
|
+
// Show selected symbols
|
|
251
|
+
if (contractList.length > 1) {
|
|
252
|
+
console.log(chalk.white(` Trading ${contractList.length} symbols:`));
|
|
253
|
+
contractList.forEach((c, i) => {
|
|
254
|
+
console.log(chalk.yellow(` ${i + 1}. ${c.name || c.symbol}`));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
149
257
|
console.log();
|
|
150
258
|
|
|
151
259
|
const contracts = await prompts.numberInput('NUMBER OF CONTRACTS:', 1, 1, 10);
|
|
@@ -197,17 +305,6 @@ const configureAlgo = async (account, contract) => {
|
|
|
197
305
|
return { contracts, dailyTarget, maxRisk, showName, enableAI };
|
|
198
306
|
};
|
|
199
307
|
|
|
200
|
-
/**
|
|
201
|
-
* Check if service supports fast path (Rithmic direct)
|
|
202
|
-
* @param {Object} service - Trading service
|
|
203
|
-
* @returns {boolean}
|
|
204
|
-
*/
|
|
205
|
-
const isRithmicFastPath = (service) => {
|
|
206
|
-
return typeof service.fastEntry === 'function' &&
|
|
207
|
-
typeof service.fastExit === 'function' &&
|
|
208
|
-
service.orderConn?.isConnected;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
308
|
/**
|
|
212
309
|
* Launch algo trading - HQX Ultra Scalping Strategy
|
|
213
310
|
* Real-time market data + Strategy signals + Auto order execution
|
|
@@ -1283,4 +1380,363 @@ const launchAlgo = async (service, account, contract, config) => {
|
|
|
1283
1380
|
await prompts.waitForEnter();
|
|
1284
1381
|
};
|
|
1285
1382
|
|
|
1383
|
+
/**
|
|
1384
|
+
* Launch multi-symbol algo trading (Rithmic only)
|
|
1385
|
+
* Uses single market feed connection with multiple subscriptions
|
|
1386
|
+
* Each symbol has its own PositionManager and strategy instance
|
|
1387
|
+
*
|
|
1388
|
+
* @param {Object} service - Rithmic trading service
|
|
1389
|
+
* @param {Object} account - Trading account
|
|
1390
|
+
* @param {Array} contracts - Array of contracts to trade
|
|
1391
|
+
* @param {Object} config - Algo configuration
|
|
1392
|
+
*/
|
|
1393
|
+
const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
|
|
1394
|
+
const { contracts: qty, dailyTarget, maxRisk, showName, enableAI } = config;
|
|
1395
|
+
|
|
1396
|
+
const accountName = showName
|
|
1397
|
+
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
1398
|
+
: 'HQX *****';
|
|
1399
|
+
const rithmicAccountId = account.rithmicAccountId || account.accountId;
|
|
1400
|
+
|
|
1401
|
+
// Build symbols string for UI
|
|
1402
|
+
const symbolNames = contracts.map(c => c.name || c.symbol);
|
|
1403
|
+
const symbolsDisplay = symbolNames.join(', ');
|
|
1404
|
+
|
|
1405
|
+
const ui = new AlgoUI({
|
|
1406
|
+
subtitle: `MULTI-SYMBOL (${contracts.length})`,
|
|
1407
|
+
mode: 'one-account'
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
// Shared stats
|
|
1411
|
+
const stats = {
|
|
1412
|
+
accountName,
|
|
1413
|
+
symbol: symbolsDisplay,
|
|
1414
|
+
qty,
|
|
1415
|
+
target: dailyTarget,
|
|
1416
|
+
risk: maxRisk,
|
|
1417
|
+
propfirm: account.propfirm || 'Unknown',
|
|
1418
|
+
platform: 'RITHMIC',
|
|
1419
|
+
pnl: null,
|
|
1420
|
+
openPnl: null,
|
|
1421
|
+
closedPnl: null,
|
|
1422
|
+
balance: null,
|
|
1423
|
+
position: 0,
|
|
1424
|
+
trades: 0,
|
|
1425
|
+
wins: 0,
|
|
1426
|
+
losses: 0,
|
|
1427
|
+
sessionPnl: 0,
|
|
1428
|
+
latency: 0,
|
|
1429
|
+
connected: false,
|
|
1430
|
+
startTime: Date.now(),
|
|
1431
|
+
aiSupervision: enableAI || false,
|
|
1432
|
+
fastPath: true,
|
|
1433
|
+
// Per-symbol tracking
|
|
1434
|
+
symbolStats: {},
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// Initialize per-symbol stats
|
|
1438
|
+
contracts.forEach(c => {
|
|
1439
|
+
const name = c.name || c.symbol;
|
|
1440
|
+
stats.symbolStats[name] = { position: 0, trades: 0, wins: 0, losses: 0, pnl: 0 };
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
let running = true;
|
|
1444
|
+
let stopReason = null;
|
|
1445
|
+
|
|
1446
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1447
|
+
// POSITION MANAGERS - One per symbol
|
|
1448
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1449
|
+
const positionManagers = {};
|
|
1450
|
+
const strategies = {};
|
|
1451
|
+
|
|
1452
|
+
contracts.forEach(contract => {
|
|
1453
|
+
const symbolName = contract.name || contract.symbol;
|
|
1454
|
+
const contractId = contract.id || contract.symbol || contract.name;
|
|
1455
|
+
const tickSize = contract.tickSize ?? null;
|
|
1456
|
+
const tickValue = contract.tickValue ?? null;
|
|
1457
|
+
|
|
1458
|
+
// Create strategy instance for this symbol
|
|
1459
|
+
const strategy = Object.create(hftStrategy);
|
|
1460
|
+
if (tickSize !== null && tickValue !== null) {
|
|
1461
|
+
strategy.initialize(contractId, tickSize, tickValue);
|
|
1462
|
+
}
|
|
1463
|
+
strategies[symbolName] = strategy;
|
|
1464
|
+
|
|
1465
|
+
// Create position manager for this symbol
|
|
1466
|
+
const pm = new PositionManager(service, strategy);
|
|
1467
|
+
if (tickSize !== null && tickValue !== null) {
|
|
1468
|
+
pm.setContractInfo(symbolName, { tickSize, tickValue, contractId });
|
|
1469
|
+
}
|
|
1470
|
+
pm.start();
|
|
1471
|
+
positionManagers[symbolName] = pm;
|
|
1472
|
+
|
|
1473
|
+
// Position manager events
|
|
1474
|
+
pm.on('entryFilled', ({ position, fillLatencyMs }) => {
|
|
1475
|
+
const side = position.side === 0 ? 'LONG' : 'SHORT';
|
|
1476
|
+
ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${position.entryPrice} | ${fillLatencyMs}ms`);
|
|
1477
|
+
stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
|
|
1481
|
+
const holdSec = (holdDurationMs / 1000).toFixed(1);
|
|
1482
|
+
if (pnlTicks !== null && tickValue !== null) {
|
|
1483
|
+
const pnlDollars = pnlTicks * tickValue;
|
|
1484
|
+
stats.sessionPnl += pnlDollars;
|
|
1485
|
+
stats.symbolStats[symbolName].pnl += pnlDollars;
|
|
1486
|
+
stats.symbolStats[symbolName].trades++;
|
|
1487
|
+
stats.trades++;
|
|
1488
|
+
|
|
1489
|
+
if (pnlDollars >= 0) {
|
|
1490
|
+
stats.wins++;
|
|
1491
|
+
stats.symbolStats[symbolName].wins++;
|
|
1492
|
+
ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} | ${holdSec}s`);
|
|
1493
|
+
} else {
|
|
1494
|
+
stats.losses++;
|
|
1495
|
+
stats.symbolStats[symbolName].losses++;
|
|
1496
|
+
ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} | ${holdSec}s`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
stats.symbolStats[symbolName].position = 0;
|
|
1500
|
+
ui.render(stats);
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
// Strategy signals
|
|
1504
|
+
strategy.on('signal', async (signal) => {
|
|
1505
|
+
if (!running) return;
|
|
1506
|
+
if (!pm.canEnter(symbolName)) return;
|
|
1507
|
+
|
|
1508
|
+
const { direction } = signal;
|
|
1509
|
+
const orderSide = direction === 'long' ? 0 : 1;
|
|
1510
|
+
const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
|
|
1511
|
+
|
|
1512
|
+
ui.addLog('entry', `[${symbolName}] ${sideStr} signal`);
|
|
1513
|
+
|
|
1514
|
+
const orderData = {
|
|
1515
|
+
accountId: rithmicAccountId,
|
|
1516
|
+
symbol: symbolName,
|
|
1517
|
+
exchange: contract.exchange || 'CME',
|
|
1518
|
+
size: qty,
|
|
1519
|
+
side: orderSide,
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
const entryResult = service.fastEntry(orderData);
|
|
1523
|
+
if (entryResult.success) {
|
|
1524
|
+
pm.registerEntry(entryResult, orderData, { tickSize, tickValue, contractId });
|
|
1525
|
+
} else {
|
|
1526
|
+
ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1532
|
+
// MARKET DATA FEED - Single connection, multiple subscriptions
|
|
1533
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1534
|
+
const marketFeed = new RithmicMarketDataFeed(service);
|
|
1535
|
+
|
|
1536
|
+
marketFeed.on('connected', () => {
|
|
1537
|
+
stats.connected = true;
|
|
1538
|
+
ui.addLog('success', 'Market feed connected');
|
|
1539
|
+
ui.render(stats);
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
marketFeed.on('tick', (tick) => {
|
|
1543
|
+
if (!running) return;
|
|
1544
|
+
|
|
1545
|
+
stats.latency = tick.latency || 0;
|
|
1546
|
+
|
|
1547
|
+
// Route tick to correct strategy based on symbol
|
|
1548
|
+
const tickSymbol = tick.symbol;
|
|
1549
|
+
|
|
1550
|
+
// Find matching strategy (ES matches ESH6, NQ matches NQH6, etc.)
|
|
1551
|
+
for (const [symbolName, strategy] of Object.entries(strategies)) {
|
|
1552
|
+
const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
|
|
1553
|
+
if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
|
|
1554
|
+
const tickData = {
|
|
1555
|
+
contractId: tick.contractId || symbolName,
|
|
1556
|
+
price: tick.price || tick.lastPrice || tick.bid,
|
|
1557
|
+
bid: tick.bid,
|
|
1558
|
+
ask: tick.ask,
|
|
1559
|
+
volume: tick.volume || tick.size || 1,
|
|
1560
|
+
timestamp: tick.timestamp || Date.now()
|
|
1561
|
+
};
|
|
1562
|
+
strategy.processTick(tickData);
|
|
1563
|
+
|
|
1564
|
+
// Update price for position manager
|
|
1565
|
+
service.emit('priceUpdate', {
|
|
1566
|
+
symbol: symbolName,
|
|
1567
|
+
price: tickData.price,
|
|
1568
|
+
timestamp: tickData.timestamp,
|
|
1569
|
+
});
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
ui.render(stats);
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
marketFeed.on('error', (err) => {
|
|
1578
|
+
ui.addLog('error', `Feed error: ${err.message}`);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
// Connect and subscribe
|
|
1582
|
+
ui.render(stats);
|
|
1583
|
+
ui.addLog('info', `MULTI-SYMBOL: ${contracts.length} symbols`);
|
|
1584
|
+
|
|
1585
|
+
try {
|
|
1586
|
+
await marketFeed.connect();
|
|
1587
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1588
|
+
|
|
1589
|
+
if (marketFeed.isConnected()) {
|
|
1590
|
+
for (const contract of contracts) {
|
|
1591
|
+
const symbolName = contract.name || contract.symbol;
|
|
1592
|
+
const exchange = contract.exchange || 'CME';
|
|
1593
|
+
marketFeed.subscribe(symbolName, exchange);
|
|
1594
|
+
ui.addLog('success', `[${symbolName}] Subscribed`);
|
|
1595
|
+
}
|
|
1596
|
+
} else {
|
|
1597
|
+
ui.addLog('error', 'Failed to connect market feed');
|
|
1598
|
+
}
|
|
1599
|
+
} catch (e) {
|
|
1600
|
+
ui.addLog('error', `Connection error: ${e.message}`);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1604
|
+
// P&L UPDATES VIA WEBSOCKET
|
|
1605
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1606
|
+
if (typeof service.on === 'function') {
|
|
1607
|
+
service.on('pnlUpdate', (pnlData) => {
|
|
1608
|
+
if (pnlData.accountId !== rithmicAccountId) return;
|
|
1609
|
+
if (pnlData.closedPositionPnl !== undefined) {
|
|
1610
|
+
stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
|
|
1611
|
+
}
|
|
1612
|
+
if (pnlData.accountBalance !== undefined) {
|
|
1613
|
+
stats.balance = parseFloat(pnlData.accountBalance);
|
|
1614
|
+
}
|
|
1615
|
+
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1620
|
+
// EMERGENCY STOP
|
|
1621
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1622
|
+
const emergencyStopAll = async () => {
|
|
1623
|
+
ui.addLog('warning', '████ EMERGENCY STOP ████');
|
|
1624
|
+
|
|
1625
|
+
// Close all positions
|
|
1626
|
+
for (const [symbolName, pm] of Object.entries(positionManagers)) {
|
|
1627
|
+
try {
|
|
1628
|
+
if (pm.hasPosition(symbolName)) {
|
|
1629
|
+
pm.closePosition(symbolName);
|
|
1630
|
+
ui.addLog('info', `[${symbolName}] Closing position...`);
|
|
1631
|
+
}
|
|
1632
|
+
} catch (e) {
|
|
1633
|
+
ui.addLog('error', `[${symbolName}] Close failed: ${e.message}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
ui.addLog('info', 'All close orders sent');
|
|
1638
|
+
ui.render(stats);
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
// Keyboard handler
|
|
1642
|
+
let emergencyStopInProgress = false;
|
|
1643
|
+
|
|
1644
|
+
const setupKeyHandler = () => {
|
|
1645
|
+
if (!process.stdin.isTTY) return;
|
|
1646
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1647
|
+
process.stdin.setRawMode(true);
|
|
1648
|
+
process.stdin.resume();
|
|
1649
|
+
|
|
1650
|
+
const onKey = async (str, key) => {
|
|
1651
|
+
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
1652
|
+
if (emergencyStopInProgress) return;
|
|
1653
|
+
emergencyStopInProgress = true;
|
|
1654
|
+
running = false;
|
|
1655
|
+
stopReason = 'manual';
|
|
1656
|
+
await emergencyStopAll();
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
process.stdin.on('keypress', onKey);
|
|
1660
|
+
return () => {
|
|
1661
|
+
process.stdin.removeListener('keypress', onKey);
|
|
1662
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1663
|
+
};
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
const cleanupKeys = setupKeyHandler();
|
|
1667
|
+
|
|
1668
|
+
// UI refresh
|
|
1669
|
+
const refreshInterval = setInterval(() => {
|
|
1670
|
+
if (running) ui.render(stats);
|
|
1671
|
+
}, 1000);
|
|
1672
|
+
|
|
1673
|
+
// Wait for stop
|
|
1674
|
+
await new Promise(resolve => {
|
|
1675
|
+
const check = setInterval(() => {
|
|
1676
|
+
if (!running) {
|
|
1677
|
+
clearInterval(check);
|
|
1678
|
+
resolve();
|
|
1679
|
+
}
|
|
1680
|
+
}, 100);
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1684
|
+
// CLEANUP
|
|
1685
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1686
|
+
clearInterval(refreshInterval);
|
|
1687
|
+
|
|
1688
|
+
// Stop all position managers
|
|
1689
|
+
for (const pm of Object.values(positionManagers)) {
|
|
1690
|
+
try { pm.stop(); } catch {}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Disconnect market feed
|
|
1694
|
+
try { await marketFeed.disconnect(); } catch {}
|
|
1695
|
+
|
|
1696
|
+
// Cleanup keyboard
|
|
1697
|
+
try { if (cleanupKeys) cleanupKeys(); } catch {}
|
|
1698
|
+
try { ui.cleanup(); } catch {}
|
|
1699
|
+
|
|
1700
|
+
try {
|
|
1701
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1702
|
+
process.stdin.resume();
|
|
1703
|
+
} catch {}
|
|
1704
|
+
|
|
1705
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1706
|
+
// SESSION SUMMARY
|
|
1707
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1708
|
+
const durationMs = Date.now() - stats.startTime;
|
|
1709
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
1710
|
+
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
1711
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
1712
|
+
const duration = hours > 0
|
|
1713
|
+
? `${hours}h ${minutes}m ${seconds}s`
|
|
1714
|
+
: minutes > 0
|
|
1715
|
+
? `${minutes}m ${seconds}s`
|
|
1716
|
+
: `${seconds}s`;
|
|
1717
|
+
|
|
1718
|
+
console.log();
|
|
1719
|
+
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
|
|
1720
|
+
console.log(chalk.cyan(' MULTI-SYMBOL SESSION SUMMARY'));
|
|
1721
|
+
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
|
|
1722
|
+
console.log();
|
|
1723
|
+
|
|
1724
|
+
for (const [symbol, symStats] of Object.entries(stats.symbolStats)) {
|
|
1725
|
+
const winRate = symStats.trades > 0 ? ((symStats.wins / symStats.trades) * 100).toFixed(0) : 0;
|
|
1726
|
+
const pnlColor = symStats.pnl >= 0 ? chalk.green : chalk.red;
|
|
1727
|
+
console.log(chalk.white(` ${symbol}:`));
|
|
1728
|
+
console.log(` Trades: ${symStats.trades} | WR: ${winRate}% | P&L: ${pnlColor('$' + symStats.pnl.toFixed(2))}`);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
console.log();
|
|
1732
|
+
const totalPnlColor = stats.sessionPnl >= 0 ? chalk.green : chalk.red;
|
|
1733
|
+
console.log(chalk.white(` TOTAL: ${stats.trades} trades | ${stats.wins}W/${stats.losses}L`));
|
|
1734
|
+
console.log(chalk.white(` Session P&L: ${totalPnlColor('$' + stats.sessionPnl.toFixed(2))}`));
|
|
1735
|
+
console.log(chalk.white(` Duration: ${duration}`));
|
|
1736
|
+
console.log();
|
|
1737
|
+
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
|
|
1738
|
+
|
|
1739
|
+
await prompts.waitForEnter();
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1286
1742
|
module.exports = { oneAccountMenu };
|