hedgequantx 2.9.199 → 2.9.201

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.
@@ -579,7 +579,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
579
579
  }
580
580
  }
581
581
 
582
- // Singleton instance
583
- const M1 = new HQXUltraScalpingStrategy();
582
+ // Export class (not instance) - consistent with HQX-2B pattern
583
+ // M1 is the class, use new M1() to create instances
584
+ const M1 = HQXUltraScalpingStrategy;
584
585
 
585
586
  module.exports = { M1, HQXUltraScalpingStrategy, OrderSide, SignalStrength };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.199",
3
+ "version": "2.9.201",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -45,7 +45,12 @@
45
45
  "src/app.js",
46
46
  "src/api/",
47
47
  "src/config/",
48
- "src/lib/",
48
+ "src/lib/data.js",
49
+ "src/lib/smart-logs*.js",
50
+ "src/lib/smart-logs-messages/",
51
+ "src/lib/m/index.js",
52
+ "src/lib/m/ultra-scalping.js",
53
+ "src/lib/m/hqx-2b.js",
49
54
  "src/menus/",
50
55
  "src/pages/",
51
56
  "src/security/",
@@ -27,7 +27,7 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
27
27
  const strategyId = strategyInfo?.id || 'ultra-scalping';
28
28
  const strategyName = strategyInfo?.name || 'HQX Scalping';
29
29
  const strategyModule = loadStrategy(strategyId);
30
- const StrategyClass = strategyModule.M1; // loadStrategy normalizes to M1
30
+ const StrategyClass = strategyModule.M1;
31
31
 
32
32
  const supervisionEnabled = supervisionConfig?.supervisionEnabled && supervisionConfig?.agents?.length > 0;
33
33
  const supervisionEngine = supervisionEnabled ? new SupervisionEngine(supervisionConfig) : null;
