hedgequantx 2.6.159 → 2.6.160
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/algo-multi.js +801 -0
- package/src/pages/algo/one-account.js +4 -1883
|
@@ -7,30 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
const chalk = require('chalk');
|
|
9
9
|
const ora = require('ora');
|
|
10
|
-
const readline = require('readline');
|
|
11
10
|
|
|
12
11
|
const { connections } = require('../../services');
|
|
13
|
-
const { AlgoUI, renderSessionSummary, renderMultiSymbolSummary } = require('./ui');
|
|
14
12
|
const { prompts } = require('../../utils');
|
|
15
|
-
const { checkMarketHours } = require('../../services/projectx/market');
|
|
16
|
-
const { FAST_SCALPING } = require('../../config/settings');
|
|
17
|
-
const { PositionManager } = require('../../services/position-manager');
|
|
18
|
-
|
|
19
|
-
// Strategy & Market Data
|
|
20
|
-
const { M1 } = require('../../../dist/lib/m/s1');
|
|
21
|
-
const { hftStrategy } = require('../../services/strategy/hft-tick');
|
|
22
|
-
const { MarketDataFeed } = require('../../../dist/lib/data');
|
|
23
|
-
const { RithmicMarketDataFeed } = require('../../services/rithmic/market-data');
|
|
24
|
-
const { algoLogger } = require('./logger');
|
|
25
|
-
const { recoveryMath } = require('../../services/strategy/recovery-math');
|
|
26
|
-
const { sessionHistory, SessionHistory } = require('../../services/session-history');
|
|
27
|
-
|
|
28
|
-
// AI Strategy Supervisor - observes, learns, and optimizes the strategy
|
|
29
|
-
const aiService = require('../../services/ai');
|
|
30
|
-
const StrategySupervisor = require('../../services/ai/strategy-supervisor');
|
|
31
13
|
|
|
32
14
|
// Shared utilities
|
|
33
|
-
const {
|
|
15
|
+
const { isRithmicFastPath, MAX_MULTI_SYMBOLS } = require('./algo-utils');
|
|
34
16
|
const { selectSymbol, configureAlgo } = require('./algo-config');
|
|
35
17
|
|
|
36
18
|
/**
|
|
@@ -99,1871 +81,10 @@ const oneAccountMenu = async (service) => {
|
|
|
99
81
|
const config = await configureAlgo(selectedAccount, contractList);
|
|
100
82
|
if (!config) return;
|
|
101
83
|
|
|
102
|
-
// Launch
|
|
103
|
-
|
|
104
|
-
await launchAlgo(accountService, selectedAccount, contractList[0], config);
|
|
105
|
-
} else {
|
|
106
|
-
await launchMultiSymbolRithmic(accountService, selectedAccount, contractList, config);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Launch algo trading - HQX Ultra Scalping Strategy
|
|
112
|
-
* Real-time market data + Strategy signals + Auto order execution
|
|
113
|
-
* AI Supervision: All connected agents monitor and supervise trading
|
|
114
|
-
*
|
|
115
|
-
* FAST PATH (Rithmic): Uses fastEntry() for ~10-50ms latency
|
|
116
|
-
* SLOW PATH (ProjectX): Uses placeOrder() for ~50-150ms latency
|
|
117
|
-
*/
|
|
118
|
-
const launchAlgo = async (service, account, contract, config) => {
|
|
119
|
-
const { contracts, dailyTarget, maxRisk, showName } = config;
|
|
120
|
-
|
|
121
|
-
// Use RAW API fields only - NO hardcoded fallbacks
|
|
122
|
-
const accountName = showName
|
|
123
|
-
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
124
|
-
: 'HQX *****';
|
|
125
|
-
const symbolName = contract.name || contract.symbol;
|
|
126
|
-
// contractId: use id for ProjectX, symbol for Rithmic
|
|
127
|
-
const contractId = contract.id || contract.symbol || contract.name;
|
|
128
|
-
const connectionType = account.platform || 'ProjectX';
|
|
129
|
-
|
|
130
|
-
// Tick size/value from API - null if not available (RULES.md compliant)
|
|
131
|
-
const tickSize = contract.tickSize ?? null;
|
|
132
|
-
const tickValue = contract.tickValue ?? null;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Determine execution path
|
|
137
|
-
const useFastPath = isRithmicFastPath(service);
|
|
138
|
-
|
|
139
|
-
const ui = new AlgoUI({ subtitle: 'HQX ULTRA SCALPING', mode: 'one-account' });
|
|
140
|
-
|
|
141
|
-
const stats = {
|
|
142
|
-
accountName,
|
|
143
|
-
symbol: symbolName,
|
|
144
|
-
qty: contracts,
|
|
145
|
-
target: dailyTarget,
|
|
146
|
-
risk: maxRisk,
|
|
147
|
-
propfirm: account.propfirm || 'Unknown',
|
|
148
|
-
platform: connectionType,
|
|
149
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
150
|
-
// R TRADER METRICS - All from Rithmic API (ACCOUNT_PNL_UPDATE 451)
|
|
151
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
152
|
-
pnl: null, // Today's P&L (openPnl + closedPnl)
|
|
153
|
-
openPnl: null, // Unrealized P&L (current position) - from INSTRUMENT_PNL_UPDATE 450
|
|
154
|
-
closedPnl: null, // Realized P&L (closed trades today)
|
|
155
|
-
balance: null, // Account Balance
|
|
156
|
-
buyingPower: null, // Available Buying Power
|
|
157
|
-
margin: null, // Margin Balance
|
|
158
|
-
netLiquidation: null, // Net Liquidation Value (balance + openPnl)
|
|
159
|
-
// Position info (like R Trader Positions panel) - from INSTRUMENT_PNL_UPDATE 450
|
|
160
|
-
position: 0, // Current position qty (+ long, - short)
|
|
161
|
-
entryPrice: 0, // Average entry price
|
|
162
|
-
lastPrice: 0, // Last market price (from ticker)
|
|
163
|
-
// Trading stats
|
|
164
|
-
trades: 0,
|
|
165
|
-
wins: 0,
|
|
166
|
-
losses: 0,
|
|
167
|
-
sessionPnl: 0, // P&L from THIS session only (HQX trades)
|
|
168
|
-
latency: 0,
|
|
169
|
-
connected: false,
|
|
170
|
-
startTime: Date.now(),
|
|
171
|
-
aiSupervision: false,
|
|
172
|
-
aiMode: null,
|
|
173
|
-
agentCount: 0, // Number of AI agents active
|
|
174
|
-
// Fast path stats
|
|
175
|
-
fastPath: useFastPath,
|
|
176
|
-
avgEntryLatency: 0,
|
|
177
|
-
avgFillLatency: 0,
|
|
178
|
-
entryLatencies: [],
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
let running = true;
|
|
182
|
-
let stopReason = null;
|
|
183
|
-
let startingPnL = null;
|
|
184
|
-
let currentPosition = 0; // Current position qty (+ long, - short)
|
|
185
|
-
let pendingOrder = false; // Prevent duplicate orders
|
|
186
|
-
let tickCount = 0;
|
|
187
|
-
let lastTradeCount = 0; // Track number of trades from API
|
|
188
|
-
let lastPositionQty = 0; // Track position changes
|
|
189
|
-
let currentTradeId = null; // Track current trade for history
|
|
190
|
-
|
|
191
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
192
|
-
// SESSION HISTORY - Save all activity for AI learning
|
|
193
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
194
|
-
sessionHistory.start(account.accountId, symbolName, account.propfirm || 'unknown', {
|
|
195
|
-
contracts,
|
|
196
|
-
dailyTarget,
|
|
197
|
-
maxRisk,
|
|
198
|
-
aiEnabled: config.enableAI,
|
|
199
|
-
agentCount: config.enableAI ? 2 : 0,
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Load historical data for AI learning
|
|
203
|
-
const historicalData = SessionHistory.getAILearningData();
|
|
204
|
-
if (historicalData.totalSessions > 0) {
|
|
205
|
-
algoLogger.info(ui, 'HISTORY LOADED',
|
|
206
|
-
`${historicalData.totalSessions} sessions | ${historicalData.totalTrades} trades | ` +
|
|
207
|
-
`WR: ${(historicalData.statistics?.winRate * 100 || 0).toFixed(0)}%`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Initialize Strategy
|
|
211
|
-
// Use HFT tick-based strategy for Rithmic (fast path), M1 for ProjectX
|
|
212
|
-
const strategy = (USE_HFT_STRATEGY && useFastPath) ? hftStrategy : M1;
|
|
213
|
-
const strategyName = (USE_HFT_STRATEGY && useFastPath) ? 'HFT-TICK' : 'M1';
|
|
214
|
-
|
|
215
|
-
// Initialize strategy with tick specs
|
|
216
|
-
if (tickSize !== null && tickValue !== null) {
|
|
217
|
-
strategy.initialize(contractId, tickSize, tickValue);
|
|
218
|
-
algoLogger.info(ui, 'STRATEGY', `${strategyName} initialized | tick=${tickSize} value=$${tickValue}`);
|
|
219
|
-
} else {
|
|
220
|
-
algoLogger.warning(ui, 'WARNING', 'Tick size/value not available from API');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Initialize Position Manager for fast path
|
|
224
|
-
let positionManager = null;
|
|
225
|
-
if (useFastPath) {
|
|
226
|
-
// Pass strategy reference so PositionManager can access math models
|
|
227
|
-
positionManager = new PositionManager(service, strategy);
|
|
228
|
-
|
|
229
|
-
// Set contract info from API (NOT hardcoded)
|
|
230
|
-
if (tickSize !== null && tickValue !== null) {
|
|
231
|
-
positionManager.setContractInfo(symbolName, {
|
|
232
|
-
tickSize,
|
|
233
|
-
tickValue,
|
|
234
|
-
contractId,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
positionManager.start();
|
|
239
|
-
|
|
240
|
-
// Listen for position manager events - CLEAN LOGS (no verbose order status)
|
|
241
|
-
positionManager.on('entryFilled', ({ orderTag, position, fillLatencyMs }) => {
|
|
242
|
-
stats.entryLatencies.push(fillLatencyMs);
|
|
243
|
-
stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
|
|
244
|
-
const side = position.side === 0 ? 'LONG' : 'SHORT';
|
|
245
|
-
const priceStr = formatPrice(position.entryPrice, tickSize || 0.25);
|
|
246
|
-
// Use 'filled' type for colored FILL icon
|
|
247
|
-
ui.addLog('filled', `${side} ${position.size}x ${symbolName} @ ${priceStr} | ${fillLatencyMs}ms`);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
positionManager.on('exitFilled', ({ orderTag, exitPrice, pnlTicks, holdDurationMs }) => {
|
|
251
|
-
const holdSec = (holdDurationMs / 1000).toFixed(1);
|
|
252
|
-
// Calculate PnL in dollars only if tickValue is available from API
|
|
253
|
-
if (pnlTicks !== null && tickValue !== null) {
|
|
254
|
-
const pnlDollars = pnlTicks * tickValue;
|
|
255
|
-
stats.sessionPnl += pnlDollars; // Track session P&L
|
|
256
|
-
|
|
257
|
-
// Record trade for Recovery Math
|
|
258
|
-
recoveryMath.recordTrade({
|
|
259
|
-
pnl: pnlDollars,
|
|
260
|
-
ticks: pnlTicks,
|
|
261
|
-
side: pnlTicks >= 0 ? 'win' : 'loss',
|
|
262
|
-
duration: holdDurationMs,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Update Recovery Mode state
|
|
266
|
-
const recovery = recoveryMath.updateSessionPnL(
|
|
267
|
-
stats.sessionPnl,
|
|
268
|
-
FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
|
|
269
|
-
FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
// Log recovery mode changes
|
|
273
|
-
if (recovery.justActivated) {
|
|
274
|
-
stats.recoveryMode = true;
|
|
275
|
-
ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}% | EV: $${recoveryMath.calcExpectedValue().toFixed(0)}`);
|
|
276
|
-
} else if (recovery.justDeactivated) {
|
|
277
|
-
stats.recoveryMode = false;
|
|
278
|
-
ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (pnlDollars >= 0) {
|
|
282
|
-
stats.wins++;
|
|
283
|
-
const priceStr = formatPrice(exitPrice, tickSize || 0.25);
|
|
284
|
-
ui.addLog('win', `+$${pnlDollars.toFixed(2)} @ ${priceStr} | ${holdSec}s`);
|
|
285
|
-
} else {
|
|
286
|
-
stats.losses++;
|
|
287
|
-
const priceStr = formatPrice(exitPrice, tickSize || 0.25);
|
|
288
|
-
ui.addLog('loss', `-$${Math.abs(pnlDollars).toFixed(2)} @ ${priceStr} | ${holdSec}s`);
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
// Log with ticks only if tickValue unavailable
|
|
292
|
-
if (pnlTicks !== null && pnlTicks >= 0) {
|
|
293
|
-
stats.wins++;
|
|
294
|
-
ui.addLog('win', `+${pnlTicks} ticks | ${holdSec}s`);
|
|
295
|
-
} else if (pnlTicks !== null) {
|
|
296
|
-
stats.losses++;
|
|
297
|
-
ui.addLog('loss', `${pnlTicks} ticks | ${holdSec}s`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
stats.trades++;
|
|
301
|
-
currentPosition = 0;
|
|
302
|
-
stats.position = 0; // Reset UI position display
|
|
303
|
-
pendingOrder = false;
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
positionManager.on('holdComplete', ({ orderTag, position }) => {
|
|
307
|
-
// Use 'ready' type for green READY icon
|
|
308
|
-
ui.addLog('ready', `Hold complete - monitoring exit`);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
positionManager.on('breakevenActivated', ({ orderTag, position, breakevenPrice, pnlTicks }) => {
|
|
312
|
-
// Use 'be' type for yellow BE icon
|
|
313
|
-
const priceStr = formatPrice(breakevenPrice, tickSize || 0.25);
|
|
314
|
-
ui.addLog('be', `Breakeven @ ${priceStr} | +${pnlTicks} ticks`)
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
positionManager.on('exitOrderFired', ({ orderTag, exitReason, latencyMs }) => {
|
|
318
|
-
// Don't log here - exitFilled will log the result
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// NOTE: Removed verbose DEBUG logs (ORDER ACCEPTED, ORDER STATUS, ORDER FILL, FILL CONFIRMED)
|
|
322
|
-
// Entry/Exit fills are logged by positionManager events (entryFilled, exitFilled)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
326
|
-
// REAL-TIME P&L VIA WEBSOCKET
|
|
327
|
-
// - Rithmic: Uses EventEmitter (service.on) for real-time updates
|
|
328
|
-
// - ProjectX: Uses HTTP polling (handled in pollPnL function below)
|
|
329
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
330
|
-
const rithmicAccountId = account.rithmicAccountId || account.accountId;
|
|
331
|
-
const serviceHasEvents = typeof service.on === 'function';
|
|
332
|
-
|
|
333
|
-
if (serviceHasEvents) {
|
|
334
|
-
// RITHMIC ONLY: Real-time P&L via WebSocket
|
|
335
|
-
service.on('pnlUpdate', (pnlData) => {
|
|
336
|
-
// Only update for our account
|
|
337
|
-
if (pnlData.accountId !== rithmicAccountId) return;
|
|
338
|
-
|
|
339
|
-
// ACCOUNT_PNL_UPDATE (451) - All R Trader account-level metrics
|
|
340
|
-
if (pnlData.closedPositionPnl !== undefined) {
|
|
341
|
-
stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
|
|
342
|
-
}
|
|
343
|
-
if (pnlData.accountBalance !== undefined) {
|
|
344
|
-
stats.balance = parseFloat(pnlData.accountBalance);
|
|
345
|
-
}
|
|
346
|
-
if (pnlData.availableBuyingPower !== undefined) {
|
|
347
|
-
stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
|
|
348
|
-
}
|
|
349
|
-
if (pnlData.marginBalance !== undefined) {
|
|
350
|
-
stats.margin = parseFloat(pnlData.marginBalance);
|
|
351
|
-
}
|
|
352
|
-
if (pnlData.netLiquidation !== undefined) {
|
|
353
|
-
stats.netLiquidation = parseFloat(pnlData.netLiquidation);
|
|
354
|
-
} else if (stats.balance !== null) {
|
|
355
|
-
stats.netLiquidation = stats.balance + (stats.openPnl || 0);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Total P&L = openPnl + closedPnl (same as R Trader)
|
|
359
|
-
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// RITHMIC ONLY: Real-time position updates
|
|
363
|
-
service.on('positionUpdate', (pos) => {
|
|
364
|
-
if (!pos || pos.accountId !== rithmicAccountId) return;
|
|
365
|
-
if (pos.symbol !== symbolName && !pos.symbol?.includes(symbolName.replace(/[A-Z]\d+$/, ''))) return;
|
|
366
|
-
|
|
367
|
-
const wasFlat = stats.position === 0;
|
|
368
|
-
stats.position = pos.quantity || 0;
|
|
369
|
-
stats.entryPrice = pos.averagePrice || 0;
|
|
370
|
-
currentPosition = pos.quantity || 0;
|
|
371
|
-
|
|
372
|
-
if (wasFlat && stats.position !== 0) {
|
|
373
|
-
stats.entryTime = Date.now();
|
|
374
|
-
} else if (stats.position === 0) {
|
|
375
|
-
stats.entryTime = null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Update Open P&L from INSTRUMENT_PNL_UPDATE (450)
|
|
379
|
-
if (pos.openPnl !== undefined && pos.openPnl !== null) {
|
|
380
|
-
stats.openPnl = pos.openPnl;
|
|
381
|
-
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
382
|
-
if (stats.balance !== null) {
|
|
383
|
-
stats.netLiquidation = stats.balance + stats.openPnl;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Initialize AI Strategy Supervisor - agents observe, learn & optimize
|
|
390
|
-
// Only if user enabled AI in config
|
|
391
|
-
let aiAgentCount = 0;
|
|
392
|
-
if (config.enableAI) {
|
|
393
|
-
const aiAgents = aiService.getAgents();
|
|
394
|
-
aiAgentCount = aiAgents.length;
|
|
395
|
-
stats.agentCount = aiAgentCount;
|
|
396
|
-
if (aiAgents.length > 0) {
|
|
397
|
-
const supervisorResult = StrategySupervisor.initialize(strategy, aiAgents, service, account.accountId);
|
|
398
|
-
stats.aiSupervision = supervisorResult.success;
|
|
399
|
-
stats.aiMode = supervisorResult.mode;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Initialize Market Data Feed
|
|
404
|
-
// Use RithmicMarketDataFeed for Rithmic accounts (fast path), MarketDataFeed for ProjectX
|
|
405
|
-
const isRithmic = useFastPath && service.tickerConn;
|
|
406
|
-
const marketFeed = isRithmic
|
|
407
|
-
? new RithmicMarketDataFeed(service)
|
|
408
|
-
: new MarketDataFeed({ propfirm: account.propfirm });
|
|
409
|
-
|
|
410
|
-
// Smart startup logs (same as HQX-TG)
|
|
411
|
-
const market = checkMarketHours();
|
|
412
|
-
const sessionName = market.session || 'AMERICAN';
|
|
413
|
-
const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
|
|
414
|
-
|
|
415
|
-
algoLogger.connectingToEngine(ui, account.accountId);
|
|
416
|
-
algoLogger.engineStarting(ui, connectionType, dailyTarget, maxRisk);
|
|
417
|
-
algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
|
|
418
|
-
|
|
419
|
-
// Log AI supervision status
|
|
420
|
-
if (stats.aiSupervision) {
|
|
421
|
-
algoLogger.info(ui, 'AI SUPERVISION', `${aiAgentCount} agent(s) - ${stats.aiMode} mode - LEARNING ACTIVE`);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Log execution path
|
|
425
|
-
if (useFastPath) {
|
|
426
|
-
algoLogger.info(ui, 'FAST PATH', `Rithmic direct | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms | Hold ${FAST_SCALPING.MIN_HOLD_MS / 1000}s`);
|
|
427
|
-
} else {
|
|
428
|
-
algoLogger.info(ui, 'SLOW PATH', `HTTP REST | Bracket orders enabled`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Handle strategy signals
|
|
432
|
-
strategy.on('signal', async (signal) => {
|
|
433
|
-
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
434
|
-
|
|
435
|
-
// Fast path: check if position manager allows new entry
|
|
436
|
-
if (useFastPath && positionManager && !positionManager.canEnter(symbolName)) {
|
|
437
|
-
algoLogger.info(ui, 'BLOCKED', 'Existing position in symbol');
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
442
|
-
|
|
443
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
444
|
-
// RECOVERY MODE - Math-based adaptive trading
|
|
445
|
-
// Uses Kelly Criterion, Expected Value, and Volatility scaling
|
|
446
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
447
|
-
if (FAST_SCALPING.RECOVERY?.ENABLED) {
|
|
448
|
-
const recoveryParams = recoveryMath.getRecoveryParams({
|
|
449
|
-
baseSize: contracts,
|
|
450
|
-
maxSize: contracts * 2,
|
|
451
|
-
atr: strategy.getATR?.() || 12,
|
|
452
|
-
confidence,
|
|
453
|
-
tickValue,
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// In recovery mode: only take positive EV trades
|
|
457
|
-
if (recoveryParams.recoveryActive && !recoveryParams.shouldTrade) {
|
|
458
|
-
ui.addLog('warning', `RECOVERY SKIP - EV: $${recoveryParams.expectedValue.toFixed(2)} (need $10+)`);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Log recovery stats periodically
|
|
463
|
-
if (recoveryParams.recoveryActive && stats.trades % 3 === 0) {
|
|
464
|
-
ui.addLog('info', `RECOVERY - Kelly: ${(recoveryParams.kelly * 100).toFixed(0)}% | WR: ${(recoveryParams.winRate * 100).toFixed(0)}% | EV: $${recoveryParams.expectedValue.toFixed(0)}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Feed signal to AI supervisor (agents observe the signal)
|
|
469
|
-
if (stats.aiSupervision) {
|
|
470
|
-
StrategySupervisor.feedSignal({ direction, entry, stopLoss, takeProfit, confidence });
|
|
471
|
-
|
|
472
|
-
// Check AI advice - agents may recommend caution based on learned patterns
|
|
473
|
-
const advice = StrategySupervisor.shouldTrade();
|
|
474
|
-
if (!advice.proceed) {
|
|
475
|
-
algoLogger.info(ui, 'AI HOLD', advice.reason);
|
|
476
|
-
return; // Skip - agents learned this pattern leads to losses
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Calculate position size with Kelly (math-based)
|
|
481
|
-
let kelly;
|
|
482
|
-
if (FAST_SCALPING.RECOVERY?.ENABLED && recoveryMath.trades.length >= 5) {
|
|
483
|
-
// Use math-based Kelly from trade history
|
|
484
|
-
kelly = recoveryMath.calcDrawdownAdjustedKelly();
|
|
485
|
-
} else {
|
|
486
|
-
// Fallback to confidence-based Kelly
|
|
487
|
-
kelly = Math.min(0.25, confidence);
|
|
488
|
-
}
|
|
489
|
-
let riskAmount = Math.round(maxRisk * kelly);
|
|
490
|
-
|
|
491
|
-
// AI may adjust size based on learning
|
|
492
|
-
if (stats.aiSupervision) {
|
|
493
|
-
const advice = StrategySupervisor.getCurrentAdvice();
|
|
494
|
-
if (advice.sizeMultiplier && advice.sizeMultiplier !== 1.0) {
|
|
495
|
-
kelly = kelly * advice.sizeMultiplier;
|
|
496
|
-
riskAmount = Math.round(riskAmount * advice.sizeMultiplier);
|
|
497
|
-
algoLogger.info(ui, 'AI ADJUST', `Size x${advice.sizeMultiplier.toFixed(2)} - ${advice.reason}`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const riskPct = Math.round((riskAmount / maxRisk) * 100);
|
|
502
|
-
|
|
503
|
-
// Place order via API
|
|
504
|
-
pendingOrder = true;
|
|
505
|
-
const orderSide = direction === 'long' ? 0 : 1; // 0=Buy, 1=Sell
|
|
506
|
-
const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
|
|
507
|
-
|
|
508
|
-
try {
|
|
509
|
-
// ═══════════════════════════════════════════════════════════════
|
|
510
|
-
// FAST PATH: Rithmic direct execution (~10-50ms)
|
|
511
|
-
// ═══════════════════════════════════════════════════════════════
|
|
512
|
-
if (useFastPath && positionManager) {
|
|
513
|
-
const orderData = {
|
|
514
|
-
accountId: account.accountId,
|
|
515
|
-
symbol: symbolName,
|
|
516
|
-
exchange: contract.exchange || 'CME',
|
|
517
|
-
size: contracts,
|
|
518
|
-
side: orderSide,
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
// CRITICAL: Use rithmicAccountId (original) not accountId (hash) for Rithmic orders
|
|
522
|
-
if (account.rithmicAccountId) {
|
|
523
|
-
orderData.accountId = account.rithmicAccountId;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Log entry attempt (single line) - use 'entry' type for cyan ENTRY icon
|
|
527
|
-
ui.addLog('entry', `${sideStr} ${contracts}x ${symbolName} | risk: $${riskAmount} (${riskPct}%)`);
|
|
528
|
-
|
|
529
|
-
// Fire-and-forget entry (no await on fill)
|
|
530
|
-
const entryResult = service.fastEntry(orderData);
|
|
531
|
-
|
|
532
|
-
if (entryResult.success) {
|
|
533
|
-
// Register with position manager for lifecycle tracking
|
|
534
|
-
const contractInfo = { tickSize, tickValue, contractId };
|
|
535
|
-
positionManager.registerEntry(entryResult, orderData, contractInfo);
|
|
536
|
-
|
|
537
|
-
currentPosition = direction === 'long' ? contracts : -contracts;
|
|
538
|
-
|
|
539
|
-
// Update avg entry latency
|
|
540
|
-
stats.avgEntryLatency = stats.entryLatencies.length > 0
|
|
541
|
-
? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
|
|
542
|
-
: entryResult.latencyMs;
|
|
543
|
-
|
|
544
|
-
// Note: Fill confirmation logged by positionManager.on('entryFilled')
|
|
545
|
-
|
|
546
|
-
} else {
|
|
547
|
-
algoLogger.orderRejected(ui, symbolName, entryResult.error || 'Fast entry failed');
|
|
548
|
-
pendingOrder = false;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ═══════════════════════════════════════════════════════════════
|
|
552
|
-
// SLOW PATH: ProjectX/Tradovate HTTP REST (~50-150ms)
|
|
553
|
-
// ═══════════════════════════════════════════════════════════════
|
|
554
|
-
} else {
|
|
555
|
-
const orderResult = await service.placeOrder({
|
|
556
|
-
accountId: account.accountId,
|
|
557
|
-
contractId: contractId,
|
|
558
|
-
type: 2, // Market order
|
|
559
|
-
side: orderSide,
|
|
560
|
-
size: contracts
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
if (orderResult.success) {
|
|
564
|
-
currentPosition = direction === 'long' ? contracts : -contracts;
|
|
565
|
-
stats.trades++;
|
|
566
|
-
const sideStr = direction === 'long' ? 'BUY' : 'SELL';
|
|
567
|
-
const positionSide = direction === 'long' ? 'LONG' : 'SHORT';
|
|
568
|
-
|
|
569
|
-
algoLogger.orderSubmitted(ui, symbolName, sideStr, contracts, entry);
|
|
570
|
-
algoLogger.orderFilled(ui, symbolName, sideStr, contracts, entry);
|
|
571
|
-
algoLogger.positionOpened(ui, symbolName, positionSide, contracts, entry);
|
|
572
|
-
algoLogger.entryConfirmed(ui, sideStr, contracts, symbolName, entry);
|
|
573
|
-
|
|
574
|
-
// Record trade entry in session history
|
|
575
|
-
currentTradeId = sessionHistory.recordEntry({
|
|
576
|
-
direction,
|
|
577
|
-
symbol: symbolName,
|
|
578
|
-
quantity: contracts,
|
|
579
|
-
entryPrice: entry,
|
|
580
|
-
stopLoss,
|
|
581
|
-
takeProfit,
|
|
582
|
-
momentum: strategy.getMomentum?.() || 0,
|
|
583
|
-
zscore: strategy.getZScore?.() || 0,
|
|
584
|
-
ofi: strategy.getOFI?.() || 0,
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// Place bracket orders (SL/TP) - SLOW PATH ONLY
|
|
588
|
-
if (stopLoss && takeProfit) {
|
|
589
|
-
// Stop Loss
|
|
590
|
-
await service.placeOrder({
|
|
591
|
-
accountId: account.accountId,
|
|
592
|
-
contractId: contractId,
|
|
593
|
-
type: 4, // Stop order
|
|
594
|
-
side: direction === 'long' ? 1 : 0, // Opposite side
|
|
595
|
-
size: contracts,
|
|
596
|
-
stopPrice: stopLoss
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
// Take Profit
|
|
600
|
-
await service.placeOrder({
|
|
601
|
-
accountId: account.accountId,
|
|
602
|
-
contractId: contractId,
|
|
603
|
-
type: 1, // Limit order
|
|
604
|
-
side: direction === 'long' ? 1 : 0,
|
|
605
|
-
size: contracts,
|
|
606
|
-
limitPrice: takeProfit
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
algoLogger.stopsSet(ui, stopLoss, takeProfit);
|
|
610
|
-
}
|
|
611
|
-
pendingOrder = false;
|
|
612
|
-
} else {
|
|
613
|
-
algoLogger.orderRejected(ui, symbolName, orderResult.error || 'Unknown error');
|
|
614
|
-
pendingOrder = false;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
} catch (e) {
|
|
618
|
-
algoLogger.error(ui, 'ORDER ERROR', e.message);
|
|
619
|
-
pendingOrder = false;
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
// Handle market data ticks
|
|
624
|
-
let lastHeartbeat = Date.now();
|
|
625
|
-
let tps = 0;
|
|
626
|
-
|
|
627
|
-
marketFeed.on('tick', (tick) => {
|
|
628
|
-
tickCount++;
|
|
629
|
-
tps++;
|
|
630
|
-
const latencyStart = Date.now();
|
|
631
|
-
|
|
632
|
-
// Debug: log first tick and confirm data flow after 5s
|
|
633
|
-
if (tickCount === 1) {
|
|
634
|
-
algoLogger.info(ui, 'FIRST TICK', `price=${parseFloat(Number(tick.price).toFixed(6))} bid=${parseFloat(Number(tick.bid).toFixed(6))} ask=${parseFloat(Number(tick.ask).toFixed(6))} vol=${tick.volume || tick.size || 0}`);
|
|
635
|
-
} else if (tickCount === 100) {
|
|
636
|
-
algoLogger.info(ui, 'DATA FLOWING', `100 ticks received - market data OK`);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Feed tick to strategy
|
|
640
|
-
const tickData = {
|
|
641
|
-
contractId: tick.contractId || contractId,
|
|
642
|
-
price: tick.price || tick.lastPrice || tick.bid,
|
|
643
|
-
bid: tick.bid,
|
|
644
|
-
ask: tick.ask,
|
|
645
|
-
volume: tick.volume || tick.size || 1,
|
|
646
|
-
side: tick.lastTradeSide || tick.side || 'unknown',
|
|
647
|
-
timestamp: tick.timestamp || Date.now()
|
|
648
|
-
};
|
|
649
|
-
|
|
650
|
-
// Update last price for UI (like R Trader)
|
|
651
|
-
stats.lastPrice = tickData.price;
|
|
652
|
-
|
|
653
|
-
// Feed tick to AI supervisor (agents observe same data as strategy)
|
|
654
|
-
if (stats.aiSupervision) {
|
|
655
|
-
StrategySupervisor.feedTick(tickData);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
strategy.processTick(tickData);
|
|
659
|
-
|
|
660
|
-
// Feed price to Recovery Math for volatility calculation
|
|
661
|
-
if (FAST_SCALPING.RECOVERY?.ENABLED && tickData.price) {
|
|
662
|
-
recoveryMath.recordPriceReturn(tickData.price, stats.lastPrice || tickData.price);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Feed price to position manager for exit monitoring (fast path)
|
|
666
|
-
if (useFastPath && positionManager) {
|
|
667
|
-
// Update latest price for position monitoring
|
|
668
|
-
service.emit('priceUpdate', {
|
|
669
|
-
symbol: symbolName,
|
|
670
|
-
price: tickData.price,
|
|
671
|
-
timestamp: tickData.timestamp,
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// Get momentum data from strategy if available
|
|
675
|
-
const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(contractId);
|
|
676
|
-
if (modelValues && typeof positionManager.updateMomentum === 'function') {
|
|
677
|
-
positionManager.updateMomentum(symbolName, {
|
|
678
|
-
ofi: modelValues.ofi || 0,
|
|
679
|
-
zscore: modelValues.zscore || 0,
|
|
680
|
-
delta: modelValues.delta || 0,
|
|
681
|
-
timestamp: tickData.timestamp,
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
stats.latency = Date.now() - latencyStart;
|
|
687
|
-
|
|
688
|
-
// Smart logs - only on STATE CHANGES (not every second when in position)
|
|
689
|
-
const now = Date.now();
|
|
690
|
-
if (now - lastHeartbeat > 1000) {
|
|
691
|
-
const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(contractId) || null;
|
|
692
|
-
|
|
693
|
-
if (modelValues && modelValues.ofi !== undefined) {
|
|
694
|
-
const ofi = modelValues.ofi || 0;
|
|
695
|
-
const delta = modelValues.delta || 0;
|
|
696
|
-
const zscore = modelValues.zscore || 0;
|
|
697
|
-
const mom = modelValues.momentum || 0;
|
|
698
|
-
|
|
699
|
-
// Use stats.position from Rithmic API (real-time WebSocket)
|
|
700
|
-
const positionQty = stats.position || 0;
|
|
701
|
-
|
|
702
|
-
if (positionQty === 0) {
|
|
703
|
-
// Not in position - show market analysis (varied messages)
|
|
704
|
-
const smartLogs = require('./smart-logs');
|
|
705
|
-
const stateLog = smartLogs.getMarketStateLog(ofi, zscore, mom, delta);
|
|
706
|
-
if (stateLog.details) {
|
|
707
|
-
ui.addLog('analysis', `${stateLog.message} - ${stateLog.details}`);
|
|
708
|
-
} else {
|
|
709
|
-
ui.addLog('info', stateLog.message);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
// When IN POSITION: Don't spam logs every second
|
|
713
|
-
// Position updates come from order fills and exit events
|
|
714
|
-
} else {
|
|
715
|
-
// Waiting for data - log every 5 seconds only
|
|
716
|
-
if (now - lastHeartbeat > 5000) {
|
|
717
|
-
const smartLogs = require('./smart-logs');
|
|
718
|
-
const scanLog = smartLogs.getScanningLog(true);
|
|
719
|
-
ui.addLog('info', `${scanLog.message} ${tps} ticks/s`);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
lastHeartbeat = now;
|
|
723
|
-
tps = 0;
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
marketFeed.on('connected', () => {
|
|
728
|
-
stats.connected = true;
|
|
729
|
-
algoLogger.dataConnected(ui, 'RTC');
|
|
730
|
-
algoLogger.algoOperational(ui, connectionType);
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
marketFeed.on('error', (err) => {
|
|
734
|
-
algoLogger.error(ui, 'MARKET ERROR', err.message);
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
marketFeed.on('disconnected', (err) => {
|
|
738
|
-
stats.connected = false;
|
|
739
|
-
algoLogger.dataDisconnected(ui, 'WEBSOCKET', err?.message);
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
// Connect to market data
|
|
743
|
-
try {
|
|
744
|
-
if (isRithmic) {
|
|
745
|
-
// Rithmic: Use existing tickerConn from RithmicService
|
|
746
|
-
algoLogger.info(ui, 'CONNECTING', `RITHMIC TICKER | ${symbolName}`);
|
|
747
|
-
|
|
748
|
-
await marketFeed.connect();
|
|
749
|
-
|
|
750
|
-
// Wait for connection to stabilize
|
|
751
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
752
|
-
|
|
753
|
-
if (marketFeed.isConnected()) {
|
|
754
|
-
// Use contract exchange or default to CME
|
|
755
|
-
const exchange = contract.exchange || 'CME';
|
|
756
|
-
marketFeed.subscribe(symbolName, exchange);
|
|
757
|
-
algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} Rithmic real-time feed active`);
|
|
758
|
-
} else {
|
|
759
|
-
algoLogger.error(ui, 'CONNECTION LOST', 'Rithmic ticker not ready');
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
// ProjectX: Use HTTP token-based WebSocket
|
|
763
|
-
const propfirmKey = (account.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
764
|
-
|
|
765
|
-
// CRITICAL: Get a fresh token for WebSocket connection
|
|
766
|
-
// TopStep invalidates WebSocket sessions for old tokens
|
|
767
|
-
algoLogger.info(ui, 'REFRESHING AUTH TOKEN...');
|
|
768
|
-
const token = await service.getFreshToken?.() || service.token || service.getToken?.();
|
|
769
|
-
|
|
770
|
-
if (!token) {
|
|
771
|
-
algoLogger.error(ui, 'NO AUTH TOKEN', 'Please reconnect');
|
|
772
|
-
} else {
|
|
773
|
-
algoLogger.info(ui, 'TOKEN OK', `${token.length} chars`);
|
|
774
|
-
algoLogger.info(ui, 'CONNECTING', `${propfirmKey.toUpperCase()} | ${contractId}`);
|
|
775
|
-
|
|
776
|
-
await marketFeed.connect(token, propfirmKey);
|
|
777
|
-
|
|
778
|
-
// Wait for connection to stabilize
|
|
779
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
780
|
-
|
|
781
|
-
if (marketFeed.isConnected()) {
|
|
782
|
-
await marketFeed.subscribe(symbolName, contractId);
|
|
783
|
-
algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} real-time feed active`);
|
|
784
|
-
} else {
|
|
785
|
-
algoLogger.error(ui, 'CONNECTION LOST', 'Before subscribe');
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
} catch (e) {
|
|
790
|
-
algoLogger.error(ui, 'CONNECTION ERROR', e.message.substring(0, 50));
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Poll account P&L and sync with real trades from API
|
|
794
|
-
// For ProjectX: This is the PRIMARY source of P&L data (no WebSocket events)
|
|
795
|
-
// For Rithmic: This is a fallback (WebSocket events are primary)
|
|
796
|
-
const pollPnL = async () => {
|
|
797
|
-
try {
|
|
798
|
-
// Get account P&L
|
|
799
|
-
const accountResult = await service.getTradingAccounts();
|
|
800
|
-
if (accountResult.success && accountResult.accounts) {
|
|
801
|
-
const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
|
|
802
|
-
if (acc) {
|
|
803
|
-
// Total day P&L
|
|
804
|
-
if (acc.profitAndLoss !== undefined && acc.profitAndLoss !== null) {
|
|
805
|
-
stats.pnl = acc.profitAndLoss;
|
|
806
|
-
}
|
|
807
|
-
// Closed P&L (realized)
|
|
808
|
-
if (acc.realizedPnl !== undefined) {
|
|
809
|
-
stats.closedPnl = acc.realizedPnl;
|
|
810
|
-
}
|
|
811
|
-
// Balance
|
|
812
|
-
if (acc.balance !== undefined) {
|
|
813
|
-
stats.balance = acc.balance;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Check positions - get Open P&L and detect when position closes
|
|
819
|
-
const posResult = await service.getPositions(account.accountId);
|
|
820
|
-
if (posResult.success && posResult.positions) {
|
|
821
|
-
const pos = posResult.positions.find(p => {
|
|
822
|
-
const sym = p.contractId || p.symbol || '';
|
|
823
|
-
return sym.includes(contract.name) || sym.includes(contractId);
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
const newPositionQty = pos?.quantity || 0;
|
|
827
|
-
|
|
828
|
-
// Update Open P&L from position (unrealized P&L)
|
|
829
|
-
if (pos && pos.unrealizedPnl !== undefined) {
|
|
830
|
-
stats.openPnl = pos.unrealizedPnl;
|
|
831
|
-
} else if (pos && pos.profitAndLoss !== undefined) {
|
|
832
|
-
stats.openPnl = pos.profitAndLoss;
|
|
833
|
-
} else if (newPositionQty === 0) {
|
|
834
|
-
stats.openPnl = 0; // Flat = no open P&L
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Recalculate total P&L if we have components
|
|
838
|
-
if (stats.openPnl !== null && stats.closedPnl !== null) {
|
|
839
|
-
stats.pnl = stats.openPnl + stats.closedPnl;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// Position just closed - cancel remaining orders and log result
|
|
843
|
-
if (lastPositionQty !== 0 && newPositionQty === 0) {
|
|
844
|
-
// Cancel all open orders to prevent new positions
|
|
845
|
-
try {
|
|
846
|
-
await service.cancelAllOrders(account.accountId);
|
|
847
|
-
algoLogger.info(ui, 'ORDERS CANCELLED', 'Position closed - brackets removed');
|
|
848
|
-
} catch (e) {
|
|
849
|
-
// Silent fail
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// Get real trade data from API
|
|
853
|
-
try {
|
|
854
|
-
const tradesResult = await service.getTrades(account.accountId);
|
|
855
|
-
if (tradesResult.success && tradesResult.trades?.length > 0) {
|
|
856
|
-
// Count completed trades (those with profitAndLoss not null)
|
|
857
|
-
const completedTrades = tradesResult.trades.filter(t => t.profitAndLoss !== null);
|
|
858
|
-
|
|
859
|
-
// Update stats from real trades
|
|
860
|
-
let wins = 0, losses = 0;
|
|
861
|
-
for (const trade of completedTrades) {
|
|
862
|
-
if (trade.profitAndLoss > 0) wins++;
|
|
863
|
-
else if (trade.profitAndLoss < 0) losses++;
|
|
864
|
-
}
|
|
865
|
-
stats.trades = completedTrades.length;
|
|
866
|
-
stats.wins = wins;
|
|
867
|
-
stats.losses = losses;
|
|
868
|
-
|
|
869
|
-
// Log the trade that just closed
|
|
870
|
-
const lastTrade = completedTrades[completedTrades.length - 1];
|
|
871
|
-
if (lastTrade) {
|
|
872
|
-
const pnl = lastTrade.profitAndLoss || 0;
|
|
873
|
-
const side = lastTrade.side === 0 ? 'LONG' : 'SHORT';
|
|
874
|
-
const exitPrice = lastTrade.price || 0;
|
|
875
|
-
|
|
876
|
-
if (pnl >= 0) {
|
|
877
|
-
algoLogger.targetHit(ui, symbolName, exitPrice, pnl);
|
|
878
|
-
} else {
|
|
879
|
-
algoLogger.stopHit(ui, symbolName, exitPrice, Math.abs(pnl));
|
|
880
|
-
}
|
|
881
|
-
algoLogger.positionClosed(ui, symbolName, side, contracts, exitPrice, pnl);
|
|
882
|
-
|
|
883
|
-
// Record in strategy for adaptation
|
|
884
|
-
strategy.recordTradeResult(pnl);
|
|
885
|
-
|
|
886
|
-
// Record trade exit in session history
|
|
887
|
-
sessionHistory.recordExit(currentTradeId, {
|
|
888
|
-
exitPrice,
|
|
889
|
-
pnl,
|
|
890
|
-
reason: pnl >= 0 ? 'takeProfit' : 'stopLoss',
|
|
891
|
-
});
|
|
892
|
-
currentTradeId = null;
|
|
893
|
-
|
|
894
|
-
// Feed trade result to AI supervisor - THIS IS WHERE AGENTS LEARN
|
|
895
|
-
if (stats.aiSupervision) {
|
|
896
|
-
StrategySupervisor.feedTradeResult({
|
|
897
|
-
side,
|
|
898
|
-
qty: contracts,
|
|
899
|
-
price: exitPrice,
|
|
900
|
-
pnl,
|
|
901
|
-
symbol: symbolName,
|
|
902
|
-
direction: side
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
// Log if AI learned something
|
|
906
|
-
const status = StrategySupervisor.getStatus();
|
|
907
|
-
if (status.patternsLearned.winning + status.patternsLearned.losing > 0) {
|
|
908
|
-
algoLogger.info(ui, 'AI LEARNING',
|
|
909
|
-
`${status.patternsLearned.winning}W/${status.patternsLearned.losing}L patterns`);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
} catch (e) {
|
|
915
|
-
// Silent fail - trades API might not be available
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
lastPositionQty = newPositionQty;
|
|
920
|
-
currentPosition = newPositionQty;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// Check target/risk
|
|
924
|
-
if (stats.pnl >= dailyTarget) {
|
|
925
|
-
stopReason = 'target';
|
|
926
|
-
running = false;
|
|
927
|
-
algoLogger.info(ui, 'DAILY TARGET REACHED', `+$${stats.pnl.toFixed(2)}`);
|
|
928
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
929
|
-
stopReason = 'risk';
|
|
930
|
-
running = false;
|
|
931
|
-
algoLogger.dailyLimitWarning(ui, stats.pnl, -maxRisk);
|
|
932
|
-
}
|
|
933
|
-
} catch (e) {
|
|
934
|
-
// Silently handle polling errors
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
|
-
|
|
938
|
-
// Start polling and UI refresh
|
|
939
|
-
// UI refreshes every 8ms (120 fps - ProMotion display support), P&L polling every 10s
|
|
940
|
-
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 8);
|
|
941
|
-
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 10000);
|
|
942
|
-
pollPnL(); // Initial poll
|
|
943
|
-
|
|
944
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
945
|
-
// ULTRA SOLID EMERGENCY STOP FUNCTION
|
|
946
|
-
// Called when X is pressed - MUST cancel all orders and flatten positions
|
|
947
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
948
|
-
const emergencyStopAll = async () => {
|
|
949
|
-
const accountId = account.rithmicAccountId || account.accountId;
|
|
950
|
-
const MAX_RETRIES = 3;
|
|
951
|
-
const TIMEOUT_MS = 5000;
|
|
952
|
-
|
|
953
|
-
ui.addLog('warning', '████ EMERGENCY STOP INITIATED ████');
|
|
954
|
-
ui.render(stats);
|
|
955
|
-
|
|
956
|
-
// Helper: run with timeout
|
|
957
|
-
const withTimeout = (promise, ms) => Promise.race([
|
|
958
|
-
promise,
|
|
959
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('TIMEOUT')), ms))
|
|
960
|
-
]);
|
|
961
|
-
|
|
962
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
963
|
-
// STEP 1: CANCEL ALL ORDERS (with retries)
|
|
964
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
965
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
966
|
-
try {
|
|
967
|
-
ui.addLog('info', `[${attempt}/${MAX_RETRIES}] Cancelling all orders...`);
|
|
968
|
-
ui.render(stats);
|
|
969
|
-
|
|
970
|
-
if (typeof service.cancelAllOrders === 'function') {
|
|
971
|
-
await withTimeout(service.cancelAllOrders(accountId), TIMEOUT_MS);
|
|
972
|
-
ui.addLog('success', 'All orders cancelled');
|
|
973
|
-
ui.render(stats);
|
|
974
|
-
break;
|
|
975
|
-
} else {
|
|
976
|
-
ui.addLog('info', 'No cancelAllOrders function - skipping');
|
|
977
|
-
break;
|
|
978
|
-
}
|
|
979
|
-
} catch (e) {
|
|
980
|
-
ui.addLog('error', `Cancel orders attempt ${attempt} failed: ${e.message}`);
|
|
981
|
-
ui.render(stats);
|
|
982
|
-
if (attempt === MAX_RETRIES) {
|
|
983
|
-
ui.addLog('error', 'FAILED TO CANCEL ORDERS AFTER 3 ATTEMPTS');
|
|
984
|
-
}
|
|
985
|
-
await new Promise(r => setTimeout(r, 500)); // Wait before retry
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
990
|
-
// STEP 2: FLATTEN POSITION (with retries)
|
|
991
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
992
|
-
const posQty = Math.abs(currentPosition || stats.position || 0);
|
|
993
|
-
|
|
994
|
-
if (posQty > 0) {
|
|
995
|
-
const closeSide = (currentPosition || stats.position) > 0 ? 1 : 0;
|
|
996
|
-
const sideStr = closeSide === 1 ? 'SELL' : 'BUY';
|
|
997
|
-
|
|
998
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
999
|
-
try {
|
|
1000
|
-
ui.addLog('info', `[${attempt}/${MAX_RETRIES}] Flattening ${posQty}x position (${sideStr})...`);
|
|
1001
|
-
ui.render(stats);
|
|
1002
|
-
|
|
1003
|
-
// Method 1: Rithmic emergencyStop (best)
|
|
1004
|
-
if (typeof service.emergencyStop === 'function') {
|
|
1005
|
-
const result = await withTimeout(service.emergencyStop(accountId), TIMEOUT_MS);
|
|
1006
|
-
if (result.success) {
|
|
1007
|
-
ui.addLog('success', 'Position FLATTENED (Rithmic emergency stop)');
|
|
1008
|
-
ui.render(stats);
|
|
1009
|
-
break;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// Method 2: Market order to close
|
|
1014
|
-
if (typeof service.placeOrder === 'function') {
|
|
1015
|
-
await withTimeout(service.placeOrder({
|
|
1016
|
-
accountId: account.accountId,
|
|
1017
|
-
contractId: contractId,
|
|
1018
|
-
type: 2, // Market order
|
|
1019
|
-
side: closeSide,
|
|
1020
|
-
size: posQty
|
|
1021
|
-
}), TIMEOUT_MS);
|
|
1022
|
-
ui.addLog('success', `Position FLATTENED (market ${sideStr} ${posQty}x)`);
|
|
1023
|
-
ui.render(stats);
|
|
1024
|
-
break;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// Method 3: Rithmic fastExit
|
|
1028
|
-
if (typeof service.fastExit === 'function') {
|
|
1029
|
-
await withTimeout(service.fastExit({
|
|
1030
|
-
accountId: accountId,
|
|
1031
|
-
symbol: symbolName,
|
|
1032
|
-
exchange: contract.exchange || 'CME',
|
|
1033
|
-
size: posQty,
|
|
1034
|
-
side: closeSide
|
|
1035
|
-
}), TIMEOUT_MS);
|
|
1036
|
-
ui.addLog('success', `Position FLATTENED (fast exit ${sideStr} ${posQty}x)`);
|
|
1037
|
-
ui.render(stats);
|
|
1038
|
-
break;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
} catch (e) {
|
|
1042
|
-
ui.addLog('error', `Flatten attempt ${attempt} failed: ${e.message}`);
|
|
1043
|
-
ui.render(stats);
|
|
1044
|
-
if (attempt === MAX_RETRIES) {
|
|
1045
|
-
ui.addLog('error', '████ FAILED TO FLATTEN - CHECK MANUALLY! ████');
|
|
1046
|
-
}
|
|
1047
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
} else {
|
|
1051
|
-
ui.addLog('success', 'No position to flatten - clean exit');
|
|
1052
|
-
ui.render(stats);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1056
|
-
// STEP 3: VERIFY (optional - check position is actually flat)
|
|
1057
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1058
|
-
try {
|
|
1059
|
-
if (typeof service.getPositions === 'function') {
|
|
1060
|
-
const posResult = await withTimeout(service.getPositions(account.accountId), 3000);
|
|
1061
|
-
if (posResult.success && posResult.positions) {
|
|
1062
|
-
const stillOpen = posResult.positions.find(p => {
|
|
1063
|
-
const sym = p.contractId || p.symbol || '';
|
|
1064
|
-
return (sym.includes(symbolName) || sym.includes(contractId)) && p.quantity !== 0;
|
|
1065
|
-
});
|
|
1066
|
-
if (stillOpen) {
|
|
1067
|
-
ui.addLog('error', `████ POSITION STILL OPEN: ${stillOpen.quantity}x ████`);
|
|
1068
|
-
ui.addLog('error', 'CLOSE MANUALLY IN R TRADER!');
|
|
1069
|
-
} else {
|
|
1070
|
-
ui.addLog('success', 'VERIFIED: Position is flat');
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
} catch (e) {
|
|
1075
|
-
// Verification failed, but main stop was attempted
|
|
1076
|
-
ui.addLog('warning', 'Could not verify position - check manually');
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
ui.addLog('info', '════════════════════════════════════');
|
|
1080
|
-
ui.render(stats);
|
|
1081
|
-
};
|
|
1082
|
-
|
|
1083
|
-
// Keyboard handler
|
|
1084
|
-
let emergencyStopInProgress = false;
|
|
1085
|
-
|
|
1086
|
-
const setupKeyHandler = () => {
|
|
1087
|
-
if (!process.stdin.isTTY) return;
|
|
1088
|
-
readline.emitKeypressEvents(process.stdin);
|
|
1089
|
-
process.stdin.setRawMode(true);
|
|
1090
|
-
process.stdin.resume();
|
|
1091
|
-
|
|
1092
|
-
const onKey = async (str, key) => {
|
|
1093
|
-
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
1094
|
-
if (emergencyStopInProgress) return; // Prevent double-trigger
|
|
1095
|
-
emergencyStopInProgress = true;
|
|
1096
|
-
running = false;
|
|
1097
|
-
stopReason = 'manual';
|
|
1098
|
-
|
|
1099
|
-
// Run emergency stop IMMEDIATELY on keypress (don't wait for main loop)
|
|
1100
|
-
await emergencyStopAll();
|
|
1101
|
-
}
|
|
1102
|
-
};
|
|
1103
|
-
process.stdin.on('keypress', onKey);
|
|
1104
|
-
return () => {
|
|
1105
|
-
process.stdin.removeListener('keypress', onKey);
|
|
1106
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1107
|
-
};
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1110
|
-
const cleanupKeys = setupKeyHandler();
|
|
1111
|
-
|
|
1112
|
-
// Wait for stop
|
|
1113
|
-
await new Promise(resolve => {
|
|
1114
|
-
const check = setInterval(() => {
|
|
1115
|
-
if (!running) {
|
|
1116
|
-
clearInterval(check);
|
|
1117
|
-
resolve();
|
|
1118
|
-
}
|
|
1119
|
-
}, 100);
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
// Cleanup with timeout protection
|
|
1123
|
-
clearInterval(refreshInterval);
|
|
1124
|
-
clearInterval(pnlInterval);
|
|
1125
|
-
|
|
1126
|
-
// Stop Position Manager (fast path)
|
|
1127
|
-
if (positionManager) {
|
|
1128
|
-
positionManager.stop();
|
|
1129
|
-
positionManager = null;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Stop AI Supervisor and get learning summary
|
|
1133
|
-
if (stats.aiSupervision) {
|
|
1134
|
-
const aiSummary = StrategySupervisor.stop();
|
|
1135
|
-
stats.aiLearning = {
|
|
1136
|
-
optimizations: aiSummary.optimizationsApplied || 0,
|
|
1137
|
-
patternsLearned: (aiSummary.winningPatterns || 0) + (aiSummary.losingPatterns || 0)
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// End session history and save to disk
|
|
1142
|
-
sessionHistory.end({
|
|
1143
|
-
totalTrades: stats.trades,
|
|
1144
|
-
wins: stats.wins,
|
|
1145
|
-
losses: stats.losses,
|
|
1146
|
-
totalPnl: stats.pnl,
|
|
1147
|
-
stopReason,
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
// Disconnect market feed with timeout
|
|
1151
|
-
try {
|
|
1152
|
-
await Promise.race([
|
|
1153
|
-
marketFeed.disconnect(),
|
|
1154
|
-
new Promise(r => setTimeout(r, 3000))
|
|
1155
|
-
]);
|
|
1156
|
-
} catch {}
|
|
1157
|
-
|
|
1158
|
-
// Cleanup keyboard handler
|
|
1159
|
-
try { if (cleanupKeys) cleanupKeys(); } catch {}
|
|
1160
|
-
|
|
1161
|
-
// Close log file with session summary
|
|
1162
|
-
try { ui.closeLog(stats); } catch {}
|
|
1163
|
-
|
|
1164
|
-
// Cleanup UI
|
|
1165
|
-
try { ui.cleanup(); } catch {}
|
|
1166
|
-
|
|
1167
|
-
// Reset stdin
|
|
1168
|
-
try {
|
|
1169
|
-
if (process.stdin.isTTY) {
|
|
1170
|
-
process.stdin.setRawMode(false);
|
|
1171
|
-
}
|
|
1172
|
-
process.stdin.resume();
|
|
1173
|
-
} catch {}
|
|
1174
|
-
|
|
1175
|
-
// Duration
|
|
1176
|
-
const durationMs = Date.now() - stats.startTime;
|
|
1177
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
1178
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
1179
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
1180
|
-
stats.duration = hours > 0
|
|
1181
|
-
? `${hours}h ${minutes}m ${seconds}s`
|
|
1182
|
-
: minutes > 0
|
|
1183
|
-
? `${minutes}m ${seconds}s`
|
|
1184
|
-
: `${seconds}s`;
|
|
1185
|
-
|
|
1186
|
-
// Summary
|
|
1187
|
-
renderSessionSummary(stats, stopReason);
|
|
1188
|
-
|
|
1189
|
-
await prompts.waitForEnter();
|
|
84
|
+
// Launch algo (unified - works for 1 or multiple symbols)
|
|
85
|
+
await launchMultiSymbolRithmic(accountService, selectedAccount, contractList, config);
|
|
1190
86
|
};
|
|
1191
87
|
|
|
1192
|
-
|
|
1193
|
-
* Launch multi-symbol algo trading (Rithmic only)
|
|
1194
|
-
* Uses single market feed connection with multiple subscriptions
|
|
1195
|
-
* Each symbol has its own PositionManager and strategy instance
|
|
1196
|
-
*
|
|
1197
|
-
* Same logic as launchAlgo but for multiple symbols
|
|
1198
|
-
*
|
|
1199
|
-
* @param {Object} service - Rithmic trading service
|
|
1200
|
-
* @param {Object} account - Trading account
|
|
1201
|
-
* @param {Array} contracts - Array of contracts to trade
|
|
1202
|
-
* @param {Object} config - Algo configuration
|
|
1203
|
-
* @param {Function} [CustomStrategyClass] - Optional custom strategy class (if not provided, uses hftStrategy)
|
|
1204
|
-
*/
|
|
1205
|
-
const launchMultiSymbolRithmic = async (service, account, contracts, config, CustomStrategyClass = null) => {
|
|
1206
|
-
const { dailyTarget, maxRisk, showName, enableAI } = config;
|
|
1207
|
-
|
|
1208
|
-
const accountName = showName
|
|
1209
|
-
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
1210
|
-
: 'HQX *****';
|
|
1211
|
-
const rithmicAccountId = account.rithmicAccountId || account.accountId;
|
|
1212
|
-
|
|
1213
|
-
// Build symbols string for UI (clean names without X1 suffix, symbol only)
|
|
1214
|
-
const symbolsDisplay = contracts.map(c => {
|
|
1215
|
-
let name = c.name || c.symbol;
|
|
1216
|
-
// Remove X1, X2 suffix (Rithmic internal suffixes)
|
|
1217
|
-
name = name.replace(/X\d+$/, '');
|
|
1218
|
-
return name;
|
|
1219
|
-
}).join(', ');
|
|
1220
|
-
|
|
1221
|
-
const ui = new AlgoUI({
|
|
1222
|
-
subtitle: `MULTI-SYMBOL (${contracts.length})`,
|
|
1223
|
-
mode: 'one-account'
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
// Calculate total qty across all symbols
|
|
1227
|
-
const totalQty = contracts.reduce((sum, c) => sum + (c.qty || 1), 0);
|
|
1228
|
-
|
|
1229
|
-
// Baseline P&L captured at session start (to show only THIS session's P&L)
|
|
1230
|
-
let baselineClosedPnl = null; // Will be set on first pnlUpdate
|
|
1231
|
-
|
|
1232
|
-
// Shared stats (same structure as launchAlgo)
|
|
1233
|
-
const stats = {
|
|
1234
|
-
accountName,
|
|
1235
|
-
symbol: symbolsDisplay,
|
|
1236
|
-
qty: totalQty,
|
|
1237
|
-
target: dailyTarget,
|
|
1238
|
-
risk: maxRisk,
|
|
1239
|
-
propfirm: account.propfirm || 'Unknown',
|
|
1240
|
-
platform: 'RITHMIC',
|
|
1241
|
-
pnl: null,
|
|
1242
|
-
openPnl: 0, // Start at 0 for session
|
|
1243
|
-
closedPnl: 0, // Start at 0 for session (will show delta from baseline)
|
|
1244
|
-
balance: null,
|
|
1245
|
-
buyingPower: null,
|
|
1246
|
-
margin: null,
|
|
1247
|
-
netLiquidation: null,
|
|
1248
|
-
position: 0,
|
|
1249
|
-
entryPrice: 0,
|
|
1250
|
-
lastPrice: 0,
|
|
1251
|
-
trades: 0,
|
|
1252
|
-
wins: 0,
|
|
1253
|
-
losses: 0,
|
|
1254
|
-
sessionPnl: 0,
|
|
1255
|
-
latency: 0,
|
|
1256
|
-
connected: false,
|
|
1257
|
-
startTime: Date.now(),
|
|
1258
|
-
aiSupervision: enableAI || false,
|
|
1259
|
-
aiMode: null,
|
|
1260
|
-
agentCount: 0,
|
|
1261
|
-
fastPath: true,
|
|
1262
|
-
avgEntryLatency: 0,
|
|
1263
|
-
avgFillLatency: 0,
|
|
1264
|
-
entryLatencies: [],
|
|
1265
|
-
// Per-symbol tracking
|
|
1266
|
-
symbolStats: {},
|
|
1267
|
-
};
|
|
1268
|
-
|
|
1269
|
-
// Initialize per-symbol stats
|
|
1270
|
-
contracts.forEach(c => {
|
|
1271
|
-
const name = c.name || c.symbol;
|
|
1272
|
-
stats.symbolStats[name] = {
|
|
1273
|
-
position: 0,
|
|
1274
|
-
trades: 0,
|
|
1275
|
-
wins: 0,
|
|
1276
|
-
losses: 0,
|
|
1277
|
-
pnl: 0,
|
|
1278
|
-
openPnl: 0, // Per-symbol unrealized P&L
|
|
1279
|
-
tickCount: 0,
|
|
1280
|
-
};
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
let running = true;
|
|
1284
|
-
let stopReason = null;
|
|
1285
|
-
let tickCount = 0;
|
|
1286
|
-
|
|
1287
|
-
// Store contract info for later use (including qty per symbol)
|
|
1288
|
-
const contractInfoMap = {};
|
|
1289
|
-
contracts.forEach(c => {
|
|
1290
|
-
const name = c.name || c.symbol;
|
|
1291
|
-
contractInfoMap[name] = {
|
|
1292
|
-
tickSize: c.tickSize ?? null,
|
|
1293
|
-
tickValue: c.tickValue ?? null,
|
|
1294
|
-
contractId: c.id || c.symbol || c.name,
|
|
1295
|
-
exchange: c.exchange || 'CME',
|
|
1296
|
-
qty: c.qty || 1, // Per-symbol quantity
|
|
1297
|
-
};
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1301
|
-
// AI SUPERVISOR INITIALIZATION (same as single-symbol)
|
|
1302
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1303
|
-
let aiAgentCount = 0;
|
|
1304
|
-
if (enableAI) {
|
|
1305
|
-
const aiAgents = aiService.getAgents();
|
|
1306
|
-
aiAgentCount = aiAgents.length;
|
|
1307
|
-
stats.agentCount = aiAgentCount;
|
|
1308
|
-
if (aiAgents.length > 0) {
|
|
1309
|
-
// Use first symbol's strategy for AI supervisor
|
|
1310
|
-
const firstContract = contracts[0];
|
|
1311
|
-
const firstSymbolName = firstContract.name || firstContract.symbol;
|
|
1312
|
-
// Strategy will be created below, so we init supervisor after strategies
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1317
|
-
// POSITION MANAGERS & STRATEGIES - One per symbol
|
|
1318
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1319
|
-
const positionManagers = {};
|
|
1320
|
-
const strategies = {};
|
|
1321
|
-
const pendingOrders = {}; // Track pending orders per symbol
|
|
1322
|
-
// No disabled symbols - all symbols can trade until Target/Risk reached
|
|
1323
|
-
|
|
1324
|
-
contracts.forEach(contract => {
|
|
1325
|
-
const symbolName = contract.name || contract.symbol;
|
|
1326
|
-
const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
|
|
1327
|
-
|
|
1328
|
-
// Create strategy instance for this symbol
|
|
1329
|
-
// Use custom strategy class if provided, otherwise use default hftStrategy
|
|
1330
|
-
let strategy;
|
|
1331
|
-
if (CustomStrategyClass) {
|
|
1332
|
-
strategy = new CustomStrategyClass();
|
|
1333
|
-
} else {
|
|
1334
|
-
strategy = Object.create(hftStrategy);
|
|
1335
|
-
}
|
|
1336
|
-
if (tickSize !== null && tickValue !== null) {
|
|
1337
|
-
strategy.initialize(contractId, tickSize, tickValue);
|
|
1338
|
-
}
|
|
1339
|
-
strategies[symbolName] = strategy;
|
|
1340
|
-
pendingOrders[symbolName] = false;
|
|
1341
|
-
|
|
1342
|
-
// Create position manager for this symbol
|
|
1343
|
-
const pm = new PositionManager(service, strategy);
|
|
1344
|
-
if (tickSize !== null && tickValue !== null) {
|
|
1345
|
-
pm.setContractInfo(symbolName, { tickSize, tickValue, contractId });
|
|
1346
|
-
}
|
|
1347
|
-
pm.start();
|
|
1348
|
-
positionManagers[symbolName] = pm;
|
|
1349
|
-
|
|
1350
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
1351
|
-
// POSITION MANAGER EVENTS (same as launchAlgo)
|
|
1352
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
1353
|
-
pm.on('entryFilled', ({ position, fillLatencyMs }) => {
|
|
1354
|
-
stats.entryLatencies.push(fillLatencyMs);
|
|
1355
|
-
stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
|
|
1356
|
-
const side = position.side === 0 ? 'LONG' : 'SHORT';
|
|
1357
|
-
const priceStr = formatPrice(position.entryPrice, tickSize || 0.25);
|
|
1358
|
-
ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${priceStr} | ${fillLatencyMs}ms`);
|
|
1359
|
-
stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
|
|
1360
|
-
ui.render(stats);
|
|
1361
|
-
});
|
|
1362
|
-
|
|
1363
|
-
pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
|
|
1364
|
-
const holdSec = (holdDurationMs / 1000).toFixed(1);
|
|
1365
|
-
if (pnlTicks !== null && tickValue !== null) {
|
|
1366
|
-
const pnlDollars = pnlTicks * tickValue;
|
|
1367
|
-
stats.sessionPnl += pnlDollars;
|
|
1368
|
-
stats.symbolStats[symbolName].pnl += pnlDollars;
|
|
1369
|
-
stats.symbolStats[symbolName].trades++;
|
|
1370
|
-
stats.trades++;
|
|
1371
|
-
|
|
1372
|
-
// Record trade for Recovery Math
|
|
1373
|
-
recoveryMath.recordTrade({
|
|
1374
|
-
pnl: pnlDollars,
|
|
1375
|
-
ticks: pnlTicks,
|
|
1376
|
-
side: pnlTicks >= 0 ? 'win' : 'loss',
|
|
1377
|
-
duration: holdDurationMs,
|
|
1378
|
-
});
|
|
1379
|
-
|
|
1380
|
-
// Update Recovery Mode state
|
|
1381
|
-
const recovery = recoveryMath.updateSessionPnL(
|
|
1382
|
-
stats.sessionPnl,
|
|
1383
|
-
FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
|
|
1384
|
-
FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
|
|
1385
|
-
);
|
|
1386
|
-
|
|
1387
|
-
if (recovery.justActivated) {
|
|
1388
|
-
stats.recoveryMode = true;
|
|
1389
|
-
ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}%`);
|
|
1390
|
-
} else if (recovery.justDeactivated) {
|
|
1391
|
-
stats.recoveryMode = false;
|
|
1392
|
-
ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
if (pnlDollars >= 0) {
|
|
1396
|
-
stats.wins++;
|
|
1397
|
-
stats.symbolStats[symbolName].wins++;
|
|
1398
|
-
const priceStr = formatPrice(exitPrice, tickSize || 0.25);
|
|
1399
|
-
ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} @ ${priceStr} | ${holdSec}s`);
|
|
1400
|
-
} else {
|
|
1401
|
-
stats.losses++;
|
|
1402
|
-
stats.symbolStats[symbolName].losses++;
|
|
1403
|
-
const priceStr = formatPrice(exitPrice, tickSize || 0.25);
|
|
1404
|
-
ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} @ ${priceStr} | ${holdSec}s`);
|
|
1405
|
-
// Symbol can trade again - no disable, continue until Target/Risk reached
|
|
1406
|
-
}
|
|
1407
|
-
} else if (pnlTicks !== null) {
|
|
1408
|
-
// Log with ticks only
|
|
1409
|
-
stats.trades++;
|
|
1410
|
-
stats.symbolStats[symbolName].trades++;
|
|
1411
|
-
if (pnlTicks >= 0) {
|
|
1412
|
-
stats.wins++;
|
|
1413
|
-
stats.symbolStats[symbolName].wins++;
|
|
1414
|
-
ui.addLog('win', `[${symbolName}] +${pnlTicks} ticks | ${holdSec}s`);
|
|
1415
|
-
} else {
|
|
1416
|
-
stats.losses++;
|
|
1417
|
-
stats.symbolStats[symbolName].losses++;
|
|
1418
|
-
ui.addLog('loss', `[${symbolName}] ${pnlTicks} ticks | ${holdSec}s`);
|
|
1419
|
-
// Symbol can trade again - no disable, continue until Target/Risk reached
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
stats.symbolStats[symbolName].position = 0;
|
|
1423
|
-
stats.symbolStats[symbolName].openPnl = 0; // Reset open P&L when position closed
|
|
1424
|
-
// Recalculate total Open P&L
|
|
1425
|
-
stats.openPnl = Object.values(stats.symbolStats).reduce((sum, s) => sum + (s.openPnl || 0), 0);
|
|
1426
|
-
pendingOrders[symbolName] = false;
|
|
1427
|
-
ui.render(stats);
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
pm.on('holdComplete', () => {
|
|
1431
|
-
ui.addLog('ready', `[${symbolName}] Hold complete - monitoring exit`);
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
pm.on('breakevenActivated', ({ breakevenPrice, pnlTicks }) => {
|
|
1435
|
-
const priceStr = formatPrice(breakevenPrice, tickSize || 0.25);
|
|
1436
|
-
ui.addLog('be', `[${symbolName}] BE @ ${priceStr} | +${pnlTicks} ticks`);
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
1440
|
-
// STRATEGY SIGNALS
|
|
1441
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
1442
|
-
strategy.on('signal', async (signal) => {
|
|
1443
|
-
if (!running) return;
|
|
1444
|
-
if (pendingOrders[symbolName]) return;
|
|
1445
|
-
if (!pm.canEnter(symbolName)) return;
|
|
1446
|
-
|
|
1447
|
-
const { direction, confidence } = signal;
|
|
1448
|
-
const orderSide = direction === 'long' ? 0 : 1;
|
|
1449
|
-
const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
|
|
1450
|
-
|
|
1451
|
-
// Use per-symbol quantity
|
|
1452
|
-
const symbolQty = contractInfoMap[symbolName].qty;
|
|
1453
|
-
|
|
1454
|
-
// Calculate risk amount
|
|
1455
|
-
const kelly = Math.min(0.25, confidence || 0.15);
|
|
1456
|
-
const riskAmount = Math.round(maxRisk * kelly);
|
|
1457
|
-
const riskPct = Math.round((riskAmount / maxRisk) * 100);
|
|
1458
|
-
|
|
1459
|
-
pendingOrders[symbolName] = true;
|
|
1460
|
-
ui.addLog('entry', `[${symbolName}] ${sideStr} ${symbolQty}x | risk: $${riskAmount} (${riskPct}%)`);
|
|
1461
|
-
|
|
1462
|
-
const orderData = {
|
|
1463
|
-
accountId: rithmicAccountId,
|
|
1464
|
-
symbol: symbolName,
|
|
1465
|
-
exchange: contractInfoMap[symbolName].exchange,
|
|
1466
|
-
size: symbolQty,
|
|
1467
|
-
side: orderSide,
|
|
1468
|
-
};
|
|
1469
|
-
|
|
1470
|
-
try {
|
|
1471
|
-
const entryResult = service.fastEntry(orderData);
|
|
1472
|
-
if (entryResult.success) {
|
|
1473
|
-
pm.registerEntry(entryResult, orderData, contractInfoMap[symbolName]);
|
|
1474
|
-
|
|
1475
|
-
// Update avg entry latency
|
|
1476
|
-
stats.avgEntryLatency = stats.entryLatencies.length > 0
|
|
1477
|
-
? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
|
|
1478
|
-
: entryResult.latencyMs;
|
|
1479
|
-
} else {
|
|
1480
|
-
ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
|
|
1481
|
-
pendingOrders[symbolName] = false;
|
|
1482
|
-
}
|
|
1483
|
-
} catch (e) {
|
|
1484
|
-
ui.addLog('error', `[${symbolName}] Order error: ${e.message}`);
|
|
1485
|
-
pendingOrders[symbolName] = false;
|
|
1486
|
-
}
|
|
1487
|
-
});
|
|
1488
|
-
});
|
|
1489
|
-
|
|
1490
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1491
|
-
// AI SUPERVISOR - Initialize after strategies are created
|
|
1492
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1493
|
-
if (enableAI && aiAgentCount > 0) {
|
|
1494
|
-
const aiAgents = aiService.getAgents();
|
|
1495
|
-
const firstSymbol = Object.keys(strategies)[0];
|
|
1496
|
-
const firstStrategy = strategies[firstSymbol];
|
|
1497
|
-
const supervisorResult = StrategySupervisor.initialize(firstStrategy, aiAgents, service, rithmicAccountId);
|
|
1498
|
-
stats.aiSupervision = supervisorResult.success;
|
|
1499
|
-
stats.aiMode = supervisorResult.mode;
|
|
1500
|
-
|
|
1501
|
-
if (stats.aiSupervision) {
|
|
1502
|
-
algoLogger.info(ui, 'AI SUPERVISION', `${aiAgentCount} agent(s) - ${stats.aiMode} mode - LEARNING ACTIVE`);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1507
|
-
// MARKET DATA FEED - Single connection, multiple subscriptions
|
|
1508
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1509
|
-
const marketFeed = new RithmicMarketDataFeed(service);
|
|
1510
|
-
|
|
1511
|
-
marketFeed.on('connected', () => {
|
|
1512
|
-
stats.connected = true;
|
|
1513
|
-
algoLogger.dataConnected(ui, 'RTC');
|
|
1514
|
-
algoLogger.algoOperational(ui, 'RITHMIC');
|
|
1515
|
-
});
|
|
1516
|
-
|
|
1517
|
-
// Smart logs state tracking
|
|
1518
|
-
let lastHeartbeat = Date.now();
|
|
1519
|
-
let tps = 0;
|
|
1520
|
-
|
|
1521
|
-
marketFeed.on('tick', (tick) => {
|
|
1522
|
-
if (!running) return;
|
|
1523
|
-
|
|
1524
|
-
tickCount++;
|
|
1525
|
-
tps++;
|
|
1526
|
-
stats.latency = tick.latency || 0;
|
|
1527
|
-
|
|
1528
|
-
// Route tick to correct strategy based on symbol
|
|
1529
|
-
const tickSymbol = tick.symbol;
|
|
1530
|
-
|
|
1531
|
-
// Find matching strategy (ES matches ESH6, NQ matches NQH6, etc.)
|
|
1532
|
-
for (const [symbolName, strategy] of Object.entries(strategies)) {
|
|
1533
|
-
const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
|
|
1534
|
-
if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
|
|
1535
|
-
stats.symbolStats[symbolName].tickCount++;
|
|
1536
|
-
|
|
1537
|
-
// Log first tick per symbol
|
|
1538
|
-
if (stats.symbolStats[symbolName].tickCount === 1) {
|
|
1539
|
-
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))}`);
|
|
1540
|
-
} else if (stats.symbolStats[symbolName].tickCount === 100) {
|
|
1541
|
-
algoLogger.info(ui, 'DATA FLOWING', `[${symbolName}] 100 ticks received`);
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
const tickData = {
|
|
1545
|
-
contractId: tick.contractId || symbolName,
|
|
1546
|
-
price: tick.price || tick.lastPrice || tick.bid,
|
|
1547
|
-
bid: tick.bid,
|
|
1548
|
-
ask: tick.ask,
|
|
1549
|
-
volume: tick.volume || tick.size || 1,
|
|
1550
|
-
side: tick.lastTradeSide || tick.side || 'unknown',
|
|
1551
|
-
timestamp: tick.timestamp || Date.now()
|
|
1552
|
-
};
|
|
1553
|
-
|
|
1554
|
-
// Update last price
|
|
1555
|
-
stats.lastPrice = tickData.price;
|
|
1556
|
-
|
|
1557
|
-
// Feed tick to strategy
|
|
1558
|
-
strategy.processTick(tickData);
|
|
1559
|
-
|
|
1560
|
-
// Update price for position manager
|
|
1561
|
-
service.emit('priceUpdate', {
|
|
1562
|
-
symbol: symbolName,
|
|
1563
|
-
price: tickData.price,
|
|
1564
|
-
timestamp: tickData.timestamp,
|
|
1565
|
-
});
|
|
1566
|
-
|
|
1567
|
-
// Get momentum data from strategy for position manager
|
|
1568
|
-
const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(symbolName);
|
|
1569
|
-
if (modelValues && positionManagers[symbolName] && typeof positionManagers[symbolName].updateMomentum === 'function') {
|
|
1570
|
-
positionManagers[symbolName].updateMomentum(symbolName, {
|
|
1571
|
-
ofi: modelValues.ofi || 0,
|
|
1572
|
-
zscore: modelValues.zscore || 0,
|
|
1573
|
-
delta: modelValues.delta || 0,
|
|
1574
|
-
timestamp: tickData.timestamp,
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
break;
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1583
|
-
// SMART LOGS - Same as single-symbol mode
|
|
1584
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1585
|
-
const now = Date.now();
|
|
1586
|
-
if (now - lastHeartbeat > 1000) {
|
|
1587
|
-
// Get model values from first active symbol's strategy
|
|
1588
|
-
const firstSymbol = Object.keys(strategies)[0];
|
|
1589
|
-
const firstStrategy = strategies[firstSymbol];
|
|
1590
|
-
const modelValues = firstStrategy?.getModelValues?.() || firstStrategy?.getModelValues?.(firstSymbol) || null;
|
|
1591
|
-
|
|
1592
|
-
if (modelValues && modelValues.ofi !== undefined) {
|
|
1593
|
-
const ofi = modelValues.ofi || 0;
|
|
1594
|
-
const delta = modelValues.delta || 0;
|
|
1595
|
-
const zscore = modelValues.zscore || 0;
|
|
1596
|
-
const mom = modelValues.momentum || 0;
|
|
1597
|
-
|
|
1598
|
-
// Check if any symbol has an open position
|
|
1599
|
-
const totalPosition = Object.values(stats.symbolStats).reduce((sum, s) => sum + Math.abs(s.position || 0), 0);
|
|
1600
|
-
|
|
1601
|
-
if (totalPosition === 0) {
|
|
1602
|
-
// Not in position - show market analysis (varied messages)
|
|
1603
|
-
const smartLogs = require('./smart-logs');
|
|
1604
|
-
const stateLog = smartLogs.getMarketStateLog(ofi, zscore, mom, delta);
|
|
1605
|
-
if (stateLog.details) {
|
|
1606
|
-
ui.addLog('analysis', `${stateLog.message} - ${stateLog.details}`);
|
|
1607
|
-
} else {
|
|
1608
|
-
ui.addLog('info', stateLog.message);
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
// When IN POSITION: Don't spam logs every second
|
|
1612
|
-
} else {
|
|
1613
|
-
// Waiting for data - log every 5 seconds only
|
|
1614
|
-
if (now - lastHeartbeat > 5000) {
|
|
1615
|
-
const smartLogs = require('./smart-logs');
|
|
1616
|
-
const scanLog = smartLogs.getScanningLog(true);
|
|
1617
|
-
ui.addLog('info', `${scanLog.message} ${tps} ticks/s`);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
lastHeartbeat = now;
|
|
1621
|
-
tps = 0;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
ui.render(stats);
|
|
1625
|
-
});
|
|
1626
|
-
|
|
1627
|
-
marketFeed.on('error', (err) => {
|
|
1628
|
-
algoLogger.error(ui, 'MARKET ERROR', err.message);
|
|
1629
|
-
});
|
|
1630
|
-
|
|
1631
|
-
marketFeed.on('disconnected', (err) => {
|
|
1632
|
-
stats.connected = false;
|
|
1633
|
-
algoLogger.dataDisconnected(ui, 'WEBSOCKET', err?.message);
|
|
1634
|
-
});
|
|
1635
|
-
|
|
1636
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1637
|
-
// STARTUP LOGS (same as launchAlgo)
|
|
1638
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1639
|
-
const market = checkMarketHours();
|
|
1640
|
-
const sessionName = market.session || 'AMERICAN';
|
|
1641
|
-
const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
|
|
1642
|
-
|
|
1643
|
-
algoLogger.connectingToEngine(ui, account.accountId);
|
|
1644
|
-
algoLogger.engineStarting(ui, 'RITHMIC', dailyTarget, maxRisk);
|
|
1645
|
-
algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
|
|
1646
|
-
algoLogger.info(ui, 'FAST PATH', `Rithmic direct | ${contracts.length} symbols | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms`);
|
|
1647
|
-
|
|
1648
|
-
ui.render(stats);
|
|
1649
|
-
|
|
1650
|
-
// Connect and subscribe
|
|
1651
|
-
try {
|
|
1652
|
-
algoLogger.info(ui, 'CONNECTING', `RITHMIC TICKER | ${contracts.length} symbols`);
|
|
1653
|
-
await marketFeed.connect();
|
|
1654
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1655
|
-
|
|
1656
|
-
if (marketFeed.isConnected()) {
|
|
1657
|
-
for (const contract of contracts) {
|
|
1658
|
-
const symbolName = contract.name || contract.symbol;
|
|
1659
|
-
const exchange = contract.exchange || 'CME';
|
|
1660
|
-
marketFeed.subscribe(symbolName, exchange);
|
|
1661
|
-
algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} (${exchange})`);
|
|
1662
|
-
}
|
|
1663
|
-
} else {
|
|
1664
|
-
algoLogger.error(ui, 'CONNECTION', 'Failed to connect market feed');
|
|
1665
|
-
}
|
|
1666
|
-
} catch (e) {
|
|
1667
|
-
algoLogger.error(ui, 'CONNECTION ERROR', e.message);
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1671
|
-
// TARGET/RISK CHECK - Stop algo when limits reached
|
|
1672
|
-
// Uses SESSION P&L (trades from this HQX session only) + Open P&L
|
|
1673
|
-
// Risk triggers if: closedPnl hits risk OR openPnl hits risk OR total hits risk
|
|
1674
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1675
|
-
const checkTargetRisk = () => {
|
|
1676
|
-
if (!running) return;
|
|
1677
|
-
|
|
1678
|
-
const closedPnl = stats.closedPnl || 0; // Session closed P&L
|
|
1679
|
-
const openPnl = stats.openPnl || 0; // Current open P&L
|
|
1680
|
-
const totalPnl = closedPnl + openPnl; // Total session P&L
|
|
1681
|
-
|
|
1682
|
-
// Daily target reached - STOP with profit
|
|
1683
|
-
if (totalPnl >= dailyTarget) {
|
|
1684
|
-
stopReason = 'target';
|
|
1685
|
-
running = false;
|
|
1686
|
-
algoLogger.info(ui, 'TARGET REACHED', `+$${totalPnl.toFixed(2)} >= $${dailyTarget}`);
|
|
1687
|
-
ui.addLog('success', `████ DAILY TARGET REACHED: +$${totalPnl.toFixed(2)} ████`);
|
|
1688
|
-
emergencyStopAll();
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// Max risk reached - STOP to protect capital
|
|
1693
|
-
// Trigger if: closed P&L hits risk OR open P&L hits risk OR total hits risk
|
|
1694
|
-
if (closedPnl <= -maxRisk) {
|
|
1695
|
-
stopReason = 'risk';
|
|
1696
|
-
running = false;
|
|
1697
|
-
algoLogger.info(ui, 'CLOSED P&L RISK', `Closed: -$${Math.abs(closedPnl).toFixed(2)} <= -$${maxRisk}`);
|
|
1698
|
-
ui.addLog('error', `████ MAX RISK (CLOSED): -$${Math.abs(closedPnl).toFixed(2)} ████`);
|
|
1699
|
-
emergencyStopAll();
|
|
1700
|
-
} else if (openPnl <= -maxRisk) {
|
|
1701
|
-
stopReason = 'risk';
|
|
1702
|
-
running = false;
|
|
1703
|
-
algoLogger.info(ui, 'OPEN P&L RISK', `Open: -$${Math.abs(openPnl).toFixed(2)} <= -$${maxRisk}`);
|
|
1704
|
-
ui.addLog('error', `████ MAX RISK (OPEN): -$${Math.abs(openPnl).toFixed(2)} ████`);
|
|
1705
|
-
emergencyStopAll();
|
|
1706
|
-
} else if (totalPnl <= -maxRisk) {
|
|
1707
|
-
stopReason = 'risk';
|
|
1708
|
-
running = false;
|
|
1709
|
-
algoLogger.info(ui, 'TOTAL P&L RISK', `Total: -$${Math.abs(totalPnl).toFixed(2)} <= -$${maxRisk}`);
|
|
1710
|
-
ui.addLog('error', `████ MAX RISK (TOTAL): -$${Math.abs(totalPnl).toFixed(2)} ████`);
|
|
1711
|
-
emergencyStopAll();
|
|
1712
|
-
}
|
|
1713
|
-
};
|
|
1714
|
-
|
|
1715
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1716
|
-
// REAL-TIME P&L VIA WEBSOCKET (same as launchAlgo)
|
|
1717
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1718
|
-
if (typeof service.on === 'function') {
|
|
1719
|
-
// Account-level P&L updates
|
|
1720
|
-
service.on('pnlUpdate', (pnlData) => {
|
|
1721
|
-
if (pnlData.accountId !== rithmicAccountId) return;
|
|
1722
|
-
|
|
1723
|
-
if (pnlData.closedPositionPnl !== undefined) {
|
|
1724
|
-
const accountClosedPnl = parseFloat(pnlData.closedPositionPnl);
|
|
1725
|
-
|
|
1726
|
-
// Capture baseline on first update (P&L at session start)
|
|
1727
|
-
if (baselineClosedPnl === null) {
|
|
1728
|
-
baselineClosedPnl = accountClosedPnl;
|
|
1729
|
-
// Baseline captured silently - no need to show to user
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
// stats.closedPnl shows ONLY this session's closed P&L (delta from baseline)
|
|
1733
|
-
stats.closedPnl = accountClosedPnl - baselineClosedPnl;
|
|
1734
|
-
}
|
|
1735
|
-
if (pnlData.accountBalance !== undefined) {
|
|
1736
|
-
stats.balance = parseFloat(pnlData.accountBalance);
|
|
1737
|
-
}
|
|
1738
|
-
if (pnlData.availableBuyingPower !== undefined) {
|
|
1739
|
-
stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
|
|
1740
|
-
}
|
|
1741
|
-
if (pnlData.marginBalance !== undefined) {
|
|
1742
|
-
stats.margin = parseFloat(pnlData.marginBalance);
|
|
1743
|
-
}
|
|
1744
|
-
if (pnlData.netLiquidation !== undefined) {
|
|
1745
|
-
stats.netLiquidation = parseFloat(pnlData.netLiquidation);
|
|
1746
|
-
} else if (stats.balance !== null) {
|
|
1747
|
-
stats.netLiquidation = stats.balance + (stats.openPnl || 0);
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
1751
|
-
|
|
1752
|
-
// Check target/risk on every P&L update
|
|
1753
|
-
checkTargetRisk();
|
|
1754
|
-
|
|
1755
|
-
ui.render(stats);
|
|
1756
|
-
});
|
|
1757
|
-
|
|
1758
|
-
// Position-level updates (for Open P&L per symbol)
|
|
1759
|
-
service.on('positionUpdate', (pos) => {
|
|
1760
|
-
if (!pos || pos.accountId !== rithmicAccountId) return;
|
|
1761
|
-
|
|
1762
|
-
const posSymbol = pos.symbol;
|
|
1763
|
-
for (const symbolName of Object.keys(stats.symbolStats)) {
|
|
1764
|
-
const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
|
|
1765
|
-
if (posSymbol === baseSymbol || posSymbol === symbolName || symbolName.startsWith(posSymbol)) {
|
|
1766
|
-
stats.symbolStats[symbolName].position = pos.quantity || 0;
|
|
1767
|
-
|
|
1768
|
-
// Update Open P&L for this symbol
|
|
1769
|
-
if (pos.openPnl !== undefined && pos.openPnl !== null) {
|
|
1770
|
-
stats.symbolStats[symbolName].openPnl = pos.openPnl;
|
|
1771
|
-
|
|
1772
|
-
// Calculate total Open P&L from all symbols
|
|
1773
|
-
stats.openPnl = Object.values(stats.symbolStats).reduce((sum, s) => sum + (s.openPnl || 0), 0);
|
|
1774
|
-
stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
|
|
1775
|
-
|
|
1776
|
-
if (stats.balance !== null) {
|
|
1777
|
-
stats.netLiquidation = stats.balance + stats.openPnl;
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
// Check target/risk on Open P&L changes too
|
|
1781
|
-
checkTargetRisk();
|
|
1782
|
-
}
|
|
1783
|
-
break;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
ui.render(stats);
|
|
1787
|
-
});
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1791
|
-
// EMERGENCY STOP - Force close ALL positions
|
|
1792
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1793
|
-
const emergencyStopAll = async () => {
|
|
1794
|
-
ui.addLog('warning', '████ EMERGENCY STOP INITIATED ████');
|
|
1795
|
-
ui.render(stats);
|
|
1796
|
-
|
|
1797
|
-
const TIMEOUT_MS = 5000;
|
|
1798
|
-
const withTimeout = (promise, ms) => Promise.race([
|
|
1799
|
-
promise,
|
|
1800
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('TIMEOUT')), ms))
|
|
1801
|
-
]);
|
|
1802
|
-
|
|
1803
|
-
try {
|
|
1804
|
-
// CRITICAL: Use ONLY Rithmic API to get REAL positions from exchange
|
|
1805
|
-
// DO NOT use local stats which can be corrupted
|
|
1806
|
-
|
|
1807
|
-
// Step 1: Cancel all pending orders first
|
|
1808
|
-
ui.addLog('info', 'Cancelling all orders...');
|
|
1809
|
-
ui.render(stats);
|
|
1810
|
-
try {
|
|
1811
|
-
if (service && typeof service.cancelAllOrders === 'function') {
|
|
1812
|
-
await withTimeout(service.cancelAllOrders(rithmicAccountId), TIMEOUT_MS);
|
|
1813
|
-
ui.addLog('success', 'All orders cancelled');
|
|
1814
|
-
}
|
|
1815
|
-
} catch (e) {
|
|
1816
|
-
ui.addLog('warning', `Cancel orders: ${e.message}`);
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
// Step 2: Use Rithmic flattenAll - reads REAL positions from exchange
|
|
1820
|
-
ui.addLog('info', 'Flattening all positions via Rithmic API...');
|
|
1821
|
-
ui.render(stats);
|
|
1822
|
-
|
|
1823
|
-
try {
|
|
1824
|
-
if (service && typeof service.flattenAll === 'function') {
|
|
1825
|
-
const result = await withTimeout(service.flattenAll(rithmicAccountId), TIMEOUT_MS);
|
|
1826
|
-
if (result && result.results) {
|
|
1827
|
-
for (const r of result.results) {
|
|
1828
|
-
if (r.success) {
|
|
1829
|
-
ui.addLog('success', `[${r.symbol}] FLATTENED`);
|
|
1830
|
-
} else {
|
|
1831
|
-
ui.addLog('error', `[${r.symbol}] ${r.error || 'Failed'}`);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
ui.addLog('success', 'Flatten all complete');
|
|
1836
|
-
} else if (service && typeof service.emergencyStop === 'function') {
|
|
1837
|
-
// Fallback to emergencyStop
|
|
1838
|
-
await withTimeout(service.emergencyStop(rithmicAccountId), TIMEOUT_MS);
|
|
1839
|
-
ui.addLog('success', 'Emergency stop complete');
|
|
1840
|
-
}
|
|
1841
|
-
} catch (e) {
|
|
1842
|
-
ui.addLog('warning', `Flatten: ${e.message}`);
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
// Step 3: Reset local state
|
|
1846
|
-
for (const [symbolName, symStats] of Object.entries(stats.symbolStats)) {
|
|
1847
|
-
symStats.position = 0;
|
|
1848
|
-
symStats.openPnl = 0;
|
|
1849
|
-
// Also reset position manager
|
|
1850
|
-
const pm = positionManagers[symbolName];
|
|
1851
|
-
if (pm) {
|
|
1852
|
-
try { pm.reset?.(); } catch {}
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
stats.openPnl = 0;
|
|
1856
|
-
|
|
1857
|
-
ui.addLog('success', '████ EMERGENCY STOP COMPLETE ████');
|
|
1858
|
-
|
|
1859
|
-
} catch (e) {
|
|
1860
|
-
ui.addLog('error', `Emergency stop error: ${e.message}`);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
ui.render(stats);
|
|
1864
|
-
};
|
|
1865
|
-
|
|
1866
|
-
// Keyboard handler
|
|
1867
|
-
let emergencyStopInProgress = false;
|
|
1868
|
-
|
|
1869
|
-
const setupKeyHandler = () => {
|
|
1870
|
-
if (!process.stdin.isTTY) return;
|
|
1871
|
-
readline.emitKeypressEvents(process.stdin);
|
|
1872
|
-
process.stdin.setRawMode(true);
|
|
1873
|
-
process.stdin.resume();
|
|
1874
|
-
|
|
1875
|
-
const onKey = async (str, key) => {
|
|
1876
|
-
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
1877
|
-
if (emergencyStopInProgress) return;
|
|
1878
|
-
emergencyStopInProgress = true;
|
|
1879
|
-
running = false;
|
|
1880
|
-
stopReason = 'manual';
|
|
1881
|
-
await emergencyStopAll();
|
|
1882
|
-
}
|
|
1883
|
-
};
|
|
1884
|
-
process.stdin.on('keypress', onKey);
|
|
1885
|
-
return () => {
|
|
1886
|
-
process.stdin.removeListener('keypress', onKey);
|
|
1887
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1888
|
-
};
|
|
1889
|
-
};
|
|
1890
|
-
|
|
1891
|
-
const cleanupKeys = setupKeyHandler();
|
|
1892
|
-
|
|
1893
|
-
// UI refresh
|
|
1894
|
-
const refreshInterval = setInterval(() => {
|
|
1895
|
-
if (running) ui.render(stats);
|
|
1896
|
-
}, 1000);
|
|
1897
|
-
|
|
1898
|
-
// Wait for stop
|
|
1899
|
-
await new Promise(resolve => {
|
|
1900
|
-
const check = setInterval(() => {
|
|
1901
|
-
if (!running) {
|
|
1902
|
-
clearInterval(check);
|
|
1903
|
-
resolve();
|
|
1904
|
-
}
|
|
1905
|
-
}, 100);
|
|
1906
|
-
});
|
|
1907
|
-
|
|
1908
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1909
|
-
// CLEANUP
|
|
1910
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1911
|
-
clearInterval(refreshInterval);
|
|
1912
|
-
|
|
1913
|
-
// Stop all position managers
|
|
1914
|
-
for (const pm of Object.values(positionManagers)) {
|
|
1915
|
-
try { pm.stop(); } catch {}
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
// Disconnect market feed with timeout
|
|
1919
|
-
try {
|
|
1920
|
-
const disconnectPromise = marketFeed.disconnect();
|
|
1921
|
-
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 2000));
|
|
1922
|
-
await Promise.race([disconnectPromise, timeoutPromise]);
|
|
1923
|
-
} catch {}
|
|
1924
|
-
|
|
1925
|
-
// Cleanup keyboard FIRST - restore stdin to normal mode
|
|
1926
|
-
try { if (cleanupKeys) cleanupKeys(); } catch {}
|
|
1927
|
-
|
|
1928
|
-
// Force stdin back to normal mode (critical for waitForEnter to work)
|
|
1929
|
-
try {
|
|
1930
|
-
if (process.stdin.isTTY) {
|
|
1931
|
-
process.stdin.setRawMode(false);
|
|
1932
|
-
}
|
|
1933
|
-
// Remove all listeners to prevent interference
|
|
1934
|
-
process.stdin.removeAllListeners('keypress');
|
|
1935
|
-
process.stdin.resume();
|
|
1936
|
-
} catch {}
|
|
1937
|
-
|
|
1938
|
-
// Small delay to let stdin settle
|
|
1939
|
-
await new Promise(r => setTimeout(r, 100));
|
|
1940
|
-
|
|
1941
|
-
// Calculate duration before closeLog
|
|
1942
|
-
const durationMs = Date.now() - stats.startTime;
|
|
1943
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
1944
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
1945
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
1946
|
-
stats.duration = hours > 0
|
|
1947
|
-
? `${hours}h ${minutes}m ${seconds}s`
|
|
1948
|
-
: minutes > 0
|
|
1949
|
-
? `${minutes}m ${seconds}s`
|
|
1950
|
-
: `${seconds}s`;
|
|
1951
|
-
|
|
1952
|
-
// Close log file with session summary
|
|
1953
|
-
try { ui.closeLog(stats); } catch {}
|
|
1954
|
-
|
|
1955
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1956
|
-
// SESSION SUMMARY (duration already calculated above)
|
|
1957
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1958
|
-
// Clear screen, show cursor, and render summary
|
|
1959
|
-
process.stdout.write('\x1B[?25h'); // Show cursor
|
|
1960
|
-
console.clear();
|
|
1961
|
-
|
|
1962
|
-
// Render multi-symbol summary with same style as single-symbol
|
|
1963
|
-
renderMultiSymbolSummary(stats, stopReason, stats.symbolStats);
|
|
1964
|
-
|
|
1965
|
-
// Wait for user to press Enter before returning to menu
|
|
1966
|
-
await prompts.waitForEnter();
|
|
1967
|
-
};
|
|
88
|
+
const { launchMultiSymbolRithmic } = require('./algo-multi');
|
|
1968
89
|
|
|
1969
90
|
module.exports = { oneAccountMenu, launchMultiSymbolRithmic, selectSymbol, configureAlgo };
|