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