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