hedgequantx 2.7.15 → 2.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/m/s1.js CHANGED
@@ -1,804 +1,423 @@
1
1
  /**
2
2
  * =============================================================================
3
- * HQX Ultra-Scalping Strategy
3
+ * HQX ULTRA SCALPING STRATEGY
4
4
  * =============================================================================
5
- * Proven edge: VWAP Mean Reversion + Order Flow Confirmation + Trailing Stops
5
+ * 6 Mathematical Models with 4-Layer Trailing Stop System
6
6
  *
7
- * BACKTEST RESULTS (7 months Dec 2020 - Jun 2021, 541M ticks):
8
- * - Net P&L: +$442,760
9
- * - Profit Factor: 18.28
10
- * - Win Rate: 93.1%
11
- * - Max Drawdown: $190
12
- * - 7,325 trades, ~$60/trade avg
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
13
  *
14
- * MODELS (Weights):
15
- * 1. VWAP Mean Reversion (25%) - Primary signal source
16
- * 2. Order Flow Analysis (30%) - Confirmation required
17
- * 3. DOM Simulation (20%) - Bid/ask pressure
18
- * 4. Microstructure (10%) - Kyle's lambda, noise ratio
19
- * 5. Volume Profile (15%) - POC, Value Area context
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
20
21
  *
21
- * KEY PARAMETERS (from profitable backtest):
22
- * - Stop: 6 ticks = $30 (1.5 points)
23
- * - Target: 8 ticks = $40 (2 points)
24
- * - Trailing: trigger at 4 ticks (50%), trail at 2 ticks
25
- * - VWAP z-score threshold: 0.8σ
26
- * - Cooldown: 30 seconds between trades
27
- * - Max 50 trades/day, stop after 3 consecutive losses
28
- * - Order flow confirmation: REQUIRED
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
29
  */
30
30
 
31
+ 'use strict';
32
+
31
33
  const EventEmitter = require('events');
32
34
  const { v4: uuidv4 } = require('uuid');
33
-
34
- const OS = { B: 0, A: 1 }; // OrderSide: Bid, Ask
35
- const SS = { W: 1, M: 2, S: 3, VS: 4, E: 5 }; // SignalStrength
35
+ const {
36
+ computeZScore,
37
+ computeVPIN,
38
+ computeKyleLambda,
39
+ applyKalmanFilter,
40
+ calculateATR,
41
+ detectVolatilityRegime,
42
+ computeOrderFlowImbalance,
43
+ } = require('./s1-models');
36
44
 
37
45
  // =============================================================================
38
- // VWAP CALCULATOR
46
+ // CONSTANTS
39
47
  // =============================================================================
40
48
 
41
- class VC {
42
- constructor() {
43
- this.d = new Map();
44
- this.psv = new Map();
45
- }
46
-
47
- gk(c) {
48
- return `${c}_${new Date().toISOString().split('T')[0]}`;
49
- }
50
-
51
- init(c) {
52
- const k = this.gk(c);
53
- if (!this.d.has(k)) {
54
- this.d.set(k, {
55
- v: 0, cv: 0, ctpv: 0,
56
- ub1: 0, ub2: 0, ub3: 0,
57
- lb1: 0, lb2: 0, lb3: 0,
58
- sd: 0, tc: 0,
59
- hod: -Infinity, lod: Infinity,
60
- lu: Date.now()
61
- });
62
- this.psv.set(k, 0);
63
- }
64
- }
65
-
66
- pt(c, h, l, cl, vol, ts = Date.now()) {
67
- const k = this.gk(c);
68
- let d = this.d.get(k);
69
- if (!d) { this.init(c); d = this.d.get(k); }
70
-
71
- const tp = (h + l + cl) / 3;
72
- d.cv += vol;
73
- d.ctpv += tp * vol;
74
- if (d.cv > 0) d.v = d.ctpv / d.cv;
75
-
76
- const p = this.psv.get(k) || 0;
77
- this.psv.set(k, p + tp * tp * vol);
78
- this.cs(k);
79
-
80
- if (h > d.hod) d.hod = h;
81
- if (l < d.lod) d.lod = l;
82
- d.tc++;
83
- d.lu = ts;
84
- }
85
-
86
- cs(k) {
87
- const d = this.d.get(k);
88
- if (!d || d.cv === 0) return;
89
-
90
- const p = this.psv.get(k) || 0;
91
- const ms = p / d.cv;
92
- const sm = d.v * d.v;
93
- const va = ms - sm;
94
- d.sd = Math.sqrt(Math.max(0, va));
95
-
96
- d.ub1 = d.v + d.sd;
97
- d.ub2 = d.v + 2 * d.sd;
98
- d.ub3 = d.v + 3 * d.sd;
99
- d.lb1 = d.v - d.sd;
100
- d.lb2 = d.v - 2 * d.sd;
101
- d.lb3 = d.v - 3 * d.sd;
102
- }
49
+ const OrderSide = { BID: 'BID', ASK: 'ASK' };
50
+ const SignalStrength = { WEAK: 'WEAK', MODERATE: 'MODERATE', STRONG: 'STRONG', VERY_STRONG: 'VERY_STRONG' };
103
51
 
104
- an(c, cp, ts = 0.25) {
105
- const k = this.gk(c);
106
- const d = this.d.get(k);
107
- if (!d || d.v === 0) return null;
108
-
109
- const pd = cp - d.v;
110
- const dt = pd / ts;
111
- const dv = d.sd > 0 ? pd / d.sd : 0;
112
-
113
- let pos;
114
- if (Math.abs(dv) < 0.1) pos = 'AT';
115
- else if (dv > 0) pos = 'ABOVE';
116
- else pos = 'BELOW';
117
-
118
- const ad = Math.abs(dv);
119
- let bl;
120
- if (ad < 1) bl = 0;
121
- else if (ad < 2) bl = 1;
122
- else if (ad < 3) bl = 2;
123
- else bl = 3;
124
-
125
- let mrp;
126
- if (ad >= 3) mrp = 0.99;
127
- else if (ad >= 2) mrp = 0.95;
128
- else if (ad >= 1.5) mrp = 0.85;
129
- else if (ad >= 1) mrp = 0.68;
130
- else mrp = 0.50;
131
-
132
- return {
133
- currentPrice: cp,
134
- vwap: d.v,
135
- deviation: dv,
136
- deviationTicks: dt,
137
- position: pos,
138
- bandLevel: bl,
139
- target: d.v,
140
- probability: mrp,
141
- isFavorableForMeanReversion: ad >= 1.5
142
- };
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;
143
68
  }
144
-
145
- get(c) { return this.d.get(this.gk(c)) || null; }
146
- reset(c) { const k = this.gk(c); this.d.delete(k); this.psv.delete(k); }
69
+ return contractId;
147
70
  }
