hedgequantx 2.9.239 → 2.9.240
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/dist/lib/m/s1-models.js +41 -51
- package/dist/lib/m/ultra-scalping.js +74 -62
- package/package.json +1 -1
package/dist/lib/m/s1-models.js
CHANGED
|
@@ -24,28 +24,30 @@
|
|
|
24
24
|
'use strict';
|
|
25
25
|
|
|
26
26
|
// =============================================================================
|
|
27
|
-
//
|
|
27
|
+
// REGIME PARAMETERS - EXACT MATCH TO PYTHON BACKTEST
|
|
28
|
+
// Backtest Result: $2,012,373.75 | 146,685 trades | 71.1% WR
|
|
29
|
+
// Z-Score Entry: >2.5 | Exit: <0.5 | Stop: 8 ticks | Target: 16 ticks
|
|
28
30
|
// =============================================================================
|
|
29
31
|
|
|
30
32
|
const REGIME_LOW = Object.freeze({
|
|
31
|
-
stopMultiplier: 0
|
|
32
|
-
targetMultiplier: 0
|
|
33
|
-
zscoreThreshold:
|
|
34
|
-
confidenceBonus: 0.
|
|
33
|
+
stopMultiplier: 1.0,
|
|
34
|
+
targetMultiplier: 1.0,
|
|
35
|
+
zscoreThreshold: 2.5, // BACKTEST EXACT: 2.5
|
|
36
|
+
confidenceBonus: 0.0
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
const REGIME_NORMAL = Object.freeze({
|
|
38
40
|
stopMultiplier: 1.0,
|
|
39
41
|
targetMultiplier: 1.0,
|
|
40
|
-
zscoreThreshold:
|
|
42
|
+
zscoreThreshold: 2.5, // BACKTEST EXACT: 2.5
|
|
41
43
|
confidenceBonus: 0.0
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
const REGIME_HIGH = Object.freeze({
|
|
45
|
-
stopMultiplier: 1.
|
|
46
|
-
targetMultiplier: 1.
|
|
47
|
-
zscoreThreshold: 2.
|
|
48
|
-
confidenceBonus:
|
|
47
|
+
stopMultiplier: 1.0,
|
|
48
|
+
targetMultiplier: 1.0,
|
|
49
|
+
zscoreThreshold: 2.5, // BACKTEST EXACT: 2.5
|
|
50
|
+
confidenceBonus: 0.0
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
// Pre-allocated result object for Kalman filter (reused)
|
|
@@ -65,61 +67,49 @@ const _regimeResult = {
|
|
|
65
67
|
// =============================================================================
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
|
-
* Compute Z-Score
|
|
69
|
-
*
|
|
70
|
+
* Compute Z-Score - EXACT MATCH TO PYTHON BACKTEST
|
|
71
|
+
*
|
|
72
|
+
* PYTHON CODE (from ultra_scalper_v2_protected.py):
|
|
73
|
+
* ```python
|
|
74
|
+
* for i in range(lookback, n):
|
|
75
|
+
* window = prices[i-lookback:i] # EXCLUDES current price!
|
|
76
|
+
* mean = np.mean(window)
|
|
77
|
+
* std = np.std(window)
|
|
78
|
+
* if std > 0:
|
|
79
|
+
* zscore[i] = (prices[i] - mean) / std
|
|
80
|
+
* ```
|
|
70
81
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @param {number}
|
|
82
|
+
* CRITICAL: Mean/std are calculated from PREVIOUS 100 prices, NOT including current!
|
|
83
|
+
*
|
|
84
|
+
* @param {number[]} prices - Price buffer
|
|
85
|
+
* @param {number} lookback - Lookback window (default 100 to match Python)
|
|
74
86
|
* @returns {number} Z-Score value
|
|
75
87
|
*/
|
|
76
|
-
function computeZScore(prices,
|
|
88
|
+
function computeZScore(prices, lookback = 100) {
|
|
77
89
|
const length = prices.length;
|
|
78
|
-
|
|
90
|
+
|
|
91
|
+
// Need at least lookback+1 prices (lookback for window + 1 for current)
|
|
92
|
+
if (length <= lookback) return 0;
|
|
79
93
|
|
|
80
94
|
const currentPrice = prices[length - 1];
|
|
81
95
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
const
|
|
96
|
+
// CRITICAL: Window is [i-lookback : i], which EXCLUDES current price
|
|
97
|
+
// This matches Python: window = prices[i-lookback:i]
|
|
98
|
+
const windowEnd = length - 1; // Exclude current price
|
|
99
|
+
const windowStart = windowEnd - lookback;
|
|
85
100
|
|
|
86
|
-
// Single-pass mean calculation
|
|
101
|
+
// Single-pass mean and std calculation over lookback window
|
|
87
102
|
let sum = 0;
|
|
88
103
|
let sumSq = 0;
|
|
89
|
-
for (let i =
|
|
104
|
+
for (let i = windowStart; i < windowEnd; i++) {
|
|
90
105
|
const p = prices[i];
|
|
91
106
|
sum += p;
|
|
92
|
-
sumSq += p * p;
|
|
107
|
+
sumSq += p * p;
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
const mean = sum /
|
|
96
|
-
const variance = (sumSq /
|
|
97
|
-
|
|
98
|
-
// Blend cumulative and rolling std if enough data (like Python backtest)
|
|
99
|
-
let std;
|
|
100
|
-
if (length >= 100) {
|
|
101
|
-
const cumulativeStd = Math.sqrt(Math.max(0, variance));
|
|
102
|
-
|
|
103
|
-
// Calculate rolling std over last 100 prices (in-place)
|
|
104
|
-
const rollingStart = length - 100;
|
|
105
|
-
let rollingSum = 0;
|
|
106
|
-
for (let i = rollingStart; i < length; i++) {
|
|
107
|
-
rollingSum += prices[i];
|
|
108
|
-
}
|
|
109
|
-
const rollingMean = rollingSum / 100;
|
|
110
|
-
|
|
111
|
-
let rollingVarSum = 0;
|
|
112
|
-
for (let i = rollingStart; i < length; i++) {
|
|
113
|
-
const diff = prices[i] - rollingMean;
|
|
114
|
-
rollingVarSum += diff * diff;
|
|
115
|
-
}
|
|
116
|
-
const rollingStd = Math.sqrt(rollingVarSum / 100);
|
|
117
|
-
|
|
118
|
-
// Blend: 30% cumulative, 70% rolling (matches Python)
|
|
119
|
-
std = cumulativeStd * 0.3 + rollingStd * 0.7;
|
|
120
|
-
} else {
|
|
121
|
-
std = Math.sqrt(Math.max(0, variance));
|
|
122
|
-
}
|
|
110
|
+
const mean = sum / lookback;
|
|
111
|
+
const variance = (sumSq / lookback) - (mean * mean);
|
|
112
|
+
const std = Math.sqrt(Math.max(0, variance));
|
|
123
113
|
|
|
124
114
|
if (std < 0.0001) return 0;
|
|
125
115
|
return (currentPrice - mean) / std;
|
|
@@ -79,21 +79,33 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
79
79
|
this.tickSize = 0.25;
|
|
80
80
|
this.tickValue = 5.0;
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
// ==========================================================================
|
|
83
|
+
// EXACT MATCH TO PYTHON BACKTEST CONFIG
|
|
84
|
+
// Result: $2,012,373.75 | 146,685 trades | 71.1% WR
|
|
85
|
+
// ==========================================================================
|
|
86
|
+
|
|
87
|
+
// Z-Score Parameters (EXACT FROM PYTHON)
|
|
88
|
+
this.zscoreLookback = 100; // PYTHON: zscore_lookback: 100
|
|
89
|
+
this.zscoreEntryThreshold = 2.5; // PYTHON: zscore_entry: 2.5
|
|
90
|
+
this.zscoreExitThreshold = 0.5; // PYTHON: zscore_exit: 0.5
|
|
91
|
+
|
|
92
|
+
// Trade Parameters (EXACT FROM PYTHON)
|
|
93
|
+
this.baseStopTicks = 8; // PYTHON: stop_ticks: 8
|
|
94
|
+
this.baseTargetTicks = 16; // PYTHON: target_ticks: 16
|
|
95
|
+
this.breakevenTicks = 4; // PYTHON: be_ticks: 4
|
|
96
|
+
this.trailActivationTicks = 6; // PYTHON: trail_activation: 6
|
|
97
|
+
this.profitLockPct = 0.5; // PYTHON: trail_pct: 0.5
|
|
98
|
+
|
|
99
|
+
// Risk Management (EXACT FROM PYTHON)
|
|
100
|
+
this.cooldownTicks = 100; // PYTHON: cooldown: 100
|
|
101
|
+
this.maxConsecutiveLosses = 10; // PYTHON: max_consecutive_losses: 10
|
|
102
|
+
|
|
103
|
+
// NOT USED AS FILTERS (Python doesn't use these)
|
|
85
104
|
this.vpinWindow = 50;
|
|
86
105
|
this.vpinToxicThreshold = 0.7;
|
|
106
|
+
this.ofiLookback = 20;
|
|
87
107
|
this.kalmanProcessNoise = 0.01;
|
|
88
108
|
this.kalmanMeasurementNoise = 0.1;
|
|
89
|
-
this.volatilityLookback = 100;
|
|
90
|
-
this.ofiLookback = 20;
|
|
91
|
-
|
|
92
|
-
// === Trade Parameters (BACKTEST VALIDATED) ===
|
|
93
|
-
this.baseStopTicks = 8; // $40
|
|
94
|
-
this.baseTargetTicks = 16; // $80
|
|
95
|
-
this.breakevenTicks = 4; // Move to BE at +4 ticks
|
|
96
|
-
this.profitLockPct = 0.5; // Lock 50% of profit
|
|
97
109
|
|
|
98
110
|
// === State Storage ===
|
|
99
111
|
this.barHistory = new Map();
|
|
@@ -102,22 +114,15 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
102
114
|
this.volumeBuffer = new Map();
|
|
103
115
|
this.tradesBuffer = new Map();
|
|
104
116
|
this.atrHistory = new Map();
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
this.tickBuffer = new Map();
|
|
108
|
-
this.lastBarTime = new Map();
|
|
109
|
-
this.barIntervalMs = 5000; // 5-second bars
|
|
117
|
+
this.tickBuffer = new Map(); // For tick aggregation
|
|
118
|
+
this.lastBarTime = new Map(); // Last bar timestamp per contract
|
|
110
119
|
|
|
111
120
|
// === Performance Tracking ===
|
|
121
|
+
this.tickCount = 0; // Total ticks processed
|
|
122
|
+
this.lastSignalTick = 0; // Tick count at last signal (for cooldown)
|
|
112
123
|
this.recentTrades = [];
|
|
113
124
|
this.winStreak = 0;
|
|
114
125
|
this.lossStreak = 0;
|
|
115
|
-
|
|
116
|
-
// === CRITICAL: Cooldown & Risk Management ===
|
|
117
|
-
this.lastSignalTime = 0;
|
|
118
|
-
this.signalCooldownMs = 30000; // 30 seconds minimum between signals
|
|
119
|
-
this.maxConsecutiveLosses = 3; // Stop trading after 3 consecutive losses
|
|
120
|
-
this.minConfidenceThreshold = 0.65; // Minimum 65% confidence (was 55%)
|
|
121
126
|
this.tradingEnabled = true;
|
|
122
127
|
}
|
|
123
128
|
|
|
@@ -171,8 +176,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
171
176
|
this.initialize(contractId);
|
|
172
177
|
}
|
|
173
178
|
|
|
174
|
-
// Track total ticks
|
|
175
|
-
this.
|
|
179
|
+
// Track total ticks (CRITICAL for tick-based cooldown like Python)
|
|
180
|
+
this.tickCount++;
|
|
176
181
|
this._lastPrice = price;
|
|
177
182
|
this._currentContractId = contractId;
|
|
178
183
|
|
|
@@ -231,23 +236,21 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
231
236
|
const vpinPct = (vpin * 100).toFixed(0);
|
|
232
237
|
const zRounded = Math.round(zscore * 10) / 10; // Round to 0.1
|
|
233
238
|
|
|
234
|
-
// Check cooldown
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const cooldownRemaining = Math.max(0, this.signalCooldownMs - timeSinceLastSignal);
|
|
239
|
+
// Check tick-based cooldown (matches Python)
|
|
240
|
+
const ticksSinceLastSignal = this.tickCount - this.lastSignalTick;
|
|
241
|
+
const cooldownRemaining = Math.max(0, this.cooldownTicks - ticksSinceLastSignal);
|
|
238
242
|
|
|
239
243
|
// Trading disabled?
|
|
240
244
|
if (!this.tradingEnabled) {
|
|
241
245
|
state = 'paused';
|
|
242
246
|
message = `[${sym}] ${priceStr} | PAUSED - ${this.lossStreak} losses | Cooldown active`;
|
|
243
247
|
}
|
|
244
|
-
// In cooldown?
|
|
245
|
-
else if (cooldownRemaining > 0 && this.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
message = `[${sym}] ${priceStr} | Cooldown ${secs}s | Z:${zRounded}σ OFI:${ofiPct}%`;
|
|
248
|
+
// In tick-based cooldown?
|
|
249
|
+
else if (cooldownRemaining > 0 && this.lastSignalTick > 0) {
|
|
250
|
+
state = `cooldown-${cooldownRemaining}`;
|
|
251
|
+
message = `[${sym}] ${priceStr} | Cooldown ${cooldownRemaining} ticks | Z:${zRounded}σ OFI:${ofiPct}%`;
|
|
249
252
|
}
|
|
250
|
-
// VPIN toxic?
|
|
253
|
+
// VPIN toxic? (info only, not a hard filter)
|
|
251
254
|
else if (vpin > this.vpinToxicThreshold) {
|
|
252
255
|
state = 'vpin-toxic';
|
|
253
256
|
message = `[${sym}] ${priceStr} | VPIN toxic ${vpinPct}% > 70% | No entry - informed traders active`;
|
|
@@ -394,51 +397,65 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
394
397
|
return null;
|
|
395
398
|
}
|
|
396
399
|
|
|
397
|
-
// CRITICAL:
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
if (
|
|
400
|
+
// CRITICAL: Tick-based cooldown (MATCHES PYTHON: cooldown = 100 ticks)
|
|
401
|
+
// Python: if i - last_trade_idx < cooldown_ticks: continue
|
|
402
|
+
const ticksSinceLastSignal = this.tickCount - this.lastSignalTick;
|
|
403
|
+
if (ticksSinceLastSignal < this.cooldownTicks) {
|
|
401
404
|
// Silent - don't spam logs
|
|
402
405
|
return null;
|
|
403
406
|
}
|
|
404
407
|
|
|
405
|
-
//
|
|
408
|
+
// Extra cooldown after losses (MATCHES PYTHON)
|
|
409
|
+
// Python: extra_cooldown = consecutive_losses * 50
|
|
410
|
+
if (this.lossStreak > 0) {
|
|
411
|
+
const extraCooldown = this.lossStreak * 50;
|
|
412
|
+
if (ticksSinceLastSignal < this.cooldownTicks + extraCooldown) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// CRITICAL: Check consecutive losses (kill switch)
|
|
406
418
|
if (this.lossStreak >= this.maxConsecutiveLosses) {
|
|
407
419
|
this.tradingEnabled = false;
|
|
408
420
|
this.emit('log', { type: 'info', message: `Trading paused: ${this.lossStreak} consecutive losses. Waiting for cooldown...` });
|
|
409
|
-
// Auto re-enable after
|
|
421
|
+
// Auto re-enable after 1 hour (matches Python: 3600_000_000 us = 1 hour)
|
|
410
422
|
setTimeout(() => {
|
|
411
423
|
this.tradingEnabled = true;
|
|
412
424
|
this.lossStreak = 0;
|
|
413
425
|
this.emit('log', { type: 'info', message: 'Trading re-enabled after cooldown' });
|
|
414
|
-
},
|
|
426
|
+
}, 3600000);
|
|
415
427
|
return null;
|
|
416
428
|
}
|
|
417
429
|
|
|
430
|
+
// =========================================================================
|
|
431
|
+
// ENTRY LOGIC - MATCHES PYTHON BACKTEST EXACTLY
|
|
432
|
+
// Python backtest: Z-Score >2.5 = entry, no OFI/VPIN/Kalman filters
|
|
433
|
+
// Result: 146,685 trades, 71.1% WR, $2,012,373.75
|
|
434
|
+
// =========================================================================
|
|
435
|
+
|
|
418
436
|
const absZscore = Math.abs(zscore);
|
|
437
|
+
|
|
438
|
+
// Z-Score threshold check (ONLY filter from Python backtest)
|
|
419
439
|
if (absZscore < volParams.zscoreThreshold) return null;
|
|
420
|
-
if (vpin > this.vpinToxicThreshold) return null;
|
|
421
440
|
|
|
441
|
+
// Determine direction based on Z-Score
|
|
422
442
|
let direction;
|
|
423
|
-
if (zscore < -volParams.zscoreThreshold) direction = 'long';
|
|
424
|
-
else if (zscore > volParams.zscoreThreshold) direction = 'short';
|
|
443
|
+
if (zscore < -volParams.zscoreThreshold) direction = 'long'; // Price below mean = buy
|
|
444
|
+
else if (zscore > volParams.zscoreThreshold) direction = 'short'; // Price above mean = sell
|
|
425
445
|
else return null;
|
|
426
|
-
|
|
427
|
-
// CRITICAL: OFI must confirm direction (stronger filter)
|
|
428
|
-
const ofiConfirms = (direction === 'long' && ofi > 0.15) || (direction === 'short' && ofi < -0.15);
|
|
429
|
-
if (!ofiConfirms) {
|
|
430
|
-
this.emit('log', { type: 'debug', message: `Signal rejected: OFI (${(ofi * 100).toFixed(1)}%) doesn't confirm ${direction}` });
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
446
|
|
|
447
|
+
// OFI/VPIN/Kalman used for confidence scoring only (NOT as hard filters)
|
|
448
|
+
const ofiConfirms = (direction === 'long' && ofi > 0.1) || (direction === 'short' && ofi < -0.1);
|
|
434
449
|
const kalmanDiff = currentPrice - kalmanEstimate;
|
|
435
450
|
const kalmanConfirms = (direction === 'long' && kalmanDiff < 0) || (direction === 'short' && kalmanDiff > 0);
|
|
451
|
+
const vpinOk = vpin < this.vpinToxicThreshold;
|
|
436
452
|
|
|
453
|
+
// Composite confidence score (informational, not blocking)
|
|
437
454
|
const scores = {
|
|
438
455
|
zscore: Math.min(1.0, absZscore / 4.0),
|
|
439
|
-
vpin: 1.0 - vpin,
|
|
456
|
+
vpin: vpinOk ? (1.0 - vpin) : 0.3,
|
|
440
457
|
kyleLambda: kyleLambda > 0.001 ? 0.5 : 0.8,
|
|
441
|
-
kalman: kalmanConfirms ? 0.8 : 0.
|
|
458
|
+
kalman: kalmanConfirms ? 0.8 : 0.5,
|
|
442
459
|
volatility: regime === 'normal' ? 0.8 : regime === 'low' ? 0.7 : 0.6,
|
|
443
460
|
ofi: ofiConfirms ? 0.9 : 0.5,
|
|
444
461
|
composite: 0
|
|
@@ -449,14 +466,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
449
466
|
|
|
450
467
|
const confidence = Math.min(1.0, scores.composite + volParams.confidenceBonus);
|
|
451
468
|
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
this.emit('log', { type: 'debug', message: `Signal rejected: confidence ${(confidence * 100).toFixed(1)}% < ${this.minConfidenceThreshold * 100}%` });
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Update last signal time
|
|
459
|
-
this.lastSignalTime = now;
|
|
469
|
+
// Update last signal tick (tick-based cooldown like Python)
|
|
470
|
+
this.lastSignalTick = this.tickCount;
|
|
460
471
|
|
|
461
472
|
const stopTicks = Math.round(this.baseStopTicks * volParams.stopMultiplier);
|
|
462
473
|
const targetTicks = Math.round(this.baseTargetTicks * volParams.targetMultiplier);
|
|
@@ -623,7 +634,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
623
634
|
tradingEnabled: this.tradingEnabled,
|
|
624
635
|
lossStreak: this.lossStreak,
|
|
625
636
|
winStreak: this.winStreak,
|
|
626
|
-
cooldownRemaining: Math.max(0, this.
|
|
637
|
+
cooldownRemaining: Math.max(0, this.cooldownTicks - (this.tickCount - this.lastSignalTick)),
|
|
638
|
+
tickCount: this.tickCount,
|
|
627
639
|
};
|
|
628
640
|
}
|
|
629
641
|
|