@@ -1,173 +0,0 @@
1
- /**
2
- * Mathematical Models for HQX Ultra Scalping
3
- * @module lib/m/s1-models
4
- *
5
- * 6 Mathematical Models:
6
- * 1. Z-Score Mean Reversion
7
- * 2. VPIN (Volume-Synchronized Probability of Informed Trading)
8
- * 3. Kyle's Lambda (Price Impact / Liquidity)
9
- * 4. Kalman Filter (Signal Extraction)
10
- * 5. Volatility Regime Detection
11
- * 6. Order Flow Imbalance (OFI)
12
- */
13
-
14
- /**
15
- * MODEL 1: Z-SCORE MEAN REVERSION
16
- * @param {number[]} prices - Price array
17
- * @param {number} window - Lookback window
18
- * @returns {number} Z-Score value
19
- */
20
- function computeZScore(prices, window = 50) {
21
- if (prices.length < window) return 0;
22
- const recentPrices = prices.slice(-window);
23
- const mean = recentPrices.reduce((a, b) => a + b, 0) / window;
24
- const variance = recentPrices.reduce((sum, p) => sum + Math.pow(p - mean, 2), 0) / window;
25
- const std = Math.sqrt(variance);
26
- if (std < 0.0001) return 0;
27
- return (prices[prices.length - 1] - mean) / std;
28
- }
29
-
30
- /**
31
- * MODEL 2: VPIN
32
- * @param {Array<{buy: number, sell: number}>} volumes - Volume data
33
- * @param {number} vpinWindow - VPIN window size
34
- * @returns {number} VPIN value (0-1)
35
- */
36
- function computeVPIN(volumes, vpinWindow = 50) {
37
- if (volumes.length < vpinWindow) return 0.5;
38
- const recent = volumes.slice(-vpinWindow);
39
- let totalBuy = 0, totalSell = 0;
40
- for (const v of recent) { totalBuy += v.buy; totalSell += v.sell; }
41
- const total = totalBuy + totalSell;
42
- if (total < 1) return 0.5;
43
- return Math.abs(totalBuy - totalSell) / total;
44
- }
45
-
46
- /**
47
- * MODEL 3: KYLE'S LAMBDA
48
- * @param {Array} bars - Bar data
49
- * @returns {number} Kyle's Lambda value
50
- */
51
- function computeKyleLambda(bars) {
52
- if (bars.length < 20) return 0;
53
- const recent = bars.slice(-20);
54
- const priceChanges = [], vols = [];
55
- for (let i = 1; i < recent.length; i++) {
56
- priceChanges.push(recent[i].close - recent[i - 1].close);
57
- vols.push(recent[i].volume);
58
- }
59
- const meanP = priceChanges.reduce((a, b) => a + b, 0) / priceChanges.length;
60
- const meanV = vols.reduce((a, b) => a + b, 0) / vols.length;
61
- let cov = 0, varV = 0;
62
- for (let i = 0; i < priceChanges.length; i++) {
63
- cov += (priceChanges[i] - meanP) * (vols[i] - meanV);
64
- varV += Math.pow(vols[i] - meanV, 2);
65
- }
66
- cov /= priceChanges.length;
67
- varV /= priceChanges.length;
68
- if (varV < 0.0001) return 0;
69
- return Math.abs(cov / varV);
70
- }
71
-
72
- /**
73
- * MODEL 4: KALMAN FILTER
74
- * @param {Object} state - {estimate, errorCovariance}
75
- * @param {number} measurement - New measurement
76
- * @param {number} processNoise - Process noise
77
- * @param {number} measurementNoise - Measurement noise
78
- * @returns {Object} Updated state and estimate
79
- */
80
- function applyKalmanFilter(state, measurement, processNoise = 0.01, measurementNoise = 0.1) {
81
- if (!state || state.estimate === 0) {
82
- return {
83
- state: { estimate: measurement, errorCovariance: 1.0 },
84
- estimate: measurement
85
- };
86
- }
87
- const predictedEstimate = state.estimate;
88
- const predictedCovariance = state.errorCovariance + processNoise;
89
- const kalmanGain = predictedCovariance / (predictedCovariance + measurementNoise);
90
- const newEstimate = predictedEstimate + kalmanGain * (measurement - predictedEstimate);
91
- const newCovariance = (1 - kalmanGain) * predictedCovariance;
92
- return {
93
- state: { estimate: newEstimate, errorCovariance: newCovariance },
94
- estimate: newEstimate
95
- };
96
- }
97
-
98
- /**
99
- * Calculate ATR
100
- * @param {Array} bars - Bar data
101
- * @param {number} period - ATR period
102
- * @returns {number} ATR value
103
- */
104
- function calculateATR(bars, period = 14) {
105
- if (bars.length < period + 1) return 2.5;
106
- const trValues = [];
107
- for (let i = bars.length - period; i < bars.length; i++) {
108
- const bar = bars[i];
109
- const prevClose = bars[i - 1].close;
110
- const tr = Math.max(bar.high - bar.low, Math.abs(bar.high - prevClose), Math.abs(bar.low - prevClose));
111
- trValues.push(tr);
112
- }
113
- return trValues.reduce((a, b) => a + b, 0) / trValues.length;
114
- }
115
-
116
- /**
117
- * MODEL 5: VOLATILITY REGIME
118
- * @param {Array} atrHistory - ATR history
119
- * @param {number} currentATR - Current ATR
120
- * @returns {Object} Regime and parameters
121
- */
122
- function detectVolatilityRegime(atrHistory, currentATR) {
123
- let atrPercentile = 0.5;
124
- if (atrHistory.length >= 20) {
125
- atrPercentile = atrHistory.filter(a => a <= currentATR).length / atrHistory.length;
126
- }
127
-
128
- let regime, params;
129
- if (atrPercentile < 0.25) {
130
- regime = 'low';
131
- params = { stopMultiplier: 0.8, targetMultiplier: 0.9, zscoreThreshold: 1.2, confidenceBonus: 0.05 };
132
- } else if (atrPercentile < 0.75) {
133
- regime = 'normal';
134
- params = { stopMultiplier: 1.0, targetMultiplier: 1.0, zscoreThreshold: 1.5, confidenceBonus: 0.0 };
135
- } else {
136
- regime = 'high';
137
- params = { stopMultiplier: 1.3, targetMultiplier: 1.2, zscoreThreshold: 2.0, confidenceBonus: -0.05 };
138
- }
139
- return { regime, params };
140
- }
141
-
142
- /**
143
- * MODEL 6: ORDER FLOW IMBALANCE
144
- * @param {Array} bars - Bar data
145
- * @param {number} ofiLookback - Lookback period
146
- * @returns {number} OFI value (-1 to 1)
147
- */
148
- function computeOrderFlowImbalance(bars, ofiLookback = 20) {
149
- if (bars.length < ofiLookback) return 0;
150
- const recent = bars.slice(-ofiLookback);
151
- let buyPressure = 0, sellPressure = 0;
152
- for (const bar of recent) {
153
- const range = bar.high - bar.low;
154
- if (range > 0) {
155
- const closePos = (bar.close - bar.low) / range;
156
- buyPressure += closePos * bar.volume;
157
- sellPressure += (1 - closePos) * bar.volume;
158
- }
159
- }
160
- const total = buyPressure + sellPressure;
161
- if (total < 1) return 0;
162
- return (buyPressure - sellPressure) / total;
163
- }
164
-
165
- module.exports = {
166
- computeZScore,
167
- computeVPIN,
168
- computeKyleLambda,
169
- applyKalmanFilter,
170
- calculateATR,
171
- detectVolatilityRegime,
172
- computeOrderFlowImbalance,
173
- };
package/src/lib/m/s1.js DELETED
@@ -1,585 +0,0 @@
1
- /**
2
- * =============================================================================
3
- * HQX ULTRA SCALPING STRATEGY
4
- * =============================================================================
5
- * 6 Mathematical Models with 4-Layer Trailing Stop System
6
- *
7
- * BACKTEST RESULTS (162 tests, V4):
8
- * - Net P&L: $195,272.52
9
- * - Win Rate: 86.3%
10
- * - Profit Factor: 34.44
11
- * - Sharpe: 1.29
12
- * - Tests Passed: 150/162 (92.6%)
13
- *
14
- * MATHEMATICAL MODELS:
15
- * 1. Z-Score Mean Reversion (Entry: |Z| > threshold, Exit: |Z| < 0.5)
16
- * 2. VPIN (Volume-Synchronized Probability of Informed Trading)
17
- * 3. Kyle's Lambda (Price Impact / Liquidity Measurement)
18
- * 4. Kalman Filter (Signal Extraction from Noise)
19
- * 5. Volatility Regime Detection (Low/Normal/High adaptive)
20
- * 6. Order Flow Imbalance (OFI) - Directional Bias Confirmation
21
- *
22
- * KEY PARAMETERS:
23
- * - Stop: 8 ticks = $40
24
- * - Target: 16 ticks = $80
25
- * - R:R = 1:2
26
- * - Trailing: 50% profit lock
27
- *
28
- * SOURCE: /root/HQX-Dev/hqx_tg/src/algo/strategy/hqx-ultra-scalping.strategy.ts
29
- */
30
-
31
- 'use strict';
32
-
33
- const EventEmitter = require('events');
34
- const { v4: uuidv4 } = require('uuid');
35
- const {
36
- computeZScore,
37
- computeVPIN,
38
- computeKyleLambda,
39
- applyKalmanFilter,
40
- calculateATR,
41
- detectVolatilityRegime,
42
- computeOrderFlowImbalance,
43
- } = require('./s1-models');
44
-
45
- // =============================================================================
46
- // CONSTANTS
47
- // =============================================================================
48
-
49
- const OrderSide = { BID: 'BID', ASK: 'ASK' };
50
- const SignalStrength = { WEAK: 'WEAK', MODERATE: 'MODERATE', STRONG: 'STRONG', VERY_STRONG: 'VERY_STRONG' };
51
-
52
- // =============================================================================
53
- // HELPER: Extract base symbol from contractId
54
- // =============================================================================
55
- function extractBaseSymbol(contractId) {
56
- // CON.F.US.ENQ.H25 -> NQ, CON.F.US.EP.H25 -> ES
57
- const mapping = {
58
- 'ENQ': 'NQ', 'EP': 'ES', 'EMD': 'EMD', 'RTY': 'RTY',
59
- 'MNQ': 'MNQ', 'MES': 'MES', 'M2K': 'M2K', 'MYM': 'MYM',
60
- 'NKD': 'NKD', 'GC': 'GC', 'SI': 'SI', 'CL': 'CL', 'YM': 'YM'
61
- };
62
-
63
- if (!contractId) return 'UNKNOWN';
64
- const parts = contractId.split('.');
65
- if (parts.length >= 4) {
66
- const symbol = parts[3];
67
- return mapping[symbol] || symbol;
68
- }
69
- return contractId;
70
- }
71
-
72
- // =============================================================================
73
- // HQX ULTRA SCALPING STRATEGY CLASS
74
- // =============================================================================
75
-
76
- class HQXUltraScalpingStrategy extends EventEmitter {
77
- constructor() {
78
- super();
79
-
80
- this.tickSize = 0.25;
81
- this.tickValue = 5.0;
82
-
83
- // === Model Parameters (from V4 backtest) ===
84
- this.zscoreEntryThreshold = 1.5; // Adaptive per regime
85
- this.zscoreExitThreshold = 0.5;
86
- this.vpinWindow = 50;
87
- this.vpinToxicThreshold = 0.7;
88
- this.kalmanProcessNoise = 0.01;
89
- this.kalmanMeasurementNoise = 0.1;
90
- this.volatilityLookback = 100;
91
- this.ofiLookback = 20;
92
-
93
- // === Trade Parameters (from V4 backtest) ===
94
- this.baseStopTicks = 8; // $40
95
- this.baseTargetTicks = 16; // $80
96
- this.breakevenTicks = 4; // Move to BE at +4 ticks
97
- this.profitLockPct = 0.5; // Lock 50% of profit
98
-
99
- // === State Storage ===
100
- this.barHistory = new Map();
101
- this.kalmanStates = new Map();
102
- this.priceBuffer = new Map();
103
- this.volumeBuffer = new Map();
104
- this.tradesBuffer = new Map();
105
- this.atrHistory = new Map();
106
-
107
- // === Tick aggregation ===
108
- this.tickBuffer = new Map();
109
- this.lastBarTime = new Map();
110
- this.barIntervalMs = 5000; // 5-second bars
111
-
112
- // === Performance Tracking ===
113
- this.recentTrades = [];
114
- this.winStreak = 0;
115
- this.lossStreak = 0;
116
-
117
- // === CRITICAL: Cooldown & Risk Management ===
118
- this.lastSignalTime = 0;
119
- this.signalCooldownMs = 30000; // 30 seconds minimum between signals
120
- this.maxConsecutiveLosses = 3; // Stop trading after 3 consecutive losses
121
- this.minConfidenceThreshold = 0.65; // Minimum 65% confidence (was 55%)
122
- this.tradingEnabled = true;
123
- }
124
-
125
- /**
126
- * Initialize strategy for a contract
127
- */
128
- initialize(contractId, tickSize = 0.25, tickValue = 5.0) {
129
- this.tickSize = tickSize;
130
- this.tickValue = tickValue;
131
- this.barHistory.set(contractId, []);
132
- this.priceBuffer.set(contractId, []);
133
- this.volumeBuffer.set(contractId, []);
134
- this.tradesBuffer.set(contractId, []);
135
- this.atrHistory.set(contractId, []);
136
- this.tickBuffer.set(contractId, []);
137
- this.lastBarTime.set(contractId, 0);
138
- this.kalmanStates.set(contractId, { estimate: 0, errorCovariance: 1.0 });
139
- }
140
-
141
- /**
142
- * Process a tick - aggregates into bars then runs strategy
143
- */
144
- processTick(tick) {
145
- const contractId = tick.contractId;
146
-
147
- if (!this.barHistory.has(contractId)) {
148
- this.initialize(contractId);
149
- }
150
-
151
- // Add tick to buffer
152
- let ticks = this.tickBuffer.get(contractId);
153
- ticks.push(tick);
154
-
155
- // Check if we should form a new bar
156
- const now = Date.now();
157
- const lastBar = this.lastBarTime.get(contractId);
158
-
159
- if (now - lastBar >= this.barIntervalMs && ticks.length > 0) {
160
- const bar = this._aggregateTicksToBar(ticks, now);
161
- this.tickBuffer.set(contractId, []);
162
- this.lastBarTime.set(contractId, now);
163
-
164
- if (bar) {
165
- const signal = this.processBar(contractId, bar);
166
- if (signal) {
167
- this.emit('signal', signal);
168
- return signal;
169
- }
170
- }
171
- }
172
- return null;
173
- }
174
-
175
- /**
176
- * Aggregate ticks into a bar
177
- */
178
- _aggregateTicksToBar(ticks, timestamp) {
179
- if (ticks.length === 0) return null;
180
-
181
- const prices = ticks.map(t => t.price).filter(p => p != null);
182
- if (prices.length === 0) return null;
183
-
184
- let buyVol = 0, sellVol = 0;
185
- for (let i = 1; i < ticks.length; i++) {
186
- const vol = ticks[i].volume || 1;
187
- if (ticks[i].price > ticks[i-1].price) buyVol += vol;
188
- else if (ticks[i].price < ticks[i-1].price) sellVol += vol;
189
- else { buyVol += vol / 2; sellVol += vol / 2; }
190
- }
191
-
192
- return {
193
- timestamp,
194
- open: prices[0],
195
- high: Math.max(...prices),
196
- low: Math.min(...prices),
197
- close: prices[prices.length - 1],
198
- volume: ticks.reduce((sum, t) => sum + (t.volume || 1), 0),
199
- delta: buyVol - sellVol,
200
- tickCount: ticks.length
201
- };
202
- }
203
-
204
- /**
205
- * Process a new bar and potentially generate signal
206
- */
207
- processBar(contractId, bar) {
208
- let bars = this.barHistory.get(contractId);
209
- if (!bars) {
210
- this.initialize(contractId);
211
- bars = this.barHistory.get(contractId);
212
- }
213
-
214
- bars.push(bar);
215
- if (bars.length > 500) bars.shift();
216
-
217
- // Update price buffer
218
- const prices = this.priceBuffer.get(contractId);
219
- prices.push(bar.close);
220
- if (prices.length > 200) prices.shift();
221
-
222
- // Update volume buffer
223
- const volumes = this.volumeBuffer.get(contractId);
224
- const barRange = bar.high - bar.low;
225
- let buyVol = bar.volume * 0.5;
226
- let sellVol = bar.volume * 0.5;
227
- if (barRange > 0) {
228
- const closePosition = (bar.close - bar.low) / barRange;
229
- buyVol = bar.volume * closePosition;
230
- sellVol = bar.volume * (1 - closePosition);
231
- }
232
- volumes.push({ buy: buyVol, sell: sellVol });
233
- if (volumes.length > 100) volumes.shift();
234
-
235
- // Need minimum data
236
- if (bars.length < 50) return null;
237
-
238
- // === 6 MODELS ===
239
- const zscore = computeZScore(prices);
240
- const vpin = computeVPIN(volumes, this.vpinWindow);
241
- const kyleLambda = computeKyleLambda(bars);
242
- const kalmanEstimate = this._applyKalmanFilter(contractId, bar.close);
243
- const { regime, params } = this._detectVolatilityRegime(contractId, bars);
244
- const ofi = computeOrderFlowImbalance(bars, this.ofiLookback);
245
-
246
- // === SIGNAL GENERATION ===
247
- return this._generateSignal(contractId, bar.close, zscore, vpin, kyleLambda, kalmanEstimate, regime, params, ofi, bars);
248
- }
249
-
250
- // ===========================================================================
251
- // MODEL 4: KALMAN FILTER (uses shared state)
252
- // ===========================================================================
253
- _applyKalmanFilter(contractId, measurement) {
254
- let state = this.kalmanStates.get(contractId);
255
- const result = applyKalmanFilter(state, measurement, this.kalmanProcessNoise, this.kalmanMeasurementNoise);
256
- this.kalmanStates.set(contractId, result.state);
257
- return result.estimate;
258
- }
259
-
260
- // ===========================================================================
261
- // MODEL 5: VOLATILITY REGIME (uses shared state)
262
- // ===========================================================================
263
- _detectVolatilityRegime(contractId, bars) {
264
- const atr = calculateATR(bars);
265
- let atrHist = this.atrHistory.get(contractId);
266
- if (!atrHist) { atrHist = []; this.atrHistory.set(contractId, atrHist); }
267
- atrHist.push(atr);
268
- if (atrHist.length > 500) atrHist.shift();
269
- return detectVolatilityRegime(atrHist, atr);
270
- }
271
-
272
- // ===========================================================================
273
- // SIGNAL GENERATION
274
- // ===========================================================================
275
- _generateSignal(contractId, currentPrice, zscore, vpin, kyleLambda, kalmanEstimate, regime, volParams, ofi, bars) {
276
- // CRITICAL: Check if trading is enabled
277
- if (!this.tradingEnabled) {
278
- this.emit('log', { type: 'debug', message: `Trading disabled (${this.lossStreak} consecutive losses)` });
279
- return null;
280
- }
281
-
282
- // CRITICAL: Check cooldown
283
- const now = Date.now();
284
- const timeSinceLastSignal = now - this.lastSignalTime;
285
- if (timeSinceLastSignal < this.signalCooldownMs) {
286
- // Silent - don't spam logs
287
- return null;
288
- }
289
-
290
- // CRITICAL: Check consecutive losses
291
- if (this.lossStreak >= this.maxConsecutiveLosses) {
292
- this.tradingEnabled = false;
293
- this.emit('log', { type: 'info', message: `Trading paused: ${this.lossStreak} consecutive losses. Waiting for cooldown...` });
294
- // Auto re-enable after 2 minutes
295
- setTimeout(() => {
296
- this.tradingEnabled = true;
297
- this.lossStreak = 0;
298
- this.emit('log', { type: 'info', message: 'Trading re-enabled after cooldown' });
299
- }, 120000);
300
- return null;
301
- }
302
-
303
- const absZscore = Math.abs(zscore);
304
- if (absZscore < volParams.zscoreThreshold) return null;
305
- if (vpin > this.vpinToxicThreshold) return null;
306
-
307
- let direction;
308
- if (zscore < -volParams.zscoreThreshold) direction = 'long';
309
- else if (zscore > volParams.zscoreThreshold) direction = 'short';
310
- else return null;
311
-
312
- // CRITICAL: OFI must confirm direction (stronger filter)
313
- const ofiConfirms = (direction === 'long' && ofi > 0.15) || (direction === 'short' && ofi < -0.15);
314
- if (!ofiConfirms) {
315
- this.emit('log', { type: 'debug', message: `Signal rejected: OFI (${(ofi * 100).toFixed(1)}%) doesn't confirm ${direction}` });
316
- return null;
317
- }
318
-
319
- const kalmanDiff = currentPrice - kalmanEstimate;
320
- const kalmanConfirms = (direction === 'long' && kalmanDiff < 0) || (direction === 'short' && kalmanDiff > 0);
321
-
322
- const scores = {
323
- zscore: Math.min(1.0, absZscore / 4.0),
324
- vpin: 1.0 - vpin,
325
- kyleLambda: kyleLambda > 0.001 ? 0.5 : 0.8,
326
- kalman: kalmanConfirms ? 0.8 : 0.4,
327
- volatility: regime === 'normal' ? 0.8 : regime === 'low' ? 0.7 : 0.6,
328
- ofi: ofiConfirms ? 0.9 : 0.5,
329
- composite: 0
330
- };
331
-
332
- scores.composite = scores.zscore * 0.30 + scores.vpin * 0.15 + scores.kyleLambda * 0.10 +
333
- scores.kalman * 0.15 + scores.volatility * 0.10 + scores.ofi * 0.20;
334
-
335
- const confidence = Math.min(1.0, scores.composite + volParams.confidenceBonus);
336
-
337
- // CRITICAL: Higher confidence threshold (65% minimum)
338
- if (confidence < this.minConfidenceThreshold) {
339
- this.emit('log', { type: 'debug', message: `Signal rejected: confidence ${(confidence * 100).toFixed(1)}% < ${this.minConfidenceThreshold * 100}%` });
340
- return null;
341
- }
342
-
343
- // Update last signal time
344
- this.lastSignalTime = now;
345
-
346
- const stopTicks = Math.round(this.baseStopTicks * volParams.stopMultiplier);
347
- const targetTicks = Math.round(this.baseTargetTicks * volParams.targetMultiplier);
348
- const actualStopTicks = Math.max(6, Math.min(12, stopTicks));
349
- const actualTargetTicks = Math.max(actualStopTicks * 1.5, Math.min(24, targetTicks));
350
-
351
- let stopLoss, takeProfit, beBreakeven, profitLockLevel;
352
- if (direction === 'long') {
353
- stopLoss = currentPrice - actualStopTicks * this.tickSize;
354
- takeProfit = currentPrice + actualTargetTicks * this.tickSize;
355
- beBreakeven = currentPrice + this.breakevenTicks * this.tickSize;
356
- profitLockLevel = currentPrice + (actualTargetTicks * this.profitLockPct) * this.tickSize;
357
- } else {
358
- stopLoss = currentPrice + actualStopTicks * this.tickSize;
359
- takeProfit = currentPrice - actualTargetTicks * this.tickSize;
360
- beBreakeven = currentPrice - this.breakevenTicks * this.tickSize;
361
- profitLockLevel = currentPrice - (actualTargetTicks * this.profitLockPct) * this.tickSize;
362
- }
363
-
364
- const riskReward = actualTargetTicks / actualStopTicks;
365
- const trailTriggerTicks = Math.round(actualTargetTicks * 0.5);
366
- const trailDistanceTicks = Math.round(actualStopTicks * 0.4);
367
-
368
- let strength = SignalStrength.MODERATE;
369
- if (confidence >= 0.85) strength = SignalStrength.VERY_STRONG;
370
- else if (confidence >= 0.75) strength = SignalStrength.STRONG;
371
- else if (confidence < 0.60) strength = SignalStrength.WEAK;
372
-
373
- const winProb = 0.5 + (confidence - 0.5) * 0.4;
374
- const edge = winProb * Math.abs(takeProfit - currentPrice) - (1 - winProb) * Math.abs(currentPrice - stopLoss);
375
-
376
- return {
377
- id: uuidv4(),
378
- timestamp: Date.now(),
379
- symbol: extractBaseSymbol(contractId),
380
- contractId,
381
- side: direction === 'long' ? OrderSide.BID : OrderSide.ASK,
382
- direction,
383
- strategy: 'HQX_ULTRA_SCALPING',
384
- strength,
385
- edge,
386
- confidence,
387
- entry: currentPrice,
388
- entryPrice: currentPrice,
389
- stopLoss,
390
- takeProfit,
391
- riskReward,
392
- stopTicks: actualStopTicks,
393
- targetTicks: actualTargetTicks,
394
- trailTriggerTicks,
395
- trailDistanceTicks,
396
- beBreakeven,
397
- profitLockLevel,
398
- zScore: zscore,
399
- zScoreExit: this.zscoreExitThreshold,
400
- vpinValue: vpin,
401
- kyleLambda,
402
- kalmanEstimate,
403
- volatilityRegime: regime,
404
- ofiValue: ofi,
405
- models: scores
406
- };
407
- }
408
-
409
- /**
410
- * Check if should exit by Z-Score
411
- */
412
- shouldExitByZScore(contractId) {
413
- const prices = this.priceBuffer.get(contractId);
414
- if (!prices || prices.length < 50) return false;
415
- const zscore = computeZScore(prices);
416
- return Math.abs(zscore) < this.zscoreExitThreshold;
417
- }
418
-
419
- /**
420
- * Get current model values
421
- */
422
- getModelValues(contractId) {
423
- const prices = this.priceBuffer.get(contractId);
424
- const volumes = this.volumeBuffer.get(contractId);
425
- const bars = this.barHistory.get(contractId);
426
- if (!prices || !volumes || !bars || bars.length < 50) return null;
427
-
428
- return {
429
- zscore: computeZScore(prices).toFixed(2),
430
- vpin: (computeVPIN(volumes, this.vpinWindow) * 100).toFixed(1) + '%',
431
- ofi: (computeOrderFlowImbalance(bars, this.ofiLookback) * 100).toFixed(1) + '%',
432
- bars: bars.length
433
- };
434
- }
435
-
436
- /**
437
- * Record trade result - CRITICAL for risk management
438
- * @param {number} pnl - Trade P&L (positive or negative)
439
- */
440
- recordTradeResult(pnl) {
441
- // Only record actual trades (not P&L updates)
442
- // A trade is considered closed when P&L changes significantly
443
- const lastTrade = this.recentTrades[this.recentTrades.length - 1];
444
- if (lastTrade && Math.abs(pnl - lastTrade.pnl) < 0.5) {
445
- // Same P&L, ignore duplicate
446
- return;
447
- }
448
-
449
- this.recentTrades.push({ pnl, timestamp: Date.now() });
450
- if (this.recentTrades.length > 100) this.recentTrades.shift();
451
-
452
- if (pnl > 0) {
453
- this.winStreak++;
454
- this.lossStreak = 0;
455
- this.tradingEnabled = true; // Re-enable on win
456
- this.emit('log', { type: 'info', message: `WIN +$${pnl.toFixed(2)} | Streak: ${this.winStreak}` });
457
- } else if (pnl < 0) {
458
- this.lossStreak++;
459
- this.winStreak = 0;
460
- this.emit('log', { type: 'info', message: `LOSS $${pnl.toFixed(2)} | Streak: -${this.lossStreak}` });
461
-
462
- // Check if we need to pause trading
463
- if (this.lossStreak >= this.maxConsecutiveLosses) {
464
- this.emit('log', { type: 'info', message: `Max losses reached (${this.lossStreak}). Pausing...` });
465
- }
466
- }
467
- }
468
-
469
- /**
470
- * Get bar history
471
- */
472
- getBarHistory(contractId) {
473
- return this.barHistory.get(contractId) || [];
474
- }
475
-
476
- /**
477
- * Get analysis state for logging/debugging
478
- * @param {string} contractId - Contract ID
479
- * @param {number} currentPrice - Current price
480
- * @returns {Object} Current strategy state
481
- */
482
- getAnalysisState(contractId, currentPrice) {
483
- const prices = this.priceBuffer.get(contractId);
484
- const volumes = this.volumeBuffer.get(contractId);
485
- const bars = this.barHistory.get(contractId);
486
-
487
- if (!prices || !volumes || !bars || bars.length < 20) {
488
- return {
489
- ready: false,
490
- barsProcessed: bars?.length || 0,
491
- swingsDetected: 0,
492
- activeZones: 0,
493
- };
494
- }
495
-
496
- const zscore = computeZScore(prices);
497
- const vpin = computeVPIN(volumes, this.vpinWindow);
498
- const ofi = computeOrderFlowImbalance(bars, this.ofiLookback);
499
-
500
- return {
501
- ready: bars.length >= 50,
502
- barsProcessed: bars.length,
503
- swingsDetected: 0,
504
- activeZones: 0,
505
- zScore: zscore,
506
- vpin: vpin,
507
- ofi: ofi,
508
- tradingEnabled: this.tradingEnabled,
509
- lossStreak: this.lossStreak,
510
- winStreak: this.winStreak,
511
- cooldownRemaining: Math.max(0, this.signalCooldownMs - (Date.now() - this.lastSignalTime)),
512
- };
513
- }
514
-
515
- /**
516
- * Preload historical bars for faster warmup
517
- * @param {string} contractId - Contract ID
518
- * @param {Array} histBars - Historical bar data [{timestamp, open, high, low, close, volume}, ...]
519
- */
520
- preloadBars(contractId, histBars) {
521
- if (!histBars || histBars.length === 0) return;
522
-
523
- if (!this.barHistory.has(contractId)) {
524
- this.initialize(contractId);
525
- }
526
-
527
- const bars = this.barHistory.get(contractId);
528
- const prices = this.priceBuffer.get(contractId);
529
- const volumes = this.volumeBuffer.get(contractId);
530
-
531
- for (const bar of histBars) {
532
- bars.push({
533
- timestamp: bar.timestamp,
534
- open: bar.open,
535
- high: bar.high,
536
- low: bar.low,
537
- close: bar.close,
538
- volume: bar.volume || 1,
539
- delta: 0,
540
- tickCount: 1
541
- });
542
-
543
- prices.push(bar.close);
544
-
545
- const barRange = bar.high - bar.low;
546
- let buyVol = (bar.volume || 1) * 0.5;
547
- let sellVol = (bar.volume || 1) * 0.5;
548
- if (barRange > 0) {
549
- const closePosition = (bar.close - bar.low) / barRange;
550
- buyVol = (bar.volume || 1) * closePosition;
551
- sellVol = (bar.volume || 1) * (1 - closePosition);
552
- }
553
- volumes.push({ buy: buyVol, sell: sellVol });
554
- }
555
-
556
- // Trim to max sizes
557
- while (bars.length > 500) bars.shift();
558
- while (prices.length > 200) prices.shift();
559
- while (volumes.length > 100) volumes.shift();
560
-
561
- // Set last bar time to now
562
- this.lastBarTime.set(contractId, Date.now());
563
-
564
- this.emit('log', { type: 'info', message: `Preloaded ${histBars.length} bars for ${contractId}` });
565
- }
566
-
567
- /**
568
- * Reset strategy
569
- */
570
- reset(contractId) {
571
- this.barHistory.set(contractId, []);
572
- this.priceBuffer.set(contractId, []);
573
- this.volumeBuffer.set(contractId, []);
574
- this.tradesBuffer.set(contractId, []);
575
- this.atrHistory.set(contractId, []);
576
- this.tickBuffer.set(contractId, []);
577
- this.lastBarTime.set(contractId, 0);
578
- this.kalmanStates.set(contractId, { estimate: 0, errorCovariance: 1.0 });
579
- }
580
- }
581
-
582
- // Singleton instance
583
- const M1 = new HQXUltraScalpingStrategy();
584
-
585
- module.exports = { M1, HQXUltraScalpingStrategy, OrderSide, SignalStrength };