hedgequantx 2.9.238 → 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
 
@@ -158,40 +163,41 @@ class HQXUltraScalpingStrategy extends EventEmitter {
158
163
  }
159
164
 
160
165
  /**
161
- * Process a tick - aggregates into bars then runs strategy
166
+ * Process a tick - TICK-BY-TICK processing (matches Python backtest)
167
+ * Each tick is treated as a single-tick "bar" for model calculations
162
168
  */
163
169
  processTick(tick) {
164
170
  const contractId = tick.contractId;
171
+ const price = tick.price;
172
+ const volume = tick.volume || 1;
173
+ const timestamp = tick.timestamp || Date.now();
165
174
 
166
175
  if (!this.barHistory.has(contractId)) {
167
176
  this.initialize(contractId);
168
177
  }
169
-
170
- // Add tick to buffer
171
- let ticks = this.tickBuffer.get(contractId);
172
- ticks.push(tick);
173
178
 
174
- // Track total ticks and last price for status log interval
175
- this._totalTicks = (this._totalTicks || 0) + 1;
176
- this._lastPrice = tick.price;
179
+ // Track total ticks (CRITICAL for tick-based cooldown like Python)
180
+ this.tickCount++;
181
+ this._lastPrice = price;
177
182
  this._currentContractId = contractId;
178
183
 
179
- // Check if we should form a new bar
180
- const lastBar = this.lastBarTime.get(contractId);
184
+ // Create single-tick bar (matches Python backtest behavior)
185
+ const bar = {
186
+ timestamp,
187
+ open: price,
188
+ high: price,
189
+ low: price,
190
+ close: price,
191
+ volume
192
+ };
181
193
 
182
- if (now - lastBar >= this.barIntervalMs && ticks.length > 0) {
183
- const bar = this._aggregateTicksToBar(ticks, now);
184
- this.tickBuffer.set(contractId, []);
185
- this.lastBarTime.set(contractId, now);
186
-
187
- if (bar) {
188
- const signal = this.processBar(contractId, bar);
189
- if (signal) {
190
- this.emit('signal', signal);
191
- return signal;
192
- }
193
- }
194
+ // Process bar and emit signal if generated
195
+ const signal = this.processBar(contractId, bar);
196
+ if (signal) {
197
+ this.emit('signal', signal);
198
+ return signal;
194
199
  }
200
+
195
201
  return null;
196
202
  }
197
203
 
@@ -230,23 +236,21 @@ class HQXUltraScalpingStrategy extends EventEmitter {
230
236
  const vpinPct = (vpin * 100).toFixed(0);
231
237
  const zRounded = Math.round(zscore * 10) / 10; // Round to 0.1
232
238
 
233
- // Check cooldown
234
- const now = Date.now();
235
- const timeSinceLastSignal = now - this.lastSignalTime;
236
- 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);
237
242
 
238
243
  // Trading disabled?
239
244
  if (!this.tradingEnabled) {
240
245
  state = 'paused';
241
246
  message = `[${sym}] ${priceStr} | PAUSED - ${this.lossStreak} losses | Cooldown active`;
242
247
  }
243
- // In cooldown?
244
- else if (cooldownRemaining > 0 && this.lastSignalTime > 0) {
245
- const secs = Math.ceil(cooldownRemaining / 1000);
246
- state = `cooldown-${secs}`;
247
- 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}%`;
248
252
  }
249
- // VPIN toxic?
253
+ // VPIN toxic? (info only, not a hard filter)
250
254
  else if (vpin > this.vpinToxicThreshold) {
251
255
  state = 'vpin-toxic';
252
256
  message = `[${sym}] ${priceStr} | VPIN toxic ${vpinPct}% > 70% | No entry - informed traders active`;
@@ -393,51 +397,65 @@ class HQXUltraScalpingStrategy extends EventEmitter {
393
397
  return null;
394
398
  }
395
399
 
396
- // CRITICAL: Check cooldown
397
- const now = Date.now();
398
- const timeSinceLastSignal = now - this.lastSignalTime;
399
- 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) {
400
404
  // Silent - don't spam logs
401
405
  return null;
402
406
  }
403
407
 
404
- // 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)
405
418
  if (this.lossStreak >= this.maxConsecutiveLosses) {
406
419
  this.tradingEnabled = false;
407
420
  this.emit('log', { type: 'info', message: `Trading paused: ${this.lossStreak} consecutive losses. Waiting for cooldown...` });
408
- // Auto re-enable after 2 minutes
421
+ // Auto re-enable after 1 hour (matches Python: 3600_000_000 us = 1 hour)
409
422
  setTimeout(() => {
410
423
  this.tradingEnabled = true;
411
424
  this.lossStreak = 0;
412
425
  this.emit('log', { type: 'info', message: 'Trading re-enabled after cooldown' });
413
- }, 120000);
426
+ }, 3600000);
414
427
  return null;
415
428
  }
416
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
+
417
436
  const absZscore = Math.abs(zscore);
437
+
438
+ // Z-Score threshold check (ONLY filter from Python backtest)
418
439
  if (absZscore < volParams.zscoreThreshold) return null;
419
- if (vpin > this.vpinToxicThreshold) return null;
420
440
 
441
+ // Determine direction based on Z-Score
421
442
  let direction;
422
- if (zscore < -volParams.zscoreThreshold) direction = 'long';
423
- 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
424
445
  else return null;
425
-
426
- // CRITICAL: OFI must confirm direction (stronger filter)
427
- const ofiConfirms = (direction === 'long' && ofi > 0.15) || (direction === 'short' && ofi < -0.15);
428
- if (!ofiConfirms) {
429
- this.emit('log', { type: 'debug', message: `Signal rejected: OFI (${(ofi * 100).toFixed(1)}%) doesn't confirm ${direction}` });
430
- return null;
431
- }
432
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);
433
449
  const kalmanDiff = currentPrice - kalmanEstimate;
