hedgequantx 2.6.63 → 2.6.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.6.63",
3
+ "version": "2.6.65",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -166,6 +166,16 @@ const FAST_SCALPING = {
166
166
  LOG_LATENCY: true,
167
167
  LATENCY_TARGET_MS: 50, // Target entry latency
168
168
  LATENCY_WARN_MS: 100, // Warn if entry takes > 100ms
169
+
170
+ // ═══════════════════════════════════════════════════════════════════════════
171
+ // RECOVERY MODE - Math-based adaptive strategy when in drawdown
172
+ // Uses Kelly Criterion, Volatility scaling, and Win Rate adjustment
173
+ // ═══════════════════════════════════════════════════════════════════════════
174
+ RECOVERY: {
175
+ ENABLED: true,
176
+ ACTIVATION_PNL: -300, // Activate recovery at -$300 session P&L
177
+ DEACTIVATION_PNL: -100, // Deactivate when recovered to -$100
178
+ },
169
179
  };
170
180
 
171
181
  // ==================== DEBUG ====================
@@ -434,10 +434,10 @@ const selectCategory = async () => {
434
434
 
435
435
  const categories = getCategories();
436
436
 
437
- // Display in 2 columns
437
+ // Display in 2 columns - Numbers in cyan, titles in yellow
438
438
  console.log(make2ColRow(
439
- chalk.green('[1] UNIFIED (RECOMMENDED)'),
440
- chalk.cyan('[2] DIRECT PROVIDERS')
439
+ chalk.cyan('[1]') + chalk.yellow(' UNIFIED (RECOMMENDED)'),
440
+ chalk.cyan('[2]') + chalk.yellow(' DIRECT PROVIDERS')
441
441
  ));
442
442
  console.log(make2ColRow(
443
443
  chalk.white(' 1 API = 100+ models'),
@@ -445,21 +445,20 @@ const selectCategory = async () => {
445
445
  ));
446
446
  console.log(makeLine(''));
447
447
  console.log(make2ColRow(
448
- chalk.yellow('[3] LOCAL (FREE)'),
449
- chalk.white('[4] CUSTOM')
448
+ chalk.cyan('[3]') + chalk.yellow(' LOCAL (FREE)'),
449
+ chalk.cyan('[4]') + chalk.yellow(' CUSTOM')
450
450
  ));
451
451
  console.log(make2ColRow(
452
452
  chalk.white(' Run on your machine'),
453
453
  chalk.white(' Self-hosted solutions')
454
454
  ));
455
- console.log(makeLine(''));
456
- console.log(makeLine(chalk.white('[<] BACK')));
457
455
 
458
456
  drawBoxFooter(boxWidth);
459
457
 
460
458
  const choice = await prompts.textInput(chalk.cyan('SELECT (1-4):'));
461
459
 
462
- if (choice === '<' || choice?.toLowerCase() === 'b') {
460
+ // Empty input = go back
461
+ if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
463
462
  return await aiAgentMenu();
464
463
  }
465
464
 
@@ -510,18 +509,23 @@ const selectProvider = async (categoryId) => {
510
509
  return await selectCategory();
511
510
  }
512
511
 
513
- // Display providers in 2 columns
512
+ // Display providers in 2 columns - Numbers in cyan, names in yellow
514
513
  for (let i = 0; i < providers.length; i += 2) {
515
514
  const left = providers[i];
516
515
  const right = providers[i + 1];
517
516
 
518
- // Provider names
519
- const leftName = `[${i + 1}] ${left.name}`;
520
- const rightName = right ? `[${i + 2}] ${right.name}` : '';
517
+ // Provider names - number in cyan, name in yellow
518
+ const leftNum = `[${i + 1}]`;
519
+ const rightNum = right ? `[${i + 2}]` : '';
520
+ const leftName = ` ${left.name}`;
521
+ const rightName = right ? ` ${right.name}` : '';
522
+
523
+ const leftFull = leftNum + leftName;
524
+ const rightFull = rightNum + rightName;
521
525
 
522
526
  console.log(make2ColRow(
523
- chalk.cyan(leftName.length > col1Width - 3 ? leftName.substring(0, col1Width - 6) + '...' : leftName),
524
- right ? chalk.cyan(rightName.length > col1Width - 3 ? rightName.substring(0, col1Width - 6) + '...' : rightName) : ''
527
+ chalk.cyan(leftNum) + chalk.yellow(leftName.length > col1Width - leftNum.length - 3 ? leftName.substring(0, col1Width - leftNum.length - 6) + '...' : leftName),
528
+ right ? chalk.cyan(rightNum) + chalk.yellow(rightName.length > col1Width - rightNum.length - 3 ? rightName.substring(0, col1Width - rightNum.length - 6) + '...' : rightName) : ''
525
529
  ));
526
530
 
527
531
  // Descriptions (truncated)
@@ -536,14 +540,13 @@ const selectProvider = async (categoryId) => {
536
540
  console.log(makeLine(''));
537
541
  }
538
542
 
539
- console.log(makeLine(chalk.white('[<] BACK')));
540
-
541
543
  drawBoxFooter(boxWidth);
542
544
 
543
545
  const maxNum = providers.length;
544
546
  const choice = await prompts.textInput(chalk.cyan(`SELECT (1-${maxNum}):`));
545
547
 
546
- if (choice === '<' || choice?.toLowerCase() === 'b') {
548
+ // Empty input = go back
549
+ if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
547
550
  return await selectCategory();
548
551
  }
549
552
 
@@ -590,15 +593,15 @@ const selectProviderOption = async (provider) => {
590
593
  console.log(makeLine(chalk.white('SELECT CONNECTION METHOD:')));
591
594
  console.log(makeLine(''));
592
595
 
593
- // Display options in 2 columns
596
+ // Display options in 2 columns - Numbers in cyan, labels in yellow
594
597
  for (let i = 0; i < provider.options.length; i += 2) {
595
598
  const left = provider.options[i];
596
599
  const right = provider.options[i + 1];
597
600
 
598
- // Option labels
601
+ // Option labels - number in cyan, label in yellow
599
602
  console.log(make2ColRow(
600
- chalk.cyan(`[${i + 1}] ${left.label}`),
601
- right ? chalk.cyan(`[${i + 2}] ${right.label}`) : ''
603
+ chalk.cyan(`[${i + 1}]`) + chalk.yellow(` ${left.label}`),
604
+ right ? chalk.cyan(`[${i + 2}]`) + chalk.yellow(` ${right.label}`) : ''
602
605
  ));
603
606
 
604
607
  // First description line
@@ -622,13 +625,12 @@ const selectProviderOption = async (provider) => {
622
625
  console.log(makeLine(''));
623
626
  }
624
627
 
625
- console.log(makeLine(chalk.white('[<] BACK')));
626
-
627
628
  drawBoxFooter(boxWidth);
628
629
 
629
630
  const choice = await prompts.textInput(chalk.cyan('SELECT:'));
630
631
 
631
- if (choice === '<' || choice?.toLowerCase() === 'b') {
632
+ // Empty input = go back
633
+ if (!choice || choice.trim() === '' || choice === '<' || choice?.toLowerCase() === 'b') {
632
634
  return await selectProvider(provider.category);
633
635
  }
634
636
 
@@ -22,6 +22,7 @@ const { hftStrategy } = require('../../services/strategy/hft-tick');
22
22
  const { MarketDataFeed } = require('../../../dist/lib/data');
23
23
  const { RithmicMarketDataFeed } = require('../../services/rithmic/market-data');
24
24
  const { algoLogger } = require('./logger');
25
+ const { recoveryMath } = require('../../services/strategy/recovery-math');
25
26
 
26
27
  // Use HFT tick-based strategy for Rithmic (fast path), M1 for ProjectX
27
28
  const USE_HFT_STRATEGY = true;
@@ -331,13 +332,36 @@ const launchAlgo = async (service, account, contract, config) => {
331
332
  if (pnlTicks !== null && tickValue !== null) {
332
333
  const pnlDollars = pnlTicks * tickValue;
333
334
  stats.sessionPnl += pnlDollars; // Track session P&L
335
+
336
+ // Record trade for Recovery Math
337
+ recoveryMath.recordTrade({
338
+ pnl: pnlDollars,
339
+ ticks: pnlTicks,
340
+ side: pnlTicks >= 0 ? 'win' : 'loss',
341
+ duration: holdDurationMs,
342
+ });
343
+
344
+ // Update Recovery Mode state
345
+ const recovery = recoveryMath.updateSessionPnL(
346
+ stats.sessionPnl,
347
+ FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
348
+ FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
349
+ );
350
+
351
+ // Log recovery mode changes
352
+ if (recovery.justActivated) {
353
+ stats.recoveryMode = true;
354
+ ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}% | EV: $${recoveryMath.calcExpectedValue().toFixed(0)}`);
355
+ } else if (recovery.justDeactivated) {
356
+ stats.recoveryMode = false;
357
+ ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
358
+ }
359
+
334
360
  if (pnlDollars >= 0) {
335
361
  stats.wins++;
336
- // Use 'win' type for green WIN icon
337
362
  ui.addLog('win', `+$${pnlDollars.toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
338
363
  } else {
339
364
  stats.losses++;
340
- // Use 'loss' type for red LOSS icon
341
365
  ui.addLog('loss', `-$${Math.abs(pnlDollars).toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
342
366
  }
343
367
  } else {
@@ -508,6 +532,31 @@ const launchAlgo = async (service, account, contract, config) => {
508
532
 
509
533
  const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
510
534
 
535
+ // ═══════════════════════════════════════════════════════════════════════════
536
+ // RECOVERY MODE - Math-based adaptive trading
537
+ // Uses Kelly Criterion, Expected Value, and Volatility scaling
538
+ // ═══════════════════════════════════════════════════════════════════════════
539
+ if (FAST_SCALPING.RECOVERY?.ENABLED) {
540
+ const recoveryParams = recoveryMath.getRecoveryParams({
541
+ baseSize: contracts,
542
+ maxSize: contracts * 2,
543
+ atr: strategy.getATR?.() || 12,
544
+ confidence,
545
+ tickValue,
546
+ });
547
+
548
+ // In recovery mode: only take positive EV trades
549
+ if (recoveryParams.recoveryActive && !recoveryParams.shouldTrade) {
550
+ ui.addLog('warning', `RECOVERY SKIP - EV: $${recoveryParams.expectedValue.toFixed(2)} (need $10+)`);
551
+ return;
552
+ }
553
+
554
+ // Log recovery stats periodically
555
+ if (recoveryParams.recoveryActive && stats.trades % 3 === 0) {
556
+ ui.addLog('info', `RECOVERY - Kelly: ${(recoveryParams.kelly * 100).toFixed(0)}% | WR: ${(recoveryParams.winRate * 100).toFixed(0)}% | EV: $${recoveryParams.expectedValue.toFixed(0)}`);
557
+ }
558
+ }
559
+
511
560
  // Feed signal to AI supervisor (agents observe the signal)
512
561
  if (stats.aiSupervision) {
513
562
  StrategySupervisor.feedSignal({ direction, entry, stopLoss, takeProfit, confidence });
@@ -520,8 +569,15 @@ const launchAlgo = async (service, account, contract, config) => {
520
569
  }
521
570
  }
522
571
 
523
- // Calculate position size with kelly
524
- let kelly = Math.min(0.25, confidence);
572
+ // Calculate position size with Kelly (math-based)
573
+ let kelly;
574
+ if (FAST_SCALPING.RECOVERY?.ENABLED && recoveryMath.trades.length >= 5) {
575
+ // Use math-based Kelly from trade history
576
+ kelly = recoveryMath.calcDrawdownAdjustedKelly();
577
+ } else {
578
+ // Fallback to confidence-based Kelly
579
+ kelly = Math.min(0.25, confidence);
580
+ }
525
581
  let riskAmount = Math.round(maxRisk * kelly);
526
582
 
527
583
  // AI may adjust size based on learning
@@ -680,6 +736,11 @@ const launchAlgo = async (service, account, contract, config) => {
680
736
 
681
737
  strategy.processTick(tickData);
682
738
 
739
+ // Feed price to Recovery Math for volatility calculation
740
+ if (FAST_SCALPING.RECOVERY?.ENABLED && tickData.price) {
741
+ recoveryMath.recordPriceReturn(tickData.price, stats.lastPrice || tickData.price);
742
+ }
743
+
683
744
  // Feed price to position manager for exit monitoring (fast path)
684
745
  if (useFastPath && positionManager) {
685
746
  // Update latest price for position monitoring
@@ -961,6 +1022,21 @@ const launchAlgo = async (service, account, contract, config) => {
961
1022
  clearInterval(refreshInterval);
962
1023
  clearInterval(pnlInterval);
963
1024
 
1025
+ // EMERGENCY STOP: Cancel all orders and flatten all positions
1026
+ if (useFastPath && service && account.rithmicAccountId) {
1027
+ try {
1028
+ ui.addLog('warning', 'EMERGENCY STOP - Cancelling orders & flattening positions...');
1029
+ const stopResult = await service.emergencyStop(account.rithmicAccountId);
1030
+ if (stopResult.success) {
1031
+ ui.addLog('success', 'All orders cancelled, positions flattened');
1032
+ } else {
1033
+ ui.addLog('error', 'Emergency stop partial - check positions manually');
1034
+ }
1035
+ } catch (e) {
1036
+ ui.addLog('error', `Emergency stop failed: ${e.message}`);
1037
+ }
1038
+ }
1039
+
964
1040
  // Stop Position Manager (fast path)
965
1041
  if (positionManager) {
966
1042
  positionManager.stop();
@@ -17,7 +17,7 @@ const {
17
17
  getPositions,
18
18
  hashAccountId,
19
19
  } = require('./accounts');
20
- const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition, fastEntry, fastExit } = require('./orders');
20
+ const { placeOrder, cancelOrder, cancelAllOrders, getOrders, getOrderHistory, closePosition, flattenAll, emergencyStop, fastEntry, fastExit } = require('./orders');
21
21
  const { decodeFrontMonthContract } = require('./protobuf');
22
22
  const { TIMEOUTS, CACHE } = require('../../config/settings');
23
23
  const { logger } = require('../../utils/logger');
@@ -472,7 +472,10 @@ class RithmicService extends EventEmitter {
472
472
  async getOrderHistory(date) { return getOrderHistory(this, date); }
473
473
  async placeOrder(orderData) { return placeOrder(this, orderData); }
474
474
  async cancelOrder(orderId) { return cancelOrder(this, orderId); }
475
+ async cancelAllOrders(accountId) { return cancelAllOrders(this, accountId); }
475
476
  async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
477
+ async flattenAll(accountId) { return flattenAll(this, accountId); }
478
+ async emergencyStop(accountId) { return emergencyStop(this, accountId); }
476
479
 
477
480
  // ==================== FAST SCALPING (Ultra-Low Latency) ====================
478
481
 
@@ -16,6 +16,9 @@ const { REQ } = require('./constants');
16
16
  const { proto } = require('./protobuf');
17
17
  const { LatencyTracker } = require('./handlers');
18
18
  const { performance } = require('perf_hooks');
19
+ const { logger } = require('../../utils/logger');
20
+
21
+ const log = logger.scope('RithmicOrders');
19
22
 
20
23
  // Debug mode - use no-op function when disabled for zero overhead
21
24
  const DEBUG = process.env.HQX_DEBUG === '1';
@@ -447,12 +450,98 @@ const closePosition = async (service, accountId, symbol) => {
447
450
  });
448
451
  };
449
452
 
453
+ /**
454
+ * Cancel all open orders for an account
455
+ * @param {RithmicService} service - The Rithmic service instance
456
+ * @param {string} accountId - Account ID
457
+ */
458
+ const cancelAllOrders = async (service, accountId) => {
459
+ if (!service.orderConn || !service.loginInfo) {
460
+ return { success: false, error: 'Not connected' };
461
+ }
462
+
463
+ try {
464
+ // Use RequestCancelAllOrders template
465
+ service.orderConn.send('RequestCancelAllOrders', {
466
+ templateId: 346, // CANCEL_ALL_ORDERS
467
+ userMsg: ['HQX-STOP'],
468
+ fcmId: service.loginInfo.fcmId,
469
+ ibId: service.loginInfo.ibId,
470
+ accountId: accountId,
471
+ });
472
+
473
+ log.info('Cancel all orders sent', { accountId });
474
+ return { success: true };
475
+ } catch (error) {
476
+ log.error('Cancel all orders failed', { error: error.message });
477
+ return { success: false, error: error.message };
478
+ }
479
+ };
480
+
481
+ /**
482
+ * Flatten all positions for an account (close all positions)
483
+ * @param {RithmicService} service - The Rithmic service instance
484
+ * @param {string} accountId - Account ID
485
+ */
486
+ const flattenAll = async (service, accountId) => {
487
+ const results = [];
488
+ const positions = Array.from(service.positions.values());
489
+ const accountPositions = positions.filter(p => p.accountId === accountId && p.quantity !== 0);
490
+
491
+ log.info('Flattening all positions', { accountId, count: accountPositions.length });
492
+
493
+ for (const position of accountPositions) {
494
+ try {
495
+ const result = await placeOrder(service, {
496
+ accountId,
497
+ symbol: position.symbol,
498
+ exchange: position.exchange,
499
+ size: Math.abs(position.quantity),
500
+ side: position.quantity > 0 ? 1 : 0, // Sell if long, Buy if short
501
+ type: 2, // Market
502
+ });
503
+ results.push({ symbol: position.symbol, ...result });
504
+ } catch (error) {
505
+ results.push({ symbol: position.symbol, success: false, error: error.message });
506
+ }
507
+ }
508
+
509
+ return { success: true, results };
510
+ };
511
+
512
+ /**
513
+ * Emergency stop - Cancel all orders and flatten all positions
514
+ * @param {RithmicService} service - The Rithmic service instance
515
+ * @param {string} accountId - Account ID
516
+ */
517
+ const emergencyStop = async (service, accountId) => {
518
+ log.warn('EMERGENCY STOP initiated', { accountId });
519
+
520
+ // 1. Cancel all orders first
521
+ const cancelResult = await cancelAllOrders(service, accountId);
522
+
523
+ // 2. Wait a moment for cancellations to process
524
+ await new Promise(r => setTimeout(r, 500));
525
+
526
+ // 3. Flatten all positions
527
+ const flattenResult = await flattenAll(service, accountId);
528
+
529
+ return {
530
+ success: cancelResult.success && flattenResult.success,
531
+ cancelled: cancelResult,
532
+ flattened: flattenResult,
533
+ };
534
+ };
535
+
450
536
  module.exports = {
451
537
  placeOrder,
452
538
  cancelOrder,
539
+ cancelAllOrders,
453
540
  getOrders,
454
541
  getOrderHistory,
455
542
  closePosition,
543
+ flattenAll,
544
+ emergencyStop,
456
545
  // Fast scalping - ultra-low latency
457
546
  fastEntry,
458
547
  fastExit,
@@ -0,0 +1,402 @@
1
+ /**
2
+ * =============================================================================
3
+ * Recovery Mode - Mathematical Models for Drawdown Recovery
4
+ * =============================================================================
5
+ *
6
+ * Uses pure math models to adapt strategy during drawdown:
7
+ *
8
+ * 1. KELLY CRITERION - Optimal position sizing based on win rate & payoff
9
+ * f* = (bp - q) / b
10
+ * where: b = win/loss ratio, p = win probability, q = 1-p
11
+ *
12
+ * 2. VOLATILITY SCALING - Adjust size inversely to recent volatility
13
+ * size_adj = base_size * (target_vol / current_vol)
14
+ *
15
+ * 3. ADAPTIVE THRESHOLDS - Dynamic TP/SL based on ATR
16
+ * TP = entry ± (ATR * tp_multiplier)
17
+ * SL = entry ± (ATR * sl_multiplier)
18
+ *
19
+ * 4. EXPECTED VALUE FILTER - Only take trades with positive EV
20
+ * EV = (win_prob * avg_win) - (loss_prob * avg_loss)
21
+ *
22
+ * 5. DRAWDOWN-ADJUSTED KELLY - Reduce risk as drawdown deepens
23
+ * kelly_adj = kelly * (1 - drawdown_pct / max_drawdown)
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const { logger } = require('../../utils/logger');
29
+ const log = logger.scope('RecoveryMath');
30
+
31
+ /**
32
+ * Recovery Math Engine
33
+ * Calculates optimal parameters during drawdown using mathematical models
34
+ */
35
+ class RecoveryMath {
36
+ constructor() {
37
+ // Trade history for calculations
38
+ this.trades = [];
39
+ this.maxTrades = 100; // Rolling window
40
+
41
+ // Volatility tracking
42
+ this.priceReturns = [];
43
+ this.maxReturns = 200;
44
+
45
+ // Session state
46
+ this.sessionPnL = 0;
47
+ this.peakPnL = 0;
48
+ this.drawdown = 0;
49
+ this.recoveryActive = false;
50
+
51
+ // Default parameters (will be adjusted dynamically)
52
+ this.baseParams = {
53
+ targetTicks: 16,
54
+ stopTicks: 20,
55
+ kellyFraction: 0.25, // Use 25% of full Kelly (conservative)
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Update session P&L and check recovery activation
61
+ * @param {number} pnl - Current session P&L
62
+ * @param {number} activationThreshold - e.g., -300
63
+ * @param {number} deactivationThreshold - e.g., -100
64
+ */
65
+ updateSessionPnL(pnl, activationThreshold = -300, deactivationThreshold = -100) {
66
+ this.sessionPnL = pnl;
67
+
68
+ // Track peak for drawdown calculation
69
+ if (pnl > this.peakPnL) {
70
+ this.peakPnL = pnl;
71
+ }
72
+
73
+ // Calculate current drawdown
74
+ this.drawdown = this.peakPnL - pnl;
75
+
76
+ // Check recovery activation
77
+ const wasActive = this.recoveryActive;
78
+
79
+ if (pnl <= activationThreshold && !this.recoveryActive) {
80
+ this.recoveryActive = true;
81
+ log.info('RECOVERY MODE ACTIVATED', { pnl, threshold: activationThreshold });
82
+ } else if (pnl >= deactivationThreshold && this.recoveryActive) {
83
+ this.recoveryActive = false;
84
+ log.info('RECOVERY MODE DEACTIVATED', { pnl, threshold: deactivationThreshold });
85
+ }
86
+
87
+ return {
88
+ active: this.recoveryActive,
89
+ justActivated: this.recoveryActive && !wasActive,
90
+ justDeactivated: !this.recoveryActive && wasActive,
91
+ drawdown: this.drawdown,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Record a completed trade for statistics
97
+ * @param {Object} trade - { pnl, ticks, side, duration }
98
+ */
99
+ recordTrade(trade) {
100
+ this.trades.push({
101
+ pnl: trade.pnl,
102
+ ticks: trade.ticks,
103
+ side: trade.side,
104
+ duration: trade.duration,
105
+ timestamp: Date.now(),
106
+ });
107
+
108
+ // Keep rolling window
109
+ if (this.trades.length > this.maxTrades) {
110
+ this.trades.shift();
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Record price return for volatility calculation
116
+ * @param {number} price - Current price
117
+ * @param {number} prevPrice - Previous price
118
+ */
119
+ recordPriceReturn(price, prevPrice) {
120
+ if (prevPrice > 0) {
121
+ const ret = (price - prevPrice) / prevPrice;
122
+ this.priceReturns.push(ret);
123
+
124
+ if (this.priceReturns.length > this.maxReturns) {
125
+ this.priceReturns.shift();
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Calculate win rate from recent trades
132
+ * @returns {number} Win rate [0, 1]
133
+ */
134
+ calcWinRate() {
135
+ if (this.trades.length < 5) return 0.5; // Default 50% if insufficient data
136
+
137
+ const wins = this.trades.filter(t => t.pnl > 0).length;
138
+ return wins / this.trades.length;
139
+ }
140
+
141
+ /**
142
+ * Calculate average win and loss
143
+ * @returns {{ avgWin: number, avgLoss: number, payoffRatio: number }}
144
+ */
145
+ calcPayoff() {
146
+ const wins = this.trades.filter(t => t.pnl > 0);
147
+ const losses = this.trades.filter(t => t.pnl < 0);
148
+
149
+ const avgWin = wins.length > 0
150
+ ? wins.reduce((s, t) => s + t.pnl, 0) / wins.length
151
+ : 80; // Default $80 (16 ticks on NQ)
152
+
153
+ const avgLoss = losses.length > 0
154
+ ? Math.abs(losses.reduce((s, t) => s + t.pnl, 0) / losses.length)
155
+ : 100; // Default $100 (20 ticks on NQ)
156
+
157
+ return {
158
+ avgWin,
159
+ avgLoss,
160
+ payoffRatio: avgWin / avgLoss,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Calculate Kelly Criterion for optimal bet sizing
166
+ * f* = (bp - q) / b
167
+ * where: b = win/loss ratio, p = win probability, q = 1-p
168
+ *
169
+ * @returns {number} Kelly fraction [0, 1]
170
+ */
171
+ calcKelly() {
172
+ const winRate = this.calcWinRate();
173
+ const { payoffRatio } = this.calcPayoff();
174
+
175
+ const p = winRate;
176
+ const q = 1 - p;
177
+ const b = payoffRatio;
178
+
179
+ // Kelly formula
180
+ let kelly = (b * p - q) / b;
181
+
182
+ // Clamp to reasonable range
183
+ kelly = Math.max(0, Math.min(1, kelly));
184
+
185
+ return kelly;
186
+ }
187
+
188
+ /**
189
+ * Calculate drawdown-adjusted Kelly
190
+ * Reduces risk as drawdown deepens
191
+ *
192
+ * @param {number} maxDrawdown - Maximum allowed drawdown (e.g., 500)
193
+ * @returns {number} Adjusted Kelly fraction
194
+ */
195
+ calcDrawdownAdjustedKelly(maxDrawdown = 500) {
196
+ const kelly = this.calcKelly();
197
+
198
+ // Reduce Kelly based on current drawdown
199
+ // At 0% drawdown: use full Kelly
200
+ // At 100% max drawdown: use 0% Kelly
201
+ const drawdownPct = Math.min(1, this.drawdown / maxDrawdown);
202
+ const adjustment = 1 - (drawdownPct * 0.5); // Max 50% reduction
203
+
204
+ return kelly * adjustment;
205
+ }
206
+
207
+ /**
208
+ * Calculate current volatility (standard deviation of returns)
209
+ * @returns {number} Volatility (annualized)
210
+ */
211
+ calcVolatility() {
212
+ if (this.priceReturns.length < 20) return 0.02; // Default 2%
213
+
214
+ const mean = this.priceReturns.reduce((s, r) => s + r, 0) / this.priceReturns.length;
215
+ const variance = this.priceReturns.reduce((s, r) => s + Math.pow(r - mean, 2), 0) / this.priceReturns.length;
216
+ const stdDev = Math.sqrt(variance);
217
+
218
+ return stdDev;
219
+ }
220
+
221
+ /**
222
+ * Calculate ATR-based thresholds
223
+ * @param {number} atr - Average True Range in ticks
224
+ * @returns {{ targetTicks: number, stopTicks: number }}
225
+ */
226
+ calcATRThresholds(atr) {
227
+ if (!atr || atr < 1) {
228
+ return {
229
+ targetTicks: this.baseParams.targetTicks,
230
+ stopTicks: this.baseParams.stopTicks,
231
+ };
232
+ }
233
+
234
+ // Target = 1.5 * ATR (capture 1.5 average moves)
235
+ // Stop = 2 * ATR (allow 2 average moves against)
236
+ const targetTicks = Math.round(atr * 1.5);
237
+ const stopTicks = Math.round(atr * 2);
238
+
239
+ // Clamp to reasonable range
240
+ return {
241
+ targetTicks: Math.max(8, Math.min(32, targetTicks)),
242
+ stopTicks: Math.max(10, Math.min(40, stopTicks)),
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Calculate Expected Value of a trade setup
248
+ * EV = (win_prob * avg_win) - (loss_prob * avg_loss)
249
+ *
250
+ * @param {number} confidence - Signal confidence [0, 1]
251
+ * @returns {number} Expected value in dollars
252
+ */
253
+ calcExpectedValue(confidence = 0.5) {
254
+ const baseWinRate = this.calcWinRate();
255
+ const { avgWin, avgLoss } = this.calcPayoff();
256
+
257
+ // Adjust win rate by signal confidence
258
+ // Higher confidence = higher expected win rate
259
+ const adjustedWinRate = baseWinRate * (0.5 + confidence * 0.5);
260
+
261
+ const ev = (adjustedWinRate * avgWin) - ((1 - adjustedWinRate) * avgLoss);
262
+
263
+ return ev;
264
+ }
265
+
266
+ /**
267
+ * Get optimal position size using Kelly and volatility scaling
268
+ * @param {number} baseSize - Normal position size
269
+ * @param {number} maxSize - Maximum allowed size
270
+ * @param {number} targetVol - Target volatility (e.g., 0.02)
271
+ * @returns {number} Optimal position size
272
+ */
273
+ calcOptimalSize(baseSize, maxSize, targetVol = 0.02) {
274
+ // Get drawdown-adjusted Kelly
275
+ const kelly = this.calcDrawdownAdjustedKelly();
276
+
277
+ // Get current volatility
278
+ const currentVol = this.calcVolatility();
279
+
280
+ // Volatility scaling: reduce size when vol is high
281
+ const volScale = currentVol > 0 ? targetVol / currentVol : 1;
282
+ const volAdjusted = Math.min(2, Math.max(0.5, volScale)); // Clamp 0.5x - 2x
283
+
284
+ // In recovery mode: use half Kelly (more conservative despite "aggressive")
285
+ // This is counter-intuitive but mathematically sound:
286
+ // - We want to recover, but not blow up
287
+ // - Half Kelly gives ~75% of optimal growth with much less variance
288
+ const kellyMultiplier = this.recoveryActive
289
+ ? this.baseParams.kellyFraction * 0.75 // More conservative in recovery
290
+ : this.baseParams.kellyFraction;
291
+
292
+ // Calculate optimal size
293
+ let optimalSize = baseSize * kelly * volAdjusted * kellyMultiplier * 4; // 4x because kelly is small
294
+
295
+ // Clamp to max
296
+ optimalSize = Math.max(1, Math.min(maxSize, Math.round(optimalSize)));
297
+
298
+ return optimalSize;
299
+ }
300
+
301
+ /**
302
+ * Get recovery parameters (thresholds, size, filter)
303
+ * All based on mathematical models
304
+ *
305
+ * @param {Object} context - { baseSize, maxSize, atr, confidence, tickValue }
306
+ * @returns {Object} Recovery parameters
307
+ */
308
+ getRecoveryParams(context) {
309
+ const { baseSize = 1, maxSize = 5, atr = 12, confidence = 0.5, tickValue = 5 } = context;
310
+
311
+ // Calculate all math-based values
312
+ const winRate = this.calcWinRate();
313
+ const { avgWin, avgLoss, payoffRatio } = this.calcPayoff();
314
+ const kelly = this.calcKelly();
315
+ const drawdownKelly = this.calcDrawdownAdjustedKelly();
316
+ const volatility = this.calcVolatility();
317
+ const ev = this.calcExpectedValue(confidence);
318
+ const { targetTicks, stopTicks } = this.calcATRThresholds(atr);
319
+ const optimalSize = this.calcOptimalSize(baseSize, maxSize);
320
+
321
+ // Should we take this trade?
322
+ // In recovery: only take positive EV trades with high confidence
323
+ const minEV = this.recoveryActive ? 10 : 0; // Require $10 EV in recovery
324
+ const shouldTrade = ev >= minEV;
325
+
326
+ return {
327
+ // Mode status
328
+ recoveryActive: this.recoveryActive,
329
+ sessionPnL: this.sessionPnL,
330
+ drawdown: this.drawdown,
331
+
332
+ // Statistics
333
+ winRate,
334
+ avgWin,
335
+ avgLoss,
336
+ payoffRatio,
337
+
338
+ // Kelly
339
+ kelly,
340
+ drawdownKelly,
341
+
342
+ // Volatility
343
+ volatility,
344
+
345
+ // Expected Value
346
+ expectedValue: ev,
347
+ shouldTrade,
348
+
349
+ // Optimal parameters
350
+ optimalSize,
351
+ targetTicks,
352
+ stopTicks,
353
+
354
+ // Breakeven (based on payoff ratio)
355
+ // If payoff > 1: early BE (lock small profit)
356
+ // If payoff < 1: late BE (need bigger moves)
357
+ breakevenTicks: payoffRatio >= 1
358
+ ? Math.max(3, Math.round(targetTicks * 0.3))
359
+ : Math.max(4, Math.round(targetTicks * 0.4)),
360
+
361
+ // Trailing (based on volatility)
362
+ // High vol: wider trail
363
+ // Low vol: tighter trail
364
+ trailingActivation: Math.round(targetTicks * 0.5),
365
+ trailingDistance: Math.max(2, Math.round(atr * 0.5)),
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Get summary for logging
371
+ */
372
+ getSummary() {
373
+ return {
374
+ active: this.recoveryActive,
375
+ sessionPnL: this.sessionPnL,
376
+ drawdown: this.drawdown,
377
+ trades: this.trades.length,
378
+ winRate: (this.calcWinRate() * 100).toFixed(1) + '%',
379
+ kelly: (this.calcKelly() * 100).toFixed(1) + '%',
380
+ ev: '$' + this.calcExpectedValue().toFixed(2),
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Reset state (new session)
386
+ */
387
+ reset() {
388
+ this.sessionPnL = 0;
389
+ this.peakPnL = 0;
390
+ this.drawdown = 0;
391
+ this.recoveryActive = false;
392
+ // Keep trade history for learning
393
+ }
394
+ }
395
+
396
+ // Singleton instance
397
+ const recoveryMath = new RecoveryMath();
398
+
399
+ module.exports = {
400
+ RecoveryMath,
401
+ recoveryMath,
402
+ };