hedgequantx 2.6.159 → 2.6.161

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.
@@ -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 { formatPrice, isRithmicFastPath, MAX_MULTI_SYMBOLS, USE_HFT_STRATEGY } = require('./algo-utils');
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 appropriate algo
103
- if (contractList.length === 1) {
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 };