148
71
 
149
72
  // =============================================================================
150
- // S1 - ULTRA SCALPING STRATEGY
73
+ // HQX ULTRA SCALPING STRATEGY CLASS
151
74
  // =============================================================================
152
75
 
153
- class S1 extends EventEmitter {
154
- constructor(cfg = {}) {
76
+ class HQXUltraScalpingStrategy extends EventEmitter {
77
+ constructor() {
155
78
  super();
156
- this.ts = cfg.tickSize || 0.25;
157
- this.tv = cfg.tickValue || 5.0;
158
- this.mrr = 1.2; // Min reward/risk
159
- this.ap = 14; // ATR period
160
- this.ah = []; // ATR history
161
- this.ba = 2.5; // Base ATR
162
- this.rt = []; // Recent trades
163
- this.ws = 0; // Win streak
164
- this.ls = 0; // Loss streak
165
- this.lsd = null; // Last session date
166
- this.bh = new Map(); // Bar history
167
- this.vc = new VC(); // VWAP calculator
168
- this.lst = 0; // Last signal time
169
- this.cd = 30000; // Cooldown 30s
170
- this.st = { s: 0, t: 0, w: 0, l: 0, p: 0 }; // Stats
171
- }
172
-
173
- initialize(c, ts = 0.25, tv = 5.0) {
174
- this.ts = ts;
175
- this.tv = tv;
176
- this.bh.set(c, []);
177
- this.vc.init(c);
178
- }
179
-
180
- processTick(t) {
181
- const { contractId, price, bid, ask, volume, side, timestamp } = t;
182
- const b = {
183
- timestamp: timestamp || Date.now(),
184
- open: price,
185
- high: price,
186
- low: price,
187
- close: price,
188
- volume: volume || 1,
189
- trades: 1,
190
- delta: side === 'buy' ? (volume || 1) : -(volume || 1),
191
- vwap: price
192
- };
193
- return this.processBar(contractId, b);
194
- }
195
-
196
- onTick(t) { return this.processTick(t); }
197
-
198
- processBar(c, b) {
199
- let bs = this.bh.get(c);
200
- if (!bs) { bs = []; this.bh.set(c, bs); }
201
-
202
- bs.push(b);
203
- if (bs.length > 500) bs.shift();
204
-
205
- if (bs.length < 50) return null;
206
-
207
- // Check session date
208
- const sd = new Date(b.timestamp).toISOString().split('T')[0];
209
- if (this.lsd !== sd) this.lsd = sd;
210
-
211
- // Update VWAP
212
- this.vc.pt(c, b.high, b.low, b.close, b.volume, b.timestamp);
213
-
214
- // Get VWAP analysis
215
- const va = this.vc.an(c, b.close, this.ts);
216
- if (!va) return null;
217
-
218
- // Calculate scores
219
- const vs = this.sv(va, b.close);
220
- const of = this.aof(bs);
221
- const os = this.sof(of);
222
- const dd = this.adb(bs);
223
- const ds = this.sdd(dd);
224
- const md = this.am(bs);
225
- const ms = this.sm(md);
226
- const pd = this.avp(bs, c, b.close);
227
- const ps = this.svp(pd, b.close);
228
-
229
- // Determine direction
230
- const { dir, st, ofc } = this.dd(va, of, dd, pd, b.close);
231
-
232
- if (dir === 'none') return null;
233
- if (st !== 'vr') return null; // VWAP reversion only
234
- if (!ofc) return null; // Require order flow confirmation
235
-
236
- // Calculate confidence
237
- const bc = vs * 0.25 + os * 0.30 + ds * 0.20 + ms * 0.10 + ps * 0.15;
238
- const cf = Math.min(1.0, bc + 0.05);
239
-
240
- // Get adaptive params
241
- const pm = this.gap(bs);
242
-
243
- if (cf < pm.ct) return null;
244
-
245
- // Cooldown check
246
- if (Date.now() - this.lst < this.cd) return null;
247
-
248
- // Entry price
249
- const ep = b.close;
250
-
251
- // Stops and targets
252
- const spt = pm.spt;
253
- const tgt = pm.tgt;
254
-
255
- let sl, tp;
256
- if (dir === 'long') {
257
- sl = ep - spt * this.ts;
258
- tp = ep + tgt * this.ts;
259
- // Adjust to VWAP if close
260
- const vt = va.vwap;
261
- if (vt > ep && vt < tp) {
262
- const vr = (vt - ep) / this.ts;
263
- if (vr >= spt * 1.5) tp = vt;
264
- }
265
- } else {
266
- sl = ep + spt * this.ts;
267
- tp = ep - tgt * this.ts;
268
- const vt = va.vwap;
269
- if (vt < ep && vt > tp) {
270
- const vr = (ep - vt) / this.ts;
271
- if (vr >= spt * 1.5) tp = vt;
272
- }
273
- }
274
-
275
- const rt = Math.abs(ep - sl) / this.ts;
276
- const rwt = Math.abs(tp - ep) / this.ts;
277
-
278
- if (rwt / rt < this.mrr) return null;
279
-
280
- // Partial targets
281
- let p1, p2;
282
- if (dir === 'long') {
283
- p1 = ep + pm.p1t * this.ts;
284
- p2 = ep + pm.p2t * this.ts;
285
- } else {
286
- p1 = ep - pm.p1t * this.ts;
287
- p2 = ep - pm.p2t * this.ts;
288
- }
289
-
290
- // Signal strength
291
- let str = SS.M;
292
- if (cf >= 0.75) str = SS.S;
293
- else if (cf >= 0.85) str = SS.VS;
294
- else if (cf < 0.55) str = SS.W;
295
-
296
- // Edge calculation
297
- const wp = 0.5 + (cf - 0.5) * 0.3;
298
- const aw = Math.abs(tp - ep);
299
- const al = Math.abs(ep - sl);
300
- const eg = wp * aw - (1 - wp) * al;
301
-
302
- this.lst = Date.now();
303
- this.st.s++;
304
-
305
- const sig = {
306
- id: uuidv4(),
307
- timestamp: Date.now(),
308
- symbol: c.split('.')[0] || c,
309
- contractId: c,
310
- side: dir === 'long' ? OS.B : OS.A,
311
- direction: dir,
312
- strategy: 'ULTRA_SCALPING_VWAP_REVERSION',
313
- strength: str,
314
- edge: eg,
315
- confidence: cf,
316
- entry: ep,
317
- entryPrice: ep,
318
- stopLoss: sl,
319
- takeProfit: tp,
320
- riskReward: rwt / rt,
321
- stopTicks: spt,
322
- targetTicks: tgt,
323
- trailTriggerTicks: pm.ttt,
324
- trailDistanceTicks: pm.tdt,
325
- partial1Price: p1,
326
- partial2Price: p2,
327
- vwapScore: vs,
328
- orderFlowScore: os,
329
- domScore: ds,
330
- microstructureScore: ms,
331
- volumeProfileScore: ps,
332
- orderFlowConfirmed: ofc,
333
- zScore: va.deviation,
334
- vwap: va.vwap,
335
- regime: pm.rg,
336
- expires: Date.now() + 60000
337
- };
338
-
339
- this.emit('signal', sig);
340
- return sig;
341
- }
342
-
343
- recordTradeResult(p) {
344
- this.rt.push({ netPnl: p, timestamp: Date.now() });
345
- if (this.rt.length > 100) this.rt.shift();
346
- if (p > 0) { this.ws++; this.ls = 0; this.st.w++; }
347
- else { this.ls++; this.ws = 0; this.st.l++; }
348
- this.st.p += p;
349
- this.st.t++;
350
- }
351
-
352
- // Score VWAP
353
- sv(v, cp) {
354
- const z = Math.abs(v.deviation);
355
- if (z < 0.5) return 0.3;
356
- else if (z < 1.0) return 0.6;
357
- else if (z < 2.0) return 0.9;
358
- else if (z < 2.5) return 0.7;
359
- else return 0.4;
360
- }
361
-
362
- // Score Order Flow
363
- sof(o) {
364
- let s = 0.5;
365
- s += o.ir * 0.3;
366
- if (o.dd) s += 0.2;
367
- if (o.ad) s += 0.15;
368
- return Math.min(1.0, s);
369
- }
370
-
371
- // Score DOM
372
- sdd(d) {
373
- let s = 0.5;
374
- s += Math.abs(d.pi) * 0.25;
375
- if (d.sb || d.sa) s += 0.15;
376
- if (d.al !== null) s += 0.1;
377
- return Math.min(1.0, s);
378
- }
379
-
380
- // Score Microstructure
381
- sm(m) {
382
- const ns = 1.0 - m.nr;
383
- const ls = 0.0001 < m.kl && m.kl < 0.001 ? 0.7 : 0.5;
384
- return ns * 0.6 + ls * 0.4;
385
- }
386
-
387
- // Score Volume Profile
388
- svp(p, cp) {
389
- let s = 0.5;
390
- const pd = Math.abs(cp - p.poc) / this.ts;
391
- if (pd < 10) s += 0.2;
392
- if (p.val <= cp && cp <= p.vah) s += 0.15;
393
- for (const h of p.hvn) {
394
- if (Math.abs(cp - h) < 5 * this.ts) { s += 0.15; break; }
395
- }
396
- return Math.min(1.0, s);
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;
397
116
  }
398
117
 
399
- // Analyze Order Flow
400
- aof(bs) {
401
- if (bs.length < 2) return { d: 0, cd: 0, dd: false, ad: false, ir: 0, as: 'neutral' };
402
-
403
- const cb = bs[bs.length - 1];
404
- const pb = bs[bs.length - 2];
405
- const br = cb.high - cb.low;
406
-
407
- let bv, sv;
408
- if (br > 0) {
409
- const cp = (cb.close - cb.low) / br;
410
- bv = cb.volume * cp;
411
- sv = cb.volume * (1 - cp);
412
- } else {
413
- bv = cb.volume * 0.5;
414
- sv = cb.volume * 0.5;
415
- }
416
-
417
- const d = bv - sv;
418
- const cd = cb.delta || d;
419
-
420
- // Detect divergence
421
- let dv = false;
422
- if (bs.length >= 5) {
423
- const rb = bs.slice(-5);
424
- const pc = rb[4].close - rb[0].close;
425
- const ds = rb.reduce((s, b) => s + (b.delta || 0), 0);
426
- if ((pc > 0 && ds < -cb.volume * 0.5) || (pc < 0 && ds > cb.volume * 0.5)) dv = true;
427
- }
428
-
429
- // Detect absorption
430
- const lb = Math.min(20, bs.length);
431
- const rbs = bs.slice(-lb);
432
- const ar = rbs.reduce((s, b) => s + (b.high - b.low), 0) / lb;
433
- const av = rbs.reduce((s, b) => s + b.volume, 0) / lb;
434
- const ab = cb.volume > av * 1.5 && br < ar * 0.6;
435
-
436
- const tv = bv + sv;
437
- const im = tv > 0 ? Math.abs(bv - sv) / tv : 0;
438
-
439
- let ag;
440
- if (bv > sv * 1.3) ag = 'buy';
441
- else if (sv > bv * 1.3) ag = 'sell';
442
- else ag = 'neutral';
443
-
444
- return { d, cd, dd: dv, ad: ab, ir: im, as: ag };
118
+ /**
119
+ * Initialize strategy for a contract
120
+ */
121
+ initialize(contractId, tickSize = 0.25, tickValue = 5.0) {
122
+ this.tickSize = tickSize;
123
+ this.tickValue = tickValue;
124
+ this.barHistory.set(contractId, []);
125
+ this.priceBuffer.set(contractId, []);
126
+ this.volumeBuffer.set(contractId, []);
127
+ this.tradesBuffer.set(contractId, []);
128
+ this.atrHistory.set(contractId, []);
129
+ this.tickBuffer.set(contractId, []);
130
+ this.lastBarTime.set(contractId, 0);
131
+ this.kalmanStates.set(contractId, { estimate: 0, errorCovariance: 1.0 });
445
132
  }
446
133
 
447
- // Analyze DOM from bars
448
- adb(bs) {
449
- const lb = Math.min(10, bs.length);
450
- if (lb < 2) return { bp: 0.5, ap: 0.5, pi: 0, sb: false, sa: false, al: null };
451
-
452
- const rbs = bs.slice(-lb);
453
- let bss = [], ass = [];
454
-
455
- for (const b of rbs) {
456
- const br = b.high - b.low;
457
- if (br > 0) {
458
- const cp = (b.close - b.low) / br;
459
- bss.push(cp * b.volume);
460
- ass.push((1 - cp) * b.volume);
461
- }
134
+ /**
135
+ * Process a tick - aggregates into bars then runs strategy
136
+ */
137
+ processTick(tick) {
138
+ const contractId = tick.contractId;
139
+
140
+ if (!this.barHistory.has(contractId)) {
141
+ this.initialize(contractId);
462
142
  }
463
143
 
464
- const t = bss.reduce((a, b) => a + b, 0) + ass.reduce((a, b) => a + b, 0);
465
- const bp = t > 0 ? bss.reduce((a, b) => a + b, 0) / t : 0.5;
466
- const ap = t > 0 ? ass.reduce((a, b) => a + b, 0) / t : 0.5;
467
- const pi = bp - ap;
468
-
469
- const lf = rbs.slice(-5);
470
- let hc = 0, lc = 0;
471
- for (const b of lf) {
472
- const br = b.high - b.low;
473
- if (br > 0) {
474
- const cp = (b.close - b.low) / br;
475
- if (cp > 0.7) hc++;
476
- if (cp < 0.3) lc++;
477
- }
478
- }
144
+ // Add tick to buffer
145
+ let ticks = this.tickBuffer.get(contractId);
146
+ ticks.push(tick);
479
147
 
480
- let al = null;
481
- const ar = rbs.reduce((s, b) => s + (b.high - b.low), 0) / lb;
482
- const av = rbs.reduce((s, b) => s + b.volume, 0) / lb;
483
- for (const b of rbs) {
484
- const br = b.high - b.low;
485
- if (b.volume > av * 1.5 && br < ar * 0.5) {
486
- al = (b.high + b.low) / 2;
487
- break;
148
+ // Check if we should form a new bar
149
+ const now = Date.now();
150
+ const lastBar = this.lastBarTime.get(contractId);
151
+
152
+ if (now - lastBar >= this.barIntervalMs && ticks.length > 0) {
153
+ const bar = this._aggregateTicksToBar(ticks, now);
154
+ this.tickBuffer.set(contractId, []);
155
+ this.lastBarTime.set(contractId, now);
156
+
157
+ if (bar) {
158
+ const signal = this.processBar(contractId, bar);
159
+ if (signal) {
160
+ this.emit('signal', signal);
161
+ return signal;
162
+ }
488
163
  }
489
164
  }
490
-
491
- return { bp, ap, pi, sb: hc >= 3, sa: lc >= 3, al };
165
+ return null;
492
166
  }
493
167
 
494
- // Analyze Microstructure
495
- am(bs) {
496
- const lb = Math.min(50, bs.length);
497
- if (lb < 10) return { kl: 0, rs: 0, es: 0, rv: 0, nr: 0.5 };
498
-
499
- const rbs = bs.slice(-lb);
500
- const pc = [], vs = [];
501
-
502
- for (let i = 1; i < rbs.length; i++) {
503
- pc.push(rbs[i].close - rbs[i - 1].close);
504
- vs.push(rbs[i].volume);
505
- }
506
-
507
- // Kyle's Lambda
508
- let kl = 0;
509
- if (pc.length > 5) {
510
- const mp = pc.reduce((a, b) => a + b, 0) / pc.length;
511
- const mv = vs.reduce((a, b) => a + b, 0) / vs.length;
512
- let cv = 0, vv = 0;
513
- for (let i = 0; i < pc.length; i++) {
514
- cv += (pc[i] - mp) * (vs[i] - mv);
515
- vv += Math.pow(vs[i] - mv, 2);
516
- }
517
- cv /= pc.length;
518
- vv /= pc.length;
519
- kl = vv > 0 ? Math.abs(cv / vv) : 0;
520
- }
521
-
522
- // Roll's Spread
523
- let rs = 0;
524
- if (pc.length > 2) {
525
- const c1 = pc.slice(1), c0 = pc.slice(0, -1);
526
- const m1 = c1.reduce((a, b) => a + b, 0) / c1.length;
527
- const m0 = c0.reduce((a, b) => a + b, 0) / c0.length;
528
- let ac = 0;
529
- for (let i = 0; i < c1.length; i++) ac += (c1[i] - m1) * (c0[i] - m0);
530
- ac /= c1.length;
531
- rs = Math.sqrt(Math.max(0, -ac)) * 2;
532
- }
533
-
534
- const es = rbs.reduce((s, b) => s + (b.high - b.low), 0) / lb;
535
-
536
- // Realized volatility
537
- const rt = [];
538
- for (let i = 1; i < rbs.length; i++) {
539
- if (rbs[i - 1].close > 0) rt.push(Math.log(rbs[i].close / rbs[i - 1].close));
540
- }
541
- let rv = 0;
542
- if (rt.length > 0) {
543
- const mr = rt.reduce((a, b) => a + b, 0) / rt.length;
544
- const va = rt.reduce((s, r) => s + Math.pow(r - mr, 2), 0) / rt.length;
545
- rv = Math.sqrt(va) * Math.sqrt(252 * 390);
168
+ /**
169
+ * Aggregate ticks into a bar
170
+ */
171
+ _aggregateTicksToBar(ticks, timestamp) {
172
+ if (ticks.length === 0) return null;
173
+
174
+ const prices = ticks.map(t => t.price).filter(p => p != null);
175
+ if (prices.length === 0) return null;
176
+
177
+ let buyVol = 0, sellVol = 0;
178
+ for (let i = 1; i < ticks.length; i++) {
179
+ const vol = ticks[i].volume || 1;
180
+ if (ticks[i].price > ticks[i-1].price) buyVol += vol;
181
+ else if (ticks[i].price < ticks[i-1].price) sellVol += vol;
182
+ else { buyVol += vol / 2; sellVol += vol / 2; }
546
183
  }
547
184
 
548
- // Noise ratio
549
- const sv = pc.length >= 10 ? Math.sqrt(pc.slice(-10).reduce((s, p) => s + p * p, 0) / 10) : 0;
550
- const lv = pc.length > 0 ? Math.sqrt(pc.reduce((s, p) => s + p * p, 0) / pc.length) : 1;
551
- const nr = lv > 0 ? Math.min(1, sv / lv) : 0.5;
552
-
553
- return { kl, rs, es, rv, nr };
185
+ return {
186
+ timestamp,
187
+ open: prices[0],
188
+ high: Math.max(...prices),
189
+ low: Math.min(...prices),
190
+ close: prices[prices.length - 1],
191
+ volume: ticks.reduce((sum, t) => sum + (t.volume || 1), 0),
192
+ delta: buyVol - sellVol,
193
+ tickCount: ticks.length
194
+ };
554
195
  }
555
196
 
556
- // Analyze Volume Profile
557
- avp(bs, c, cp) {
558
- const lb = Math.min(100, bs.length);
559
- if (lb < 10) return { poc: cp, vah: cp, val: cp, hvn: [], lvn: [] };
560
-
561
- const rbs = bs.slice(-lb);
562
- const vap = new Map();
563
-
564
- for (const b of rbs) {
565
- const br = b.high - b.low;
566
- const nl = Math.max(1, Math.round(br / this.ts));
567
- const vpl = b.volume / nl;
568
- let p = b.low;
569
- while (p <= b.high) {
570
- const r = Math.round(p / this.ts) * this.ts;
571
- vap.set(r, (vap.get(r) || 0) + vpl);
572
- p += this.ts;
573
- }
574
- }
575
-
576
- if (vap.size === 0) return { poc: cp, vah: cp, val: cp, hvn: [], lvn: [] };
577
-
578
- // POC
579
- let poc = cp, mv = 0;
580
- for (const [p, v] of vap) {
581
- if (v > mv) { mv = v; poc = p; }
197
+ /**
198
+ * Process a new bar and potentially generate signal
199
+ */
200
+ processBar(contractId, bar) {
201
+ let bars = this.barHistory.get(contractId);
202
+ if (!bars) {
203
+ this.initialize(contractId);
204
+ bars = this.barHistory.get(contractId);
582
205
  }
583
206
 
584
- // Value Area
585
- const tv = Array.from(vap.values()).reduce((a, b) => a + b, 0);
586
- const tgv = tv * 0.7;
587
- const sp = Array.from(vap.keys()).sort((a, b) => a - b);
588
- const pi = sp.indexOf(poc);
589
- let vv = vap.get(poc) || 0, li = pi, hi = pi;
590
-
591
- while (vv < tgv && (li > 0 || hi < sp.length - 1)) {
592
- const lv = li > 0 ? (vap.get(sp[li - 1]) || 0) : 0;
593
- const hv = hi < sp.length - 1 ? (vap.get(sp[hi + 1]) || 0) : 0;
594
- if (lv >= hv && li > 0) { li--; vv += vap.get(sp[li]) || 0; }
595
- else if (hi < sp.length - 1) { hi++; vv += vap.get(sp[hi]) || 0; }
596
- else break;
207
+ bars.push(bar);
208
+ if (bars.length > 500) bars.shift();
209
+
210
+ // Update price buffer
211
+ const prices = this.priceBuffer.get(contractId);
212
+ prices.push(bar.close);
213
+ if (prices.length > 200) prices.shift();
214
+
215
+ // Update volume buffer
216
+ const volumes = this.volumeBuffer.get(contractId);
217
+ const barRange = bar.high - bar.low;
218
+ let buyVol = bar.volume * 0.5;
219
+ let sellVol = bar.volume * 0.5;
220
+ if (barRange > 0) {
221
+ const closePosition = (bar.close - bar.low) / barRange;
222
+ buyVol = bar.volume * closePosition;
223
+ sellVol = bar.volume * (1 - closePosition);
597
224
  }
598
-
599
- const val = sp[li], vah = sp[hi];
600
- const av = tv / vap.size;
601
- const hvn = [], lvn = [];
602
-
603
- for (const [p, v] of vap) {
604
- if (v > av * 1.5) hvn.push(p);
605
- if (v < av * 0.5) lvn.push(p);
606
- }
607
-
608
- return { poc, vah, val, hvn, lvn };
225
+ volumes.push({ buy: buyVol, sell: sellVol });
226
+ if (volumes.length > 100) volumes.shift();
227
+
228
+ // Need minimum data
229
+ if (bars.length < 50) return null;
230
+
231
+ // === 6 MODELS ===
232
+ const zscore = computeZScore(prices);
233
+ const vpin = computeVPIN(volumes, this.vpinWindow);
234
+ const kyleLambda = computeKyleLambda(bars);
235
+ const kalmanEstimate = this._applyKalmanFilter(contractId, bar.close);
236
+ const { regime, params } = this._detectVolatilityRegime(contractId, bars);
237
+ const ofi = computeOrderFlowImbalance(bars, this.ofiLookback);
238
+
239
+ // === SIGNAL GENERATION ===
240
+ return this._generateSignal(contractId, bar.close, zscore, vpin, kyleLambda, kalmanEstimate, regime, params, ofi, bars);
609
241
  }
610
242
 
611
- // Determine Direction
612
- dd(v, o, d, p, cp) {
613
- let ls = 0, ss = 0, st = 'mixed', ofc = false;
614
-
615
- // VWAP Mean Reversion (0.8 std dev)
616
- if (v.deviation < -0.8) {
617
- ls += 2;
618
- st = 'vr';
619
- if (o.as === 'buy' || o.dd) ofc = true;
620
- } else if (v.deviation > 0.8) {
621
- ss += 2;
622
- st = 'vr';
623
- if (o.as === 'sell' || o.dd) ofc = true;
624
- }
625
-
626
- // Order flow strength
627
- if (o.as === 'buy' && o.ir > 0.35) ls += 1;
628
- else if (o.as === 'sell' && o.ir > 0.35) ss += 1;
629
-
630
- // Delta Divergence
631
- if (o.dd) {
632
- if (o.cd < 0) { ls += 2; st = 'dv'; ofc = true; }
633
- else { ss += 2; st = 'dv'; ofc = true; }
634
- }
635
-
636
- // DOM Pressure
637
- if (d.pi > 0.35) ls += 1;
638
- else if (d.pi < -0.35) ss += 1;
639
-
640
- // Stacked Orders
641
- if (d.sb) ls += 1;
642
- if (d.sa) ss += 1;
643
-
644
- // Volume Profile (POC attraction)
645
- if (cp < p.poc - 5 * this.ts) ls += 1;
646
- else if (cp > p.poc + 5 * this.ts) ss += 1;
647
-
648
- // Decision
649
- if (ls >= 2 && ls > ss) return { dir: 'long', st, ofc };
650
- else if (ss >= 2 && ss > ls) return { dir: 'short', st, ofc };
651
- else return { dir: 'none', st: 'none', ofc: false };
243
+ // ===========================================================================
244
+ // MODEL 4: KALMAN FILTER (uses shared state)
245
+ // ===========================================================================
246
+ _applyKalmanFilter(contractId, measurement) {
247
+ let state = this.kalmanStates.get(contractId);
248
+ const result = applyKalmanFilter(state, measurement, this.kalmanProcessNoise, this.kalmanMeasurementNoise);
249
+ this.kalmanStates.set(contractId, result.state);
250
+ return result.estimate;
652
251
  }
653
252
 
654
- // Calculate ATR
655
- catr(bs) {
656
- if (bs.length < this.ap + 1) return this.ba;
657
-
658
- const tv = [];
659
- for (let i = bs.length - this.ap; i < bs.length; i++) {
660
- const b = bs[i];
661
- const pc = bs[i - 1].close;
662
- const tr = Math.max(b.high - b.low, Math.abs(b.high - pc), Math.abs(b.low - pc));
663
- tv.push(tr);
664
- }
665
-
666
- const a = tv.reduce((a, b) => a + b, 0) / tv.length;
667
- this.ah.push(a);
668
- if (this.ah.length > 500) this.ah.shift();
669
-
670
- return a;
253
+ // ===========================================================================
254
+ // MODEL 5: VOLATILITY REGIME (uses shared state)
255
+ // ===========================================================================
256
+ _detectVolatilityRegime(contractId, bars) {
257
+ const atr = calculateATR(bars);
258
+ let atrHist = this.atrHistory.get(contractId);
259
+ if (!atrHist) { atrHist = []; this.atrHistory.set(contractId, atrHist); }
260
+ atrHist.push(atr);
261
+ if (atrHist.length > 500) atrHist.shift();
262
+ return detectVolatilityRegime(atrHist, atr);
671
263
  }
672
264
 
673
- // Get Adaptive Params
674
- gap(bs) {
675
- const a = this.catr(bs);
676
- const at = a / this.ts;
677
-
678
- let ap = 0.5;
679
- if (this.ah.length >= 20) ap = this.ah.filter(x => x <= a).length / this.ah.length;
680
-
681
- let rg, vm;
682
- if (ap < 0.25) { rg = 'low'; vm = 0.9; }
683
- else if (ap < 0.70) { rg = 'normal'; vm = 1.0; }
684
- else if (ap < 0.90) { rg = 'high'; vm = 1.15; }
685
- else { rg = 'extreme'; vm = 1.25; }
265
+ // ===========================================================================
266
+ // SIGNAL GENERATION
267
+ // ===========================================================================
268
+ _generateSignal(contractId, currentPrice, zscore, vpin, kyleLambda, kalmanEstimate, regime, volParams, ofi, bars) {
269
+ const absZscore = Math.abs(zscore);
270
+ if (absZscore < volParams.zscoreThreshold) return null;
271
+ if (vpin > this.vpinToxicThreshold) return null;
272
+
273
+ let direction;
274
+ if (zscore < -volParams.zscoreThreshold) direction = 'long';
275
+ else if (zscore > volParams.zscoreThreshold) direction = 'short';
276
+ else return null;
277
+
278
+ const ofiConfirms = (direction === 'long' && ofi > 0.1) || (direction === 'short' && ofi < -0.1);
279
+ const kalmanDiff = currentPrice - kalmanEstimate;
280
+ const kalmanConfirms = (direction === 'long' && kalmanDiff < 0) || (direction === 'short' && kalmanDiff > 0);
281
+
282
+ const scores = {
283
+ zscore: Math.min(1.0, absZscore / 4.0),
284
+ vpin: 1.0 - vpin,
285
+ kyleLambda: kyleLambda > 0.001 ? 0.5 : 0.8,
286
+ kalman: kalmanConfirms ? 0.8 : 0.4,
287
+ volatility: regime === 'normal' ? 0.8 : regime === 'low' ? 0.7 : 0.6,
288
+ ofi: ofiConfirms ? 0.9 : 0.5,
289
+ composite: 0
290
+ };
686
291
 
687
- const pm = this.gpm();
292
+ scores.composite = scores.zscore * 0.30 + scores.vpin * 0.15 + scores.kyleLambda * 0.10 +
293
+ scores.kalman * 0.15 + scores.volatility * 0.10 + scores.ofi * 0.20;
688
294
 
689
- // Stop: 6 ticks base, scale 5-8
690
- const bs6 = 6;
691
- let spt = Math.round(bs6 * vm);
692
- spt = Math.max(5, Math.min(8, spt));
295
+ const confidence = Math.min(1.0, scores.composite + volParams.confidenceBonus);
296
+ if (confidence < 0.55) return null;
693
297
 
694
- // Target: 8 ticks base, scale 6-10
695
- const bt8 = 8;
696
- let tgt = Math.round(bt8 * vm);
697
- tgt = Math.max(6, Math.min(10, tgt));
298
+ const stopTicks = Math.round(this.baseStopTicks * volParams.stopMultiplier);
299
+ const targetTicks = Math.round(this.baseTargetTicks * volParams.targetMultiplier);
300
+ const actualStopTicks = Math.max(6, Math.min(12, stopTicks));
301
+ const actualTargetTicks = Math.max(actualStopTicks * 1.5, Math.min(24, targetTicks));
698
302
 
699
- if (tgt / spt < 1.2) tgt = Math.round(spt * 1.33);
303
+ let stopLoss, takeProfit, beBreakeven, profitLockLevel;
304
+ if (direction === 'long') {
305
+ stopLoss = currentPrice - actualStopTicks * this.tickSize;
306
+ takeProfit = currentPrice + actualTargetTicks * this.tickSize;
307
+ beBreakeven = currentPrice + this.breakevenTicks * this.tickSize;
308
+ profitLockLevel = currentPrice + (actualTargetTicks * this.profitLockPct) * this.tickSize;
309
+ } else {
310
+ stopLoss = currentPrice + actualStopTicks * this.tickSize;
311
+ takeProfit = currentPrice - actualTargetTicks * this.tickSize;
312
+ beBreakeven = currentPrice - this.breakevenTicks * this.tickSize;
313
+ profitLockLevel = currentPrice - (actualTargetTicks * this.profitLockPct) * this.tickSize;
314
+ }
700
315
 
701
- // Trailing
702
- const ttt = Math.max(3, Math.min(5, Math.round(tgt * 0.5)));
703
- const tdt = Math.max(2, Math.min(3, Math.round(spt * 0.35)));
316
+ const riskReward = actualTargetTicks / actualStopTicks;
317
+ const trailTriggerTicks = Math.round(actualTargetTicks * 0.5);
318
+ const trailDistanceTicks = Math.round(actualStopTicks * 0.4);
704
319
 
705
- // Confidence threshold
706
- const bc = 0.45;
707
- const ra = { low: 0.05, normal: 0.0, high: 0.03, extreme: 0.10 };
708
- let ct = bc + ra[rg];
709
- ct = Math.min(0.65, ct * pm);
320
+ let strength = SignalStrength.MODERATE;
321
+ if (confidence >= 0.85) strength = SignalStrength.VERY_STRONG;
322
+ else if (confidence >= 0.75) strength = SignalStrength.STRONG;
323
+ else if (confidence < 0.60) strength = SignalStrength.WEAK;
710
324
 
711
- // Partials
712
- const p1t = Math.round(tgt * 0.5);
713
- const p2t = Math.round(tgt * 0.75);
325
+ const winProb = 0.5 + (confidence - 0.5) * 0.4;
326
+ const edge = winProb * Math.abs(takeProfit - currentPrice) - (1 - winProb) * Math.abs(currentPrice - stopLoss);
714
327
 
715
- return { spt, tgt, ct, ttt, tdt, p1t, p2t, rg, at, vm };
328
+ return {
329
+ id: uuidv4(),
330
+ timestamp: Date.now(),
331
+ symbol: extractBaseSymbol(contractId),
332
+ contractId,
333
+ side: direction === 'long' ? OrderSide.BID : OrderSide.ASK,
334
+ direction,
335
+ strategy: 'HQX_ULTRA_SCALPING',
336
+ strength,
337
+ edge,
338
+ confidence,
339
+ entry: currentPrice,
340
+ entryPrice: currentPrice,
341
+ stopLoss,
342
+ takeProfit,
343
+ riskReward,
344
+ stopTicks: actualStopTicks,
345
+ targetTicks: actualTargetTicks,
346
+ trailTriggerTicks,
347
+ trailDistanceTicks,
348
+ beBreakeven,
349
+ profitLockLevel,
350
+ zScore: zscore,
351
+ zScoreExit: this.zscoreExitThreshold,
352
+ vpinValue: vpin,
353
+ kyleLambda,
354
+ kalmanEstimate,
355
+ volatilityRegime: regime,
356
+ ofiValue: ofi,
357
+ models: scores
358
+ };
716
359
  }
717
360
 
718
- // Performance Multiplier
719
- gpm() {
720
- if (this.rt.length === 0) return 1.0;
721
-
722
- const r = this.rt.slice(-20);
723
- const w = r.filter(t => t.netPnl > 0).length;
724
- const wr = w / r.length;
725
-
726
- let m = 1.0;
727
-
728
- if (this.ls >= 2) {
729
- m += 0.05 * this.ls;
730
- m = Math.min(m, 1.3);
731
- }
732
-
733
- if (r.length >= 5 && wr < 0.40) m *= 1.1;
734
- if (this.ws >= 4 && wr > 0.50) m *= 0.95;
735
-
736
- return Math.max(0.9, Math.min(1.3, m));
361
+ /**
362
+ * Check if should exit by Z-Score
363
+ */
364
+ shouldExitByZScore(contractId) {
365
+ const prices = this.priceBuffer.get(contractId);
366
+ if (!prices || prices.length < 50) return false;
367
+ const zscore = computeZScore(prices);
368
+ return Math.abs(zscore) < this.zscoreExitThreshold;
737
369
  }
738
370
 
739
- getBarHistory(c) { return this.bh.get(c) || []; }
740
- reset(c) { this.bh.set(c, []); this.lsd = null; this.vc.reset(c); }
741
- getStats() { return this.st; }
742
-
743
- getAnalysisState(c, cp) {
744
- const bs = this.bh.get(c) || [];
745
- if (bs.length < 10) return { ready: false, message: 'Collecting...' };
746
-
747
- const va = this.vc.an(c, cp, this.ts);
748
- const of = this.aof(bs);
749
- const pm = this.gap(bs);
371
+ /**
372
+ * Get current model values
373
+ */
374
+ getModelValues(contractId) {
375
+ const prices = this.priceBuffer.get(contractId);
376
+ const volumes = this.volumeBuffer.get(contractId);
377
+ const bars = this.barHistory.get(contractId);
378
+ if (!prices || !volumes || !bars || bars.length < 50) return null;
750
379
 
751
380
  return {
752
- ready: true,
753
- vwap: va?.vwap || 0,
754
- zScore: va?.deviation || 0,
755
- orderFlow: of.as,
756
- ratio: of.ir,
757
- regime: pm.rg,
758
- stopTicks: pm.spt,
759
- targetTicks: pm.tgt,
760
- barsProcessed: bs.length
381
+ zscore: computeZScore(prices).toFixed(2),
382
+ vpin: (computeVPIN(volumes, this.vpinWindow) * 100).toFixed(1) + '%',
383
+ ofi: (computeOrderFlowImbalance(bars, this.ofiLookback) * 100).toFixed(1) + '%',
384
+ bars: bars.length
761
385
  };
762
386
  }
763
- }
764
387
 
765
- // =============================================================================
766
- // M1 - STRATEGY WRAPPER
767
- // =============================================================================
388
+ /**
389
+ * Record trade result
390
+ */
391
+ recordTradeResult(pnl) {
392
+ this.recentTrades.push({ pnl, timestamp: Date.now() });
393
+ if (this.recentTrades.length > 100) this.recentTrades.shift();
394
+ if (pnl > 0) { this.winStreak++; this.lossStreak = 0; }
395
+ else { this.lossStreak++; this.winStreak = 0; }
396
+ }
768
397
 
769
- class M1 extends EventEmitter {
770
- constructor(cfg = {}) {
771
- super();
772
- this.cfg = cfg;
773
- this.s = new S1(cfg);
774
- this.s.on('signal', (sig) => {
775
- this.emit('signal', {
776
- side: sig.direction === 'long' ? 'buy' : 'sell',
777
- action: 'open',
778
- reason: `Z=${sig.zScore.toFixed(2)}, cf=${(sig.confidence * 100).toFixed(0)}%`,
779
- ...sig
780
- });
781
- });
398
+ /**
399
+ * Get bar history
400
+ */
401
+ getBarHistory(contractId) {
402
+ return this.barHistory.get(contractId) || [];
782
403
  }
783
404
 
784
- processTick(t) { return this.s.processTick(t); }
785
- onTick(t) { return this.processTick(t); }
786
- processBar(c, b) { return this.s.processBar(c, b); }
787
- onTrade(t) {
788
- this.s.processTick({
789
- contractId: t.contractId || t.symbol,
790
- price: t.price,
791
- volume: t.size || t.volume || 1,
792
- side: t.side,
793
- timestamp: t.timestamp || Date.now()
794
- });
405
+ /**
406
+ * Reset strategy
407
+ */
408
+ reset(contractId) {
409
+ this.barHistory.set(contractId, []);
410
+ this.priceBuffer.set(contractId, []);
411
+ this.volumeBuffer.set(contractId, []);
412
+ this.tradesBuffer.set(contractId, []);
413
+ this.atrHistory.set(contractId, []);
414
+ this.tickBuffer.set(contractId, []);
415
+ this.lastBarTime.set(contractId, 0);
416
+ this.kalmanStates.set(contractId, { estimate: 0, errorCovariance: 1.0 });
795
417
  }
796
- initialize(c, ts, tv) { this.s.initialize(c, ts, tv); }
797
- getAnalysisState(c, p) { return this.s.getAnalysisState(c, p); }
798
- recordTradeResult(p) { this.s.recordTradeResult(p); }
799
- reset(c) { this.s.reset(c); this.emit('log', { type: 'info', message: 'Reset' }); }
800
- getStats() { return this.s.getStats(); }
801
- generateSignal(p) { return null; }
802
418
  }
803
419
 
804
- module.exports = { S1, M1, VC, OS, SS };
420
+ // Singleton instance
421
+ const M1 = new HQXUltraScalpingStrategy();
422
+
423
+ module.exports = { M1, HQXUltraScalpingStrategy, OrderSide, SignalStrength };