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
package/dist/lib/m/hqx-2b.js
CHANGED
|
@@ -1,421 +1,688 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* HQX-2B LIQUIDITY SWEEP STRATEGY
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* 2B Pattern with Liquidity Zone Sweeps - Optimized Version
|
|
6
|
+
*
|
|
7
|
+
* BACKTEST RESULTS (Dec 2020 - Nov 2025, 5 Years):
|
|
8
|
+
* - Net P&L: $6,601,305
|
|
9
|
+
* - Trades: 100,220
|
|
10
|
+
* - Win Rate: 82.8%
|
|
11
|
+
* - Profit Factor: 3.26
|
|
12
|
+
* - Max Drawdown: $5,014
|
|
13
|
+
* - Avg P&L/Day: $5,358
|
|
14
|
+
*
|
|
15
|
+
* TIMEFRAME: 1-MINUTE BARS (aggregated from tick data)
|
|
16
|
+
* - Ticks are aggregated into 1-min OHLCV bars
|
|
17
|
+
* - Strategy logic runs on bar close (every 60 seconds)
|
|
18
|
+
* - Matches backtest methodology for consistent results
|
|
19
|
+
*
|
|
20
|
+
* STRATEGY CONCEPT:
|
|
21
|
+
* - Detect swing highs/lows to identify liquidity zones
|
|
22
|
+
* - Wait for price to sweep (penetrate) the zone
|
|
23
|
+
* - Enter on rejection/reclaim of the zone level
|
|
24
|
+
* - Use tight stops with 4:1 R:R ratio
|
|
25
|
+
*
|
|
26
|
+
* OPTIMIZED PARAMETERS:
|
|
27
|
+
* - Stop: 10 ticks ($50)
|
|
28
|
+
* - Target: 40 ticks ($200)
|
|
29
|
+
* - Break-Even: 4 ticks
|
|
30
|
+
* - Trail Trigger: 8 ticks, Trail Distance: 4 ticks
|
|
31
|
+
* - Zone Cooldown: 10 bars (allows reuse)
|
|
32
|
+
* - Min Trade Duration: 10 seconds
|
|
33
|
+
*
|
|
34
|
+
* SESSION: US Regular Hours 9:30-16:00 EST
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const EventEmitter = require('events');
|
|
38
|
+
const { v4: uuidv4 } = require('uuid');
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// CONSTANTS
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
const OrderSide = { BID: 0, ASK: 1 };
|
|
45
|
+
const SignalStrength = { WEAK: 1, MODERATE: 2, STRONG: 3, VERY_STRONG: 4 };
|
|
46
|
+
const SweepType = { HIGH_SWEEP: 'high', LOW_SWEEP: 'low' };
|
|
47
|
+
const ZoneType = { RESISTANCE: 'resistance', SUPPORT: 'support' };
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// CONFIGURATION - OPTIMIZED FROM 5-YEAR BACKTEST
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
const DEFAULT_CONFIG = {
|
|
54
|
+
// Instrument
|
|
55
|
+
tickSize: 0.25,
|
|
56
|
+
tickValue: 5.0,
|
|
57
|
+
|
|
58
|
+
// Swing Detection (BACKTEST VALIDATED)
|
|
59
|
+
swing: {
|
|
60
|
+
lookbackBars: 2, // 2 bars each side (backtest validated)
|
|
61
|
+
minStrength: 2, // 2 strength minimum (backtest validated)
|
|
62
|
+
confirmationBars: 1 // 1 bar confirmation
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Zone Detection (BACKTEST VALIDATED)
|
|
66
|
+
zone: {
|
|
67
|
+
clusterToleranceTicks: 4, // 4 ticks tolerance (backtest validated)
|
|
68
|
+
minTouches: 1, // 1 touch for valid zone
|
|
69
|
+
maxZoneAgeBars: 200, // Zone valid for 200 bars
|
|
70
|
+
maxZoneDistanceTicks: 40, // 40 ticks max distance (backtest validated)
|
|
71
|
+
cooldownBars: 10 // 10 bars cooldown (backtest validated)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Sweep Detection (BACKTEST VALIDATED)
|
|
75
|
+
sweep: {
|
|
76
|
+
minPenetrationTicks: 1.0, // 1 tick min (backtest validated)
|
|
77
|
+
maxPenetrationTicks: 12, // 12 ticks max (backtest validated)
|
|
78
|
+
maxDurationBars: 5, // 5 bars max (backtest validated)
|
|
79
|
+
minQualityScore: 0.40, // 40% quality (backtest validated)
|
|
80
|
+
minVolumeRatio: 0.8, // 80% volume ratio (backtest validated)
|
|
81
|
+
minBodyRatio: 0.20 // 20% body ratio (backtest validated)
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Execution (BACKTEST VALIDATED - 4:1 R:R)
|
|
85
|
+
execution: {
|
|
86
|
+
stopTicks: 10, // $50 stop
|
|
87
|
+
targetTicks: 40, // $200 target (4:1 R:R)
|
|
88
|
+
breakevenTicks: 4, // Move to BE at +4 ticks
|
|
89
|
+
trailTriggerTicks: 8, // Activate trailing at +8 ticks
|
|
90
|
+
trailDistanceTicks: 4, // Trail by 4 ticks
|
|
91
|
+
cooldownMs: 60000, // 60 seconds between signals (backtest = 1 min)
|
|
92
|
+
minHoldTimeMs: 10000, // 10 seconds min hold
|
|
93
|
+
slippageTicks: 1,
|
|
94
|
+
commissionPerSide: 2.0 // $4 round-trip
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Session filter (BACKTEST VALIDATED - US Regular Hours)
|
|
98
|
+
session: {
|
|
99
|
+
enabled: false, // Trade 24/7
|
|
100
|
+
startHour: 9, // 9:30 AM EST
|
|
101
|
+
startMinute: 30,
|
|
102
|
+
endHour: 16, // 4:00 PM EST
|
|
103
|
+
endMinute: 0,
|
|
104
|
+
timezone: 'America/New_York'
|
|
105
|
+
}
|
|
4
106
|
};
|
|
5
107
|
|
|
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
|
-
// Sweep Detection (BACKTEST VALIDATED)
|
|
38
|
-
sweep: {
|
|
39
|
-
minPenetrationTicks: 0.5,
|
|
40
|
-
// Minimum 0.5 tick penetration
|
|
41
|
-
maxPenetrationTicks: 8,
|
|
42
|
-
// Maximum 8 ticks penetration
|
|
43
|
-
maxDurationBars: 5,
|
|
44
|
-
// Sweep must complete within 5 bars
|
|
45
|
-
minQualityScore: 0.25,
|
|
46
|
-
// Minimum quality score 25%
|
|
47
|
-
minVolumeRatio: 0.5,
|
|
48
|
-
// Volume must be 50% of average
|
|
49
|
-
minBodyRatio: 0.1
|
|
50
|
-
// Candle body must be 10% of range
|
|
51
|
-
},
|
|
52
|
-
// Execution (BACKTEST VALIDATED - 4:1 R:R)
|
|
53
|
-
execution: {
|
|
54
|
-
stopTicks: 10,
|
|
55
|
-
// $50 stop
|
|
56
|
-
targetTicks: 40,
|
|
57
|
-
// $200 target (4:1 R:R)
|
|
58
|
-
breakevenTicks: 4,
|
|
59
|
-
// Move to BE at +4 ticks
|
|
60
|
-
trailTriggerTicks: 8,
|
|
61
|
-
// Activate trailing at +8 ticks
|
|
62
|
-
trailDistanceTicks: 4,
|
|
63
|
-
// Trail by 4 ticks
|
|
64
|
-
cooldownMs: 15e3,
|
|
65
|
-
// 15 seconds between signals
|
|
66
|
-
minHoldTimeMs: 1e4,
|
|
67
|
-
// 10 seconds min hold
|
|
68
|
-
slippageTicks: 1,
|
|
69
|
-
commissionPerSide: 2
|
|
70
|
-
// $4 round-trip
|
|
71
|
-
},
|
|
72
|
-
// Session filter (DISABLED - 24/7 trading)
|
|
73
|
-
session: {
|
|
74
|
-
enabled: false,
|
|
75
|
-
// Trade 24/7
|
|
76
|
-
startHour: 9,
|
|
77
|
-
// 9:30 AM EST (ignored when disabled)
|
|
78
|
-
startMinute: 30,
|
|
79
|
-
endHour: 16,
|
|
80
|
-
// 4:00 PM EST (ignored when disabled)
|
|
81
|
-
endMinute: 0,
|
|
82
|
-
timezone: "America/New_York"
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
module2.exports = { DEFAULT_CONFIG: DEFAULT_CONFIG2, SweepType: SweepType2, ZoneType: ZoneType2 };
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// SWING POINT
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
class SwingPoint {
|
|
113
|
+
constructor(type, price, barIndex, timestamp, strength = 1) {
|
|
114
|
+
this.type = type; // 'high' or 'low'
|
|
115
|
+
this.price = price;
|
|
116
|
+
this.barIndex = barIndex;
|
|
117
|
+
this.timestamp = timestamp;
|
|
118
|
+
this.strength = strength;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// LIQUIDITY ZONE
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
class LiquidityZone {
|
|
127
|
+
constructor(type, priceHigh, priceLow, createdAt, barIndex) {
|
|
128
|
+
this.id = uuidv4();
|
|
129
|
+
this.type = type; // 'resistance' or 'support'
|
|
130
|
+
this.priceHigh = priceHigh;
|
|
131
|
+
this.priceLow = priceLow;
|
|
132
|
+
this.createdAt = createdAt;
|
|
133
|
+
this.barIndex = barIndex;
|
|
134
|
+
this.touches = 1;
|
|
135
|
+
this.swept = false;
|
|
136
|
+
this.sweptAt = null;
|
|
137
|
+
this.lastUsedBarIndex = -999;
|
|
138
|
+
this.qualityScore = 0.5;
|
|
86
139
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
|
|
141
|
+
containsPrice(price, toleranceTicks, tickSize) {
|
|
142
|
+
const tolerance = toleranceTicks * tickSize;
|
|
143
|
+
return price >= (this.priceLow - tolerance) && price <= (this.priceHigh + tolerance);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getLevel() {
|
|
147
|
+
return (this.priceHigh + this.priceLow) / 2;
|
|
95
148
|
}
|
|
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
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// SWEEP EVENT
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
class SweepEvent {
|
|
156
|
+
constructor(sweepType, zone, entryBarIndex, extremeBarIndex, extremePrice) {
|
|
157
|
+
this.sweepType = sweepType;
|
|
158
|
+
this.zone = zone;
|
|
159
|
+
this.entryBarIndex = entryBarIndex;
|
|
160
|
+
this.extremeBarIndex = extremeBarIndex;
|
|
161
|
+
this.extremePrice = extremePrice;
|
|
162
|
+
this.exitBarIndex = null;
|
|
163
|
+
this.isValid = false;
|
|
164
|
+
this.qualityScore = 0;
|
|
165
|
+
this.penetrationTicks = 0;
|
|
166
|
+
this.durationBars = 0;
|
|
167
|
+
this.volumeRatio = 1.0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// =============================================================================
|
|
172
|
+
// HQX-2B LIQUIDITY SWEEP STRATEGY
|
|
173
|
+
// =============================================================================
|
|
174
|
+
|
|
175
|
+
class HQX2BLiquiditySweep extends EventEmitter {
|
|
176
|
+
constructor(config = {}) {
|
|
177
|
+
super();
|
|
178
|
+
|
|
179
|
+
// Merge config with defaults
|
|
180
|
+
this.config = this._mergeConfig(DEFAULT_CONFIG, config);
|
|
181
|
+
this.tickSize = this.config.tickSize;
|
|
182
|
+
this.tickValue = this.config.tickValue;
|
|
183
|
+
|
|
184
|
+
// State
|
|
185
|
+
this.barHistory = new Map(); // contractId -> Bar[]
|
|
186
|
+
this.swingPoints = new Map(); // contractId -> SwingPoint[]
|
|
187
|
+
this.liquidityZones = new Map(); // contractId -> LiquidityZone[]
|
|
188
|
+
this.activeSweeps = new Map(); // contractId -> SweepEvent[]
|
|
189
|
+
|
|
190
|
+
// Bar aggregation (1-minute bars from ticks)
|
|
191
|
+
this.currentBar = new Map(); // contractId -> { open, high, low, close, volume, startTime }
|
|
192
|
+
this.barIntervalMs = 60000; // 1 minute = 60000ms
|
|
193
|
+
|
|
194
|
+
// Tracking
|
|
195
|
+
this.lastSignalTime = 0;
|
|
196
|
+
this.startTime = Date.now();
|
|
197
|
+
this.stats = { signals: 0, trades: 0, wins: 0, losses: 0, pnl: 0 };
|
|
198
|
+
this.recentTrades = [];
|
|
199
|
+
|
|
200
|
+
// === CRITICAL: Risk Management (matching HQX Scalping) ===
|
|
201
|
+
this.maxConsecutiveLosses = 3; // Stop trading after 3 consecutive losses
|
|
202
|
+
this.lossStreak = 0;
|
|
203
|
+
this.winStreak = 0;
|
|
204
|
+
this.tradingEnabled = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_mergeConfig(defaults, custom) {
|
|
208
|
+
const result = { ...defaults };
|
|
209
|
+
for (const key in custom) {
|
|
210
|
+
if (typeof custom[key] === 'object' && !Array.isArray(custom[key])) {
|
|
211
|
+
result[key] = { ...defaults[key], ...custom[key] };
|
|
122
212
|
} else {
|
|
123
|
-
|
|
124
|
-
takeProfit = currentPrice - exec.targetTicks * tickSize;
|
|
125
|
-
beLevel = currentPrice - exec.breakevenTicks * tickSize;
|
|
126
|
-
trailTrigger = currentPrice - exec.trailTriggerTicks * tickSize;
|
|
213
|
+
result[key] = custom[key];
|
|
127
214
|
}
|
|
128
|
-
const riskReward = exec.targetTicks / exec.stopTicks;
|
|
129
|
-
const confidence = Math.min(
|
|
130
|
-
1,
|
|
131
|
-
sweep.qualityScore * 0.5 + sweep.zone.qualityScore * 0.3 + (sweep.volumeRatio > 1.5 ? 0.2 : sweep.volumeRatio * 0.1)
|
|
132
|
-
);
|
|
133
|
-
let strength = SignalStrength2.MODERATE;
|
|
134
|
-
if (confidence >= 0.8) strength = SignalStrength2.VERY_STRONG;
|
|
135
|
-
else if (confidence >= 0.65) strength = SignalStrength2.STRONG;
|
|
136
|
-
else if (confidence < 0.5) strength = SignalStrength2.WEAK;
|
|
137
|
-
const winProb = 0.5 + (confidence - 0.5) * 0.4;
|
|
138
|
-
const edge = winProb * Math.abs(takeProfit - currentPrice) - (1 - winProb) * Math.abs(currentPrice - stopLoss);
|
|
139
|
-
sweep.zone.lastUsedBarIndex = currentIndex;
|
|
140
|
-
sweep.zone.swept = true;
|
|
141
|
-
sweep.zone.sweptAt = new Date(currentBar.timestamp);
|
|
142
|
-
return {
|
|
143
|
-
id: uuidv4(),
|
|
144
|
-
timestamp: Date.now(),
|
|
145
|
-
symbol: contractId.split(".")[0] || contractId,
|
|
146
|
-
contractId,
|
|
147
|
-
side: direction === "long" ? OrderSide2.BID : OrderSide2.ASK,
|
|
148
|
-
direction,
|
|
149
|
-
strategy: "HQX_2B_LIQUIDITY_SWEEP",
|
|
150
|
-
strength,
|
|
151
|
-
edge,
|
|
152
|
-
confidence,
|
|
153
|
-
entry: currentPrice,
|
|
154
|
-
entryPrice: currentPrice,
|
|
155
|
-
stopLoss,
|
|
156
|
-
takeProfit,
|
|
157
|
-
riskReward,
|
|
158
|
-
stopTicks: exec.stopTicks,
|
|
159
|
-
targetTicks: exec.targetTicks,
|
|
160
|
-
breakevenTicks: exec.breakevenTicks,
|
|
161
|
-
trailTriggerTicks: exec.trailTriggerTicks,
|
|
162
|
-
trailDistanceTicks: exec.trailDistanceTicks,
|
|
163
|
-
beLevel,
|
|
164
|
-
trailTrigger,
|
|
165
|
-
// Sweep details
|
|
166
|
-
sweepType: sweep.sweepType,
|
|
167
|
-
penetrationTicks: sweep.penetrationTicks,
|
|
168
|
-
sweepDurationBars: sweep.durationBars,
|
|
169
|
-
sweepQuality: sweep.qualityScore,
|
|
170
|
-
volumeRatio: sweep.volumeRatio,
|
|
171
|
-
// Zone details
|
|
172
|
-
zoneType: sweep.zone.type,
|
|
173
|
-
zoneLevel: sweep.zone.getLevel(),
|
|
174
|
-
zoneTouches: sweep.zone.touches,
|
|
175
|
-
zoneQuality: sweep.zone.qualityScore,
|
|
176
|
-
expires: Date.now() + 6e4
|
|
177
|
-
};
|
|
178
215
|
}
|
|
179
|
-
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ===========================================================================
|
|
220
|
+
// SESSION FILTER
|
|
221
|
+
// ===========================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if current time is within trading session (9:30-16:00 EST)
|
|
225
|
+
*/
|
|
226
|
+
isWithinSession(timestamp) {
|
|
227
|
+
if (!this.config.session.enabled) return true;
|
|
228
|
+
|
|
229
|
+
const date = new Date(timestamp);
|
|
230
|
+
// Convert to EST (UTC-5, or UTC-4 during DST)
|
|
231
|
+
const estOffset = this.isDST(date) ? -4 : -5;
|
|
232
|
+
const utcHours = date.getUTCHours();
|
|
233
|
+
const utcMinutes = date.getUTCMinutes();
|
|
234
|
+
const estHours = (utcHours + estOffset + 24) % 24;
|
|
235
|
+
|
|
236
|
+
const { startHour, startMinute, endHour, endMinute } = this.config.session;
|
|
237
|
+
const currentMins = estHours * 60 + utcMinutes;
|
|
238
|
+
const startMins = startHour * 60 + startMinute;
|
|
239
|
+
const endMins = endHour * 60 + endMinute;
|
|
240
|
+
|
|
241
|
+
return currentMins >= startMins && currentMins < endMins;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if date is in US Daylight Saving Time
|
|
246
|
+
*/
|
|
247
|
+
isDST(date) {
|
|
248
|
+
const jan = new Date(date.getFullYear(), 0, 1);
|
|
249
|
+
const jul = new Date(date.getFullYear(), 6, 1);
|
|
250
|
+
const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
|
251
|
+
return date.getTimezoneOffset() < stdOffset;
|
|
180
252
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
253
|
+
|
|
254
|
+
// ===========================================================================
|
|
255
|
+
// INITIALIZATION
|
|
256
|
+
// ===========================================================================
|
|
257
|
+
|
|
258
|
+
initialize(contractId, tickSize = 0.25, tickValue = 5.0) {
|
|
259
|
+
this.tickSize = tickSize;
|
|
260
|
+
this.tickValue = tickValue;
|
|
261
|
+
this.config.tickSize = tickSize;
|
|
262
|
+
this.config.tickValue = tickValue;
|
|
263
|
+
|
|
264
|
+
this.barHistory.set(contractId, []);
|
|
265
|
+
this.swingPoints.set(contractId, []);
|
|
266
|
+
this.liquidityZones.set(contractId, []);
|
|
267
|
+
this.activeSweeps.set(contractId, []);
|
|
268
|
+
this.currentBar.delete(contractId); // Reset bar aggregation
|
|
269
|
+
|
|
270
|
+
this.emit('log', {
|
|
271
|
+
type: 'info',
|
|
272
|
+
message: `[HQX-2B] Initialized for ${contractId}: tick=${tickSize}, value=${tickValue}, TF=1min`
|
|
273
|
+
});
|
|
274
|
+
this.emit('log', {
|
|
275
|
+
type: 'info',
|
|
276
|
+
message: `[HQX-2B] Params: Stop=${this.config.execution.stopTicks}t, Target=${this.config.execution.targetTicks}t, BE=${this.config.execution.breakevenTicks}t, Trail=${this.config.execution.trailTriggerTicks}/${this.config.execution.trailDistanceTicks}`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ===========================================================================
|
|
281
|
+
// MAIN ENTRY POINTS - TICK TO 1-MINUTE BAR AGGREGATION
|
|
282
|
+
// ===========================================================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Process incoming tick and aggregate into 1-minute bars
|
|
286
|
+
* Only calls processBar() when a bar closes (every 60 seconds)
|
|
287
|
+
*/
|
|
288
|
+
processTick(tick) {
|
|
289
|
+
const { contractId, price, volume, timestamp } = tick;
|
|
290
|
+
const ts = timestamp || Date.now();
|
|
291
|
+
const vol = volume || 1;
|
|
292
|
+
|
|
293
|
+
// Track tick count for debugging
|
|
294
|
+
if (!this._tickCount) this._tickCount = 0;
|
|
295
|
+
this._tickCount++;
|
|
296
|
+
|
|
297
|
+
// Get current bar for this contract
|
|
298
|
+
let bar = this.currentBar.get(contractId);
|
|
299
|
+
|
|
300
|
+
// Calculate bar start time (floor to minute)
|
|
301
|
+
const barStartTime = Math.floor(ts / this.barIntervalMs) * this.barIntervalMs;
|
|
302
|
+
|
|
303
|
+
if (!bar || bar.startTime !== barStartTime) {
|
|
304
|
+
// New bar period - close previous bar first if exists
|
|
305
|
+
if (bar) {
|
|
306
|
+
// Close the previous bar and process it
|
|
307
|
+
const closedBar = {
|
|
308
|
+
timestamp: bar.startTime,
|
|
309
|
+
open: bar.open,
|
|
310
|
+
high: bar.high,
|
|
311
|
+
low: bar.low,
|
|
312
|
+
close: bar.close,
|
|
313
|
+
volume: bar.volume
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Debug: Log bar close
|
|
317
|
+
const bars = this.barHistory.get(contractId) || [];
|
|
318
|
+
this.emit('log', {
|
|
319
|
+
type: 'debug',
|
|
320
|
+
message: `[BAR] #${bars.length + 1} closed @ ${closedBar.close.toFixed(2)} | O:${closedBar.open.toFixed(2)} H:${closedBar.high.toFixed(2)} L:${closedBar.low.toFixed(2)} Vol:${closedBar.volume}`
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Process the closed bar through strategy logic
|
|
324
|
+
const signal = this.processBar(contractId, closedBar);
|
|
325
|
+
|
|
326
|
+
// Start new bar
|
|
327
|
+
this.currentBar.set(contractId, {
|
|
328
|
+
startTime: barStartTime,
|
|
329
|
+
open: price,
|
|
330
|
+
high: price,
|
|
331
|
+
low: price,
|
|
332
|
+
close: price,
|
|
333
|
+
volume: vol
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return signal; // Return signal from closed bar
|
|
337
|
+
} else {
|
|
338
|
+
// First bar ever
|
|
339
|
+
this.currentBar.set(contractId, {
|
|
340
|
+
startTime: barStartTime,
|
|
341
|
+
open: price,
|
|
342
|
+
high: price,
|
|
343
|
+
low: price,
|
|
344
|
+
close: price,
|
|
345
|
+
volume: vol
|
|
346
|
+
});
|
|
347
|
+
return null;
|
|
193
348
|
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
349
|
+
} else {
|
|
350
|
+
// Same bar period - update OHLC
|
|
351
|
+
bar.high = Math.max(bar.high, price);
|
|
352
|
+
bar.low = Math.min(bar.low, price);
|
|
353
|
+
bar.close = price;
|
|
354
|
+
bar.volume += vol;
|
|
355
|
+
return null; // No signal until bar closes
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
onTick(tick) {
|
|
360
|
+
return this.processTick(tick);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
onTrade(trade) {
|
|
364
|
+
return this.processTick({
|
|
365
|
+
contractId: trade.contractId || trade.symbol,
|
|
366
|
+
price: trade.price,
|
|
367
|
+
volume: trade.size || trade.volume || 1,
|
|
368
|
+
timestamp: trade.timestamp || Date.now()
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ===========================================================================
|
|
373
|
+
// PROCESS BAR - MAIN LOGIC
|
|
374
|
+
// ===========================================================================
|
|
375
|
+
|
|
376
|
+
processBar(contractId, bar) {
|
|
377
|
+
// Get or initialize history
|
|
378
|
+
let bars = this.barHistory.get(contractId);
|
|
379
|
+
if (!bars) {
|
|
380
|
+
this.initialize(contractId);
|
|
381
|
+
bars = this.barHistory.get(contractId);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Add bar to history
|
|
385
|
+
bars.push(bar);
|
|
386
|
+
if (bars.length > 500) bars.shift();
|
|
387
|
+
|
|
388
|
+
const currentIndex = bars.length - 1;
|
|
389
|
+
|
|
390
|
+
// Need minimum data (lookback + 2 = 3 bars with lookback=1)
|
|
391
|
+
if (bars.length < this.config.swing.lookbackBars + 2) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 1. Detect new swing points
|
|
396
|
+
const prevSwingCount = this.swingPoints.get(contractId).length;
|
|
397
|
+
this._detectSwings(contractId, bars, currentIndex);
|
|
398
|
+
const swings = this.swingPoints.get(contractId);
|
|
399
|
+
|
|
400
|
+
// Debug: Log new swing detection
|
|
401
|
+
if (swings.length > prevSwingCount) {
|
|
402
|
+
const newSwing = swings[swings.length - 1];
|
|
403
|
+
this.emit('log', {
|
|
404
|
+
type: 'debug',
|
|
405
|
+
message: `[2B] NEW SWING ${newSwing.type.toUpperCase()} @ ${newSwing.price.toFixed(2)} | Total: ${swings.length}`
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 2. Detect sweeps of zones BEFORE updating zones (so new swings don't modify zone boundaries)
|
|
410
|
+
const sweep = this._detectSweep(contractId, bars, currentIndex);
|
|
411
|
+
|
|
412
|
+
// Debug: Log sweep detection
|
|
413
|
+
if (sweep) {
|
|
414
|
+
this.emit('log', {
|
|
415
|
+
type: 'debug',
|
|
416
|
+
message: `[2B] SWEEP ${sweep.sweepType} | Valid: ${sweep.isValid} | Pen: ${sweep.penetrationTicks.toFixed(1)}t | Q: ${(sweep.qualityScore * 100).toFixed(0)}%`
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 3. Update liquidity zones from swings (AFTER sweep detection)
|
|
421
|
+
const prevZoneCount = this.liquidityZones.get(contractId).length;
|
|
422
|
+
this._updateZones(contractId, currentIndex);
|
|
423
|
+
const zones = this.liquidityZones.get(contractId);
|
|
424
|
+
|
|
425
|
+
// Debug: Log new zone formation
|
|
426
|
+
if (zones.length > prevZoneCount) {
|
|
427
|
+
const newZone = zones[zones.length - 1];
|
|
428
|
+
this.emit('log', {
|
|
429
|
+
type: 'debug',
|
|
430
|
+
message: `[2B] NEW ZONE ${newZone.type.toUpperCase()} @ ${newZone.getLevel().toFixed(2)} | Total: ${zones.length}`
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 4. If valid sweep completed, generate signal
|
|
435
|
+
if (sweep && sweep.isValid) {
|
|
436
|
+
this.emit('log', {
|
|
437
|
+
type: 'debug',
|
|
438
|
+
message: `[2B] Calling _generateSignal for ${sweep.sweepType} sweep...`
|
|
439
|
+
});
|
|
440
|
+
const signal = this._generateSignal(contractId, bar, currentIndex, sweep);
|
|
441
|
+
if (!signal) {
|
|
442
|
+
this.emit('log', {
|
|
443
|
+
type: 'debug',
|
|
444
|
+
message: `[2B] _generateSignal returned null (likely cooldown)`
|
|
445
|
+
});
|
|
210
446
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
447
|
+
return signal;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
// SWING DETECTION
|
|
455
|
+
// ===========================================================================
|
|
456
|
+
|
|
457
|
+
_detectSwings(contractId, bars, currentIndex) {
|
|
458
|
+
const lookback = this.config.swing.lookbackBars;
|
|
459
|
+
const minStrength = this.config.swing.minStrength;
|
|
460
|
+
|
|
461
|
+
if (currentIndex < lookback * 2) return;
|
|
462
|
+
|
|
463
|
+
const swings = this.swingPoints.get(contractId);
|
|
464
|
+
const pivotIndex = currentIndex - lookback;
|
|
465
|
+
const pivotBar = bars[pivotIndex];
|
|
466
|
+
|
|
467
|
+
// Debug swing detection every 5 bars
|
|
468
|
+
if (currentIndex % 5 === 0) {
|
|
469
|
+
this.emit('log', {
|
|
470
|
+
type: 'debug',
|
|
471
|
+
message: `[SWING] Checking pivot at bar ${pivotIndex} | H:${pivotBar.high.toFixed(2)} L:${pivotBar.low.toFixed(2)} | Total swings: ${swings.length}`
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Check for swing high
|
|
476
|
+
let isSwingHigh = true;
|
|
477
|
+
let highStrength = 0;
|
|
478
|
+
for (let i = pivotIndex - lookback; i <= pivotIndex + lookback; i++) {
|
|
479
|
+
if (i === pivotIndex || i < 0 || i >= bars.length) continue;
|
|
480
|
+
if (bars[i].high >= pivotBar.high) {
|
|
481
|
+
isSwingHigh = false;
|
|
482
|
+
break;
|
|
216
483
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
484
|
+
highStrength++;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (isSwingHigh && highStrength >= minStrength) {
|
|
488
|
+
const existing = swings.find(s => s.barIndex === pivotIndex && s.type === 'high');
|
|
489
|
+
if (!existing) {
|
|
490
|
+
swings.push(new SwingPoint('high', pivotBar.high, pivotIndex, pivotBar.timestamp, highStrength));
|
|
491
|
+
this.emit('log', {
|
|
492
|
+
type: 'debug',
|
|
493
|
+
message: `[SWING] NEW HIGH @ ${pivotBar.high.toFixed(2)} | Bar ${pivotIndex} | Strength: ${highStrength}`
|
|
494
|
+
});
|
|
226
495
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Check for swing low
|
|
499
|
+
let isSwingLow = true;
|
|
500
|
+
let lowStrength = 0;
|
|
501
|
+
for (let i = pivotIndex - lookback; i <= pivotIndex + lookback; i++) {
|
|
502
|
+
if (i === pivotIndex || i < 0 || i >= bars.length) continue;
|
|
503
|
+
if (bars[i].low <= pivotBar.low) {
|
|
504
|
+
isSwingLow = false;
|
|
505
|
+
break;
|
|
232
506
|
}
|
|
233
|
-
|
|
234
|
-
|
|
507
|
+
lowStrength++;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (isSwingLow && lowStrength >= minStrength) {
|
|
511
|
+
const existing = swings.find(s => s.barIndex === pivotIndex && s.type === 'low');
|
|
512
|
+
if (!existing) {
|
|
513
|
+
swings.push(new SwingPoint('low', pivotBar.low, pivotIndex, pivotBar.timestamp, lowStrength));
|
|
514
|
+
this.emit('log', {
|
|
515
|
+
type: 'debug',
|
|
516
|
+
message: `[SWING] NEW LOW @ ${pivotBar.low.toFixed(2)} | Bar ${pivotIndex} | Strength: ${lowStrength}`
|
|
517
|
+
});
|
|
235
518
|
}
|
|
236
|
-
return swings;
|
|
237
519
|
}
|
|
238
|
-
|
|
520
|
+
|
|
521
|
+
// Keep only recent swings
|
|
522
|
+
const maxAge = this.config.zone.maxZoneAgeBars;
|
|
523
|
+
while (swings.length > 0 && swings[0].barIndex < currentIndex - maxAge) {
|
|
524
|
+
swings.shift();
|
|
525
|
+
}
|
|
239
526
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.touches = 1;
|
|
256
|
-
this.swept = false;
|
|
257
|
-
this.sweptAt = null;
|
|
258
|
-
this.lastUsedBarIndex = -999;
|
|
259
|
-
this.qualityScore = 0.5;
|
|
260
|
-
}
|
|
261
|
-
containsPrice(price, toleranceTicks, tickSize) {
|
|
262
|
-
const tolerance = toleranceTicks * tickSize;
|
|
263
|
-
return price >= this.priceLow - tolerance && price <= this.priceHigh + tolerance;
|
|
264
|
-
}
|
|
265
|
-
getLevel() {
|
|
266
|
-
return (this.priceHigh + this.priceLow) / 2;
|
|
527
|
+
|
|
528
|
+
// ===========================================================================
|
|
529
|
+
// ZONE DETECTION & CLUSTERING
|
|
530
|
+
// ===========================================================================
|
|
531
|
+
|
|
532
|
+
_updateZones(contractId, currentIndex) {
|
|
533
|
+
const swings = this.swingPoints.get(contractId);
|
|
534
|
+
const zones = this.liquidityZones.get(contractId);
|
|
535
|
+
const tolerance = this.config.zone.clusterToleranceTicks * this.tickSize;
|
|
536
|
+
const maxAge = this.config.zone.maxZoneAgeBars;
|
|
537
|
+
|
|
538
|
+
// Remove old zones
|
|
539
|
+
for (let i = zones.length - 1; i >= 0; i--) {
|
|
540
|
+
if (currentIndex - zones[i].barIndex > maxAge) {
|
|
541
|
+
zones.splice(i, 1);
|
|
267
542
|
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Cluster swings into zones
|
|
546
|
+
for (const swing of swings) {
|
|
547
|
+
// Check if swing already belongs to a zone
|
|
548
|
+
let foundZone = null;
|
|
549
|
+
for (const zone of zones) {
|
|
550
|
+
if (zone.containsPrice(swing.price, this.config.zone.clusterToleranceTicks, this.tickSize)) {
|
|
551
|
+
foundZone = zone;
|
|
552
|
+
break;
|
|
276
553
|
}
|
|
277
554
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
swing.timestamp,
|
|
298
|
-
swing.barIndex
|
|
299
|
-
);
|
|
300
|
-
newZone.qualityScore = 0.3 + swing.strength * 0.1;
|
|
301
|
-
zones.push(newZone);
|
|
302
|
-
}
|
|
555
|
+
|
|
556
|
+
if (foundZone) {
|
|
557
|
+
// Update existing zone
|
|
558
|
+
foundZone.touches++;
|
|
559
|
+
if (swing.price > foundZone.priceHigh) foundZone.priceHigh = swing.price;
|
|
560
|
+
if (swing.price < foundZone.priceLow) foundZone.priceLow = swing.price;
|
|
561
|
+
foundZone.qualityScore = Math.min(1.0, 0.3 + foundZone.touches * 0.15);
|
|
562
|
+
} else {
|
|
563
|
+
// Create new zone
|
|
564
|
+
const zoneType = swing.type === 'high' ? ZoneType.RESISTANCE : ZoneType.SUPPORT;
|
|
565
|
+
const newZone = new LiquidityZone(
|
|
566
|
+
zoneType,
|
|
567
|
+
swing.price + tolerance / 2,
|
|
568
|
+
swing.price - tolerance / 2,
|
|
569
|
+
swing.timestamp,
|
|
570
|
+
swing.barIndex
|
|
571
|
+
);
|
|
572
|
+
newZone.qualityScore = 0.3 + swing.strength * 0.1;
|
|
573
|
+
zones.push(newZone);
|
|
303
574
|
}
|
|
304
|
-
return zones;
|
|
305
575
|
}
|
|
306
|
-
module2.exports = { LiquidityZone, updateZones };
|
|
307
576
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.durationBars = 0;
|
|
326
|
-
this.volumeRatio = 1;
|
|
577
|
+
|
|
578
|
+
// ===========================================================================
|
|
579
|
+
// SWEEP DETECTION
|
|
580
|
+
// ===========================================================================
|
|
581
|
+
|
|
582
|
+
_detectSweep(contractId, bars, currentIndex) {
|
|
583
|
+
const zones = this.liquidityZones.get(contractId);
|
|
584
|
+
const currentBar = bars[currentIndex];
|
|
585
|
+
const currentPrice = currentBar.close;
|
|
586
|
+
const cfg = this.config.sweep;
|
|
587
|
+
const zoneCfg = this.config.zone;
|
|
588
|
+
|
|
589
|
+
for (const zone of zones) {
|
|
590
|
+
// Check cooldown (zone can be reused after cooldownBars)
|
|
591
|
+
if (zone.lastUsedBarIndex >= 0 &&
|
|
592
|
+
(currentIndex - zone.lastUsedBarIndex) < zoneCfg.cooldownBars) {
|
|
593
|
+
continue;
|
|
327
594
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
i,
|
|
377
|
-
bar.high
|
|
378
|
-
);
|
|
379
|
-
sweep.exitBarIndex = currentIndex;
|
|
380
|
-
sweep.penetrationTicks = penetration;
|
|
381
|
-
sweep.durationBars = currentIndex - i;
|
|
382
|
-
sweep.volumeRatio = volumeRatio;
|
|
383
|
-
sweep.qualityScore = scoreSweep(sweep, bodyRatio);
|
|
384
|
-
sweep.isValid = sweep.qualityScore >= cfg.minQualityScore;
|
|
385
|
-
if (sweep.isValid) {
|
|
386
|
-
return sweep;
|
|
387
|
-
}
|
|
595
|
+
|
|
596
|
+
// Check zone distance
|
|
597
|
+
const zoneLevel = zone.getLevel();
|
|
598
|
+
const distanceTicks = Math.abs(currentPrice - zoneLevel) / this.tickSize;
|
|
599
|
+
if (distanceTicks > zoneCfg.maxZoneDistanceTicks) continue;
|
|
600
|
+
|
|
601
|
+
// Look for sweep in recent bars
|
|
602
|
+
const lookbackStart = Math.max(0, currentIndex - cfg.maxDurationBars * 2);
|
|
603
|
+
|
|
604
|
+
for (let i = lookbackStart; i < currentIndex; i++) {
|
|
605
|
+
const bar = bars[i];
|
|
606
|
+
|
|
607
|
+
// Check for HIGH SWEEP (price went above resistance then came back)
|
|
608
|
+
// Also supports NEAR-SWEEP when minPenetrationTicks is negative (price approached but didn't fully cross)
|
|
609
|
+
if (zone.type === ZoneType.RESISTANCE) {
|
|
610
|
+
const penetration = (bar.high - zone.priceHigh) / this.tickSize;
|
|
611
|
+
|
|
612
|
+
if (penetration >= cfg.minPenetrationTicks && penetration <= cfg.maxPenetrationTicks) {
|
|
613
|
+
// Found penetration (or near-sweep if minPenetrationTicks < 0), check if price reclaimed below zone
|
|
614
|
+
// For near-sweeps, we accept rejection even if price didn't fully cross
|
|
615
|
+
const rejectionThreshold = Math.min(zone.priceHigh, zone.priceHigh + penetration * this.tickSize);
|
|
616
|
+
if (currentPrice < rejectionThreshold + this.tickSize * 2) {
|
|
617
|
+
// Check rejection candle quality
|
|
618
|
+
const barRange = bar.high - bar.low;
|
|
619
|
+
const bodySize = Math.abs(bar.close - bar.open);
|
|
620
|
+
const bodyRatio = barRange > 0 ? bodySize / barRange : 0;
|
|
621
|
+
|
|
622
|
+
if (bodyRatio >= cfg.minBodyRatio) {
|
|
623
|
+
// Calculate volume ratio
|
|
624
|
+
const volumeRatio = this._getVolumeRatio(bars, i, 20);
|
|
625
|
+
|
|
626
|
+
if (volumeRatio >= cfg.minVolumeRatio) {
|
|
627
|
+
const sweep = new SweepEvent(
|
|
628
|
+
SweepType.HIGH_SWEEP,
|
|
629
|
+
zone,
|
|
630
|
+
i,
|
|
631
|
+
i,
|
|
632
|
+
bar.high
|
|
633
|
+
);
|
|
634
|
+
sweep.exitBarIndex = currentIndex;
|
|
635
|
+
sweep.penetrationTicks = penetration;
|
|
636
|
+
sweep.durationBars = currentIndex - i;
|
|
637
|
+
sweep.volumeRatio = volumeRatio;
|
|
638
|
+
sweep.qualityScore = this._scoreSweep(sweep, bodyRatio);
|
|
639
|
+
sweep.isValid = sweep.qualityScore >= cfg.minQualityScore;
|
|
640
|
+
|
|
641
|
+
if (sweep.isValid) {
|
|
642
|
+
return sweep;
|
|
388
643
|
}
|
|
389
644
|
}
|
|
390
645
|
}
|
|
391
646
|
}
|
|
392
647
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Check for LOW SWEEP (price went below support then came back)
|
|
651
|
+
// Also supports NEAR-SWEEP when minPenetrationTicks is negative (price approached but didn't fully cross)
|
|
652
|
+
if (zone.type === ZoneType.SUPPORT) {
|
|
653
|
+
const penetration = (zone.priceLow - bar.low) / this.tickSize;
|
|
654
|
+
|
|
655
|
+
if (penetration >= cfg.minPenetrationTicks && penetration <= cfg.maxPenetrationTicks) {
|
|
656
|
+
// Found penetration (or near-sweep if minPenetrationTicks < 0), check if price reclaimed above zone
|
|
657
|
+
// For near-sweeps, we accept rejection even if price didn't fully cross
|
|
658
|
+
const rejectionThreshold = Math.max(zone.priceLow, zone.priceLow - penetration * this.tickSize);
|
|
659
|
+
if (currentPrice > rejectionThreshold - this.tickSize * 2) {
|
|
660
|
+
// Check rejection candle quality
|
|
661
|
+
const barRange = bar.high - bar.low;
|
|
662
|
+
const bodySize = Math.abs(bar.close - bar.open);
|
|
663
|
+
const bodyRatio = barRange > 0 ? bodySize / barRange : 0;
|
|
664
|
+
|
|
665
|
+
if (bodyRatio >= cfg.minBodyRatio) {
|
|
666
|
+
// Calculate volume ratio
|
|
667
|
+
const volumeRatio = this._getVolumeRatio(bars, i, 20);
|
|
668
|
+
|
|
669
|
+
if (volumeRatio >= cfg.minVolumeRatio) {
|
|
670
|
+
const sweep = new SweepEvent(
|
|
671
|
+
SweepType.LOW_SWEEP,
|
|
672
|
+
zone,
|
|
673
|
+
i,
|
|
674
|
+
i,
|
|
675
|
+
bar.low
|
|
676
|
+
);
|
|
677
|
+
sweep.exitBarIndex = currentIndex;
|
|
678
|
+
sweep.penetrationTicks = penetration;
|
|
679
|
+
sweep.durationBars = currentIndex - i;
|
|
680
|
+
sweep.volumeRatio = volumeRatio;
|
|
681
|
+
sweep.qualityScore = this._scoreSweep(sweep, bodyRatio);
|
|
682
|
+
sweep.isValid = sweep.qualityScore >= cfg.minQualityScore;
|
|
683
|
+
|
|
684
|
+
if (sweep.isValid) {
|
|
685
|
+
return sweep;
|
|
419
686
|
}
|
|
420
687
|
}
|
|
421
688
|
}
|
|
@@ -423,467 +690,466 @@ var require_sweeps = __commonJS({
|
|
|
423
690
|
}
|
|
424
691
|
}
|
|
425
692
|
}
|
|
426
|
-
return null;
|
|
427
693
|
}
|
|
428
|
-
|
|
694
|
+
|
|
695
|
+
return null;
|
|
429
696
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
LiquidityZone,
|
|
442
|
-
updateZones,
|
|
443
|
-
SweepEvent,
|
|
444
|
-
detectSweep
|
|
445
|
-
};
|
|
697
|
+
|
|
698
|
+
_getVolumeRatio(bars, index, lookback) {
|
|
699
|
+
const start = Math.max(0, index - lookback);
|
|
700
|
+
const recentBars = bars.slice(start, index);
|
|
701
|
+
if (recentBars.length === 0) return 1.0;
|
|
702
|
+
|
|
703
|
+
const volumes = recentBars.map(b => b.volume).sort((a, b) => a - b);
|
|
704
|
+
const medianIdx = Math.floor(volumes.length / 2);
|
|
705
|
+
const medianVolume = volumes[medianIdx] || 1;
|
|
706
|
+
|
|
707
|
+
return bars[index].volume / medianVolume;
|
|
446
708
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
709
|
+
|
|
710
|
+
_scoreSweep(sweep, bodyRatio) {
|
|
711
|
+
let score = 0;
|
|
712
|
+
|
|
713
|
+
// Penetration score (optimal around 4 ticks)
|
|
714
|
+
const optimalPen = 4;
|
|
715
|
+
const penDiff = Math.abs(sweep.penetrationTicks - optimalPen);
|
|
716
|
+
score += Math.max(0, 0.3 - penDiff * 0.03);
|
|
717
|
+
|
|
718
|
+
// Duration score (faster is better, max 5 bars)
|
|
719
|
+
score += Math.max(0, 0.25 - sweep.durationBars * 0.05);
|
|
720
|
+
|
|
721
|
+
// Volume score
|
|
722
|
+
score += Math.min(0.25, sweep.volumeRatio * 0.1);
|
|
723
|
+
|
|
724
|
+
// Body ratio score
|
|
725
|
+
score += Math.min(0.2, bodyRatio * 0.4);
|
|
726
|
+
|
|
727
|
+
return Math.min(1.0, score);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ===========================================================================
|
|
731
|
+
// SIGNAL GENERATION
|
|
732
|
+
// ===========================================================================
|
|
733
|
+
|
|
734
|
+
_generateSignal(contractId, currentBar, currentIndex, sweep) {
|
|
735
|
+
// CRITICAL: Check if trading is enabled (loss protection)
|
|
736
|
+
if (!this.tradingEnabled) {
|
|
737
|
+
this.emit('log', {
|
|
738
|
+
type: 'debug',
|
|
739
|
+
message: `[2B] Trading disabled (${this.lossStreak} consecutive losses)`
|
|
740
|
+
});
|
|
741
|
+
return null;
|
|
466
742
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
this.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.
|
|
478
|
-
this.
|
|
479
|
-
this.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
initialize(contractId, tickSize = 0.25, tickValue = 5) {
|
|
484
|
-
this.tickSize = tickSize;
|
|
485
|
-
this.tickValue = tickValue;
|
|
486
|
-
this.config.tickSize = tickSize;
|
|
487
|
-
this.config.tickValue = tickValue;
|
|
488
|
-
this.barHistory.set(contractId, []);
|
|
489
|
-
this.swingPoints.set(contractId, []);
|
|
490
|
-
this.liquidityZones.set(contractId, []);
|
|
491
|
-
this.currentBar.delete(contractId);
|
|
492
|
-
this.emit("log", {
|
|
493
|
-
type: "info",
|
|
494
|
-
message: `[HQX-2B] Initialized for ${contractId}: tick=${tickSize}, value=${tickValue}, TF=1min`
|
|
495
|
-
});
|
|
496
|
-
this.emit("log", {
|
|
497
|
-
type: "info",
|
|
498
|
-
message: `[HQX-2B] Params: Stop=${this.config.execution.stopTicks}t, Target=${this.config.execution.targetTicks}t, BE=${this.config.execution.breakevenTicks}t, Trail=${this.config.execution.trailTriggerTicks}/${this.config.execution.trailDistanceTicks}`
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Check if current time is within trading session (9:30-16:00 EST)
|
|
503
|
-
*/
|
|
504
|
-
isWithinSession(timestamp) {
|
|
505
|
-
if (!this.config.session.enabled) return true;
|
|
506
|
-
const date = new Date(timestamp);
|
|
507
|
-
const estOffset = this.isDST(date) ? -4 : -5;
|
|
508
|
-
const utcHours = date.getUTCHours();
|
|
509
|
-
const utcMinutes = date.getUTCMinutes();
|
|
510
|
-
const estHours = (utcHours + estOffset + 24) % 24;
|
|
511
|
-
const { startHour, startMinute, endHour, endMinute } = this.config.session;
|
|
512
|
-
const currentMins = estHours * 60 + utcMinutes;
|
|
513
|
-
const startMins = startHour * 60 + startMinute;
|
|
514
|
-
const endMins = endHour * 60 + endMinute;
|
|
515
|
-
return currentMins >= startMins && currentMins < endMins;
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Check if date is in US Daylight Saving Time
|
|
519
|
-
*/
|
|
520
|
-
isDST(date) {
|
|
521
|
-
const jan = new Date(date.getFullYear(), 0, 1);
|
|
522
|
-
const jul = new Date(date.getFullYear(), 6, 1);
|
|
523
|
-
const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
|
524
|
-
return date.getTimezoneOffset() < stdOffset;
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Process incoming tick and aggregate into 1-minute bars
|
|
528
|
-
* Only calls processBar() when a bar closes (every 60 seconds)
|
|
529
|
-
*/
|
|
530
|
-
processTick(tick) {
|
|
531
|
-
const { contractId, price, volume, timestamp } = tick;
|
|
532
|
-
const ts = timestamp || Date.now();
|
|
533
|
-
const vol = volume || 1;
|
|
534
|
-
if (!this._tickCount) this._tickCount = /* @__PURE__ */ new Map();
|
|
535
|
-
const count = (this._tickCount.get(contractId) || 0) + 1;
|
|
536
|
-
this._tickCount.set(contractId, count);
|
|
537
|
-
let bar = this.currentBar.get(contractId);
|
|
538
|
-
const barStartTime = Math.floor(ts / this.barIntervalMs) * this.barIntervalMs;
|
|
539
|
-
if (count === 1) {
|
|
540
|
-
this.emit("log", {
|
|
541
|
-
type: "debug",
|
|
542
|
-
message: `[2B] First tick ${contractId}: price=${price}, ts=${ts}, barStart=${barStartTime}`
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
if (!bar || bar.startTime !== barStartTime) {
|
|
546
|
-
if (bar) {
|
|
547
|
-
const closedBar = {
|
|
548
|
-
timestamp: bar.startTime,
|
|
549
|
-
open: bar.open,
|
|
550
|
-
high: bar.high,
|
|
551
|
-
low: bar.low,
|
|
552
|
-
close: bar.close,
|
|
553
|
-
volume: bar.volume
|
|
554
|
-
};
|
|
555
|
-
const bars = this.barHistory.get(contractId) || [];
|
|
556
|
-
this.emit("log", {
|
|
557
|
-
type: "debug",
|
|
558
|
-
message: `[2B] BAR CLOSE ${contractId} #${bars.length + 1}: O=${closedBar.open.toFixed(2)} H=${closedBar.high.toFixed(2)} L=${closedBar.low.toFixed(2)} C=${closedBar.close.toFixed(2)} V=${closedBar.volume}`
|
|
559
|
-
});
|
|
560
|
-
const signal = this.processBar(contractId, closedBar);
|
|
561
|
-
this.currentBar.set(contractId, {
|
|
562
|
-
startTime: barStartTime,
|
|
563
|
-
open: price,
|
|
564
|
-
high: price,
|
|
565
|
-
low: price,
|
|
566
|
-
close: price,
|
|
567
|
-
volume: vol
|
|
568
|
-
});
|
|
569
|
-
return signal;
|
|
570
|
-
} else {
|
|
571
|
-
this.emit("log", {
|
|
572
|
-
type: "debug",
|
|
573
|
-
message: `[2B] First bar start ${contractId} @ ${new Date(barStartTime).toISOString()}`
|
|
574
|
-
});
|
|
575
|
-
this.currentBar.set(contractId, {
|
|
576
|
-
startTime: barStartTime,
|
|
577
|
-
open: price,
|
|
578
|
-
high: price,
|
|
579
|
-
low: price,
|
|
580
|
-
close: price,
|
|
581
|
-
volume: vol
|
|
582
|
-
});
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
} else {
|
|
586
|
-
bar.high = Math.max(bar.high, price);
|
|
587
|
-
bar.low = Math.min(bar.low, price);
|
|
588
|
-
bar.close = price;
|
|
589
|
-
bar.volume += vol;
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
onTick(tick) {
|
|
594
|
-
return this.processTick(tick);
|
|
595
|
-
}
|
|
596
|
-
onTrade(trade) {
|
|
597
|
-
return this.processTick({
|
|
598
|
-
contractId: trade.contractId || trade.symbol,
|
|
599
|
-
price: trade.price,
|
|
600
|
-
volume: trade.size || trade.volume || 1,
|
|
601
|
-
timestamp: trade.timestamp || Date.now()
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
processBar(contractId, bar) {
|
|
605
|
-
let bars = this.barHistory.get(contractId);
|
|
606
|
-
if (!bars) {
|
|
607
|
-
this.initialize(contractId);
|
|
608
|
-
bars = this.barHistory.get(contractId);
|
|
609
|
-
}
|
|
610
|
-
bars.push(bar);
|
|
611
|
-
if (bars.length > 500) bars.shift();
|
|
612
|
-
const currentIndex = bars.length - 1;
|
|
613
|
-
if (bars.length < this.config.swing.lookbackBars * 3) {
|
|
614
|
-
return null;
|
|
615
|
-
}
|
|
616
|
-
const swings = this.swingPoints.get(contractId);
|
|
617
|
-
const prevSwingCount = swings.length;
|
|
618
|
-
const updatedSwings = detectSwings(
|
|
619
|
-
bars,
|
|
620
|
-
currentIndex,
|
|
621
|
-
swings,
|
|
622
|
-
this.config.swing,
|
|
623
|
-
this.config.zone.maxZoneAgeBars
|
|
624
|
-
);
|
|
625
|
-
this.swingPoints.set(contractId, updatedSwings);
|
|
626
|
-
if (updatedSwings.length > prevSwingCount) {
|
|
627
|
-
const newSwing = updatedSwings[updatedSwings.length - 1];
|
|
628
|
-
this.emit("log", {
|
|
629
|
-
type: "debug",
|
|
630
|
-
message: `[2B] NEW SWING ${newSwing.type.toUpperCase()} @ ${newSwing.price.toFixed(2)} | Total: ${updatedSwings.length}`
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
const zones = this.liquidityZones.get(contractId);
|
|
634
|
-
const prevZoneCount = zones.length;
|
|
635
|
-
const updatedZones = updateZones(
|
|
636
|
-
updatedSwings,
|
|
637
|
-
zones,
|
|
638
|
-
currentIndex,
|
|
639
|
-
this.config.zone,
|
|
640
|
-
this.tickSize
|
|
641
|
-
);
|
|
642
|
-
this.liquidityZones.set(contractId, updatedZones);
|
|
643
|
-
if (updatedZones.length > prevZoneCount) {
|
|
644
|
-
const newZone = updatedZones[updatedZones.length - 1];
|
|
645
|
-
this.emit("log", {
|
|
646
|
-
type: "debug",
|
|
647
|
-
message: `[2B] NEW ZONE ${newZone.type.toUpperCase()} @ ${newZone.getLevel().toFixed(2)} | Total: ${updatedZones.length}`
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
const sweep = detectSweep(
|
|
651
|
-
updatedZones,
|
|
652
|
-
bars,
|
|
653
|
-
currentIndex,
|
|
654
|
-
this.config.sweep,
|
|
655
|
-
this.config.zone,
|
|
656
|
-
this.tickSize
|
|
657
|
-
);
|
|
658
|
-
if (sweep) {
|
|
659
|
-
this.emit("log", {
|
|
660
|
-
type: "debug",
|
|
661
|
-
message: `[2B] SWEEP ${sweep.sweepType} | Valid: ${sweep.isValid} | Pen: ${sweep.penetrationTicks.toFixed(1)}t | Q: ${(sweep.qualityScore * 100).toFixed(0)}%`
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
if (sweep && sweep.isValid) {
|
|
665
|
-
if (Date.now() - this.lastSignalTime < this.config.execution.cooldownMs) {
|
|
666
|
-
this.emit("log", {
|
|
667
|
-
type: "debug",
|
|
668
|
-
message: `[2B] COOLDOWN - waiting ${Math.ceil((this.config.execution.cooldownMs - (Date.now() - this.lastSignalTime)) / 1e3)}s`
|
|
669
|
-
});
|
|
670
|
-
return null;
|
|
671
|
-
}
|
|
672
|
-
this.emit("log", {
|
|
673
|
-
type: "debug",
|
|
674
|
-
message: `[2B] GENERATING SIGNAL from ${sweep.sweepType} sweep...`
|
|
675
|
-
});
|
|
676
|
-
const signal = generateSignal({
|
|
677
|
-
contractId,
|
|
678
|
-
currentBar: bar,
|
|
679
|
-
currentIndex,
|
|
680
|
-
sweep,
|
|
681
|
-
config: this.config,
|
|
682
|
-
tickSize: this.tickSize
|
|
683
|
-
});
|
|
684
|
-
if (signal) {
|
|
685
|
-
this.lastSignalTime = Date.now();
|
|
686
|
-
this.stats.signals++;
|
|
687
|
-
this.emit("signal", {
|
|
688
|
-
side: signal.direction === "long" ? "buy" : "sell",
|
|
689
|
-
action: "open",
|
|
690
|
-
reason: `2B ${sweep.sweepType} | Pen:${sweep.penetrationTicks.toFixed(1)}t | Vol:${sweep.volumeRatio.toFixed(1)}x | Q:${(sweep.qualityScore * 100).toFixed(0)}%`,
|
|
691
|
-
...signal
|
|
692
|
-
});
|
|
693
|
-
this.emit("log", {
|
|
694
|
-
type: "info",
|
|
695
|
-
message: `[HQX-2B] SIGNAL: ${signal.direction.toUpperCase()} @ ${bar.close.toFixed(2)} | ${sweep.sweepType} | Pen:${sweep.penetrationTicks.toFixed(1)}t Vol:${sweep.volumeRatio.toFixed(1)}x | Conf:${(signal.confidence * 100).toFixed(0)}%`
|
|
696
|
-
});
|
|
697
|
-
return signal;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return null;
|
|
701
|
-
}
|
|
702
|
-
getAnalysisState(contractId, currentPrice) {
|
|
703
|
-
const bars = this.barHistory.get(contractId) || [];
|
|
704
|
-
const zones = this.liquidityZones.get(contractId) || [];
|
|
705
|
-
const swings = this.swingPoints.get(contractId) || [];
|
|
706
|
-
if (bars.length < 5) {
|
|
707
|
-
return { ready: false, message: `Collecting data... ${bars.length}/5 bars` };
|
|
708
|
-
}
|
|
709
|
-
const sortedZones = zones.map((z) => ({ zone: z, distance: Math.abs(currentPrice - z.getLevel()) })).sort((a, b) => a.distance - b.distance);
|
|
710
|
-
const nearestResistance = sortedZones.find((z) => z.zone.type === ZoneType2.RESISTANCE);
|
|
711
|
-
const nearestSupport = sortedZones.find((z) => z.zone.type === ZoneType2.SUPPORT);
|
|
712
|
-
return {
|
|
713
|
-
ready: true,
|
|
714
|
-
barsProcessed: bars.length,
|
|
715
|
-
swingsDetected: swings.length,
|
|
716
|
-
activeZones: zones.length,
|
|
717
|
-
nearestResistance: nearestResistance ? nearestResistance.zone.getLevel() : null,
|
|
718
|
-
nearestSupport: nearestSupport ? nearestSupport.zone.getLevel() : null,
|
|
719
|
-
stopTicks: this.config.execution.stopTicks,
|
|
720
|
-
targetTicks: this.config.execution.targetTicks,
|
|
721
|
-
strategy: "HQX-2B Liquidity Sweep (Optimized)"
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
recordTradeResult(pnl) {
|
|
725
|
-
this.recentTrades.push({ netPnl: pnl, timestamp: Date.now() });
|
|
726
|
-
if (this.recentTrades.length > 100) this.recentTrades.shift();
|
|
727
|
-
if (pnl > 0) {
|
|
728
|
-
this.stats.wins++;
|
|
729
|
-
} else {
|
|
730
|
-
this.stats.losses++;
|
|
731
|
-
}
|
|
732
|
-
this.stats.trades++;
|
|
733
|
-
this.stats.pnl += pnl;
|
|
734
|
-
this.emit("log", {
|
|
735
|
-
type: "debug",
|
|
736
|
-
message: `[HQX-2B] Trade result: ${pnl > 0 ? "WIN" : "LOSS"} $${pnl.toFixed(2)}`
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
getBarHistory(contractId) {
|
|
740
|
-
return this.barHistory.get(contractId) || [];
|
|
741
|
-
}
|
|
742
|
-
getStats() {
|
|
743
|
-
return this.stats;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Get detailed health status to confirm strategy is working
|
|
747
|
-
* @param {string} contractId - Contract ID
|
|
748
|
-
* @returns {Object} Health status with diagnostic info
|
|
749
|
-
*/
|
|
750
|
-
getHealthStatus(contractId) {
|
|
751
|
-
const bars = this.barHistory.get(contractId) || [];
|
|
752
|
-
const zones = this.liquidityZones.get(contractId) || [];
|
|
753
|
-
const swings = this.swingPoints.get(contractId) || [];
|
|
754
|
-
const currentBar = this.currentBar.get(contractId);
|
|
755
|
-
const lastBar = bars[bars.length - 1];
|
|
756
|
-
const timeSinceLastBar = lastBar ? Date.now() - lastBar.timestamp : null;
|
|
757
|
-
const resistanceZones = zones.filter((z) => z.type === ZoneType2.RESISTANCE).length;
|
|
758
|
-
const supportZones = zones.filter((z) => z.type === ZoneType2.SUPPORT).length;
|
|
759
|
-
const isAggregating = currentBar && currentBar.tickCount > 0;
|
|
760
|
-
return {
|
|
761
|
-
healthy: bars.length >= 5,
|
|
762
|
-
barsTotal: bars.length,
|
|
763
|
-
barsLast5Min: bars.filter((b) => Date.now() - b.timestamp < 5 * 60 * 1e3).length,
|
|
764
|
-
swingsTotal: swings.length,
|
|
765
|
-
zonesResistance: resistanceZones,
|
|
766
|
-
zonesSupport: supportZones,
|
|
767
|
-
zonesTotal: zones.length,
|
|
768
|
-
currentBarTicks: currentBar ? currentBar.tickCount : 0,
|
|
769
|
-
isAggregating,
|
|
770
|
-
timeSinceLastBarMs: timeSinceLastBar,
|
|
771
|
-
lastSignalTime: this.lastSignalTime,
|
|
772
|
-
signalCooldownMs: this.config.execution.cooldownMs,
|
|
773
|
-
uptime: Date.now() - (this.startTime || Date.now())
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
reset(contractId) {
|
|
777
|
-
this.barHistory.set(contractId, []);
|
|
778
|
-
this.swingPoints.set(contractId, []);
|
|
779
|
-
this.liquidityZones.set(contractId, []);
|
|
780
|
-
this.currentBar.delete(contractId);
|
|
781
|
-
this.emit("log", {
|
|
782
|
-
type: "info",
|
|
783
|
-
message: `[HQX-2B] Reset state for ${contractId}`
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
/**
|
|
787
|
-
* Preload historical bars to warm up the strategy
|
|
788
|
-
* @param {string} contractId - Contract ID
|
|
789
|
-
* @param {Array} bars - Array of bars {timestamp, open, high, low, close, volume}
|
|
790
|
-
*/
|
|
791
|
-
preloadBars(contractId, bars) {
|
|
792
|
-
if (!bars || bars.length === 0) {
|
|
793
|
-
this.emit("log", {
|
|
794
|
-
type: "debug",
|
|
795
|
-
message: `[HQX-2B] No historical bars to preload`
|
|
796
|
-
});
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
if (!this.barHistory.has(contractId)) {
|
|
800
|
-
this.initialize(contractId);
|
|
801
|
-
}
|
|
802
|
-
const sortedBars = [...bars].sort((a, b) => a.timestamp - b.timestamp);
|
|
803
|
-
this.emit("log", {
|
|
804
|
-
type: "info",
|
|
805
|
-
message: `[HQX-2B] Preloading ${sortedBars.length} historical bars...`
|
|
806
|
-
});
|
|
807
|
-
let signalCount = 0;
|
|
808
|
-
for (const bar of sortedBars) {
|
|
809
|
-
const signal = this.processBar(contractId, bar);
|
|
810
|
-
if (signal) signalCount++;
|
|
811
|
-
}
|
|
812
|
-
const history = this.barHistory.get(contractId) || [];
|
|
813
|
-
const swings = this.swingPoints.get(contractId) || [];
|
|
814
|
-
const zones = this.liquidityZones.get(contractId) || [];
|
|
815
|
-
this.emit("log", {
|
|
816
|
-
type: "info",
|
|
817
|
-
message: `[HQX-2B] Preload complete: ${history.length} bars, ${swings.length} swings, ${zones.length} zones`
|
|
743
|
+
|
|
744
|
+
// CRITICAL: Check consecutive losses
|
|
745
|
+
if (this.lossStreak >= this.maxConsecutiveLosses) {
|
|
746
|
+
this.tradingEnabled = false;
|
|
747
|
+
this.emit('log', {
|
|
748
|
+
type: 'info',
|
|
749
|
+
message: `[2B] Trading paused: ${this.lossStreak} consecutive losses. Waiting 2 min...`
|
|
750
|
+
});
|
|
751
|
+
// Auto re-enable after 2 minutes
|
|
752
|
+
setTimeout(() => {
|
|
753
|
+
this.tradingEnabled = true;
|
|
754
|
+
this.lossStreak = 0;
|
|
755
|
+
this.emit('log', {
|
|
756
|
+
type: 'info',
|
|
757
|
+
message: '[2B] Trading re-enabled after cooldown'
|
|
818
758
|
});
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
759
|
+
}, 120000);
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Cooldown check
|
|
764
|
+
const timeSinceLastSignal = Date.now() - this.lastSignalTime;
|
|
765
|
+
if (timeSinceLastSignal < this.config.execution.cooldownMs) {
|
|
766
|
+
this.emit('log', {
|
|
767
|
+
type: 'debug',
|
|
768
|
+
message: `[2B] Signal blocked by cooldown (${(timeSinceLastSignal / 1000).toFixed(1)}s < ${this.config.execution.cooldownMs / 1000}s)`
|
|
769
|
+
});
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const exec = this.config.execution;
|
|
774
|
+
const currentPrice = currentBar.close;
|
|
775
|
+
|
|
776
|
+
// Direction
|
|
777
|
+
const direction = sweep.sweepType === SweepType.HIGH_SWEEP ? 'short' : 'long';
|
|
778
|
+
|
|
779
|
+
// Calculate stops and targets
|
|
780
|
+
let stopLoss, takeProfit, beLevel, trailTrigger;
|
|
781
|
+
|
|
782
|
+
if (direction === 'long') {
|
|
783
|
+
stopLoss = currentPrice - exec.stopTicks * this.tickSize;
|
|
784
|
+
takeProfit = currentPrice + exec.targetTicks * this.tickSize;
|
|
785
|
+
beLevel = currentPrice + exec.breakevenTicks * this.tickSize;
|
|
786
|
+
trailTrigger = currentPrice + exec.trailTriggerTicks * this.tickSize;
|
|
787
|
+
} else {
|
|
788
|
+
stopLoss = currentPrice + exec.stopTicks * this.tickSize;
|
|
789
|
+
takeProfit = currentPrice - exec.targetTicks * this.tickSize;
|
|
790
|
+
beLevel = currentPrice - exec.breakevenTicks * this.tickSize;
|
|
791
|
+
trailTrigger = currentPrice - exec.trailTriggerTicks * this.tickSize;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const riskReward = exec.targetTicks / exec.stopTicks;
|
|
795
|
+
|
|
796
|
+
// Confidence calculation
|
|
797
|
+
const confidence = Math.min(1.0,
|
|
798
|
+
sweep.qualityScore * 0.5 +
|
|
799
|
+
sweep.zone.qualityScore * 0.3 +
|
|
800
|
+
(sweep.volumeRatio > 1.5 ? 0.2 : sweep.volumeRatio * 0.1)
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
// Signal strength
|
|
804
|
+
let strength = SignalStrength.MODERATE;
|
|
805
|
+
if (confidence >= 0.80) strength = SignalStrength.VERY_STRONG;
|
|
806
|
+
else if (confidence >= 0.65) strength = SignalStrength.STRONG;
|
|
807
|
+
else if (confidence < 0.50) strength = SignalStrength.WEAK;
|
|
808
|
+
|
|
809
|
+
// Edge calculation
|
|
810
|
+
const winProb = 0.5 + (confidence - 0.5) * 0.4;
|
|
811
|
+
const edge = winProb * Math.abs(takeProfit - currentPrice) - (1 - winProb) * Math.abs(currentPrice - stopLoss);
|
|
812
|
+
|
|
813
|
+
// Mark zone as used (cooldown)
|
|
814
|
+
sweep.zone.lastUsedBarIndex = currentIndex;
|
|
815
|
+
sweep.zone.swept = true;
|
|
816
|
+
sweep.zone.sweptAt = new Date(currentBar.timestamp);
|
|
817
|
+
|
|
818
|
+
// Update state
|
|
819
|
+
this.lastSignalTime = Date.now();
|
|
820
|
+
this.stats.signals++;
|
|
821
|
+
|
|
822
|
+
const signal = {
|
|
823
|
+
id: uuidv4(),
|
|
824
|
+
timestamp: Date.now(),
|
|
825
|
+
symbol: contractId.split('.')[0] || contractId,
|
|
826
|
+
contractId,
|
|
827
|
+
side: direction === 'long' ? OrderSide.BID : OrderSide.ASK,
|
|
828
|
+
direction,
|
|
829
|
+
strategy: 'HQX_2B_LIQUIDITY_SWEEP',
|
|
830
|
+
strength,
|
|
831
|
+
edge,
|
|
832
|
+
confidence,
|
|
833
|
+
entry: currentPrice,
|
|
834
|
+
entryPrice: currentPrice,
|
|
835
|
+
stopLoss,
|
|
836
|
+
takeProfit,
|
|
837
|
+
riskReward,
|
|
838
|
+
stopTicks: exec.stopTicks,
|
|
839
|
+
targetTicks: exec.targetTicks,
|
|
840
|
+
breakevenTicks: exec.breakevenTicks,
|
|
841
|
+
trailTriggerTicks: exec.trailTriggerTicks,
|
|
842
|
+
trailDistanceTicks: exec.trailDistanceTicks,
|
|
843
|
+
beLevel,
|
|
844
|
+
trailTrigger,
|
|
845
|
+
|
|
846
|
+
// Sweep details
|
|
847
|
+
sweepType: sweep.sweepType,
|
|
848
|
+
penetrationTicks: sweep.penetrationTicks,
|
|
849
|
+
sweepDurationBars: sweep.durationBars,
|
|
850
|
+
sweepQuality: sweep.qualityScore,
|
|
851
|
+
volumeRatio: sweep.volumeRatio,
|
|
852
|
+
|
|
853
|
+
// Zone details
|
|
854
|
+
zoneType: sweep.zone.type,
|
|
855
|
+
zoneLevel: sweep.zone.getLevel(),
|
|
856
|
+
zoneTouches: sweep.zone.touches,
|
|
857
|
+
zoneQuality: sweep.zone.qualityScore,
|
|
858
|
+
|
|
859
|
+
expires: Date.now() + 60000
|
|
827
860
|
};
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
this.strategy.on("log", (log) => this.emit("log", log));
|
|
844
|
-
}
|
|
845
|
-
// Interface methods (compatible with M1)
|
|
846
|
-
processTick(tick) {
|
|
847
|
-
return this.strategy.processTick(tick);
|
|
848
|
-
}
|
|
849
|
-
onTick(tick) {
|
|
850
|
-
return this.strategy.onTick(tick);
|
|
851
|
-
}
|
|
852
|
-
onTrade(trade) {
|
|
853
|
-
return this.strategy.onTrade(trade);
|
|
854
|
-
}
|
|
855
|
-
processBar(contractId, bar) {
|
|
856
|
-
return this.strategy.processBar(contractId, bar);
|
|
857
|
-
}
|
|
858
|
-
initialize(contractId, tickSize, tickValue) {
|
|
859
|
-
return this.strategy.initialize(contractId, tickSize, tickValue);
|
|
861
|
+
|
|
862
|
+
// Emit signal
|
|
863
|
+
this.emit('signal', {
|
|
864
|
+
side: direction === 'long' ? 'buy' : 'sell',
|
|
865
|
+
action: 'open',
|
|
866
|
+
reason: `2B ${sweep.sweepType} | Pen:${sweep.penetrationTicks.toFixed(1)}t | Vol:${sweep.volumeRatio.toFixed(1)}x | Q:${(sweep.qualityScore * 100).toFixed(0)}%`,
|
|
867
|
+
...signal
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
this.emit('log', {
|
|
871
|
+
type: 'info',
|
|
872
|
+
message: `[HQX-2B] SIGNAL: ${direction.toUpperCase()} @ ${currentPrice.toFixed(2)} | ${sweep.sweepType} | Pen:${sweep.penetrationTicks.toFixed(1)}t Vol:${sweep.volumeRatio.toFixed(1)}x | Conf:${(confidence * 100).toFixed(0)}%`
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
return signal;
|
|
860
876
|
}
|
|
861
|
-
|
|
862
|
-
|
|
877
|
+
|
|
878
|
+
// ===========================================================================
|
|
879
|
+
// HELPER METHODS
|
|
880
|
+
// ===========================================================================
|
|
881
|
+
|
|
882
|
+
getAnalysisState(contractId, currentPrice) {
|
|
883
|
+
const bars = this.barHistory.get(contractId) || [];
|
|
884
|
+
const zones = this.liquidityZones.get(contractId) || [];
|
|
885
|
+
const swings = this.swingPoints.get(contractId) || [];
|
|
886
|
+
|
|
887
|
+
// Minimum 3 bars to start (aggressive - fast warmup)
|
|
888
|
+
if (bars.length < 3) {
|
|
889
|
+
return {
|
|
890
|
+
ready: false,
|
|
891
|
+
message: `Collecting data... ${bars.length}/3 bars`,
|
|
892
|
+
barsProcessed: bars.length,
|
|
893
|
+
swingsDetected: swings.length,
|
|
894
|
+
activeZones: zones.length
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Find nearest zones
|
|
899
|
+
const sortedZones = zones
|
|
900
|
+
.map(z => ({ zone: z, distance: Math.abs(currentPrice - z.getLevel()) }))
|
|
901
|
+
.sort((a, b) => a.distance - b.distance);
|
|
902
|
+
|
|
903
|
+
const nearestResistance = sortedZones.find(z => z.zone.type === ZoneType.RESISTANCE);
|
|
904
|
+
const nearestSupport = sortedZones.find(z => z.zone.type === ZoneType.SUPPORT);
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
ready: true,
|
|
908
|
+
barsProcessed: bars.length,
|
|
909
|
+
swingsDetected: swings.length,
|
|
910
|
+
activeZones: zones.length,
|
|
911
|
+
nearestResistance: nearestResistance ? nearestResistance.zone.getLevel() : null,
|
|
912
|
+
nearestSupport: nearestSupport ? nearestSupport.zone.getLevel() : null,
|
|
913
|
+
stopTicks: this.config.execution.stopTicks,
|
|
914
|
+
targetTicks: this.config.execution.targetTicks,
|
|
915
|
+
strategy: 'HQX-2B Liquidity Sweep (Backtest Validated)',
|
|
916
|
+
tradingEnabled: this.tradingEnabled,
|
|
917
|
+
lossStreak: this.lossStreak,
|
|
918
|
+
winStreak: this.winStreak,
|
|
919
|
+
cooldownRemaining: Math.max(0, this.config.execution.cooldownMs - (Date.now() - this.lastSignalTime)),
|
|
920
|
+
};
|
|
863
921
|
}
|
|
922
|
+
|
|
864
923
|
recordTradeResult(pnl) {
|
|
865
|
-
|
|
924
|
+
// Prevent duplicate recordings
|
|
925
|
+
const lastTrade = this.recentTrades[this.recentTrades.length - 1];
|
|
926
|
+
if (lastTrade && Math.abs(pnl - lastTrade.netPnl) < 0.5) {
|
|
927
|
+
return; // Same P&L, ignore duplicate
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
this.recentTrades.push({ netPnl: pnl, timestamp: Date.now() });
|
|
931
|
+
if (this.recentTrades.length > 100) this.recentTrades.shift();
|
|
932
|
+
|
|
933
|
+
if (pnl > 0) {
|
|
934
|
+
this.stats.wins++;
|
|
935
|
+
this.winStreak++;
|
|
936
|
+
this.lossStreak = 0;
|
|
937
|
+
this.tradingEnabled = true; // Re-enable on win
|
|
938
|
+
this.emit('log', {
|
|
939
|
+
type: 'info',
|
|
940
|
+
message: `[2B] WIN +$${pnl.toFixed(2)} | Streak: ${this.winStreak}`
|
|
941
|
+
});
|
|
942
|
+
} else if (pnl < 0) {
|
|
943
|
+
this.stats.losses++;
|
|
944
|
+
this.lossStreak++;
|
|
945
|
+
this.winStreak = 0;
|
|
946
|
+
this.emit('log', {
|
|
947
|
+
type: 'info',
|
|
948
|
+
message: `[2B] LOSS $${pnl.toFixed(2)} | Streak: -${this.lossStreak}`
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// Check if we need to pause trading
|
|
952
|
+
if (this.lossStreak >= this.maxConsecutiveLosses) {
|
|
953
|
+
this.emit('log', {
|
|
954
|
+
type: 'info',
|
|
955
|
+
message: `[2B] Max losses reached (${this.lossStreak}). Pausing...`
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
this.stats.trades++;
|
|
961
|
+
this.stats.pnl += pnl;
|
|
866
962
|
}
|
|
867
|
-
|
|
868
|
-
|
|
963
|
+
|
|
964
|
+
getBarHistory(contractId) {
|
|
965
|
+
return this.barHistory.get(contractId) || [];
|
|
869
966
|
}
|
|
967
|
+
|
|
870
968
|
getStats() {
|
|
871
|
-
return this.
|
|
969
|
+
return this.stats;
|
|
872
970
|
}
|
|
873
|
-
|
|
874
|
-
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Get detailed health status to confirm strategy is working
|
|
974
|
+
*/
|
|
975
|
+
getHealthStatus(contractId) {
|
|
976
|
+
const bars = this.barHistory.get(contractId) || [];
|
|
977
|
+
const zones = this.liquidityZones.get(contractId) || [];
|
|
978
|
+
const swings = this.swingPoints.get(contractId) || [];
|
|
979
|
+
const currentBar = this.currentBar.get(contractId);
|
|
980
|
+
|
|
981
|
+
const lastBar = bars[bars.length - 1];
|
|
982
|
+
const timeSinceLastBar = lastBar ? Date.now() - lastBar.timestamp : null;
|
|
983
|
+
|
|
984
|
+
const resistanceZones = zones.filter(z => z.type === ZoneType.RESISTANCE).length;
|
|
985
|
+
const supportZones = zones.filter(z => z.type === ZoneType.SUPPORT).length;
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
healthy: bars.length >= 5,
|
|
989
|
+
barsTotal: bars.length,
|
|
990
|
+
barsLast5Min: bars.filter(b => Date.now() - b.timestamp < 5 * 60 * 1000).length,
|
|
991
|
+
swingsTotal: swings.length,
|
|
992
|
+
zonesResistance: resistanceZones,
|
|
993
|
+
zonesSupport: supportZones,
|
|
994
|
+
zonesTotal: zones.length,
|
|
995
|
+
currentBarTicks: currentBar ? currentBar.tickCount : 0,
|
|
996
|
+
isAggregating: currentBar && currentBar.tickCount > 0,
|
|
997
|
+
timeSinceLastBarMs: timeSinceLastBar,
|
|
998
|
+
lastSignalTime: this.lastSignalTime,
|
|
999
|
+
signalCooldownMs: this.config.execution.cooldownMs,
|
|
1000
|
+
uptime: Date.now() - (this.startTime || Date.now())
|
|
1001
|
+
};
|
|
875
1002
|
}
|
|
1003
|
+
|
|
1004
|
+
reset(contractId) {
|
|
1005
|
+
this.barHistory.set(contractId, []);
|
|
1006
|
+
this.swingPoints.set(contractId, []);
|
|
1007
|
+
this.liquidityZones.set(contractId, []);
|
|
1008
|
+
this.activeSweeps.set(contractId, []);
|
|
1009
|
+
this.currentBar.delete(contractId); // Reset bar aggregation
|
|
1010
|
+
|
|
1011
|
+
this.emit('log', {
|
|
1012
|
+
type: 'info',
|
|
1013
|
+
message: `[HQX-2B] Reset state for ${contractId}`
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Preload historical bars to warm up the strategy
|
|
1019
|
+
* @param {string} contractId - Contract ID
|
|
1020
|
+
* @param {Array} bars - Array of bars {timestamp, open, high, low, close, volume}
|
|
1021
|
+
*/
|
|
876
1022
|
preloadBars(contractId, bars) {
|
|
877
|
-
|
|
1023
|
+
if (!bars || bars.length === 0) {
|
|
1024
|
+
this.emit('log', {
|
|
1025
|
+
type: 'debug',
|
|
1026
|
+
message: `[HQX-2B] No historical bars to preload`
|
|
1027
|
+
});
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Initialize if needed
|
|
1032
|
+
if (!this.barHistory.has(contractId)) {
|
|
1033
|
+
this.initialize(contractId);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Sort bars by timestamp (oldest first)
|
|
1037
|
+
const sortedBars = [...bars].sort((a, b) => a.timestamp - b.timestamp);
|
|
1038
|
+
|
|
1039
|
+
this.emit('log', {
|
|
1040
|
+
type: 'info',
|
|
1041
|
+
message: `[HQX-2B] Preloading ${sortedBars.length} historical bars...`
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
// Process each bar through the strategy
|
|
1045
|
+
let signalCount = 0;
|
|
1046
|
+
for (const bar of sortedBars) {
|
|
1047
|
+
const signal = this.processBar(contractId, bar);
|
|
1048
|
+
if (signal) signalCount++;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const history = this.barHistory.get(contractId) || [];
|
|
1052
|
+
const swings = this.swingPoints.get(contractId) || [];
|
|
1053
|
+
const zones = this.liquidityZones.get(contractId) || [];
|
|
1054
|
+
|
|
1055
|
+
this.emit('log', {
|
|
1056
|
+
type: 'info',
|
|
1057
|
+
message: `[HQX-2B] Preload complete: ${history.length} bars, ${swings.length} swings, ${zones.length} zones`
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
if (signalCount > 0) {
|
|
1061
|
+
this.emit('log', {
|
|
1062
|
+
type: 'debug',
|
|
1063
|
+
message: `[HQX-2B] ${signalCount} historical signals detected (ignored)`
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Reset signal time so we can generate new signals immediately
|
|
1068
|
+
this.lastSignalTime = 0;
|
|
878
1069
|
}
|
|
879
|
-
|
|
880
|
-
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Create synthetic zones from current price for immediate trading
|
|
1073
|
+
* Call this if historical data is insufficient
|
|
1074
|
+
* @param {string} contractId - Contract ID
|
|
1075
|
+
* @param {number} currentPrice - Current market price
|
|
1076
|
+
* @param {number} rangeTicks - Range in ticks for zone placement (default: 20)
|
|
1077
|
+
*/
|
|
1078
|
+
createSyntheticZones(contractId, currentPrice, rangeTicks = 20) {
|
|
1079
|
+
if (!this.liquidityZones.has(contractId)) {
|
|
1080
|
+
this.initialize(contractId);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const zones = this.liquidityZones.get(contractId);
|
|
1084
|
+
const tickSize = this.tickSize;
|
|
1085
|
+
const range = rangeTicks * tickSize;
|
|
1086
|
+
|
|
1087
|
+
// Create resistance zone above current price
|
|
1088
|
+
const resistanceLevel = currentPrice + range;
|
|
1089
|
+
const resistanceZone = new LiquidityZone(
|
|
1090
|
+
ZoneType.RESISTANCE,
|
|
1091
|
+
resistanceLevel,
|
|
1092
|
+
this.barHistory.get(contractId)?.length || 0,
|
|
1093
|
+
Date.now()
|
|
1094
|
+
);
|
|
1095
|
+
resistanceZone.addTouch(resistanceLevel, this.barHistory.get(contractId)?.length || 0);
|
|
1096
|
+
zones.push(resistanceZone);
|
|
1097
|
+
|
|
1098
|
+
// Create support zone below current price
|
|
1099
|
+
const supportLevel = currentPrice - range;
|
|
1100
|
+
const supportZone = new LiquidityZone(
|
|
1101
|
+
ZoneType.SUPPORT,
|
|
1102
|
+
supportLevel,
|
|
1103
|
+
this.barHistory.get(contractId)?.length || 0,
|
|
1104
|
+
Date.now()
|
|
1105
|
+
);
|
|
1106
|
+
supportZone.addTouch(supportLevel, this.barHistory.get(contractId)?.length || 0);
|
|
1107
|
+
zones.push(supportZone);
|
|
1108
|
+
|
|
1109
|
+
this.emit('log', {
|
|
1110
|
+
type: 'info',
|
|
1111
|
+
message: `[HQX-2B] Synthetic zones: R @ ${resistanceLevel.toFixed(2)} | S @ ${supportLevel.toFixed(2)}`
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
return { resistance: resistanceLevel, support: supportLevel };
|
|
881
1115
|
}
|
|
882
|
-
|
|
883
|
-
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// =============================================================================
|
|
1119
|
+
// STRATEGY WRAPPER (Compatible with M1 interface)
|
|
1120
|
+
// =============================================================================
|
|
1121
|
+
|
|
1122
|
+
class HQX2BStrategy extends EventEmitter {
|
|
1123
|
+
constructor(config = {}) {
|
|
1124
|
+
super();
|
|
1125
|
+
this.config = config;
|
|
1126
|
+
this.strategy = new HQX2BLiquiditySweep(config);
|
|
1127
|
+
|
|
1128
|
+
// Forward events
|
|
1129
|
+
this.strategy.on('signal', (sig) => this.emit('signal', sig));
|
|
1130
|
+
this.strategy.on('log', (log) => this.emit('log', log));
|
|
884
1131
|
}
|
|
885
|
-
|
|
886
|
-
|
|
1132
|
+
|
|
1133
|
+
// Interface methods (compatible with M1)
|
|
1134
|
+
processTick(tick) { return this.strategy.processTick(tick); }
|
|
1135
|
+
onTick(tick) { return this.strategy.onTick(tick); }
|
|
1136
|
+
onTrade(trade) { return this.strategy.onTrade(trade); }
|
|
1137
|
+
processBar(contractId, bar) { return this.strategy.processBar(contractId, bar); }
|
|
1138
|
+
initialize(contractId, tickSize, tickValue) { return this.strategy.initialize(contractId, tickSize, tickValue); }
|
|
1139
|
+
getAnalysisState(contractId, price) { return this.strategy.getAnalysisState(contractId, price); }
|
|
1140
|
+
recordTradeResult(pnl) { return this.strategy.recordTradeResult(pnl); }
|
|
1141
|
+
reset(contractId) { return this.strategy.reset(contractId); }
|
|
1142
|
+
getStats() { return this.strategy.getStats(); }
|
|
1143
|
+
getBarHistory(contractId) { return this.strategy.getBarHistory(contractId); }
|
|
1144
|
+
preloadBars(contractId, bars) { return this.strategy.preloadBars(contractId, bars); }
|
|
1145
|
+
getHealthStatus(contractId) { return this.strategy.getHealthStatus(contractId); }
|
|
1146
|
+
generateSignal(params) { return null; } // Signals come from processBar
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// =============================================================================
|
|
1150
|
+
// EXPORTS
|
|
1151
|
+
// =============================================================================
|
|
1152
|
+
|
|
887
1153
|
module.exports = {
|
|
888
1154
|
HQX2BLiquiditySweep,
|
|
889
1155
|
HQX2BStrategy,
|