hedgequantx 2.6.127 → 2.6.129
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 +663 -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,550 @@ 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
|
+
* Same logic as launchAlgo but for multiple symbols
|
|
1389
|
+
*
|
|
1390
|
+
* @param {Object} service - Rithmic trading service
|
|
1391
|
+
* @param {Object} account - Trading account
|
|
1392
|
+
* @param {Array} contracts - Array of contracts to trade
|
|
1393
|
+
* @param {Object} config - Algo configuration
|
|
1394
|
+
*/
|
|
1395
|
+
const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
|
|
1396
|
+
const { contracts: qty, dailyTarget, maxRisk, showName, enableAI } = config;
|
|
1397
|
+
|
|
1398
|
+
const accountName = showName
|
|
1399
|
+
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
1400
|
+
: 'HQX *****';
|
|
1401
|
+
const rithmicAccountId = account.rithmicAccountId || account.accountId;
|
|
1402
|
+
|
|
1403
|
+
// Build symbols string for UI
|
|
1404
|
+
const symbolNames = contracts.map(c => c.name || c.symbol);
|
|
1405
|
+
const symbolsDisplay = symbolNames.join(', ');
|
|
1406
|
+
|
|
1407
|
+
const ui = new AlgoUI({
|
|
1408
|
+
subtitle: `MULTI-SYMBOL (${contracts.length})`,
|
|
1409
|
+
mode: 'one-account'
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
// Shared stats (same structure as launchAlgo)
|
|
1413
|
+
const stats = {
|
|
1414
|
+
accountName,
|
|
1415
|
+
symbol: symbolsDisplay,
|
|
1416
|
+
qty,
|
|
1417
|
+
target: dailyTarget,
|
|
1418
|
+
risk: maxRisk,
|
|
1419
|
+
propfirm: account.propfirm || 'Unknown',
|
|
1420
|
+
platform: 'RITHMIC',
|
|
1421
|
+
pnl: null,
|
|
1422
|
+
openPnl: null,
|
|
1423
|
+
closedPnl: null,
|
|
1424
|
+
balance: null,
|
|
1425
|
+
buyingPower: null,
|
|
1426
|
+
margin: null,
|
|
1427
|
+
netLiquidation: null,
|
|
1428
|
+
position: 0,
|
|
1429
|
+
entryPrice: 0,
|
|
1430
|
+
lastPrice: 0,
|
|
1431
|
+
trades: 0,
|
|
1432
|
+
wins: 0,
|
|
1433
|
+
losses: 0,
|
|
1434
|
+
sessionPnl: 0,
|
|
1435
|
+
latency: 0,
|
|
1436
|
+
connected: false,
|
|
1437
|
+
startTime: Date.now(),
|
|
1438
|
+
aiSupervision: enableAI || false,
|
|
1439
|
+
aiMode: null,
|
|
1440
|
+
agentCount: 0,
|
|
1441
|
+
fastPath: true,
|
|
1442
|
+
avgEntryLatency: 0,
|
|
1443
|
+
avgFillLatency: 0,
|
|
1444
|
+
entryLatencies: [],
|
|
1445
|
+
// Per-symbol tracking
|
|
1446
|
+
symbolStats: {},
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
// Initialize per-symbol stats
|
|
1450
|
+
contracts.forEach(c => {
|
|
1451
|
+
const name = c.name || c.symbol;
|
|
1452
|
+
stats.symbolStats[name] = {
|
|
1453
|
+
position: 0,
|
|
1454
|
+
trades: 0,
|
|
1455
|
+
wins: 0,
|
|
1456
|
+
losses: 0,
|
|
1457
|
+
pnl: 0,
|
|
1458
|
+
tickCount: 0,
|
|
1459
|
+
};
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
let running = true;
|
|
1463
|
+
let stopReason = null;
|
|
1464
|
+
let tickCount = 0;
|
|
1465
|
+
|
|
1466
|
+
// Store contract info for later use
|
|
1467
|
+
const contractInfoMap = {};
|
|
1468
|
+
contracts.forEach(c => {
|
|
1469
|
+
const name = c.name || c.symbol;
|
|
1470
|
+
contractInfoMap[name] = {
|
|
1471
|
+
tickSize: c.tickSize ?? null,
|
|
1472
|
+
tickValue: c.tickValue ?? null,
|
|
1473
|
+
contractId: c.id || c.symbol || c.name,
|
|
1474
|
+
exchange: c.exchange || 'CME',
|
|
1475
|
+
};
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1479
|
+
// POSITION MANAGERS & STRATEGIES - One per symbol
|
|
1480
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1481
|
+
const positionManagers = {};
|
|
1482
|
+
const strategies = {};
|
|
1483
|
+
const pendingOrders = {}; // Track pending orders per symbol
|
|
1484
|
+
|
|
1485
|
+
contracts.forEach(contract => {
|
|
1486
|
+
const symbolName = contract.name || contract.symbol;
|
|
1487
|
+
const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
|
|
1488
|
+
|
|
1489
|
+
// Create strategy instance for this symbol
|
|
1490
|
+
const strategy = Object.create(hftStrategy);
|
|
1491
|
+
if (tickSize !== null && tickValue !== null) {
|
|
1492
|
+
strategy.initialize(contractId, tickSize, tickValue);
|
|
1493
|
+
}
|
|
1494
|
+
strategies[symbolName] = strategy;
|
|
1495
|
+
pendingOrders[symbolName] = false;
|
|
1496
|
+
|
|
1497
|
+
// Create position manager for this symbol
|
|
1498
|
+
const pm = new PositionManager(service, strategy);
|
|
1499
|
+
if (tickSize !== null && tickValue !== null) {
|
|
1500
|
+
pm.setContractInfo(symbolName, { tickSize, tickValue, contractId });
|
|
1501
|
+
}
|
|
1502
|
+
pm.start();
|
|
1503
|
+
positionManagers[symbolName] = pm;
|
|
1504
|
+
|
|
1505
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1506
|
+
// POSITION MANAGER EVENTS (same as launchAlgo)
|
|
1507
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1508
|
+
pm.on('entryFilled', ({ position, fillLatencyMs }) => {
|
|
1509
|
+
stats.entryLatencies.push(fillLatencyMs);
|
|
1510
|
+
stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
|
|
1511
|
+
const side = position.side === 0 ? 'LONG' : 'SHORT';
|
|
1512
|
+
ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${position.entryPrice} | ${fillLatencyMs}ms`);
|
|
1513
|
+
stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
|
|
1514
|
+
ui.render(stats);
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
|
|
1518
|
+
const holdSec = (holdDurationMs / 1000).toFixed(1);
|
|
1519
|
+
if (pnlTicks !== null && tickValue !== null) {
|
|
1520
|
+
const pnlDollars = pnlTicks * tickValue;
|
|
1521
|
+
stats.sessionPnl += pnlDollars;
|
|
1522
|
+
stats.symbolStats[symbolName].pnl += pnlDollars;
|
|
1523
|
+
stats.symbolStats[symbolName].trades++;
|
|
1524
|
+
stats.trades++;
|
|
1525
|
+
|
|
1526
|
+
// Record trade for Recovery Math
|
|
1527
|
+
recoveryMath.recordTrade({
|
|
1528
|
+
pnl: pnlDollars,
|
|
1529
|
+
ticks: pnlTicks,
|
|
1530
|
+
side: pnlTicks >= 0 ? 'win' : 'loss',
|
|
1531
|
+
duration: holdDurationMs,
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
// Update Recovery Mode state
|
|
1535
|
+
const recovery = recoveryMath.updateSessionPnL(
|
|
1536
|
+
stats.sessionPnl,
|
|
1537
|
+
FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
|
|
1538
|
+
FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
if (recovery.justActivated) {
|
|
1542
|
+
stats.recoveryMode = true;
|
|
1543
|
+
ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}%`);
|
|
1544
|
+
} else if (recovery.justDeactivated) {
|
|
1545
|
+
stats.recoveryMode = false;
|
|
1546
|
+
ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (pnlDollars >= 0) {
|
|
1550
|
+
stats.wins++;
|
|
1551
|
+
stats.symbolStats[symbolName].wins++;
|
|
1552
|
+
ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
|
|
1553
|
+
} else {
|
|
1554
|
+
stats.losses++;
|
|
1555
|
+
stats.symbolStats[symbolName].losses++;
|
|
1556
|
+
ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
|
|
1557
|
+
}
|
|
1558
|
+
} else if (pnlTicks !== null) {
|
|
1559
|
+
// Log with ticks only
|
|
1560
|
+
stats.trades++;
|
|
1561
|
+
stats.symbolStats[symbolName].trades++;
|
|
1562
|
+
if (pnlTicks >= 0) {
|
|
1563
|
+
stats.wins++;
|
|
1564
|
+
stats.symbolStats[symbolName].wins++;
|
|
1565
|
+
ui.addLog('win', `[${symbolName}] +${pnlTicks} ticks | ${holdSec}s`);
|
|
1566
|
+
} else {
|
|
1567
|
+
stats.losses++;
|
|
1568
|
+
stats.symbolStats[symbolName].losses++;
|
|
1569
|
+
ui.addLog('loss', `[${symbolName}] ${pnlTicks} ticks | ${holdSec}s`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
stats.symbolStats[symbolName].position = 0;
|
|
1573
|
+
pendingOrders[symbolName] = false;
|
|
1574
|
+
ui.render(stats);
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
pm.on('holdComplete', () => {
|
|
1578
|
+
ui.addLog('ready', `[${symbolName}] Hold complete - monitoring exit`);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
pm.on('breakevenActivated', ({ breakevenPrice, pnlTicks }) => {
|
|
1582
|
+
ui.addLog('be', `[${symbolName}] BE @ ${breakevenPrice} | +${pnlTicks} ticks`);
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1586
|
+
// STRATEGY SIGNALS
|
|
1587
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1588
|
+
strategy.on('signal', async (signal) => {
|
|
1589
|
+
if (!running) return;
|
|
1590
|
+
if (pendingOrders[symbolName]) return;
|
|
1591
|
+
if (!pm.canEnter(symbolName)) return;
|
|
1592
|
+
|
|
1593
|
+
const { direction, confidence } = signal;
|
|
1594
|
+
const orderSide = direction === 'long' ? 0 : 1;
|
|
1595
|
+
const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
|
|
1596
|
+
|
|
1597
|
+
// Calculate risk amount
|
|
1598
|
+
const kelly = Math.min(0.25, confidence || 0.15);
|
|
1599
|
+
const riskAmount = Math.round(maxRisk * kelly);
|
|
1600
|
+
const riskPct = Math.round((riskAmount / maxRisk) * 100);
|
|
1601
|
+
|
|
1602
|
+
pendingOrders[symbolName] = true;
|
|
1603
|
+
ui.addLog('entry', `[${symbolName}] ${sideStr} ${qty}x | risk: $${riskAmount} (${riskPct}%)`);
|
|
1604
|
+
|
|
1605
|
+
const orderData = {
|
|
1606
|
+
accountId: rithmicAccountId,
|
|
1607
|
+
symbol: symbolName,
|
|
1608
|
+
exchange: contractInfoMap[symbolName].exchange,
|
|
1609
|
+
size: qty,
|
|
1610
|
+
side: orderSide,
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
try {
|
|
1614
|
+
const entryResult = service.fastEntry(orderData);
|
|
1615
|
+
if (entryResult.success) {
|
|
1616
|
+
pm.registerEntry(entryResult, orderData, contractInfoMap[symbolName]);
|
|
1617
|
+
|
|
1618
|
+
// Update avg entry latency
|
|
1619
|
+
stats.avgEntryLatency = stats.entryLatencies.length > 0
|
|
1620
|
+
? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
|
|
1621
|
+
: entryResult.latencyMs;
|
|
1622
|
+
} else {
|
|
1623
|
+
ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
|
|
1624
|
+
pendingOrders[symbolName] = false;
|
|
1625
|
+
}
|
|
1626
|
+
} catch (e) {
|
|
1627
|
+
ui.addLog('error', `[${symbolName}] Order error: ${e.message}`);
|
|
1628
|
+
pendingOrders[symbolName] = false;
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1634
|
+
// MARKET DATA FEED - Single connection, multiple subscriptions
|
|
1635
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1636
|
+
const marketFeed = new RithmicMarketDataFeed(service);
|
|
1637
|
+
|
|
1638
|
+
marketFeed.on('connected', () => {
|
|
1639
|
+
stats.connected = true;
|
|
1640
|
+
algoLogger.dataConnected(ui, 'RTC');
|
|
1641
|
+
algoLogger.algoOperational(ui, 'RITHMIC');
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
marketFeed.on('tick', (tick) => {
|
|
1645
|
+
if (!running) return;
|
|
1646
|
+
|
|
1647
|
+
tickCount++;
|
|
1648
|
+
stats.latency = tick.latency || 0;
|
|
1649
|
+
|
|
1650
|
+
// Route tick to correct strategy based on symbol
|
|
1651
|
+
const tickSymbol = tick.symbol;
|
|
1652
|
+
|
|
1653
|
+
// Find matching strategy (ES matches ESH6, NQ matches NQH6, etc.)
|
|
1654
|
+
for (const [symbolName, strategy] of Object.entries(strategies)) {
|
|
1655
|
+
const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
|
|
1656
|
+
if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
|
|
1657
|
+
stats.symbolStats[symbolName].tickCount++;
|
|
1658
|
+
|
|
1659
|
+
// Log first tick per symbol
|
|
1660
|
+
if (stats.symbolStats[symbolName].tickCount === 1) {
|
|
1661
|
+
algoLogger.info(ui, 'FIRST TICK', `[${symbolName}] price=${tick.price} bid=${tick.bid} ask=${tick.ask}`);
|
|
1662
|
+
} else if (stats.symbolStats[symbolName].tickCount === 100) {
|
|
1663
|
+
algoLogger.info(ui, 'DATA FLOWING', `[${symbolName}] 100 ticks received`);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const tickData = {
|
|
1667
|
+
contractId: tick.contractId || symbolName,
|
|
1668
|
+
price: tick.price || tick.lastPrice || tick.bid,
|
|
1669
|
+
bid: tick.bid,
|
|
1670
|
+
ask: tick.ask,
|
|
1671
|
+
volume: tick.volume || tick.size || 1,
|
|
1672
|
+
side: tick.lastTradeSide || tick.side || 'unknown',
|
|
1673
|
+
timestamp: tick.timestamp || Date.now()
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
// Update last price
|
|
1677
|
+
stats.lastPrice = tickData.price;
|
|
1678
|
+
|
|
1679
|
+
// Feed tick to strategy
|
|
1680
|
+
strategy.processTick(tickData);
|
|
1681
|
+
|
|
1682
|
+
// Update price for position manager
|
|
1683
|
+
service.emit('priceUpdate', {
|
|
1684
|
+
symbol: symbolName,
|
|
1685
|
+
price: tickData.price,
|
|
1686
|
+
timestamp: tickData.timestamp,
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
// Get momentum data from strategy for position manager
|
|
1690
|
+
const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(symbolName);
|
|
1691
|
+
if (modelValues && positionManagers[symbolName] && typeof positionManagers[symbolName].updateMomentum === 'function') {
|
|
1692
|
+
positionManagers[symbolName].updateMomentum(symbolName, {
|
|
1693
|
+
ofi: modelValues.ofi || 0,
|
|
1694
|
+
zscore: modelValues.zscore || 0,
|
|
1695
|
+
delta: modelValues.delta || 0,
|
|
1696
|
+
timestamp: tickData.timestamp,
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
ui.render(stats);
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
marketFeed.on('error', (err) => {
|
|
1708
|
+
algoLogger.error(ui, 'MARKET ERROR', err.message);
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
marketFeed.on('disconnected', (err) => {
|
|
1712
|
+
stats.connected = false;
|
|
1713
|
+
algoLogger.dataDisconnected(ui, 'WEBSOCKET', err?.message);
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1717
|
+
// STARTUP LOGS (same as launchAlgo)
|
|
1718
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1719
|
+
const market = checkMarketHours();
|
|
1720
|
+
const sessionName = market.session || 'AMERICAN';
|
|
1721
|
+
const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
|
|
1722
|
+
|
|
1723
|
+
algoLogger.connectingToEngine(ui, account.accountId);
|
|
1724
|
+
algoLogger.engineStarting(ui, 'RITHMIC', dailyTarget, maxRisk);
|
|
1725
|
+
algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
|
|
1726
|
+
algoLogger.info(ui, 'FAST PATH', `Rithmic direct | ${contracts.length} symbols | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms`);
|
|
1727
|
+
|
|
1728
|
+
ui.render(stats);
|
|
1729
|
+
|
|
1730
|
+
// Connect and subscribe
|
|
1731
|
+
try {
|
|
1732
|
+
algoLogger.info(ui, 'CONNECTING', `RITHMIC TICKER | ${contracts.length} symbols`);
|
|
1733
|
+
await marketFeed.connect();
|
|
1734
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1735
|
+
|
|
1736
|
+
if (marketFeed.isConnected()) {
|
|
1737
|
+
for (const contract of contracts) {
|
|
1738
|
+
const symbolName = contract.name || contract.symbol;
|
|
1739
|
+
const exchange = contract.exchange || 'CME';
|
|
1740
|
+
marketFeed.subscribe(symbolName, exchange);
|
|
1741
|
+
algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} (${exchange})`);
|
|
1742
|
+
}
|
|
1743
|
+
} else {
|
|
1744
|
+
algoLogger.error(ui, 'CONNECTION', 'Failed to connect market feed');
|
|
1745
|
+
}
|
|
1746
|
+
} catch (e) {
|
|
1747
|
+
algoLogger.error(ui, 'CONNECTION ERROR', e.message);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1751
|
+
// REAL-TIME P&L VIA WEBSOCKET (same as launchAlgo)
|
|
1752
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1753
|
+
if (typeof service.on === 'function') {
|
|
1754
|
+
// Account-level P&L updates
|
|
1755
|
+
service.on('pnlUpdate', (pnlData) => {
|
|
1756
|
+
if (pnlData.accountId !== rithmicAccountId) return;
|
|
1757
|
+
|
|
1758
|
+
if (pnlData.closedPositionPnl !== undefined) {
|
|
1759
|
+
stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
|
|
1760
|
+
}
|
|
1761
|
+
if (pnlData.accountBalance !== undefined) {
|
|
1762
|
+
stats.balance = parseFloat(pnlData.accountBalance);
|
|
1763
|
+
}
|
|
1764
|
+
if (pnlData.availableBuyingPower !== undefined) {
|
|
1765
|
+
stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
|
|
1766
|
+
}
|
|
1767
|
+
if (pnlData.marginBalance !== undefined) {
|
|
1768
|
+
stats.margin = parseFloat(pnlData.marginBalance);
|
|
1769
|
+
}
|
|
1770
|
+
if (pnlData.netLiquidation !== undefined) {
|
|
1771
|
+
stats.netLiquidation = parseFloat(pnlData.netLiquidation);
|
|
1772
|
+
} else if (stats.balance !== null) {
|
|
1773
|
+
stats.netLiquidation = stats.balance + (stats.openPnl || 0);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
1777
|
+
ui.render(stats);
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
// Position-level updates (for Open P&L)
|
|
1781
|
+
service.on('positionUpdate', (pos) => {
|
|
1782
|
+
if (!pos || pos.accountId !== rithmicAccountId) return;
|
|
1783
|
+
|
|
1784
|
+
const posSymbol = pos.symbol;
|
|
1785
|
+
for (const symbolName of Object.keys(stats.symbolStats)) {
|
|
1786
|
+
const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
|
|
1787
|
+
if (posSymbol === baseSymbol || posSymbol === symbolName || symbolName.startsWith(posSymbol)) {
|
|
1788
|
+
stats.symbolStats[symbolName].position = pos.quantity || 0;
|
|
1789
|
+
|
|
1790
|
+
// Update Open P&L from position
|
|
1791
|
+
if (pos.openPnl !== undefined && pos.openPnl !== null) {
|
|
1792
|
+
stats.openPnl = pos.openPnl;
|
|
1793
|
+
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
1794
|
+
|
|
1795
|
+
if (stats.balance !== null) {
|
|
1796
|
+
stats.netLiquidation = stats.balance + stats.openPnl;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
ui.render(stats);
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1807
|
+
// EMERGENCY STOP
|
|
1808
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1809
|
+
const emergencyStopAll = async () => {
|
|
1810
|
+
ui.addLog('warning', '████ EMERGENCY STOP ████');
|
|
1811
|
+
|
|
1812
|
+
// Close all positions
|
|
1813
|
+
for (const [symbolName, pm] of Object.entries(positionManagers)) {
|
|
1814
|
+
try {
|
|
1815
|
+
if (pm.hasPosition(symbolName)) {
|
|
1816
|
+
pm.closePosition(symbolName);
|
|
1817
|
+
ui.addLog('info', `[${symbolName}] Closing position...`);
|
|
1818
|
+
}
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
ui.addLog('error', `[${symbolName}] Close failed: ${e.message}`);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
ui.addLog('info', 'All close orders sent');
|
|
1825
|
+
ui.render(stats);
|
|
1826
|
+
};
|
|
1827
|
+
|
|
1828
|
+
// Keyboard handler
|
|
1829
|
+
let emergencyStopInProgress = false;
|
|
1830
|
+
|
|
1831
|
+
const setupKeyHandler = () => {
|
|
1832
|
+
if (!process.stdin.isTTY) return;
|
|
1833
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1834
|
+
process.stdin.setRawMode(true);
|
|
1835
|
+
process.stdin.resume();
|
|
1836
|
+
|
|
1837
|
+
const onKey = async (str, key) => {
|
|
1838
|
+
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
1839
|
+
if (emergencyStopInProgress) return;
|
|
1840
|
+
emergencyStopInProgress = true;
|
|
1841
|
+
running = false;
|
|
1842
|
+
stopReason = 'manual';
|
|
1843
|
+
await emergencyStopAll();
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
process.stdin.on('keypress', onKey);
|
|
1847
|
+
return () => {
|
|
1848
|
+
process.stdin.removeListener('keypress', onKey);
|
|
1849
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1850
|
+
};
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
const cleanupKeys = setupKeyHandler();
|
|
1854
|
+
|
|
1855
|
+
// UI refresh
|
|
1856
|
+
const refreshInterval = setInterval(() => {
|
|
1857
|
+
if (running) ui.render(stats);
|
|
1858
|
+
}, 1000);
|
|
1859
|
+
|
|
1860
|
+
// Wait for stop
|
|
1861
|
+
await new Promise(resolve => {
|
|
1862
|
+
const check = setInterval(() => {
|
|
1863
|
+
if (!running) {
|
|
1864
|
+
clearInterval(check);
|
|
1865
|
+
resolve();
|
|
1866
|
+
}
|
|
1867
|
+
}, 100);
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1871
|
+
// CLEANUP
|
|
1872
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1873
|
+
clearInterval(refreshInterval);
|
|
1874
|
+
|
|
1875
|
+
// Stop all position managers
|
|
1876
|
+
for (const pm of Object.values(positionManagers)) {
|
|
1877
|
+
try { pm.stop(); } catch {}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Disconnect market feed
|
|
1881
|
+
try { await marketFeed.disconnect(); } catch {}
|
|
1882
|
+
|
|
1883
|
+
// Cleanup keyboard
|
|
1884
|
+
try { if (cleanupKeys) cleanupKeys(); } catch {}
|
|
1885
|
+
try { ui.cleanup(); } catch {}
|
|
1886
|
+
|
|
1887
|
+
try {
|
|
1888
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1889
|
+
process.stdin.resume();
|
|
1890
|
+
} catch {}
|
|
1891
|
+
|
|
1892
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1893
|
+
// SESSION SUMMARY
|
|
1894
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1895
|
+
const durationMs = Date.now() - stats.startTime;
|
|
1896
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
1897
|
+
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
1898
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
1899
|
+
const duration = hours > 0
|
|
1900
|
+
? `${hours}h ${minutes}m ${seconds}s`
|
|
1901
|
+
: minutes > 0
|
|
1902
|
+
? `${minutes}m ${seconds}s`
|
|
1903
|
+
: `${seconds}s`;
|
|
1904
|
+
|
|
1905
|
+
console.log();
|
|
1906
|
+
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
|
|
1907
|
+
console.log(chalk.cyan(' MULTI-SYMBOL SESSION SUMMARY'));
|
|
1908
|
+
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
|
|
1909
|
+
console.log();
|
|
1910
|
+
|
|
1911
|
+
for (const [symbol, symStats] of Object.entries(stats.symbolStats)) {
|
|
1912
|
+
const winRate = symStats.trades > 0 ? ((symStats.wins / symStats.trades) * 100).toFixed(0) : 0;
|
|
1913
|
+
const pnlColor = symStats.pnl >= 0 ? chalk.green : chalk.red;
|
|
1914
|
+
console.log(chalk.white(` ${symbol}:`));
|
|
1915
|
+
console.log(` Trades: ${symStats.trades} | WR: ${winRate}% | P&L: ${pnlColor('$' + symStats.pnl.toFixed(2))}`);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
console.log();
|
|
1919
|
+
const totalPnlColor = stats.sessionPnl >= 0 ? chalk.green : chalk.red;
|
|
1920
|
+
console.log(chalk.white(` TOTAL: ${stats.trades} trades | ${stats.wins}W/${stats.losses}L`));
|
|
1921
|
+
console.log(chalk.white(` Session P&L: ${totalPnlColor('$' + stats.sessionPnl.toFixed(2))}`));
|
|
1922
|
+
console.log(chalk.white(` Duration: ${duration}`));
|
|
1923
|
+
console.log();
|
|
1924
|
+
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
|
|
1925
|
+
|
|
1926
|
+
await prompts.waitForEnter();
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1286
1929
|
module.exports = { oneAccountMenu };
|