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