hedgequantx 2.6.64 → 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
package/src/config/settings.js
CHANGED
|
@@ -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 ====================
|
package/src/menus/ai-agent.js
CHANGED
|
@@ -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.
|
|
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.
|
|
449
|
-
chalk.
|
|
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
|
-
|
|
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
|
|
520
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
524
|
-
let kelly
|
|
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
|
|
@@ -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
|
+
};
|