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.
- package/dist/lib/m/hqx-2b.js +1099 -833
- package/dist/lib/m/s1-models.js +173 -0
- package/dist/lib/m/ultra-scalping.js +558 -678
- package/package.json +1 -1
- package/src/lib/m/s1.js +167 -5
|
@@ -1,705 +1,585 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
172
|
+
return null;
|
|
258
173
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
this.
|
|
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.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
669
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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.
|
|
473
|
+
return this.barHistory.get(contractId) || [];
|
|
685
474
|
}
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
693
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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 };
|