434
450
  const kalmanConfirms = (direction === 'long' && kalmanDiff < 0) || (direction === 'short' && kalmanDiff > 0);
451
+ const vpinOk = vpin < this.vpinToxicThreshold;
435
452
 
453
+ // Composite confidence score (informational, not blocking)
436
454
  const scores = {
437
455
  zscore: Math.min(1.0, absZscore / 4.0),
438
- vpin: 1.0 - vpin,
456
+ vpin: vpinOk ? (1.0 - vpin) : 0.3,
439
457
  kyleLambda: kyleLambda > 0.001 ? 0.5 : 0.8,
440
- kalman: kalmanConfirms ? 0.8 : 0.4,
458
+ kalman: kalmanConfirms ? 0.8 : 0.5,
441
459
  volatility: regime === 'normal' ? 0.8 : regime === 'low' ? 0.7 : 0.6,
442
460
  ofi: ofiConfirms ? 0.9 : 0.5,
443
461
  composite: 0
@@ -448,14 +466,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
448
466
 
449
467
  const confidence = Math.min(1.0, scores.composite + volParams.confidenceBonus);
450
468
 
451
- // CRITICAL: Higher confidence threshold (65% minimum)
452
- if (confidence < this.minConfidenceThreshold) {
453
- this.emit('log', { type: 'debug', message: `Signal rejected: confidence ${(confidence * 100).toFixed(1)}% < ${this.minConfidenceThreshold * 100}%` });
454
- return null;
455
- }
456
-
457
- // Update last signal time
458
- this.lastSignalTime = now;
469
+ // Update last signal tick (tick-based cooldown like Python)
470
+ this.lastSignalTick = this.tickCount;
459
471
 
460
472
  const stopTicks = Math.round(this.baseStopTicks * volParams.stopMultiplier);
461
473
  const targetTicks = Math.round(this.baseTargetTicks * volParams.targetMultiplier);
@@ -622,7 +634,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
622
634
  tradingEnabled: this.tradingEnabled,
623
635
  lossStreak: this.lossStreak,
624
636
  winStreak: this.winStreak,
625
- 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,
626
639
  };
627
640
  }
628
641
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.238",
3
+ "version": "2.9.240",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {