hedgequantx 2.6.163 → 2.7.1
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/README.md +15 -88
- package/bin/cli.js +0 -11
- package/dist/lib/api.jsc +0 -0
- package/dist/lib/api2.jsc +0 -0
- package/dist/lib/core.jsc +0 -0
- package/dist/lib/core2.jsc +0 -0
- package/dist/lib/data.js +1 -1
- package/dist/lib/data.jsc +0 -0
- package/dist/lib/data2.jsc +0 -0
- package/dist/lib/decoder.jsc +0 -0
- package/dist/lib/m/mod1.jsc +0 -0
- package/dist/lib/m/mod2.jsc +0 -0
- package/dist/lib/n/r1.jsc +0 -0
- package/dist/lib/n/r2.jsc +0 -0
- package/dist/lib/n/r3.jsc +0 -0
- package/dist/lib/n/r4.jsc +0 -0
- package/dist/lib/n/r5.jsc +0 -0
- package/dist/lib/n/r6.jsc +0 -0
- package/dist/lib/n/r7.jsc +0 -0
- package/dist/lib/o/util1.jsc +0 -0
- package/dist/lib/o/util2.jsc +0 -0
- package/package.json +8 -5
- package/src/app.js +40 -162
- package/src/config/constants.js +31 -33
- package/src/config/propfirms.js +13 -217
- package/src/config/settings.js +0 -43
- package/src/lib/api.js +198 -0
- package/src/lib/api2.js +353 -0
- package/src/lib/core.js +539 -0
- package/src/lib/core2.js +341 -0
- package/src/lib/data.js +555 -0
- package/src/lib/data2.js +492 -0
- package/src/lib/decoder.js +599 -0
- package/src/lib/m/s1.js +804 -0
- package/src/lib/m/s2.js +34 -0
- package/src/lib/n/r1.js +454 -0
- package/src/lib/n/r2.js +514 -0
- package/src/lib/n/r3.js +631 -0
- package/src/lib/n/r4.js +401 -0
- package/src/lib/n/r5.js +335 -0
- package/src/lib/n/r6.js +425 -0
- package/src/lib/n/r7.js +530 -0
- package/src/lib/o/l1.js +44 -0
- package/src/lib/o/l2.js +427 -0
- package/src/lib/python-bridge.js +206 -0
- package/src/menus/connect.js +14 -176
- package/src/menus/dashboard.js +65 -110
- package/src/pages/accounts.js +18 -18
- package/src/pages/algo/copy-trading.js +210 -240
- package/src/pages/algo/index.js +41 -104
- package/src/pages/algo/one-account.js +386 -33
- package/src/pages/algo/ui.js +312 -151
- package/src/pages/orders.js +3 -3
- package/src/pages/positions.js +3 -3
- package/src/pages/stats/chart.js +74 -0
- package/src/pages/stats/display.js +228 -0
- package/src/pages/stats/index.js +236 -0
- package/src/pages/stats/metrics.js +213 -0
- package/src/pages/user.js +6 -6
- package/src/services/hqx-server/constants.js +55 -0
- package/src/services/hqx-server/index.js +401 -0
- package/src/services/hqx-server/latency.js +81 -0
- package/src/services/index.js +12 -3
- package/src/services/rithmic/accounts.js +7 -32
- package/src/services/rithmic/connection.js +1 -204
- package/src/services/rithmic/contracts.js +116 -99
- package/src/services/rithmic/handlers.js +21 -196
- package/src/services/rithmic/index.js +63 -120
- package/src/services/rithmic/market.js +31 -0
- package/src/services/rithmic/orders.js +5 -111
- package/src/services/rithmic/protobuf.js +384 -138
- package/src/services/session.js +22 -173
- package/src/ui/box.js +10 -18
- package/src/ui/index.js +1 -3
- package/src/ui/menu.js +1 -1
- package/src/utils/prompts.js +2 -2
- package/dist/lib/m/s1.js +0 -1
- package/src/menus/ai-agent-connect.js +0 -181
- package/src/menus/ai-agent-models.js +0 -219
- package/src/menus/ai-agent-oauth.js +0 -292
- package/src/menus/ai-agent-ui.js +0 -141
- package/src/menus/ai-agent.js +0 -484
- package/src/pages/algo/algo-config.js +0 -195
- package/src/pages/algo/algo-multi.js +0 -801
- package/src/pages/algo/algo-utils.js +0 -58
- package/src/pages/algo/copy-engine.js +0 -449
- package/src/pages/algo/custom-strategy.js +0 -459
- package/src/pages/algo/logger.js +0 -245
- package/src/pages/algo/smart-logs-data.js +0 -218
- package/src/pages/algo/smart-logs.js +0 -387
- package/src/pages/algo/ui-constants.js +0 -144
- package/src/pages/algo/ui-summary.js +0 -184
- package/src/pages/stats-calculations.js +0 -191
- package/src/pages/stats-ui.js +0 -381
- package/src/pages/stats.js +0 -339
- package/src/services/ai/client-analysis.js +0 -194
- package/src/services/ai/client-models.js +0 -333
- package/src/services/ai/client.js +0 -343
- package/src/services/ai/index.js +0 -384
- package/src/services/ai/oauth-anthropic.js +0 -265
- package/src/services/ai/oauth-gemini.js +0 -223
- package/src/services/ai/oauth-iflow.js +0 -269
- package/src/services/ai/oauth-openai.js +0 -233
- package/src/services/ai/oauth-qwen.js +0 -279
- package/src/services/ai/providers/direct-providers.js +0 -323
- package/src/services/ai/providers/index.js +0 -62
- package/src/services/ai/providers/other-providers.js +0 -104
- package/src/services/ai/proxy-install.js +0 -249
- package/src/services/ai/proxy-manager.js +0 -494
- package/src/services/ai/proxy-remote.js +0 -161
- package/src/services/ai/strategy-supervisor.js +0 -1312
- package/src/services/ai/supervisor-data.js +0 -195
- package/src/services/ai/supervisor-optimize.js +0 -215
- package/src/services/ai/supervisor-sync.js +0 -178
- package/src/services/ai/supervisor-utils.js +0 -158
- package/src/services/ai/supervisor.js +0 -484
- package/src/services/ai/validation.js +0 -250
- package/src/services/hqx-server-events.js +0 -110
- package/src/services/hqx-server-handlers.js +0 -217
- package/src/services/hqx-server-latency.js +0 -136
- package/src/services/hqx-server.js +0 -403
- package/src/services/position-constants.js +0 -28
- package/src/services/position-exit-logic.js +0 -174
- package/src/services/position-manager.js +0 -438
- package/src/services/position-momentum.js +0 -206
- package/src/services/projectx/accounts.js +0 -142
- package/src/services/projectx/index.js +0 -443
- package/src/services/projectx/market.js +0 -172
- package/src/services/projectx/stats.js +0 -110
- package/src/services/projectx/trading.js +0 -180
- package/src/services/rithmic/latency-tracker.js +0 -182
- package/src/services/rithmic/market-data-decoders.js +0 -229
- package/src/services/rithmic/market-data.js +0 -272
- package/src/services/rithmic/orders-fast.js +0 -246
- package/src/services/rithmic/proto-decoders.js +0 -403
- package/src/services/rithmic/specs.js +0 -146
- package/src/services/rithmic/trade-history.js +0 -254
- package/src/services/session-history.js +0 -475
- package/src/services/strategy/hft-signal-calc.js +0 -147
- package/src/services/strategy/hft-tick.js +0 -407
- package/src/services/strategy/recovery-math.js +0 -402
- package/src/services/tradovate/constants.js +0 -109
- package/src/services/tradovate/index.js +0 -392
- package/src/services/tradovate/market.js +0 -47
- package/src/services/tradovate/orders.js +0 -145
- package/src/services/tradovate/websocket.js +0 -97
|
@@ -1,801 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Multi-Symbol Algo Trading (Rithmic)
|
|
3
|
-
* Handles trading multiple symbols simultaneously
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
const chalk = require('chalk');
|
|
9
|
-
const readline = require('readline');
|
|
10
|
-
|
|
11
|
-
const { AlgoUI, renderMultiSymbolSummary } = require('./ui');
|
|
12
|
-
const { prompts } = require('../../utils');
|
|
13
|
-
const { FAST_SCALPING } = require('../../config/settings');
|
|
14
|
-
const { PositionManager } = require('../../services/position-manager');
|
|
15
|
-
const { hftStrategy } = require('../../services/strategy/hft-tick');
|
|
16
|
-
const { RithmicMarketDataFeed } = require('../../services/rithmic/market-data');
|
|
17
|
-
const { algoLogger } = require('./logger');
|
|
18
|
-
const { recoveryMath } = require('../../services/strategy/recovery-math');
|
|
19
|
-
const aiService = require('../../services/ai');
|
|
20
|
-
const StrategySupervisor = require('../../services/ai/strategy-supervisor');
|
|
21
|
-
const { formatPrice } = require('./algo-utils');
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Launch multi-symbol algo trading (Rithmic only)
|
|
25
|
-
* Uses single market feed connection with multiple subscriptions
|
|
26
|
-
* Each symbol has its own PositionManager and strategy instance
|
|
27
|
-
*
|
|
28
|
-
* Same logic as launchAlgo but for multiple symbols
|
|
29
|
-
*
|
|
30
|
-
* @param {Object} service - Rithmic trading service
|
|
31
|
-
* @param {Object} account - Trading account
|
|
32
|
-
* @param {Array} contracts - Array of contracts to trade
|
|
33
|
-
* @param {Object} config - Algo configuration
|
|
34
|
-
* @param {Function} [CustomStrategyClass] - Optional custom strategy class (if not provided, uses hftStrategy)
|
|
35
|
-
*/
|
|
36
|
-
const launchMultiSymbolRithmic = async (service, account, contracts, config, CustomStrategyClass = null) => {
|
|
37
|
-
const { dailyTarget, maxRisk, showName, enableAI } = config;
|
|
38
|
-
|
|
39
|
-
const accountName = showName
|
|
40
|
-
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
41
|
-
: 'HQX *****';
|
|
42
|
-
const rithmicAccountId = account.rithmicAccountId || account.accountId;
|
|
43
|
-
|
|
44
|
-
// Build symbols string for UI (clean names without X1 suffix, symbol only)
|
|
45
|
-
const symbolsDisplay = contracts.map(c => {
|
|
46
|
-
let name = c.name || c.symbol;
|
|
47
|
-
// Remove X1, X2 suffix (Rithmic internal suffixes)
|
|
48
|
-
name = name.replace(/X\d+$/, '');
|
|
49
|
-
return name;
|
|
50
|
-
}).join(', ');
|
|
51
|
-
|
|
52
|
-
const ui = new AlgoUI({
|
|
53
|
-
subtitle: `MULTI-SYMBOL (${contracts.length})`,
|
|
54
|
-
mode: 'one-account'
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Calculate total qty across all symbols
|
|
58
|
-
const totalQty = contracts.reduce((sum, c) => sum + (c.qty || 1), 0);
|
|
59
|
-
|
|
60
|
-
// Baseline P&L captured at session start (to show only THIS session's P&L)
|
|
61
|
-
let baselineClosedPnl = null; // Will be set on first pnlUpdate
|
|
62
|
-
|
|
63
|
-
// Shared stats (same structure as launchAlgo)
|
|
64
|
-
const stats = {
|
|
65
|
-
accountName,
|
|
66
|
-
symbol: symbolsDisplay,
|
|
67
|
-
qty: totalQty,
|
|
68
|
-
target: dailyTarget,
|
|
69
|
-
risk: maxRisk,
|
|
70
|
-
propfirm: account.propfirm || 'Unknown',
|
|
71
|
-
platform: 'RITHMIC',
|
|
72
|
-
pnl: null,
|
|
73
|
-
openPnl: 0, // Start at 0 for session
|
|
74
|
-
closedPnl: 0, // Start at 0 for session (will show delta from baseline)
|
|
75
|
-
balance: null,
|
|
76
|
-
buyingPower: null,
|
|
77
|
-
margin: null,
|
|
78
|
-
netLiquidation: null,
|
|
79
|
-
position: 0,
|
|
80
|
-
entryPrice: 0,
|
|
81
|
-
lastPrice: 0,
|
|
82
|
-
trades: 0,
|
|
83
|
-
wins: 0,
|
|
84
|
-
losses: 0,
|
|
85
|
-
sessionPnl: 0,
|
|
86
|
-
latency: 0,
|
|
87
|
-
connected: false,
|
|
88
|
-
startTime: Date.now(),
|
|
89
|
-
aiSupervision: enableAI || false,
|
|
90
|
-
aiMode: null,
|
|
91
|
-
agentCount: 0,
|
|
92
|
-
fastPath: true,
|
|
93
|
-
avgEntryLatency: 0,
|
|
94
|
-
avgFillLatency: 0,
|
|
95
|
-
entryLatencies: [],
|
|
96
|
-
// Per-symbol tracking
|
|
97
|
-
symbolStats: {},
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Initialize per-symbol stats
|
|
101
|
-
contracts.forEach(c => {
|
|
102
|
-
const name = c.name || c.symbol;
|
|
103
|
-
stats.symbolStats[name] = {
|
|
104
|
-
position: 0,
|
|
105
|
-
trades: 0,
|
|
106
|
-
wins: 0,
|
|
107
|
-
losses: 0,
|
|
108
|
-
pnl: 0,
|
|
109
|
-
openPnl: 0, // Per-symbol unrealized P&L
|
|
110
|
-
tickCount: 0,
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
let running = true;
|
|
115
|
-
let stopReason = null;
|
|
116
|
-
let tickCount = 0;
|
|
117
|
-
|
|
118
|
-
// Store contract info for later use (including qty per symbol)
|
|
119
|
-
const contractInfoMap = {};
|
|
120
|
-
contracts.forEach(c => {
|
|
121
|
-
const name = c.name || c.symbol;
|
|
122
|
-
contractInfoMap[name] = {
|
|
123
|
-
tickSize: c.tickSize ?? null,
|
|
124
|
-
tickValue: c.tickValue ?? null,
|
|
125
|
-
contractId: c.id || c.symbol || c.name,
|
|
126
|
-
exchange: c.exchange || 'CME',
|
|
127
|
-
qty: c.qty || 1, // Per-symbol quantity
|
|
128
|
-
};
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
132
|
-
// AI SUPERVISOR INITIALIZATION (same as single-symbol)
|
|
133
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
134
|
-
let aiAgentCount = 0;
|
|
135
|
-
if (enableAI) {
|
|
136
|
-
const aiAgents = aiService.getAgents();
|
|
137
|
-
aiAgentCount = aiAgents.length;
|
|
138
|
-
stats.agentCount = aiAgentCount;
|
|
139
|
-
if (aiAgents.length > 0) {
|
|
140
|
-
// Use first symbol's strategy for AI supervisor
|
|
141
|
-
const firstContract = contracts[0];
|
|
142
|
-
const firstSymbolName = firstContract.name || firstContract.symbol;
|
|
143
|
-
// Strategy will be created below, so we init supervisor after strategies
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
148
|
-
// POSITION MANAGERS & STRATEGIES - One per symbol
|
|
149
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
150
|
-
const positionManagers = {};
|
|
151
|
-
const strategies = {};
|
|
152
|
-
const pendingOrders = {}; // Track pending orders per symbol
|
|
153
|
-
// No disabled symbols - all symbols can trade until Target/Risk reached
|
|
154
|
-
|
|
155
|
-
contracts.forEach(contract => {
|
|
156
|
-
const symbolName = contract.name || contract.symbol;
|
|
157
|
-
const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
|
|
158
|
-
|
|
159
|
-
// Create strategy instance for this symbol
|
|
160
|
-
// Use custom strategy class if provided, otherwise use default hftStrategy
|
|
161
|
-
let strategy;
|
|
162
|
-
if (CustomStrategyClass) {
|
|
163
|
-
strategy = new CustomStrategyClass();
|
|
164
|
-
} else {
|
|
165
|
-
strategy = Object.create(hftStrategy);
|
|
166
|
-
}
|
|
167
|
-
if (tickSize !== null && tickValue !== null) {
|
|
168
|
-
strategy.initialize(contractId, tickSize, tickValue);
|
|
169
|
-
}
|
|
170
|
-
strategies[symbolName] = strategy;
|
|
171
|
-
pendingOrders[symbolName] = false;
|
|
172
|
-
|
|
173
|
-
// Create position manager for this symbol
|
|
174
|
-
const pm = new PositionManager(service, strategy);
|
|
175
|
-
if (tickSize !== null && tickValue !== null) {
|
|
176
|
-
pm.setContractInfo(symbolName, { tickSize, tickValue, contractId });
|
|
177
|
-
}
|
|
178
|
-
pm.start();
|
|
179
|
-
positionManagers[symbolName] = pm;
|
|
180
|
-
|
|
181
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
182
|
-
// POSITION MANAGER EVENTS (same as launchAlgo)
|
|
183
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
184
|
-
pm.on('entryFilled', ({ position, fillLatencyMs }) => {
|
|
185
|
-
stats.entryLatencies.push(fillLatencyMs);
|
|
186
|
-
stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
|
|
187
|
-
const side = position.side === 0 ? 'LONG' : 'SHORT';
|
|
188
|
-
const priceStr = formatPrice(position.entryPrice, tickSize || 0.25);
|
|
189
|
-
ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${priceStr} | ${fillLatencyMs}ms`);
|
|
190
|
-
stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
|
|
191
|
-
ui.render(stats);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
|
|
195
|
-
const holdSec = (holdDurationMs / 1000).toFixed(1);
|
|
196
|
-
if (pnlTicks !== null && tickValue !== null) {
|
|
197
|
-
const pnlDollars = pnlTicks * tickValue;
|
|
198
|
-
stats.sessionPnl += pnlDollars;
|
|
199
|
-
stats.symbolStats[symbolName].pnl += pnlDollars;
|
|
200
|
-
stats.symbolStats[symbolName].trades++;
|
|
201
|
-
stats.trades++;
|
|
202
|
-
|
|
203
|
-
// Record trade for Recovery Math
|
|
204
|
-
recoveryMath.recordTrade({
|
|
205
|
-
pnl: pnlDollars,
|
|
206
|
-
ticks: pnlTicks,
|
|
207
|
-
side: pnlTicks >= 0 ? 'win' : 'loss',
|
|
208
|
-
duration: holdDurationMs,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Update Recovery Mode state
|
|
212
|
-
const recovery = recoveryMath.updateSessionPnL(
|
|
213
|
-
stats.sessionPnl,
|
|
214
|
-
FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
|
|
215
|
-
FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
if (recovery.justActivated) {
|
|
219
|
-
stats.recoveryMode = true;
|
|
220
|
-
ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}%`);
|
|
221
|
-
} else if (recovery.justDeactivated) {
|
|
222
|
-
stats.recoveryMode = false;
|
|
223
|
-
ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (pnlDollars >= 0) {
|
|
227
|
-
stats.wins++;
|
|
228
|
-
stats.symbolStats[symbolName].wins++;
|
|
229
|
-
const priceStr = formatPrice(exitPrice, tickSize || 0.25);
|
|
230
|
-
ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} @ ${priceStr} | ${holdSec}s`);
|
|
231
|
-
} else {
|
|
232
|
-
stats.losses++;
|
|
233
|
-
stats.symbolStats[symbolName].losses++;
|
|
234
|
-
const priceStr = formatPrice(exitPrice, tickSize || 0.25);
|
|
235
|
-
ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} @ ${priceStr} | ${holdSec}s`);
|
|
236
|
-
// Symbol can trade again - no disable, continue until Target/Risk reached
|
|
237
|
-
}
|
|
238
|
-
} else if (pnlTicks !== null) {
|
|
239
|
-
// Log with ticks only
|
|
240
|
-
stats.trades++;
|
|
241
|
-
stats.symbolStats[symbolName].trades++;
|
|
242
|
-
if (pnlTicks >= 0) {
|
|
243
|
-
stats.wins++;
|
|
244
|
-
stats.symbolStats[symbolName].wins++;
|
|
245
|
-
ui.addLog('win', `[${symbolName}] +${pnlTicks} ticks | ${holdSec}s`);
|
|
246
|
-
} else {
|
|
247
|
-
stats.losses++;
|
|
248
|
-
stats.symbolStats[symbolName].losses++;
|
|
249
|
-
ui.addLog('loss', `[${symbolName}] ${pnlTicks} ticks | ${holdSec}s`);
|
|
250
|
-
// Symbol can trade again - no disable, continue until Target/Risk reached
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
stats.symbolStats[symbolName].position = 0;
|
|
254
|
-
stats.symbolStats[symbolName].openPnl = 0; // Reset open P&L when position closed
|
|
255
|
-
// Recalculate total Open P&L
|
|
256
|
-
stats.openPnl = Object.values(stats.symbolStats).reduce((sum, s) => sum + (s.openPnl || 0), 0);
|
|
257
|
-
pendingOrders[symbolName] = false;
|
|
258
|
-
ui.render(stats);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
pm.on('holdComplete', () => {
|
|
262
|
-
ui.addLog('ready', `[${symbolName}] Hold complete - monitoring exit`);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
pm.on('breakevenActivated', ({ breakevenPrice, pnlTicks }) => {
|
|
266
|
-
const priceStr = formatPrice(breakevenPrice, tickSize || 0.25);
|
|
267
|
-
ui.addLog('be', `[${symbolName}] BE @ ${priceStr} | +${pnlTicks} ticks`);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
271
|
-
// STRATEGY SIGNALS
|
|
272
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
273
|
-
strategy.on('signal', async (signal) => {
|
|
274
|
-
if (!running) return;
|
|
275
|
-
if (pendingOrders[symbolName]) return;
|
|
276
|
-
if (!pm.canEnter(symbolName)) return;
|
|
277
|
-
|
|
278
|
-
const { direction, confidence } = signal;
|
|
279
|
-
const orderSide = direction === 'long' ? 0 : 1;
|
|
280
|
-
const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
|
|
281
|
-
|
|
282
|
-
// Use per-symbol quantity
|
|
283
|
-
const symbolQty = contractInfoMap[symbolName].qty;
|
|
284
|
-
|
|
285
|
-
// Calculate risk amount
|
|
286
|
-
const kelly = Math.min(0.25, confidence || 0.15);
|
|
287
|
-
const riskAmount = Math.round(maxRisk * kelly);
|
|
288
|
-
const riskPct = Math.round((riskAmount / maxRisk) * 100);
|
|
289
|
-
|
|
290
|
-
pendingOrders[symbolName] = true;
|
|
291
|
-
ui.addLog('entry', `[${symbolName}] ${sideStr} ${symbolQty}x | risk: $${riskAmount} (${riskPct}%)`);
|
|
292
|
-
|
|
293
|
-
const orderData = {
|
|
294
|
-
accountId: rithmicAccountId,
|
|
295
|
-
symbol: symbolName,
|
|
296
|
-
exchange: contractInfoMap[symbolName].exchange,
|
|
297
|
-
size: symbolQty,
|
|
298
|
-
side: orderSide,
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
const entryResult = service.fastEntry(orderData);
|
|
303
|
-
if (entryResult.success) {
|
|
304
|
-
pm.registerEntry(entryResult, orderData, contractInfoMap[symbolName]);
|
|
305
|
-
|
|
306
|
-
// Update avg entry latency
|
|
307
|
-
stats.avgEntryLatency = stats.entryLatencies.length > 0
|
|
308
|
-
? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
|
|
309
|
-
: entryResult.latencyMs;
|
|
310
|
-
} else {
|
|
311
|
-
ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
|
|
312
|
-
pendingOrders[symbolName] = false;
|
|
313
|
-
}
|
|
314
|
-
} catch (e) {
|
|
315
|
-
ui.addLog('error', `[${symbolName}] Order error: ${e.message}`);
|
|
316
|
-
pendingOrders[symbolName] = false;
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
322
|
-
// AI SUPERVISOR - Initialize after strategies are created
|
|
323
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
324
|
-
if (enableAI && aiAgentCount > 0) {
|
|
325
|
-
const aiAgents = aiService.getAgents();
|
|
326
|
-
const firstSymbol = Object.keys(strategies)[0];
|
|
327
|
-
const firstStrategy = strategies[firstSymbol];
|
|
328
|
-
const supervisorResult = StrategySupervisor.initialize(firstStrategy, aiAgents, service, rithmicAccountId);
|
|
329
|
-
stats.aiSupervision = supervisorResult.success;
|
|
330
|
-
stats.aiMode = supervisorResult.mode;
|
|
331
|
-
|
|
332
|
-
if (stats.aiSupervision) {
|
|
333
|
-
algoLogger.info(ui, 'AI SUPERVISION', `${aiAgentCount} agent(s) - ${stats.aiMode} mode - LEARNING ACTIVE`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
338
|
-
// MARKET DATA FEED - Single connection, multiple subscriptions
|
|
339
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
340
|
-
const marketFeed = new RithmicMarketDataFeed(service);
|
|
341
|
-
|
|
342
|
-
marketFeed.on('connected', () => {
|
|
343
|
-
stats.connected = true;
|
|
344
|
-
algoLogger.dataConnected(ui, 'RTC');
|
|
345
|
-
algoLogger.algoOperational(ui, 'RITHMIC');
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Smart logs state tracking
|
|
349
|
-
let lastHeartbeat = Date.now();
|
|
350
|
-
let tps = 0;
|
|
351
|
-
|
|
352
|
-
marketFeed.on('tick', (tick) => {
|
|
353
|
-
if (!running) return;
|
|
354
|
-
|
|
355
|
-
tickCount++;
|
|
356
|
-
tps++;
|
|
357
|
-
stats.latency = tick.latency || 0;
|
|
358
|
-
|
|
359
|
-
// Route tick to correct strategy based on symbol
|
|
360
|
-
const tickSymbol = tick.symbol;
|
|
361
|
-
|
|
362
|
-
// Find matching strategy (ES matches ESH6, NQ matches NQH6, etc.)
|
|
363
|
-
for (const [symbolName, strategy] of Object.entries(strategies)) {
|
|
364
|
-
const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
|
|
365
|
-
if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
|
|
366
|
-
stats.symbolStats[symbolName].tickCount++;
|
|
367
|
-
|
|
368
|
-
// Log first tick per symbol
|
|
369
|
-
if (stats.symbolStats[symbolName].tickCount === 1) {
|
|
370
|
-
algoLogger.info(ui, 'FIRST TICK', `[${symbolName}] price=${parseFloat(Number(tick.price).toFixed(6))} bid=${parseFloat(Number(tick.bid).toFixed(6))} ask=${parseFloat(Number(tick.ask).toFixed(6))}`);
|
|
371
|
-
} else if (stats.symbolStats[symbolName].tickCount === 100) {
|
|
372
|
-
algoLogger.info(ui, 'DATA FLOWING', `[${symbolName}] 100 ticks received`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const tickData = {
|
|
376
|
-
contractId: tick.contractId || symbolName,
|
|
377
|
-
price: tick.price || tick.lastPrice || tick.bid,
|
|
378
|
-
bid: tick.bid,
|
|
379
|
-
ask: tick.ask,
|
|
380
|
-
volume: tick.volume || tick.size || 1,
|
|
381
|
-
side: tick.lastTradeSide || tick.side || 'unknown',
|
|
382
|
-
timestamp: tick.timestamp || Date.now()
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
// Update last price
|
|
386
|
-
stats.lastPrice = tickData.price;
|
|
387
|
-
|
|
388
|
-
// Feed tick to strategy
|
|
389
|
-
strategy.processTick(tickData);
|
|
390
|
-
|
|
391
|
-
// Update price for position manager
|
|
392
|
-
service.emit('priceUpdate', {
|
|
393
|
-
symbol: symbolName,
|
|
394
|
-
price: tickData.price,
|
|
395
|
-
timestamp: tickData.timestamp,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// Get momentum data from strategy for position manager
|
|
399
|
-
const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(symbolName);
|
|
400
|
-
if (modelValues && positionManagers[symbolName] && typeof positionManagers[symbolName].updateMomentum === 'function') {
|
|
401
|
-
positionManagers[symbolName].updateMomentum(symbolName, {
|
|
402
|
-
ofi: modelValues.ofi || 0,
|
|
403
|
-
zscore: modelValues.zscore || 0,
|
|
404
|
-
delta: modelValues.delta || 0,
|
|
405
|
-
timestamp: tickData.timestamp,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
414
|
-
// SMART LOGS - Same as single-symbol mode
|
|
415
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
416
|
-
const now = Date.now();
|
|
417
|
-
if (now - lastHeartbeat > 1000) {
|
|
418
|
-
// Get model values from first active symbol's strategy
|
|
419
|
-
const firstSymbol = Object.keys(strategies)[0];
|
|
420
|
-
const firstStrategy = strategies[firstSymbol];
|
|
421
|
-
const modelValues = firstStrategy?.getModelValues?.() || firstStrategy?.getModelValues?.(firstSymbol) || null;
|
|
422
|
-
|
|
423
|
-
if (modelValues && modelValues.ofi !== undefined) {
|
|
424
|
-
const ofi = modelValues.ofi || 0;
|
|
425
|
-
const delta = modelValues.delta || 0;
|
|
426
|
-
const zscore = modelValues.zscore || 0;
|
|
427
|
-
const mom = modelValues.momentum || 0;
|
|
428
|
-
|
|
429
|
-
// Check if any symbol has an open position
|
|
430
|
-
const totalPosition = Object.values(stats.symbolStats).reduce((sum, s) => sum + Math.abs(s.position || 0), 0);
|
|
431
|
-
|
|
432
|
-
if (totalPosition === 0) {
|
|
433
|
-
// Not in position - show market analysis (varied messages)
|
|
434
|
-
const smartLogs = require('./smart-logs');
|
|
435
|
-
const stateLog = smartLogs.getMarketStateLog(ofi, zscore, mom, delta);
|
|
436
|
-
if (stateLog.details) {
|
|
437
|
-
ui.addLog('analysis', `${stateLog.message} - ${stateLog.details}`);
|
|
438
|
-
} else {
|
|
439
|
-
ui.addLog('info', stateLog.message);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// When IN POSITION: Don't spam logs every second
|
|
443
|
-
} else {
|
|
444
|
-
// Waiting for data - log every 5 seconds only
|
|
445
|
-
if (now - lastHeartbeat > 5000) {
|
|
446
|
-
const smartLogs = require('./smart-logs');
|
|
447
|
-
const scanLog = smartLogs.getScanningLog(true);
|
|
448
|
-
ui.addLog('info', `${scanLog.message} ${tps} ticks/s`);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
lastHeartbeat = now;
|
|
452
|
-
tps = 0;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
ui.render(stats);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
marketFeed.on('error', (err) => {
|
|
459
|
-
algoLogger.error(ui, 'MARKET ERROR', err.message);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
marketFeed.on('disconnected', (err) => {
|
|
463
|
-
stats.connected = false;
|
|
464
|
-
algoLogger.dataDisconnected(ui, 'WEBSOCKET', err?.message);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
468
|
-
// STARTUP LOGS (same as launchAlgo)
|
|
469
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
470
|
-
const market = checkMarketHours();
|
|
471
|
-
const sessionName = market.session || 'AMERICAN';
|
|
472
|
-
const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
|
|
473
|
-
|
|
474
|
-
algoLogger.connectingToEngine(ui, account.accountId);
|
|
475
|
-
algoLogger.engineStarting(ui, 'RITHMIC', dailyTarget, maxRisk);
|
|
476
|
-
algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
|
|
477
|
-
algoLogger.info(ui, 'FAST PATH', `Rithmic direct | ${contracts.length} symbols | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms`);
|
|
478
|
-
|
|
479
|
-
ui.render(stats);
|
|
480
|
-
|
|
481
|
-
// Connect and subscribe
|
|
482
|
-
try {
|
|
483
|
-
algoLogger.info(ui, 'CONNECTING', `RITHMIC TICKER | ${contracts.length} symbols`);
|
|
484
|
-
await marketFeed.connect();
|
|
485
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
486
|
-
|
|
487
|
-
if (marketFeed.isConnected()) {
|
|
488
|
-
for (const contract of contracts) {
|
|
489
|
-
const symbolName = contract.name || contract.symbol;
|
|
490
|
-
const exchange = contract.exchange || 'CME';
|
|
491
|
-
marketFeed.subscribe(symbolName, exchange);
|
|
492
|
-
algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} (${exchange})`);
|
|
493
|
-
}
|
|
494
|
-
} else {
|
|
495
|
-
algoLogger.error(ui, 'CONNECTION', 'Failed to connect market feed');
|
|
496
|
-
}
|
|
497
|
-
} catch (e) {
|
|
498
|
-
algoLogger.error(ui, 'CONNECTION ERROR', e.message);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
502
|
-
// TARGET/RISK CHECK - Stop algo when limits reached
|
|
503
|
-
// Uses SESSION P&L (trades from this HQX session only) + Open P&L
|
|
504
|
-
// Risk triggers if: closedPnl hits risk OR openPnl hits risk OR total hits risk
|
|
505
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
506
|
-
const checkTargetRisk = () => {
|
|
507
|
-
if (!running) return;
|
|
508
|
-
|
|
509
|
-
const closedPnl = stats.closedPnl || 0; // Session closed P&L
|
|
510
|
-
const openPnl = stats.openPnl || 0; // Current open P&L
|
|
511
|
-
const totalPnl = closedPnl + openPnl; // Total session P&L
|
|
512
|
-
|
|
513
|
-
// Daily target reached - STOP with profit
|
|
514
|
-
if (totalPnl >= dailyTarget) {
|
|
515
|
-
stopReason = 'target';
|
|
516
|
-
running = false;
|
|
517
|
-
algoLogger.info(ui, 'TARGET REACHED', `+$${totalPnl.toFixed(2)} >= $${dailyTarget}`);
|
|
518
|
-
ui.addLog('success', `████ DAILY TARGET REACHED: +$${totalPnl.toFixed(2)} ████`);
|
|
519
|
-
emergencyStopAll();
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Max risk reached - STOP to protect capital
|
|
524
|
-
// Trigger if: closed P&L hits risk OR open P&L hits risk OR total hits risk
|
|
525
|
-
if (closedPnl <= -maxRisk) {
|
|
526
|
-
stopReason = 'risk';
|
|
527
|
-
running = false;
|
|
528
|
-
algoLogger.info(ui, 'CLOSED P&L RISK', `Closed: -$${Math.abs(closedPnl).toFixed(2)} <= -$${maxRisk}`);
|
|
529
|
-
ui.addLog('error', `████ MAX RISK (CLOSED): -$${Math.abs(closedPnl).toFixed(2)} ████`);
|
|
530
|
-
emergencyStopAll();
|
|
531
|
-
} else if (openPnl <= -maxRisk) {
|
|
532
|
-
stopReason = 'risk';
|
|
533
|
-
running = false;
|
|
534
|
-
algoLogger.info(ui, 'OPEN P&L RISK', `Open: -$${Math.abs(openPnl).toFixed(2)} <= -$${maxRisk}`);
|
|
535
|
-
ui.addLog('error', `████ MAX RISK (OPEN): -$${Math.abs(openPnl).toFixed(2)} ████`);
|
|
536
|
-
emergencyStopAll();
|
|
537
|
-
} else if (totalPnl <= -maxRisk) {
|
|
538
|
-
stopReason = 'risk';
|
|
539
|
-
running = false;
|
|
540
|
-
algoLogger.info(ui, 'TOTAL P&L RISK', `Total: -$${Math.abs(totalPnl).toFixed(2)} <= -$${maxRisk}`);
|
|
541
|
-
ui.addLog('error', `████ MAX RISK (TOTAL): -$${Math.abs(totalPnl).toFixed(2)} ████`);
|
|
542
|
-
emergencyStopAll();
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
547
|
-
// REAL-TIME P&L VIA WEBSOCKET (same as launchAlgo)
|
|
548
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
549
|
-
if (typeof service.on === 'function') {
|
|
550
|
-
// Account-level P&L updates
|
|
551
|
-
service.on('pnlUpdate', (pnlData) => {
|
|
552
|
-
if (pnlData.accountId !== rithmicAccountId) return;
|
|
553
|
-
|
|
554
|
-
if (pnlData.closedPositionPnl !== undefined) {
|
|
555
|
-
const accountClosedPnl = parseFloat(pnlData.closedPositionPnl);
|
|
556
|
-
|
|
557
|
-
// Capture baseline on first update (P&L at session start)
|
|
558
|
-
if (baselineClosedPnl === null) {
|
|
559
|
-
baselineClosedPnl = accountClosedPnl;
|
|
560
|
-
// Baseline captured silently - no need to show to user
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// stats.closedPnl shows ONLY this session's closed P&L (delta from baseline)
|
|
564
|
-
stats.closedPnl = accountClosedPnl - baselineClosedPnl;
|
|
565
|
-
}
|
|
566
|
-
if (pnlData.accountBalance !== undefined) {
|
|
567
|
-
stats.balance = parseFloat(pnlData.accountBalance);
|
|
568
|
-
}
|
|
569
|
-
if (pnlData.availableBuyingPower !== undefined) {
|
|
570
|
-
stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
|
|
571
|
-
}
|
|
572
|
-
if (pnlData.marginBalance !== undefined) {
|
|
573
|
-
stats.margin = parseFloat(pnlData.marginBalance);
|
|
574
|
-
}
|
|
575
|
-
if (pnlData.netLiquidation !== undefined) {
|
|
576
|
-
stats.netLiquidation = parseFloat(pnlData.netLiquidation);
|
|
577
|
-
} else if (stats.balance !== null) {
|
|
578
|
-
stats.netLiquidation = stats.balance + (stats.openPnl || 0);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
582
|
-
|
|
583
|
-
// Check target/risk on every P&L update
|
|
584
|
-
checkTargetRisk();
|
|
585
|
-
|
|
586
|
-
ui.render(stats);
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// Position-level updates (for Open P&L per symbol)
|
|
590
|
-
service.on('positionUpdate', (pos) => {
|
|
591
|
-
if (!pos || pos.accountId !== rithmicAccountId) return;
|
|
592
|
-
|
|
593
|
-
const posSymbol = pos.symbol;
|
|
594
|
-
for (const symbolName of Object.keys(stats.symbolStats)) {
|
|
595
|
-
const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
|
|
596
|
-
if (posSymbol === baseSymbol || posSymbol === symbolName || symbolName.startsWith(posSymbol)) {
|
|
597
|
-
stats.symbolStats[symbolName].position = pos.quantity || 0;
|
|
598
|
-
|
|
599
|
-
// Update Open P&L for this symbol
|
|
600
|
-
if (pos.openPnl !== undefined && pos.openPnl !== null) {
|
|
601
|
-
stats.symbolStats[symbolName].openPnl = pos.openPnl;
|
|
602
|
-
|
|
603
|
-
// Calculate total Open P&L from all symbols
|
|
604
|
-
stats.openPnl = Object.values(stats.symbolStats).reduce((sum, s) => sum + (s.openPnl || 0), 0);
|
|
605
|
-
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
606
|
-
|
|
607
|
-
if (stats.balance !== null) {
|
|
608
|
-
stats.netLiquidation = stats.balance + stats.openPnl;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Check target/risk on Open P&L changes too
|
|
612
|
-
checkTargetRisk();
|
|
613
|
-
}
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
ui.render(stats);
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
622
|
-
// EMERGENCY STOP - Force close ALL positions
|
|
623
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
624
|
-
const emergencyStopAll = async () => {
|
|
625
|
-
ui.addLog('warning', '████ EMERGENCY STOP INITIATED ████');
|
|
626
|
-
ui.render(stats);
|
|
627
|
-
|
|
628
|
-
const TIMEOUT_MS = 5000;
|
|
629
|
-
const withTimeout = (promise, ms) => Promise.race([
|
|
630
|
-
promise,
|
|
631
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('TIMEOUT')), ms))
|
|
632
|
-
]);
|
|
633
|
-
|
|
634
|
-
try {
|
|
635
|
-
// CRITICAL: Use ONLY Rithmic API to get REAL positions from exchange
|
|
636
|
-
// DO NOT use local stats which can be corrupted
|
|
637
|
-
|
|
638
|
-
// Step 1: Cancel all pending orders first
|
|
639
|
-
ui.addLog('info', 'Cancelling all orders...');
|
|
640
|
-
ui.render(stats);
|
|
641
|
-
try {
|
|
642
|
-
if (service && typeof service.cancelAllOrders === 'function') {
|
|
643
|
-
await withTimeout(service.cancelAllOrders(rithmicAccountId), TIMEOUT_MS);
|
|
644
|
-
ui.addLog('success', 'All orders cancelled');
|
|
645
|
-
}
|
|
646
|
-
} catch (e) {
|
|
647
|
-
ui.addLog('warning', `Cancel orders: ${e.message}`);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Step 2: Use Rithmic flattenAll - reads REAL positions from exchange
|
|
651
|
-
ui.addLog('info', 'Flattening all positions via Rithmic API...');
|
|
652
|
-
ui.render(stats);
|
|
653
|
-
|
|
654
|
-
try {
|
|
655
|
-
if (service && typeof service.flattenAll === 'function') {
|
|
656
|
-
const result = await withTimeout(service.flattenAll(rithmicAccountId), TIMEOUT_MS);
|
|
657
|
-
if (result && result.results) {
|
|
658
|
-
for (const r of result.results) {
|
|
659
|
-
if (r.success) {
|
|
660
|
-
ui.addLog('success', `[${r.symbol}] FLATTENED`);
|
|
661
|
-
} else {
|
|
662
|
-
ui.addLog('error', `[${r.symbol}] ${r.error || 'Failed'}`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
ui.addLog('success', 'Flatten all complete');
|
|
667
|
-
} else if (service && typeof service.emergencyStop === 'function') {
|
|
668
|
-
// Fallback to emergencyStop
|
|
669
|
-
await withTimeout(service.emergencyStop(rithmicAccountId), TIMEOUT_MS);
|
|
670
|
-
ui.addLog('success', 'Emergency stop complete');
|
|
671
|
-
}
|
|
672
|
-
} catch (e) {
|
|
673
|
-
ui.addLog('warning', `Flatten: ${e.message}`);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Step 3: Reset local state
|
|
677
|
-
for (const [symbolName, symStats] of Object.entries(stats.symbolStats)) {
|
|
678
|
-
symStats.position = 0;
|
|
679
|
-
symStats.openPnl = 0;
|
|
680
|
-
// Also reset position manager
|
|
681
|
-
const pm = positionManagers[symbolName];
|
|
682
|
-
if (pm) {
|
|
683
|
-
try { pm.reset?.(); } catch {}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
stats.openPnl = 0;
|
|
687
|
-
|
|
688
|
-
ui.addLog('success', '████ EMERGENCY STOP COMPLETE ████');
|
|
689
|
-
|
|
690
|
-
} catch (e) {
|
|
691
|
-
ui.addLog('error', `Emergency stop error: ${e.message}`);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
ui.render(stats);
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
// Keyboard handler
|
|
698
|
-
let emergencyStopInProgress = false;
|
|
699
|
-
|
|
700
|
-
const setupKeyHandler = () => {
|
|
701
|
-
if (!process.stdin.isTTY) return;
|
|
702
|
-
readline.emitKeypressEvents(process.stdin);
|
|
703
|
-
process.stdin.setRawMode(true);
|
|
704
|
-
process.stdin.resume();
|
|
705
|
-
|
|
706
|
-
const onKey = async (str, key) => {
|
|
707
|
-
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
708
|
-
if (emergencyStopInProgress) return;
|
|
709
|
-
emergencyStopInProgress = true;
|
|
710
|
-
running = false;
|
|
711
|
-
stopReason = 'manual';
|
|
712
|
-
await emergencyStopAll();
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
process.stdin.on('keypress', onKey);
|
|
716
|
-
return () => {
|
|
717
|
-
process.stdin.removeListener('keypress', onKey);
|
|
718
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
719
|
-
};
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
const cleanupKeys = setupKeyHandler();
|
|
723
|
-
|
|
724
|
-
// UI refresh
|
|
725
|
-
const refreshInterval = setInterval(() => {
|
|
726
|
-
if (running) ui.render(stats);
|
|
727
|
-
}, 1000);
|
|
728
|
-
|
|
729
|
-
// Wait for stop
|
|
730
|
-
await new Promise(resolve => {
|
|
731
|
-
const check = setInterval(() => {
|
|
732
|
-
if (!running) {
|
|
733
|
-
clearInterval(check);
|
|
734
|
-
resolve();
|
|
735
|
-
}
|
|
736
|
-
}, 100);
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
740
|
-
// CLEANUP
|
|
741
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
742
|
-
clearInterval(refreshInterval);
|
|
743
|
-
|
|
744
|
-
// Stop all position managers
|
|
745
|
-
for (const pm of Object.values(positionManagers)) {
|
|
746
|
-
try { pm.stop(); } catch {}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Disconnect market feed with timeout
|
|
750
|
-
try {
|
|
751
|
-
const disconnectPromise = marketFeed.disconnect();
|
|
752
|
-
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 2000));
|
|
753
|
-
await Promise.race([disconnectPromise, timeoutPromise]);
|
|
754
|
-
} catch {}
|
|
755
|
-
|
|
756
|
-
// Cleanup keyboard FIRST - restore stdin to normal mode
|
|
757
|
-
try { if (cleanupKeys) cleanupKeys(); } catch {}
|
|
758
|
-
|
|
759
|
-
// Force stdin back to normal mode (critical for waitForEnter to work)
|
|
760
|
-
try {
|
|
761
|
-
if (process.stdin.isTTY) {
|
|
762
|
-
process.stdin.setRawMode(false);
|
|
763
|
-
}
|
|
764
|
-
// Remove all listeners to prevent interference
|
|
765
|
-
process.stdin.removeAllListeners('keypress');
|
|
766
|
-
process.stdin.resume();
|
|
767
|
-
} catch {}
|
|
768
|
-
|
|
769
|
-
// Small delay to let stdin settle
|
|
770
|
-
await new Promise(r => setTimeout(r, 100));
|
|
771
|
-
|
|
772
|
-
// Calculate duration before closeLog
|
|
773
|
-
const durationMs = Date.now() - stats.startTime;
|
|
774
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
775
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
776
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
777
|
-
stats.duration = hours > 0
|
|
778
|
-
? `${hours}h ${minutes}m ${seconds}s`
|
|
779
|
-
: minutes > 0
|
|
780
|
-
? `${minutes}m ${seconds}s`
|
|
781
|
-
: `${seconds}s`;
|
|
782
|
-
|
|
783
|
-
// Close log file with session summary
|
|
784
|
-
try { ui.closeLog(stats); } catch {}
|
|
785
|
-
|
|
786
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
787
|
-
// SESSION SUMMARY (duration already calculated above)
|
|
788
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
789
|
-
// Clear screen, show cursor, and render summary
|
|
790
|
-
process.stdout.write('\x1B[?25h'); // Show cursor
|
|
791
|
-
console.clear();
|
|
792
|
-
|
|
793
|
-
// Render multi-symbol summary with same style as single-symbol
|
|
794
|
-
renderMultiSymbolSummary(stats, stopReason, stats.symbolStats);
|
|
795
|
-
|
|
796
|
-
// Wait for user to press Enter before returning to menu
|
|
797
|
-
await prompts.waitForEnter();
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
module.exports = { launchMultiSymbolRithmic };
|