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.
@@ -24,28 +24,30 @@
24
24
  'use strict';
25
25
 
26
26
  // =============================================================================
27
- // PRE-ALLOCATED REGIME PARAMETERS (avoid object creation)
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.8,
32
- targetMultiplier: 0.9,
33
- zscoreThreshold: 1.2,
34
- confidenceBonus: 0.05
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: 1.5,
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.3,
46
- targetMultiplier: 1.2,
47
- zscoreThreshold: 2.0,
48
- confidenceBonus: -0.05
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 with zero intermediate array allocations
69
- * Uses in-place calculation with index arithmetic
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
- * @param {number[]} prices - Price buffer (circular or linear)
72
- * @param {number} length - Actual number of valid prices
73
- * @param {number} window - Lookback window (default 50)
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, window = 50) {
88
+ function computeZScore(prices, lookback = 100) {
77
89
  const length = prices.length;
78
- if (length === 0) return 0;
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
- // Determine effective window
83
- const n = length < window ? length : window;
84
- const startIdx = length - n;
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 (no slice, no reduce)
101
+ // Single-pass mean and std calculation over lookback window
87
102
  let sum = 0;
88
103
  let sumSq = 0;
89
- for (let i = startIdx; i < length; i++) {
104
+ for (let i = windowStart; i < windowEnd; i++) {
90
105
  const p = prices[i];
91
106
  sum += p;
92
- sumSq += p * p; // Faster than Math.pow(p, 2)
107
+ sumSq += p * p;
93
108
  }
94
109
 
95
- const mean = sum / n;
96
- const variance = (sumSq / n) - (mean * mean);
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
- // === Model Parameters (BACKTEST VALIDATED - $2,012,373.75) ===
83
- this.zscoreEntryThreshold = 2.5; // BACKTEST: Z-Score Entry >2.5
84
- this.zscoreExitThreshold = 0.5;
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
- // === Tick aggregation ===
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 and last price
175
- this._totalTicks = (this._totalTicks || 0) + 1;
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 now = Date.now();
236
- const timeSinceLastSignal = now - this.lastSignalTime;
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.lastSignalTime > 0) {
246
- const secs = Math.ceil(cooldownRemaining / 1000);
247
- state = `cooldown-${secs}`;
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: Check cooldown
398
- const now = Date.now();
399
- const timeSinceLastSignal = now - this.lastSignalTime;
400
- if (timeSinceLastSignal < this.signalCooldownMs) {
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
- // CRITICAL: Check consecutive losses
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 2 minutes
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
- }, 120000);
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.4,
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
- // CRITICAL: Higher confidence threshold (65% minimum)
453
- if (confidence < this.minConfidenceThreshold) {
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.signalCooldownMs - (Date.now() - this.lastSignalTime)),
637
+ cooldownRemaining: Math.max(0, this.cooldownTicks - (this.tickCount - this.lastSignalTick)),
638
+ tickCount: this.tickCount,
627
639
  };
628
640
  }
629
641
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.239",
3
+ "version": "2.9.240",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {