hedgequantx 2.9.197 → 2.9.199

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.
@@ -1,705 +1,585 @@
1
- var __getOwnPropNames = Object.getOwnPropertyNames;
2
- var __commonJS = (cb, mod) => function __require() {
3
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4
- };
5
-
6
- // ultra-scalping/config.js
7
- var require_config = __commonJS({
8
- "ultra-scalping/config.js"(exports2, module2) {
9
- var DEFAULT_CONFIG = {
10
- // Model Parameters
11
- zscoreEntryThreshold: 1.5,
12
- // Live trading threshold (backtest: 2.5)
13
- zscoreExitThreshold: 0.5,
14
- vpinWindow: 50,
15
- vpinToxicThreshold: 0.7,
16
- // Skip if VPIN > 0.7
17
- volatilityLookback: 100,
18
- ofiLookback: 20,
19
- // Trade Parameters
20
- baseStopTicks: 8,
21
- // $40
22
- baseTargetTicks: 16,
23
- // $80
24
- breakevenTicks: 4,
25
- // Move to BE at +4 ticks
26
- profitLockPct: 0.5,
27
- // Lock 50% of profit
28
- minConfidence: 0.55,
29
- // Minimum composite confidence
30
- cooldownMs: 3e4,
31
- // 30 seconds between signals
32
- minHoldTimeMs: 1e4,
33
- // Minimum 10 seconds hold
34
- // Model Weights (from Python backtest)
35
- weights: {
36
- zscore: 0.3,
37
- // 30%
38
- ofi: 0.2,
39
- // 20%
40
- vpin: 0.15,
41
- // 15%
42
- kalman: 0.15,
43
- // 15%
44
- kyleLambda: 0.1,
45
- // 10%
46
- volatility: 0.1
47
- // 10%
48
- },
49
- // Session (Futures Market Hours - Sunday 18:00 to Friday 17:00 EST)
50
- session: {
51
- enabled: false,
52
- // Trade anytime markets are open
53
- timezone: "America/New_York"
54
- }
55
- };
56
- module2.exports = { DEFAULT_CONFIG };
57
- }
58
- });
59
-
60
- // common/types.js
61
- var require_types = __commonJS({
62
- "common/types.js"(exports2, module2) {
63
- var OrderSide2 = { BID: 0, ASK: 1 };
64
- var SignalStrength2 = { WEAK: 1, MODERATE: 2, STRONG: 3, VERY_STRONG: 4 };
65
- module2.exports = { OrderSide: OrderSide2, SignalStrength: SignalStrength2 };
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;
66
68
  }
67
- });
68
-
69
- // ultra-scalping/signal.js
70
- var require_signal = __commonJS({
71
- "ultra-scalping/signal.js"(exports2, module2) {
72
- var { v4: uuidv4 } = require("uuid");
73
- var { OrderSide: OrderSide2, SignalStrength: SignalStrength2 } = require_types();
74
- function generateSignal(params) {
75
- const {
76
- contractId,
77
- currentPrice,
78
- zscore,
79
- vpin,
80
- kyleLambda,
81
- kalmanEstimate,
82
- regime,
83
- volParams,
84
- ofi,
85
- config,
86
- tickSize
87
- } = params;
88
- const absZscore = Math.abs(zscore);
89
- if (absZscore < volParams.zscoreThreshold) {
90
- return null;
91
- }
92
- if (vpin > config.vpinToxicThreshold) {
93
- return null;
94
- }
95
- let direction;
96
- if (zscore < -volParams.zscoreThreshold) {
97
- direction = "long";
98
- } else if (zscore > volParams.zscoreThreshold) {
99
- direction = "short";
100
- } else {
101
- return null;
102
- }
103
- const ofiConfirms = direction === "long" && ofi > 0.1 || direction === "short" && ofi < -0.1;
104
- const kalmanDiff = currentPrice - kalmanEstimate;
105
- const kalmanConfirms = direction === "long" && kalmanDiff < 0 || direction === "short" && kalmanDiff > 0;
106
- const scores = {
107
- zscore: Math.min(1, absZscore / 4),
108
- // Normalize to 0-1
109
- vpin: 1 - vpin,
110
- // Lower VPIN = better
111
- kyleLambda: kyleLambda > 1e-3 ? 0.5 : 0.8,
112
- // Moderate lambda is good
113
- kalman: kalmanConfirms ? 0.8 : 0.4,
114
- volatility: regime === "normal" ? 0.8 : regime === "low" ? 0.7 : 0.6,
115
- ofi: ofiConfirms ? 0.9 : 0.5,
116
- composite: 0
117
- // Calculated below
118
- };
119
- scores.composite = scores.zscore * config.weights.zscore + // 30%
120
- scores.vpin * config.weights.vpin + // 15%
121
- scores.kyleLambda * config.weights.kyleLambda + // 10%
122
- scores.kalman * config.weights.kalman + // 15%
123
- scores.volatility * config.weights.volatility + // 10%
124
- scores.ofi * config.weights.ofi;
125
- const confidence = Math.min(1, scores.composite + volParams.confidenceBonus);
126
- if (confidence < config.minConfidence) {
127
- return null;
128
- }
129
- const stopTicks = Math.round(config.baseStopTicks * volParams.stopMultiplier);
130
- const targetTicks = Math.round(config.baseTargetTicks * volParams.targetMultiplier);
131
- const actualStopTicks = Math.max(6, Math.min(12, stopTicks));
132
- const actualTargetTicks = Math.max(actualStopTicks * 1.5, Math.min(24, targetTicks));
133
- let stopLoss, takeProfit, beBreakeven, profitLockLevel;
134
- if (direction === "long") {
135
- stopLoss = currentPrice - actualStopTicks * tickSize;
136
- takeProfit = currentPrice + actualTargetTicks * tickSize;
137
- beBreakeven = currentPrice + config.breakevenTicks * tickSize;
138
- profitLockLevel = currentPrice + actualTargetTicks * config.profitLockPct * tickSize;
139
- } else {
140
- stopLoss = currentPrice + actualStopTicks * tickSize;
141
- takeProfit = currentPrice - actualTargetTicks * tickSize;
142
- beBreakeven = currentPrice - config.breakevenTicks * tickSize;
143
- profitLockLevel = currentPrice - actualTargetTicks * config.profitLockPct * tickSize;
144
- }
145
- const riskReward = actualTargetTicks / actualStopTicks;
146
- const trailTriggerTicks = Math.round(actualTargetTicks * 0.5);
147
- const trailDistanceTicks = Math.round(actualStopTicks * 0.4);
148
- let strength = SignalStrength2.MODERATE;
149
- if (confidence >= 0.85) strength = SignalStrength2.VERY_STRONG;
150
- else if (confidence >= 0.75) strength = SignalStrength2.STRONG;
151
- else if (confidence < 0.6) strength = SignalStrength2.WEAK;
152
- const winProb = 0.5 + (confidence - 0.5) * 0.4;
153
- const edge = winProb * Math.abs(takeProfit - currentPrice) - (1 - winProb) * Math.abs(currentPrice - stopLoss);
154
- return {
155
- id: uuidv4(),
156
- timestamp: Date.now(),
157
- symbol: contractId.split(".")[0] || contractId,
158
- contractId,
159
- side: direction === "long" ? OrderSide2.BID : OrderSide2.ASK,
160
- direction,
161
- strategy: "HQX_ULTRA_SCALPING_6MODELS",
162
- strength,
163
- edge,
164
- confidence,
165
- entry: currentPrice,
166
- entryPrice: currentPrice,
167
- stopLoss,
168
- takeProfit,
169
- riskReward,
170
- stopTicks: actualStopTicks,
171
- targetTicks: actualTargetTicks,
172
- trailTriggerTicks,
173
- trailDistanceTicks,
174
- beBreakeven,
175
- profitLockLevel,
176
- // Model values for debugging/monitoring
177
- zScore: zscore,
178
- zScoreExit: config.zscoreExitThreshold,
179
- vpinValue: vpin,
180
- kyleLambda,
181
- kalmanEstimate,
182
- volatilityRegime: regime,
183
- ofiValue: ofi,
184
- models: scores,
185
- // Order flow confirmation flag
186
- orderFlowConfirmed: ofiConfirms,
187
- kalmanConfirmed: kalmanConfirms,
188
- expires: Date.now() + 6e4
189
- };
190
- }
191
- module2.exports = { generateSignal };
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;
192
123
  }
193
- });
194
-
195
- // ultra-scalping/models/zscore.js
196
- var require_zscore = __commonJS({
197
- "ultra-scalping/models/zscore.js"(exports2, module2) {
198
- function computeZScore(prices, window = 50) {
199
- if (prices.length < window) return 0;
200
- const recentPrices = prices.slice(-window);
201
- const mean = recentPrices.reduce((a, b) => a + b, 0) / window;
202
- const variance = recentPrices.reduce((sum, p) => sum + Math.pow(p - mean, 2), 0) / window;
203
- const std = Math.sqrt(variance);
204
- if (std < 1e-4) return 0;
205
- const currentPrice = prices[prices.length - 1];
206
- return (currentPrice - mean) / std;
207
- }
208
- module2.exports = { computeZScore };
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 });
209
139
  }
210
- });
211
-
212
- // ultra-scalping/models/vpin.js
213
- var require_vpin = __commonJS({
214
- "ultra-scalping/models/vpin.js"(exports2, module2) {
215
- function computeVPIN(volumes, vpinWindow = 50) {
216
- if (volumes.length < vpinWindow) return 0.5;
217
- const recentVolumes = volumes.slice(-vpinWindow);
218
- let totalBuy = 0;
219
- let totalSell = 0;
220
- for (const v of recentVolumes) {
221
- totalBuy += v.buy;
222
- totalSell += v.sell;
223
- }
224
- const totalVolume = totalBuy + totalSell;
225
- if (totalVolume < 1) return 0.5;
226
- return Math.abs(totalBuy - totalSell) / totalVolume;
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);
227
149
  }
228
- module2.exports = { computeVPIN };
229
- }
230
- });
231
-
232
- // ultra-scalping/models/kyle.js
233
- var require_kyle = __commonJS({
234
- "ultra-scalping/models/kyle.js"(exports2, module2) {
235
- function computeKyleLambda(bars) {
236
- if (bars.length < 20) return 0;
237
- const recentBars = bars.slice(-20);
238
- const priceChanges = [];
239
- const volumes = [];
240
- for (let i = 1; i < recentBars.length; i++) {
241
- priceChanges.push(recentBars[i].close - recentBars[i - 1].close);
242
- volumes.push(recentBars[i].volume);
243
- }
244
- const meanPrice = priceChanges.reduce((a, b) => a + b, 0) / priceChanges.length;
245
- const meanVol = volumes.reduce((a, b) => a + b, 0) / volumes.length;
246
- let covariance = 0;
247
- let varianceVol = 0;
248
- for (let i = 0; i < priceChanges.length; i++) {
249
- covariance += (priceChanges[i] - meanPrice) * (volumes[i] - meanVol);
250
- varianceVol += Math.pow(volumes[i] - meanVol, 2);
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
+ }
251
170
  }
252
- covariance /= priceChanges.length;
253
- varianceVol /= priceChanges.length;
254
- if (varianceVol < 1e-4) return 0;
255
- return Math.abs(covariance / varianceVol);
256
171
  }
257
- module2.exports = { computeKyleLambda };
172
+ return null;
258
173
  }
259
- });
260
-
261
- // ultra-scalping/models/kalman.js
262
- var require_kalman = __commonJS({
263
- "ultra-scalping/models/kalman.js"(exports2, module2) {
264
- var KALMAN_PROCESS_NOISE = 0.01;
265
- var KALMAN_MEASUREMENT_NOISE = 0.1;
266
- function applyKalmanFilter(state, measurement) {
267
- if (!state || state.estimate === 0) {
268
- return {
269
- estimate: measurement,
270
- errorCovariance: 1,
271
- newEstimate: measurement
272
- };
273
- }
274
- const predictedEstimate = state.estimate;
275
- const predictedCovariance = state.errorCovariance + KALMAN_PROCESS_NOISE;
276
- const kalmanGain = predictedCovariance / (predictedCovariance + KALMAN_MEASUREMENT_NOISE);
277
- const newEstimate = predictedEstimate + kalmanGain * (measurement - predictedEstimate);
278
- const newCovariance = (1 - kalmanGain) * predictedCovariance;
279
- return {
280
- estimate: newEstimate,
281
- errorCovariance: newCovariance,
282
- newEstimate
283
- };
284
- }
285
- function createKalmanState() {
286
- return {
287
- estimate: 0,
288
- errorCovariance: 1
289
- };
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; }
290
190
  }
291
- module2.exports = {
292
- applyKalmanFilter,
293
- createKalmanState,
294
- KALMAN_PROCESS_NOISE,
295
- KALMAN_MEASUREMENT_NOISE
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
296
201
  };
297
202
  }
298
- });
299
-
300
- // ultra-scalping/models/volatility.js
301
- var require_volatility = __commonJS({
302
- "ultra-scalping/models/volatility.js"(exports2, module2) {
303
- function calculateATR(bars, period = 14) {
304
- if (bars.length < period + 1) return 2.5;
305
- const trValues = [];
306
- for (let i = bars.length - period; i < bars.length; i++) {
307
- const bar = bars[i];
308
- const prevClose = bars[i - 1].close;
309
- const tr = Math.max(
310
- bar.high - bar.low,
311
- Math.abs(bar.high - prevClose),
312
- Math.abs(bar.low - prevClose)
313
- );
314
- trValues.push(tr);
315
- }
316
- return trValues.reduce((a, b) => a + b, 0) / trValues.length;
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);
317
212
  }
318
- function detectVolatilityRegime(atr, atrHistory, tickSize) {
319
- let atrPercentile = 0.5;
320
- if (atrHistory.length >= 20) {
321
- atrPercentile = atrHistory.filter((a) => a <= atr).length / atrHistory.length;
322
- }
323
- let regime, params;
324
- if (atrPercentile < 0.25) {
325
- regime = "low";
326
- params = {
327
- stopMultiplier: 0.8,
328
- targetMultiplier: 0.9,
329
- zscoreThreshold: 1.2,
330
- confidenceBonus: 0.05
331
- };
332
- } else if (atrPercentile < 0.75) {
333
- regime = "normal";
334
- params = {
335
- stopMultiplier: 1,
336
- targetMultiplier: 1,
337
- zscoreThreshold: 1.5,
338
- confidenceBonus: 0
339
- };
340
- } else {
341
- regime = "high";
342
- params = {
343
- stopMultiplier: 1.3,
344
- targetMultiplier: 1.2,
345
- zscoreThreshold: 2,
346
- confidenceBonus: -0.05
347
- };
348
- }
349
- return { regime, params, atrPercentile };
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);
350
231
  }
351
- module2.exports = { calculateATR, detectVolatilityRegime };
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);
352
248
  }
353
- });
354
-
355
- // ultra-scalping/models/ofi.js
356
- var require_ofi = __commonJS({
357
- "ultra-scalping/models/ofi.js"(exports2, module2) {
358
- function computeOrderFlowImbalance(bars, lookback = 20) {
359
- if (bars.length < lookback) return 0;
360
- const recentBars = bars.slice(-lookback);
361
- let totalBuyPressure = 0;
362
- let totalSellPressure = 0;
363
- for (const bar of recentBars) {
364
- const barRange = bar.high - bar.low;
365
- if (barRange > 0) {
366
- const closePosition = (bar.close - bar.low) / barRange;
367
- totalBuyPressure += closePosition * bar.volume;
368
- totalSellPressure += (1 - closePosition) * bar.volume;
369
- }
370
- }
371
- const totalPressure = totalBuyPressure + totalSellPressure;
372
- if (totalPressure < 1) return 0;
373
- return (totalBuyPressure - totalSellPressure) / totalPressure;
374
- }
375
- module2.exports = { computeOrderFlowImbalance };
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;
376
258
  }
377
- });
378
-
379
- // ultra-scalping/models/index.js
380
- var require_models = __commonJS({
381
- "ultra-scalping/models/index.js"(exports2, module2) {
382
- var { computeZScore } = require_zscore();
383
- var { computeVPIN } = require_vpin();
384
- var { computeKyleLambda } = require_kyle();
385
- var { applyKalmanFilter, createKalmanState } = require_kalman();
386
- var { calculateATR, detectVolatilityRegime } = require_volatility();
387
- var { computeOrderFlowImbalance } = require_ofi();
388
- module2.exports = {
389
- computeZScore,
390
- computeVPIN,
391
- computeKyleLambda,
392
- applyKalmanFilter,
393
- createKalmanState,
394
- calculateATR,
395
- detectVolatilityRegime,
396
- computeOrderFlowImbalance
397
- };
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);
398
270
  }
399
- });
400
-
401
- // ultra-scalping/core.js
402
- var require_core = __commonJS({
403
- "ultra-scalping/core.js"(exports2, module2) {
404
- var EventEmitter2 = require("events");
405
- var { DEFAULT_CONFIG } = require_config();
406
- var { generateSignal } = require_signal();
407
- var {
408
- computeZScore,
409
- computeVPIN,
410
- computeKyleLambda,
411
- applyKalmanFilter,
412
- createKalmanState,
413
- calculateATR,
414
- detectVolatilityRegime,
415
- computeOrderFlowImbalance
416
- } = require_models();
417
- var HQXUltraScalping2 = class extends EventEmitter2 {
418
- constructor(config = {}) {
419
- super();
420
- this.tickSize = config.tickSize || 0.25;
421
- this.tickValue = config.tickValue || 5;
422
- this.config = { ...DEFAULT_CONFIG, ...config };
423
- this.barHistory = /* @__PURE__ */ new Map();
424
- this.priceBuffer = /* @__PURE__ */ new Map();
425
- this.volumeBuffer = /* @__PURE__ */ new Map();
426
- this.kalmanStates = /* @__PURE__ */ new Map();
427
- this.atrHistory = /* @__PURE__ */ new Map();
428
- this.recentTrades = [];
429
- this.winStreak = 0;
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;
430
297
  this.lossStreak = 0;
431
- this.lastSignalTime = 0;
432
- this.stats = { signals: 0, trades: 0, wins: 0, losses: 0, pnl: 0 };
433
- }
434
- initialize(contractId, tickSize = 0.25, tickValue = 5) {
435
- this.tickSize = tickSize;
436
- this.tickValue = tickValue;
437
- this.barHistory.set(contractId, []);
438
- this.priceBuffer.set(contractId, []);
439
- this.volumeBuffer.set(contractId, []);
440
- this.atrHistory.set(contractId, []);
441
- this.kalmanStates.set(contractId, createKalmanState());
442
- this.emit("log", {
443
- type: "info",
444
- message: `[HQX-UltraScalping] Initialized for ${contractId}: tick=${tickSize}, value=${tickValue}`
445
- });
446
- this.emit("log", {
447
- type: "info",
448
- message: `[HQX-UltraScalping] 6 Models: Z-Score(30%), OFI(20%), VPIN(15%), Kalman(15%), Kyle(10%), Vol(10%)`
449
- });
450
- }
451
- processTick(tick) {
452
- const { contractId, price, volume, timestamp } = tick;
453
- const bar = {
454
- timestamp: timestamp || Date.now(),
455
- open: price,
456
- high: price,
457
- low: price,
458
- close: price,
459
- volume: volume || 1
460
- };
461
- return this.processBar(contractId, bar);
462
- }
463
- onTick(tick) {
464
- return this.processTick(tick);
465
- }
466
- onTrade(trade) {
467
- return this.processTick({
468
- contractId: trade.contractId || trade.symbol,
469
- price: trade.price,
470
- volume: trade.size || trade.volume || 1,
471
- side: trade.side,
472
- timestamp: trade.timestamp || Date.now()
473
- });
474
- }
475
- processBar(contractId, bar) {
476
- let bars = this.barHistory.get(contractId);
477
- if (!bars) {
478
- this.initialize(contractId);
479
- bars = this.barHistory.get(contractId);
480
- }
481
- bars.push(bar);
482
- if (bars.length > 500) bars.shift();
483
- const prices = this.priceBuffer.get(contractId);
484
- prices.push(bar.close);
485
- if (prices.length > 200) prices.shift();
486
- const volumes = this.volumeBuffer.get(contractId);
487
- const barRange = bar.high - bar.low;
488
- let buyVol = bar.volume * 0.5;
489
- let sellVol = bar.volume * 0.5;
490
- if (barRange > 0) {
491
- const closePosition = (bar.close - bar.low) / barRange;
492
- buyVol = bar.volume * closePosition;
493
- sellVol = bar.volume * (1 - closePosition);
494
- }
495
- volumes.push({ buy: buyVol, sell: sellVol });
496
- if (volumes.length > 100) volumes.shift();
497
- if (bars.length < 50) return null;
498
- const zscore = computeZScore(prices);
499
- const vpin = computeVPIN(volumes, this.config.vpinWindow);
500
- const kyleLambda = computeKyleLambda(bars);
501
- const kalmanState = this.kalmanStates.get(contractId);
502
- const kalmanResult = applyKalmanFilter(kalmanState, bar.close);
503
- this.kalmanStates.set(contractId, {
504
- estimate: kalmanResult.estimate,
505
- errorCovariance: kalmanResult.errorCovariance
506
- });
507
- const kalmanEstimate = kalmanResult.newEstimate;
508
- const atr = calculateATR(bars);
509
- const atrHist = this.atrHistory.get(contractId);
510
- atrHist.push(atr);
511
- if (atrHist.length > 500) atrHist.shift();
512
- const { regime, params: volParams } = detectVolatilityRegime(atr, atrHist, this.tickSize);
513
- const ofi = computeOrderFlowImbalance(bars, this.config.ofiLookback);
514
- if (Date.now() - this.lastSignalTime < this.config.cooldownMs) {
515
- return null;
516
- }
517
- const signal = generateSignal({
518
- contractId,
519
- currentPrice: bar.close,
520
- zscore,
521
- vpin,
522
- kyleLambda,
523
- kalmanEstimate,
524
- regime,
525
- volParams,
526
- ofi,
527
- config: this.config,
528
- tickSize: this.tickSize
529
- });
530
- if (signal) {
531
- this.lastSignalTime = Date.now();
532
- this.stats.signals++;
533
- this.emit("signal", {
534
- side: signal.direction === "long" ? "buy" : "sell",
535
- action: "open",
536
- reason: `Z=${zscore.toFixed(2)}, VPIN=${(vpin * 100).toFixed(0)}%, OFI=${(ofi * 100).toFixed(0)}%, cf=${(signal.confidence * 100).toFixed(0)}%`,
537
- ...signal
538
- });
539
- this.emit("log", {
540
- type: "info",
541
- message: `[HQX] SIGNAL: ${signal.direction.toUpperCase()} @ ${bar.close.toFixed(2)} | Z:${zscore.toFixed(2)} VPIN:${(vpin * 100).toFixed(0)}% OFI:${(ofi * 100).toFixed(0)}% Kyle:${kyleLambda.toFixed(5)} Regime:${regime} | Conf:${(signal.confidence * 100).toFixed(0)}%`
542
- });
543
- }
544
- return signal;
545
- }
546
- shouldExitByZScore(contractId) {
547
- const prices = this.priceBuffer.get(contractId);
548
- if (!prices || prices.length < 50) return false;
549
- const zscore = computeZScore(prices);
550
- return Math.abs(zscore) < this.config.zscoreExitThreshold;
551
- }
552
- getModelValues(contractId) {
553
- const prices = this.priceBuffer.get(contractId);
554
- const volumes = this.volumeBuffer.get(contractId);
555
- const bars = this.barHistory.get(contractId);
556
- if (!prices || !volumes || !bars || bars.length < 50) {
557
- return null;
558
- }
559
- const zscore = computeZScore(prices);
560
- const vpin = computeVPIN(volumes, this.config.vpinWindow);
561
- const kyleLambda = computeKyleLambda(bars);
562
- const ofi = computeOrderFlowImbalance(bars, this.config.ofiLookback);
563
- return {
564
- zscore: Math.min(1, Math.abs(zscore) / 4),
565
- vpin: 1 - vpin,
566
- kyleLambda: kyleLambda > 1e-3 ? 0.5 : 0.8,
567
- kalman: 0.7,
568
- volatility: 0.7,
569
- ofi: Math.abs(ofi) > 0.1 ? 0.8 : 0.5,
570
- composite: 0.7,
571
- raw: { zscore, vpin, kyleLambda, ofi }
572
- };
573
- }
574
- getAnalysisState(contractId, currentPrice) {
575
- const bars = this.barHistory.get(contractId) || [];
576
- if (bars.length < 50) {
577
- return { ready: false, message: `Collecting data... ${bars.length}/50 bars` };
578
- }
579
- const prices = this.priceBuffer.get(contractId) || [];
580
- const volumes = this.volumeBuffer.get(contractId) || [];
581
- const atrHist = this.atrHistory.get(contractId) || [];
582
- const zscore = computeZScore(prices);
583
- const vpin = computeVPIN(volumes, this.config.vpinWindow);
584
- const ofi = computeOrderFlowImbalance(bars, this.config.ofiLookback);
585
- const kyleLambda = computeKyleLambda(bars);
586
- const atr = calculateATR(bars);
587
- const { regime, params } = detectVolatilityRegime(atr, atrHist, this.tickSize);
588
- return {
589
- ready: true,
590
- zScore: zscore,
591
- vpin,
592
- ofi,
593
- kyleLambda,
594
- regime,
595
- stopTicks: Math.round(this.config.baseStopTicks * params.stopMultiplier),
596
- targetTicks: Math.round(this.config.baseTargetTicks * params.targetMultiplier),
597
- threshold: params.zscoreThreshold,
598
- barsProcessed: bars.length,
599
- models: "6 (Z-Score, VPIN, Kyle, Kalman, Vol, OFI)"
600
- };
601
- }
602
- recordTradeResult(pnl) {
603
- this.recentTrades.push({ netPnl: pnl, timestamp: Date.now() });
604
- if (this.recentTrades.length > 100) this.recentTrades.shift();
605
- if (pnl > 0) {
606
- this.winStreak++;
607
- this.lossStreak = 0;
608
- this.stats.wins++;
609
- } else {
610
- this.lossStreak++;
611
- this.winStreak = 0;
612
- this.stats.losses++;
613
- }
614
- this.stats.trades++;
615
- this.stats.pnl += pnl;
616
- this.emit("log", {
617
- type: "debug",
618
- message: `[HQX] Trade result: ${pnl > 0 ? "WIN" : "LOSS"} $${pnl.toFixed(2)}, streak: ${pnl > 0 ? this.winStreak : -this.lossStreak}`
619
- });
620
- }
621
- getBarHistory(contractId) {
622
- return this.barHistory.get(contractId) || [];
623
- }
624
- getStats() {
625
- return this.stats;
626
- }
627
- reset(contractId) {
628
- this.barHistory.set(contractId, []);
629
- this.priceBuffer.set(contractId, []);
630
- this.volumeBuffer.set(contractId, []);
631
- this.atrHistory.set(contractId, []);
632
- this.kalmanStates.set(contractId, createKalmanState());
633
- this.emit("log", {
634
- type: "info",
635
- message: `[HQX-UltraScalping] Reset state for ${contractId}`
636
- });
637
- }
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
638
406
  };
639
- module2.exports = { HQXUltraScalping: HQXUltraScalping2 };
640
- }
641
- });
642
-
643
- // ultra-scalping/index.js
644
- var EventEmitter = require("events");
645
- var { HQXUltraScalping } = require_core();
646
- var { OrderSide, SignalStrength } = require_types();
647
- var UltraScalpingStrategy = class extends EventEmitter {
648
- constructor(config = {}) {
649
- super();
650
- this.config = config;
651
- this.strategy = new HQXUltraScalping(config);
652
- this.strategy.on("signal", (sig) => this.emit("signal", sig));
653
- this.strategy.on("log", (log) => this.emit("log", log));
654
- }
655
- // Interface methods (compatible with M1)
656
- processTick(tick) {
657
- return this.strategy.processTick(tick);
658
- }
659
- onTick(tick) {
660
- return this.strategy.onTick(tick);
661
- }
662
- onTrade(trade) {
663
- return this.strategy.onTrade(trade);
664
- }
665
- processBar(contractId, bar) {
666
- return this.strategy.processBar(contractId, bar);
667
407
  }
668
- initialize(contractId, tickSize, tickValue) {
669
- return this.strategy.initialize(contractId, tickSize, tickValue);
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;
670
417
  }
671
- getAnalysisState(contractId, price) {
672
- return this.strategy.getAnalysisState(contractId, price);
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
+ };
673
434
  }
435
+
436
+ /**
437
+ * Record trade result - CRITICAL for risk management
438
+ * @param {number} pnl - Trade P&L (positive or negative)
439
+ */
674
440
  recordTradeResult(pnl) {
675
- return this.strategy.recordTradeResult(pnl);
676
- }
677
- reset(contractId) {
678
- return this.strategy.reset(contractId);
679
- }
680
- getStats() {
681
- return this.strategy.getStats();
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
+ }
682
467
  }
468
+
469
+ /**
470
+ * Get bar history
471
+ */
683
472
  getBarHistory(contractId) {
684
- return this.strategy.getBarHistory(contractId);
473
+ return this.barHistory.get(contractId) || [];
685
474
  }
686
- getModelValues(contractId) {
687
- return this.strategy.getModelValues(contractId);
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
+ };
688
513
  }
689
- shouldExitByZScore(contractId) {
690
- return this.strategy.shouldExitByZScore(contractId);
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}` });
691
565
  }
692
- generateSignal(params) {
693
- return null;
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 });
694
579
  }
695
- // Signals come from processBar
696
- };
697
- module.exports = {
698
- HQXUltraScalping,
699
- UltraScalpingStrategy,
700
- // Aliases for backward compatibility
701
- M1: UltraScalpingStrategy,
702
- S1: HQXUltraScalping,
703
- OrderSide,
704
- SignalStrength
705
- };
580
+ }
581
+
582
+ // Singleton instance
583
+ const M1 = new HQXUltraScalpingStrategy();
584
+
585
+ module.exports = { M1, HQXUltraScalpingStrategy, OrderSide, SignalStrength };