@tradejs/strategy-head-and-shoulders 3.0.0

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/index.cjs ADDED
@@ -0,0 +1,1074 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ HeadAndShouldersStrategyDefinition: () => HeadAndShouldersStrategyDefinition,
24
+ default: () => index_default,
25
+ getBuiltInStrategyDefaultConfig: () => getBuiltInStrategyDefaultConfig,
26
+ headAndShouldersAiAdapter: () => headAndShouldersAiAdapter,
27
+ headAndShouldersDefaultConfig: () => config,
28
+ headAndShouldersManifest: () => headAndShouldersManifest,
29
+ strategyEntries: () => strategyEntries
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+ var import_config4 = require("@tradejs/core/config");
33
+
34
+ // src/HeadAndShoulders/config.ts
35
+ var import_constants = require("@tradejs/core/constants");
36
+ var config = {
37
+ ENV: "BACKTEST",
38
+ INTERVAL: "15",
39
+ MAKE_ORDERS: true,
40
+ CLOSE_OPPOSITE_POSITIONS: false,
41
+ BACKTEST_PRICE_MODE: "open",
42
+ AI_ENABLED: false,
43
+ AI_MODE: "llm",
44
+ ML_ENABLED: false,
45
+ ML_THRESHOLD: 0.1,
46
+ MIN_AI_QUALITY: 4,
47
+ FEE_PERCENT: import_constants.FEE_PERCENT,
48
+ MAX_LOSS_VALUE: 10,
49
+ MA_FAST: 14,
50
+ MA_MEDIUM: 49,
51
+ MA_SLOW: 50,
52
+ OBV_SMA: 10,
53
+ ATR: 14,
54
+ ATR_PCT_SHORT: 7,
55
+ ATR_PCT_LONG: 30,
56
+ BB: 20,
57
+ BB_STD: 2,
58
+ MACD_FAST: 12,
59
+ MACD_SLOW: 26,
60
+ MACD_SIGNAL: 9,
61
+ HEADSHOULDERS_PIVOT_LOOKBACK: 3,
62
+ HEADSHOULDERS_SHOULDER_TOLERANCE_PCT: 15,
63
+ HEADSHOULDERS_MIN_HEAD_PROMINENCE_RATIO: 0.2,
64
+ HEADSHOULDERS_TARGET_HEIGHT_PCT: 100,
65
+ HEADSHOULDERS_TARGET_HEIGHT_PCT_LONG: 120,
66
+ HEADSHOULDERS_TARGET_HEIGHT_PCT_SHORT: 100,
67
+ HEADSHOULDERS_STOP_BUFFER_HEIGHT_PCT: 5,
68
+ HEADSHOULDERS_MIN_HEAD_HEIGHT_PCT: 0.8,
69
+ HEADSHOULDERS_MIN_HEAD_HEIGHT_ATR: 2.5,
70
+ HEADSHOULDERS_ATR_PERIOD: 14,
71
+ HEADSHOULDERS_MIN_PATTERN_BARS: 8,
72
+ HEADSHOULDERS_MAX_PATTERN_BARS: 180,
73
+ HEADSHOULDERS_MIN_PATTERN_SYMMETRY_RATIO: 0.45,
74
+ HEADSHOULDERS_MAX_NECKLINE_SLOPE_RATIO: 0.2,
75
+ HEADSHOULDERS_MAX_PATTERN_AGE_BARS: 220,
76
+ HEADSHOULDERS_PRIOR_TREND_LOOKBACK: 80,
77
+ HEADSHOULDERS_MAX_PRIOR_MOVE_ATR: 3,
78
+ HEADSHOULDERS_MIN_BREAKOUT_DISTANCE_ATR: 0.2,
79
+ HEADSHOULDERS_MAX_BREAKOUT_DISTANCE_HEIGHT_RATIO: 0.35,
80
+ HEADSHOULDERS_MAX_BREAKOUT_DISTANCE_PCT: 0.8,
81
+ HEADSHOULDERS_MAX_BREAKOUT_DELAY_BARS: 8,
82
+ HEADSHOULDERS_REQUIRE_BREAKOUT_CROSS: false,
83
+ HEADSHOULDERS_ENTRY_MODE: "close_acceptance",
84
+ HEADSHOULDERS_CONFIRMATION_MAX_BARS: 2,
85
+ HEADSHOULDERS_MIN_CONFIRMATION_BODY_ATR: 0,
86
+ HEADSHOULDERS_MAX_CONFIRMATION_CLOSE_LOCATION: 1,
87
+ HEADSHOULDERS_CONFIRMATION_VOLUME_PERIOD: 20,
88
+ HEADSHOULDERS_MIN_CONFIRMATION_VOLUME_REL: 0.8,
89
+ HEADSHOULDERS_RETEST_MAX_BARS: 4,
90
+ HEADSHOULDERS_RETEST_TOLERANCE_ATR: 0.25,
91
+ HEADSHOULDERS_MIN_SIGNAL_BODY_STRENGTH: 0,
92
+ HEADSHOULDERS_MIN_SIGNAL_BODY_STRENGTH_LONG: 0.5,
93
+ HEADSHOULDERS_MIN_SIGNAL_BODY_STRENGTH_SHORT: 0,
94
+ HEADSHOULDERS_MIN_ENTRY_HEAD_HEIGHT_ATR: 0,
95
+ HEADSHOULDERS_MIN_ENTRY_HEAD_HEIGHT_ATR_LONG: 4,
96
+ HEADSHOULDERS_MIN_ENTRY_HEAD_HEIGHT_ATR_SHORT: 0,
97
+ HEADSHOULDERS_EXIT_ON_OPPOSITE_PATTERN: false,
98
+ LONG: {
99
+ enable: false,
100
+ direction: "LONG",
101
+ minRiskRatio: 0.7
102
+ },
103
+ SHORT: {
104
+ enable: true,
105
+ direction: "SHORT",
106
+ minRiskRatio: 0.7
107
+ }
108
+ };
109
+
110
+ // src/HeadAndShoulders/core.ts
111
+ var import_math = require("@tradejs/core/math");
112
+
113
+ // src/HeadAndShoulders/engine.ts
114
+ var import_config = require("@tradejs/strategy-kit/config");
115
+ var asNumber = (value) => {
116
+ const numeric = Number(value);
117
+ return Number.isFinite(numeric) ? numeric : null;
118
+ };
119
+ var clampNumber = ({
120
+ value,
121
+ fallback,
122
+ min,
123
+ max
124
+ }) => {
125
+ const numeric = Number(value);
126
+ const resolved = Number.isFinite(numeric) ? numeric : fallback;
127
+ return Math.min(max ?? Infinity, Math.max(min, resolved));
128
+ };
129
+ var getConfigNumbers = (config2) => ({
130
+ pivotLookback: Math.max(
131
+ 1,
132
+ Math.floor(Number(config2.HEADSHOULDERS_PIVOT_LOOKBACK ?? 3))
133
+ ),
134
+ shoulderTolerancePct: clampNumber({
135
+ value: config2.HEADSHOULDERS_SHOULDER_TOLERANCE_PCT,
136
+ fallback: 20,
137
+ min: 0
138
+ }),
139
+ minHeadProminenceRatio: clampNumber({
140
+ value: config2.HEADSHOULDERS_MIN_HEAD_PROMINENCE_RATIO,
141
+ fallback: 0.2,
142
+ min: 0
143
+ }),
144
+ targetHeightPctLong: clampNumber({
145
+ value: (0, import_config.resolveDirectionalConfigNumber)({
146
+ config: config2,
147
+ key: "HEADSHOULDERS_TARGET_HEIGHT_PCT",
148
+ direction: "LONG",
149
+ fallback: 100
150
+ }),
151
+ fallback: 100,
152
+ min: 0
153
+ }),
154
+ targetHeightPctShort: clampNumber({
155
+ value: (0, import_config.resolveDirectionalConfigNumber)({
156
+ config: config2,
157
+ key: "HEADSHOULDERS_TARGET_HEIGHT_PCT",
158
+ direction: "SHORT",
159
+ fallback: 100
160
+ }),
161
+ fallback: 100,
162
+ min: 0
163
+ }),
164
+ stopBufferHeightPct: clampNumber({
165
+ value: config2.HEADSHOULDERS_STOP_BUFFER_HEIGHT_PCT,
166
+ fallback: 5,
167
+ min: 0
168
+ }),
169
+ minHeadHeightPct: clampNumber({
170
+ value: config2.HEADSHOULDERS_MIN_HEAD_HEIGHT_PCT,
171
+ fallback: 0.8,
172
+ min: 0
173
+ }),
174
+ minHeadHeightAtr: clampNumber({
175
+ value: config2.HEADSHOULDERS_MIN_HEAD_HEIGHT_ATR,
176
+ fallback: 2,
177
+ min: 0
178
+ }),
179
+ atrPeriod: Math.max(
180
+ 2,
181
+ Math.floor(Number(config2.HEADSHOULDERS_ATR_PERIOD ?? 14))
182
+ ),
183
+ minPatternBars: Math.max(
184
+ 4,
185
+ Math.floor(Number(config2.HEADSHOULDERS_MIN_PATTERN_BARS ?? 8))
186
+ ),
187
+ maxPatternBars: Math.max(
188
+ 4,
189
+ Math.floor(Number(config2.HEADSHOULDERS_MAX_PATTERN_BARS ?? 180))
190
+ ),
191
+ minPatternSymmetryRatio: clampNumber({
192
+ value: config2.HEADSHOULDERS_MIN_PATTERN_SYMMETRY_RATIO,
193
+ fallback: 0.25,
194
+ min: 0,
195
+ max: 1
196
+ }),
197
+ maxNecklineSlopeRatio: clampNumber({
198
+ value: config2.HEADSHOULDERS_MAX_NECKLINE_SLOPE_RATIO,
199
+ fallback: 0.5,
200
+ min: 0
201
+ }),
202
+ maxPatternAgeBars: Math.max(
203
+ 4,
204
+ Math.floor(Number(config2.HEADSHOULDERS_MAX_PATTERN_AGE_BARS ?? 220))
205
+ ),
206
+ priorTrendLookback: Math.max(
207
+ 1,
208
+ Math.floor(Number(config2.HEADSHOULDERS_PRIOR_TREND_LOOKBACK ?? 80))
209
+ ),
210
+ maxPriorMoveAtr: clampNumber({
211
+ value: config2.HEADSHOULDERS_MAX_PRIOR_MOVE_ATR,
212
+ fallback: 0,
213
+ min: 0
214
+ }),
215
+ minBreakoutDistanceAtr: clampNumber({
216
+ value: config2.HEADSHOULDERS_MIN_BREAKOUT_DISTANCE_ATR,
217
+ fallback: 0.05,
218
+ min: 0
219
+ }),
220
+ maxBreakoutDistanceHeightRatio: clampNumber({
221
+ value: config2.HEADSHOULDERS_MAX_BREAKOUT_DISTANCE_HEIGHT_RATIO,
222
+ fallback: 0.35,
223
+ min: 0
224
+ }),
225
+ maxBreakoutDistancePct: clampNumber({
226
+ value: config2.HEADSHOULDERS_MAX_BREAKOUT_DISTANCE_PCT,
227
+ fallback: 0.8,
228
+ min: 0
229
+ }),
230
+ maxBreakoutDelayBars: Math.max(
231
+ 0,
232
+ Math.floor(Number(config2.HEADSHOULDERS_MAX_BREAKOUT_DELAY_BARS ?? 0))
233
+ ),
234
+ requireBreakoutCross: Boolean(config2.HEADSHOULDERS_REQUIRE_BREAKOUT_CROSS),
235
+ entryMode: config2.HEADSHOULDERS_ENTRY_MODE ?? "close_acceptance",
236
+ confirmationMaxBars: Math.max(
237
+ 1,
238
+ Math.floor(Number(config2.HEADSHOULDERS_CONFIRMATION_MAX_BARS ?? 2))
239
+ ),
240
+ minConfirmationBodyAtr: clampNumber({
241
+ value: config2.HEADSHOULDERS_MIN_CONFIRMATION_BODY_ATR,
242
+ fallback: 0,
243
+ min: 0
244
+ }),
245
+ maxConfirmationCloseLocation: clampNumber({
246
+ value: config2.HEADSHOULDERS_MAX_CONFIRMATION_CLOSE_LOCATION,
247
+ fallback: 1,
248
+ min: 0,
249
+ max: 1
250
+ }),
251
+ confirmationVolumePeriod: Math.max(
252
+ 2,
253
+ Math.floor(Number(config2.HEADSHOULDERS_CONFIRMATION_VOLUME_PERIOD ?? 20))
254
+ ),
255
+ minConfirmationVolumeRel: clampNumber({
256
+ value: config2.HEADSHOULDERS_MIN_CONFIRMATION_VOLUME_REL,
257
+ fallback: 0,
258
+ min: 0
259
+ }),
260
+ retestMaxBars: Math.max(
261
+ 1,
262
+ Math.floor(Number(config2.HEADSHOULDERS_RETEST_MAX_BARS ?? 4))
263
+ ),
264
+ retestToleranceAtr: clampNumber({
265
+ value: config2.HEADSHOULDERS_RETEST_TOLERANCE_ATR,
266
+ fallback: 0.25,
267
+ min: 0
268
+ })
269
+ });
270
+ var calculateAtr = (candles, period) => {
271
+ const relevant = candles.slice(-(period + 1));
272
+ if (relevant.length < 2) return null;
273
+ const trueRanges = [];
274
+ for (let index = 1; index < relevant.length; index += 1) {
275
+ const candle = relevant[index];
276
+ const previous = relevant[index - 1];
277
+ const high = asNumber(candle?.high);
278
+ const low = asNumber(candle?.low);
279
+ const previousClose = asNumber(previous?.close);
280
+ if (high == null || low == null || previousClose == null) continue;
281
+ trueRanges.push(
282
+ Math.max(
283
+ high - low,
284
+ Math.abs(high - previousClose),
285
+ Math.abs(low - previousClose)
286
+ )
287
+ );
288
+ }
289
+ if (trueRanges.length === 0) return null;
290
+ return trueRanges.reduce((sum, value) => sum + value, 0) / trueRanges.length;
291
+ };
292
+ var pushBoundedCandle = (state, candle, maxCandles) => {
293
+ state.currentIndex += 1;
294
+ state.candles.push(candle);
295
+ if (state.candles.length > maxCandles) {
296
+ const overflow = state.candles.length - maxCandles;
297
+ state.candles.splice(0, overflow);
298
+ state.candleStartIndex += overflow;
299
+ }
300
+ return state.currentIndex;
301
+ };
302
+ var getBufferedCandle = (state, absoluteIndex) => state.candles[absoluteIndex - state.candleStartIndex] ?? null;
303
+ var calculateAtrAt = ({
304
+ state,
305
+ absoluteIndex,
306
+ period
307
+ }) => {
308
+ const trueRanges = [];
309
+ for (let index = absoluteIndex - period + 1; index <= absoluteIndex; index += 1) {
310
+ const candle = getBufferedCandle(state, index);
311
+ const previous = getBufferedCandle(state, index - 1);
312
+ const high = asNumber(candle?.high);
313
+ const low = asNumber(candle?.low);
314
+ const previousClose = asNumber(previous?.close);
315
+ if (high == null || low == null || previousClose == null) continue;
316
+ trueRanges.push(
317
+ Math.max(
318
+ high - low,
319
+ Math.abs(high - previousClose),
320
+ Math.abs(low - previousClose)
321
+ )
322
+ );
323
+ }
324
+ if (trueRanges.length === 0) return null;
325
+ return trueRanges.reduce((sum, value) => sum + value, 0) / trueRanges.length;
326
+ };
327
+ var resolveConfirmedPivot = ({
328
+ state,
329
+ candidateIndex,
330
+ lookback,
331
+ atrPeriod,
332
+ priorTrendLookback
333
+ }) => {
334
+ const candidate = getBufferedCandle(state, candidateIndex);
335
+ const high = asNumber(candidate?.high);
336
+ const low = asNumber(candidate?.low);
337
+ if (!candidate || high == null || low == null) return null;
338
+ const window = [];
339
+ for (let index = candidateIndex - lookback; index <= candidateIndex + lookback; index += 1) {
340
+ const candle = getBufferedCandle(state, index);
341
+ if (!candle) return null;
342
+ window.push(candle);
343
+ }
344
+ const otherCandles = window.filter((_, index) => index !== lookback);
345
+ const isHigh = otherCandles.every((candle) => high >= Number(candle.high)) && otherCandles.some((candle) => high > Number(candle.high));
346
+ const isLow = otherCandles.every((candle) => low <= Number(candle.low)) && otherCandles.some((candle) => low < Number(candle.low));
347
+ if (isHigh === isLow) return null;
348
+ const atr = calculateAtrAt({
349
+ state,
350
+ absoluteIndex: candidateIndex,
351
+ period: atrPeriod
352
+ });
353
+ const priorClose = asNumber(
354
+ getBufferedCandle(state, candidateIndex - priorTrendLookback)?.close
355
+ );
356
+ const priorMove = priorClose == null ? null : isHigh ? high - priorClose : priorClose - low;
357
+ return {
358
+ timestamp: candidate.timestamp,
359
+ index: candidateIndex,
360
+ value: isHigh ? high : low,
361
+ kind: isHigh ? "high" : "low",
362
+ atr,
363
+ priorMoveAtr: priorMove != null && atr != null && atr > 0 ? priorMove / atr : null
364
+ };
365
+ };
366
+ var recordPivot = (state, pivot) => {
367
+ if (!pivot) return;
368
+ const latest = state.pivots[state.pivots.length - 1];
369
+ if (!latest || latest.kind !== pivot.kind) {
370
+ state.pivots.push(pivot);
371
+ if (state.pivots.length > 20) state.pivots.shift();
372
+ return;
373
+ }
374
+ const isMoreExtreme = pivot.kind === "high" ? pivot.value >= latest.value : pivot.value <= latest.value;
375
+ if (isMoreExtreme) state.pivots[state.pivots.length - 1] = pivot;
376
+ };
377
+ var lineValueAt = ({
378
+ left,
379
+ right,
380
+ index
381
+ }) => {
382
+ const duration = right.index - left.index;
383
+ if (duration <= 0) return left.value;
384
+ return left.value + (right.value - left.value) / duration * (index - left.index);
385
+ };
386
+ var clonePattern = (pattern) => pattern ? { ...pattern, pivots: [...pattern.pivots] } : null;
387
+ var clonePending = (pending) => pending ? {
388
+ ...pending,
389
+ pattern: clonePattern(pending.pattern)
390
+ } : null;
391
+ var rememberConsumed = (state, setupId) => {
392
+ if (state.consumedSetupIds.includes(setupId)) return;
393
+ state.consumedSetupIds.push(setupId);
394
+ if (state.consumedSetupIds.length > 64) state.consumedSetupIds.shift();
395
+ };
396
+ var isBeyondNeckline = (direction, close, neckline, minimumDistance) => direction === "LONG" ? close >= neckline + minimumDistance : close <= neckline - minimumDistance;
397
+ var buildBreakoutPattern = ({
398
+ state,
399
+ candle,
400
+ prevClose,
401
+ atr,
402
+ direction,
403
+ options
404
+ }) => {
405
+ if (state.pivots.length < 5) return null;
406
+ const pivots = state.pivots.slice(-5);
407
+ const [leftShoulder, leftNeck, head, rightNeck, rightShoulder] = pivots;
408
+ const expectedKinds = direction === "SHORT" ? ["high", "low", "high", "low", "high"] : ["low", "high", "low", "high", "low"];
409
+ if (pivots.some((pivot, index) => pivot.kind !== expectedKinds[index])) {
410
+ return null;
411
+ }
412
+ const close = asNumber(candle.close);
413
+ if (close == null) return null;
414
+ const necklineAtHead = lineValueAt({
415
+ left: leftNeck,
416
+ right: rightNeck,
417
+ index: head.index
418
+ });
419
+ const directionMultiplier = direction === "LONG" ? 1 : -1;
420
+ const headHeight = direction === "LONG" ? necklineAtHead - head.value : head.value - necklineAtHead;
421
+ if (headHeight <= 0) return null;
422
+ const headHeightPct = necklineAtHead !== 0 ? headHeight / Math.abs(necklineAtHead) * 100 : 0;
423
+ const headHeightAtr = atr != null && atr > 0 ? headHeight / atr : 0;
424
+ const leftHeadProminence = direction === "LONG" ? leftShoulder.value - head.value : head.value - leftShoulder.value;
425
+ const rightHeadProminence = direction === "LONG" ? rightShoulder.value - head.value : head.value - rightShoulder.value;
426
+ const leftHeadProminenceRatio = leftHeadProminence / headHeight;
427
+ const rightHeadProminenceRatio = rightHeadProminence / headHeight;
428
+ const shoulderDifferencePct = Math.abs(leftShoulder.value - rightShoulder.value) / headHeight * 100;
429
+ if (headHeightPct < options.minHeadHeightPct || headHeightAtr < options.minHeadHeightAtr || leftHeadProminenceRatio < options.minHeadProminenceRatio || rightHeadProminenceRatio < options.minHeadProminenceRatio || shoulderDifferencePct > options.shoulderTolerancePct) {
430
+ return null;
431
+ }
432
+ const leftShoulderNeckline = lineValueAt({
433
+ left: leftNeck,
434
+ right: rightNeck,
435
+ index: leftShoulder.index
436
+ });
437
+ const rightShoulderNeckline = lineValueAt({
438
+ left: leftNeck,
439
+ right: rightNeck,
440
+ index: rightShoulder.index
441
+ });
442
+ const shouldersRemainOnPatternSide = direction === "LONG" ? leftShoulder.value < leftShoulderNeckline && rightShoulder.value < rightShoulderNeckline : leftShoulder.value > leftShoulderNeckline && rightShoulder.value > rightShoulderNeckline;
443
+ if (!shouldersRemainOnPatternSide) return null;
444
+ const patternDurationBars = rightShoulder.index - leftShoulder.index;
445
+ const leftHalfBars = head.index - leftShoulder.index;
446
+ const rightHalfBars = rightShoulder.index - head.index;
447
+ const patternSymmetryRatio = Math.max(leftHalfBars, rightHalfBars) > 0 ? Math.min(leftHalfBars, rightHalfBars) / Math.max(leftHalfBars, rightHalfBars) : 0;
448
+ const patternAgeBars = state.currentIndex - leftShoulder.index;
449
+ const breakoutDelayBars = state.currentIndex - rightShoulder.index;
450
+ const necklineSlopePerBar = (rightNeck.value - leftNeck.value) / (rightNeck.index - leftNeck.index);
451
+ const necklineSlopeRatio = Math.abs(rightNeck.value - leftNeck.value) / headHeight;
452
+ const priorMoveAtr = leftShoulder.priorMoveAtr ?? null;
453
+ if (patternDurationBars < options.minPatternBars || patternDurationBars > options.maxPatternBars || patternSymmetryRatio < options.minPatternSymmetryRatio || necklineSlopeRatio > options.maxNecklineSlopeRatio || patternAgeBars > options.maxPatternAgeBars || options.maxBreakoutDelayBars > 0 && breakoutDelayBars > options.maxBreakoutDelayBars || options.maxPriorMoveAtr > 0 && (priorMoveAtr == null || priorMoveAtr > options.maxPriorMoveAtr)) {
454
+ return null;
455
+ }
456
+ const neckline = lineValueAt({
457
+ left: leftNeck,
458
+ right: rightNeck,
459
+ index: state.currentIndex
460
+ });
461
+ const breakoutDistance = Math.abs(close - neckline);
462
+ const breakoutDistancePct = neckline !== 0 ? breakoutDistance / Math.abs(neckline) * 100 : 0;
463
+ const breakoutDistanceAtr = atr != null && atr > 0 ? breakoutDistance / atr : 0;
464
+ const breakoutDistanceHeightRatio = breakoutDistance / headHeight;
465
+ if (!isBeyondNeckline(direction, close, neckline, 0) || breakoutDistanceAtr < options.minBreakoutDistanceAtr || options.maxBreakoutDistanceHeightRatio > 0 && breakoutDistanceHeightRatio > options.maxBreakoutDistanceHeightRatio || options.maxBreakoutDistancePct > 0 && breakoutDistancePct > options.maxBreakoutDistancePct) {
466
+ return null;
467
+ }
468
+ const previousNeckline = lineValueAt({
469
+ left: leftNeck,
470
+ right: rightNeck,
471
+ index: state.currentIndex - 1
472
+ });
473
+ const breakoutCrossedOnSignalBar = prevClose != null && (direction === "LONG" ? prevClose <= previousNeckline && close > neckline : prevClose >= previousNeckline && close < neckline);
474
+ if (options.requireBreakoutCross && !breakoutCrossedOnSignalBar) return null;
475
+ const kind = direction === "LONG" ? "inverse_head_and_shoulders" : "head_and_shoulders";
476
+ const setupId = `${kind}:${pivots.map((pivot) => pivot.timestamp).join(":")}`;
477
+ if (state.consumedSetupIds.includes(setupId)) return null;
478
+ return {
479
+ setupId,
480
+ kind,
481
+ direction,
482
+ entryMode: options.entryMode,
483
+ entryStage: "breakout",
484
+ pivots,
485
+ neckline,
486
+ necklineSlopePerBar,
487
+ targetPrice: neckline + directionMultiplier * headHeight * ((direction === "LONG" ? options.targetHeightPctLong : options.targetHeightPctShort) / 100),
488
+ stopLossPrice: head.value - directionMultiplier * headHeight * (options.stopBufferHeightPct / 100),
489
+ headHeight,
490
+ headHeightPct,
491
+ headHeightAtr,
492
+ shoulderDifferencePct,
493
+ leftHeadProminenceRatio,
494
+ rightHeadProminenceRatio,
495
+ patternDurationBars,
496
+ patternSymmetryRatio,
497
+ patternAgeBars,
498
+ necklineSlopeRatio,
499
+ priorMoveAtr,
500
+ breakoutDistancePct,
501
+ breakoutDistanceAtr,
502
+ breakoutDistanceHeightRatio,
503
+ breakoutDelayBars,
504
+ breakoutCrossedOnSignalBar,
505
+ breakoutTimestamp: candle.timestamp,
506
+ confirmationBars: 0,
507
+ confirmationBodyAtr: null,
508
+ confirmationCloseLocation: null,
509
+ confirmationVolumeRel: null,
510
+ timestamp: candle.timestamp,
511
+ close
512
+ };
513
+ };
514
+ var getPatternNecklineAt = (pattern, index) => {
515
+ const leftNeck = pattern.pivots[1];
516
+ return leftNeck.value + pattern.necklineSlopePerBar * (index - leftNeck.index);
517
+ };
518
+ var resolvePending = ({
519
+ state,
520
+ candle,
521
+ atr,
522
+ options
523
+ }) => {
524
+ const pending = state.pending;
525
+ if (!pending) return null;
526
+ const confirmationBars = state.currentIndex - pending.breakoutIndex;
527
+ if (confirmationBars < 1) return null;
528
+ const close = asNumber(candle.close);
529
+ const open = asNumber(candle.open);
530
+ const high = asNumber(candle.high);
531
+ const low = asNumber(candle.low);
532
+ if (close == null || open == null || high == null || low == null) return null;
533
+ const pattern = pending.pattern;
534
+ const invalidated = pattern.direction === "LONG" ? low <= pattern.stopLossPrice : high >= pattern.stopLossPrice;
535
+ const maxBars = pending.mode === "retest" ? options.retestMaxBars : options.confirmationMaxBars;
536
+ if (invalidated || confirmationBars > maxBars) {
537
+ rememberConsumed(state, pending.setupId);
538
+ state.pending = null;
539
+ return null;
540
+ }
541
+ const neckline = getPatternNecklineAt(pattern, state.currentIndex);
542
+ const effectiveAtr = atr != null && atr > 0 ? atr : pattern.headHeight;
543
+ const minimumDistance = effectiveAtr * options.minBreakoutDistanceAtr;
544
+ const closeAccepted = isBeyondNeckline(
545
+ pattern.direction,
546
+ close,
547
+ neckline,
548
+ minimumDistance
549
+ );
550
+ const confirmationBody = pattern.direction === "LONG" ? close - open : open - close;
551
+ const confirmationBodyAtr = effectiveAtr > 0 ? confirmationBody / effectiveAtr : 0;
552
+ const candleRange = high - low;
553
+ const confirmationCloseLocation = candleRange > 0 ? pattern.direction === "LONG" ? (high - close) / candleRange : (close - low) / candleRange : 1;
554
+ const priorVolumeCandles = state.candles.slice(
555
+ -(options.confirmationVolumePeriod + 1),
556
+ -1
557
+ );
558
+ const priorVolumes = priorVolumeCandles.map((item) => asNumber(item.volume)).filter((value) => value != null && value >= 0);
559
+ const averagePriorVolume = priorVolumes.length > 0 ? priorVolumes.reduce((sum, value) => sum + value, 0) / priorVolumes.length : null;
560
+ const currentVolume = asNumber(candle.volume);
561
+ const confirmationVolumeRel = currentVolume != null && averagePriorVolume != null && averagePriorVolume > 0 ? currentVolume / averagePriorVolume : null;
562
+ const confirmationQualityAccepted = (options.minConfirmationBodyAtr <= 0 || confirmationBodyAtr >= options.minConfirmationBodyAtr) && confirmationCloseLocation <= options.maxConfirmationCloseLocation && (options.minConfirmationVolumeRel <= 0 || confirmationVolumeRel != null && confirmationVolumeRel >= options.minConfirmationVolumeRel);
563
+ let entryStage = null;
564
+ if (pending.mode === "close_acceptance") {
565
+ if (closeAccepted && confirmationQualityAccepted) {
566
+ entryStage = "close_accepted";
567
+ }
568
+ } else {
569
+ const tolerance = effectiveAtr * options.retestToleranceAtr;
570
+ const touched = pattern.direction === "LONG" ? low <= neckline + tolerance && low >= neckline - tolerance : high >= neckline - tolerance && high <= neckline + tolerance;
571
+ if (touched && closeAccepted && confirmationQualityAccepted) {
572
+ entryStage = "retest_held";
573
+ }
574
+ }
575
+ if (!entryStage) return null;
576
+ rememberConsumed(state, pending.setupId);
577
+ state.pending = null;
578
+ return {
579
+ ...pattern,
580
+ entryStage,
581
+ neckline,
582
+ confirmationBars,
583
+ confirmationBodyAtr,
584
+ confirmationCloseLocation,
585
+ confirmationVolumeRel,
586
+ timestamp: candle.timestamp,
587
+ close
588
+ };
589
+ };
590
+ var buildHeadAndShouldersSignalContext = (pattern) => ({
591
+ setupId: pattern.setupId,
592
+ patternKind: pattern.kind,
593
+ signalDirection: pattern.direction,
594
+ entryMode: pattern.entryMode,
595
+ entryStage: pattern.entryStage,
596
+ neckline: pattern.neckline,
597
+ necklineSlopePerBar: pattern.necklineSlopePerBar,
598
+ targetPrice: pattern.targetPrice,
599
+ stopLossPrice: pattern.stopLossPrice,
600
+ headHeight: pattern.headHeight,
601
+ headHeightPct: pattern.headHeightPct,
602
+ headHeightAtr: pattern.headHeightAtr,
603
+ shoulderDifferencePct: pattern.shoulderDifferencePct,
604
+ leftHeadProminenceRatio: pattern.leftHeadProminenceRatio,
605
+ rightHeadProminenceRatio: pattern.rightHeadProminenceRatio,
606
+ patternDurationBars: pattern.patternDurationBars,
607
+ patternSymmetryRatio: pattern.patternSymmetryRatio,
608
+ patternAgeBars: pattern.patternAgeBars,
609
+ necklineSlopeRatio: pattern.necklineSlopeRatio,
610
+ priorMoveAtr: pattern.priorMoveAtr,
611
+ breakoutDistancePct: pattern.breakoutDistancePct,
612
+ breakoutDistanceAtr: pattern.breakoutDistanceAtr,
613
+ breakoutDistanceHeightRatio: pattern.breakoutDistanceHeightRatio,
614
+ breakoutDelayBars: pattern.breakoutDelayBars,
615
+ breakoutCrossedOnSignalBar: pattern.breakoutCrossedOnSignalBar,
616
+ breakoutTimestamp: pattern.breakoutTimestamp,
617
+ confirmationBars: pattern.confirmationBars,
618
+ confirmationBodyAtr: pattern.confirmationBodyAtr,
619
+ confirmationCloseLocation: pattern.confirmationCloseLocation,
620
+ confirmationVolumeRel: pattern.confirmationVolumeRel,
621
+ currentPrice: pattern.close,
622
+ pivots: pattern.pivots.map(({ timestamp, value, kind }) => ({
623
+ timestamp,
624
+ value,
625
+ kind
626
+ }))
627
+ });
628
+ var createHeadAndShouldersEngine = ({
629
+ config: config2,
630
+ initialCandles = []
631
+ }) => {
632
+ const options = getConfigNumbers(config2);
633
+ const state = {
634
+ candles: [],
635
+ candleStartIndex: 0,
636
+ currentIndex: -1,
637
+ pivots: [],
638
+ pattern: null,
639
+ pending: null,
640
+ consumedSetupIds: [],
641
+ lastTimestamp: null
642
+ };
643
+ const snapshot = () => ({
644
+ pattern: clonePattern(state.pattern),
645
+ pending: clonePending(state.pending),
646
+ pivots: state.pivots.map((pivot) => ({ ...pivot }))
647
+ });
648
+ const apply = (candle) => {
649
+ if (state.lastTimestamp === candle.timestamp) return snapshot();
650
+ state.lastTimestamp = candle.timestamp;
651
+ state.pattern = null;
652
+ const previous = state.candles[state.candles.length - 1];
653
+ const prevClose = previous ? asNumber(previous.close) : null;
654
+ const maxCandles = Math.max(
655
+ options.pivotLookback * 2 + 1,
656
+ options.atrPeriod + 1,
657
+ options.priorTrendLookback + options.pivotLookback + 1,
658
+ options.confirmationVolumePeriod + 1
659
+ );
660
+ const currentIndex = pushBoundedCandle(state, candle, maxCandles);
661
+ const atr = calculateAtr(state.candles, options.atrPeriod);
662
+ const pendingPattern = resolvePending({ state, candle, atr, options });
663
+ recordPivot(
664
+ state,
665
+ resolveConfirmedPivot({
666
+ state,
667
+ candidateIndex: currentIndex - options.pivotLookback,
668
+ lookback: options.pivotLookback,
669
+ atrPeriod: options.atrPeriod,
670
+ priorTrendLookback: options.priorTrendLookback
671
+ })
672
+ );
673
+ if (pendingPattern) {
674
+ state.pattern = pendingPattern;
675
+ return snapshot();
676
+ }
677
+ if (state.pending) return snapshot();
678
+ const breakout = buildBreakoutPattern({
679
+ state,
680
+ candle,
681
+ prevClose,
682
+ atr,
683
+ direction: "SHORT",
684
+ options
685
+ }) ?? buildBreakoutPattern({
686
+ state,
687
+ candle,
688
+ prevClose,
689
+ atr,
690
+ direction: "LONG",
691
+ options
692
+ });
693
+ if (!breakout) return snapshot();
694
+ if (options.entryMode === "breakout") {
695
+ rememberConsumed(state, breakout.setupId);
696
+ state.pattern = breakout;
697
+ return snapshot();
698
+ }
699
+ state.pending = {
700
+ setupId: breakout.setupId,
701
+ mode: options.entryMode,
702
+ stage: options.entryMode === "retest" ? "retest_pending" : "neckline_crossed",
703
+ breakoutIndex: currentIndex,
704
+ pattern: breakout
705
+ };
706
+ return snapshot();
707
+ };
708
+ for (const candle of initialCandles) apply(candle);
709
+ return { next: apply, getState: snapshot };
710
+ };
711
+
712
+ // src/HeadAndShoulders/figures.ts
713
+ var buildHeadAndShouldersFigures = ({
714
+ pattern,
715
+ entryTimestamp,
716
+ entryPrice
717
+ }) => {
718
+ const color = pattern.direction === "LONG" ? "#22c55e" : "#ef4444";
719
+ const [leftShoulder, leftNeck, head, rightNeck, rightShoulder] = pattern.pivots;
720
+ const patternPoints = [
721
+ leftShoulder,
722
+ leftNeck,
723
+ head,
724
+ rightNeck,
725
+ rightShoulder
726
+ ].map(({ timestamp, value }) => ({ timestamp, value }));
727
+ const lines = [
728
+ {
729
+ id: `headshoulders-pattern-${entryTimestamp}`,
730
+ kind: `headshoulders_${pattern.kind}_pattern`,
731
+ points: [
732
+ ...patternPoints,
733
+ { timestamp: entryTimestamp, value: entryPrice }
734
+ ],
735
+ color,
736
+ width: 2,
737
+ style: "solid"
738
+ },
739
+ {
740
+ id: `headshoulders-neckline-${entryTimestamp}`,
741
+ kind: "headshoulders_neckline",
742
+ points: [
743
+ { timestamp: leftNeck.timestamp, value: leftNeck.value },
744
+ { timestamp: entryTimestamp, value: pattern.neckline }
745
+ ],
746
+ color: "#f59e0b",
747
+ width: 2,
748
+ style: "dashed"
749
+ },
750
+ {
751
+ id: `headshoulders-target-${entryTimestamp}`,
752
+ kind: "headshoulders_target",
753
+ points: [
754
+ { timestamp: rightNeck.timestamp, value: pattern.targetPrice },
755
+ { timestamp: entryTimestamp, value: pattern.targetPrice }
756
+ ],
757
+ color: "#22c55e",
758
+ width: 1,
759
+ style: "dashed"
760
+ },
761
+ {
762
+ id: `headshoulders-stop-${entryTimestamp}`,
763
+ kind: "headshoulders_stop",
764
+ points: [
765
+ { timestamp: head.timestamp, value: pattern.stopLossPrice },
766
+ { timestamp: entryTimestamp, value: pattern.stopLossPrice }
767
+ ],
768
+ color: "#ef4444",
769
+ width: 1,
770
+ style: "dashed"
771
+ }
772
+ ];
773
+ const points = [
774
+ {
775
+ id: `headshoulders-pivots-${entryTimestamp}`,
776
+ kind: `headshoulders_${pattern.kind}_pivots`,
777
+ points: patternPoints,
778
+ color,
779
+ radius: 4
780
+ },
781
+ {
782
+ id: `headshoulders-entry-${entryTimestamp}`,
783
+ kind: "headshoulders_entry",
784
+ points: [{ timestamp: entryTimestamp, value: entryPrice }],
785
+ color,
786
+ radius: 5
787
+ }
788
+ ];
789
+ return { lines, points };
790
+ };
791
+
792
+ // src/HeadAndShoulders/filters.ts
793
+ var import_config2 = require("@tradejs/strategy-kit/config");
794
+ var asPositiveThreshold = (value) => {
795
+ const parsed = Number(value);
796
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
797
+ };
798
+ var getHeadAndShouldersCoreFilterSkipCode = ({
799
+ pattern,
800
+ config: config2,
801
+ baseContext
802
+ }) => {
803
+ const direction = pattern.direction;
804
+ const minBodyStrength = asPositiveThreshold(
805
+ (0, import_config2.resolveDirectionalConfigNumber)({
806
+ config: config2,
807
+ key: "HEADSHOULDERS_MIN_SIGNAL_BODY_STRENGTH",
808
+ direction,
809
+ fallback: 0
810
+ })
811
+ );
812
+ if (minBodyStrength != null) {
813
+ const bodyStrength = Number(baseContext?.regime?.momentum?.bodyStrength);
814
+ if (!Number.isFinite(bodyStrength) || bodyStrength < minBodyStrength) {
815
+ return "HEADSHOULDERS_SIGNAL_BODY_TOO_WEAK";
816
+ }
817
+ }
818
+ const minHeadHeightAtr = asPositiveThreshold(
819
+ (0, import_config2.resolveDirectionalConfigNumber)({
820
+ config: config2,
821
+ key: "HEADSHOULDERS_MIN_ENTRY_HEAD_HEIGHT_ATR",
822
+ direction,
823
+ fallback: 0
824
+ })
825
+ );
826
+ if (minHeadHeightAtr != null && (!Number.isFinite(pattern.headHeightAtr) || pattern.headHeightAtr < minHeadHeightAtr)) {
827
+ return "HEADSHOULDERS_HEAD_TOO_SHALLOW_AT_ENTRY";
828
+ }
829
+ return null;
830
+ };
831
+
832
+ // src/HeadAndShoulders/core.ts
833
+ var import_risk = require("@tradejs/strategy-kit/risk");
834
+ var isOpenPosition = (position) => Boolean(
835
+ position && typeof position.price === "number" && Number.isFinite(position.price) && typeof position.qty === "number" && Number.isFinite(position.qty) && position.qty > 0 && (position.direction === "LONG" || position.direction === "SHORT")
836
+ );
837
+ var buildHeadAndShouldersStateKey = (config2) => JSON.stringify({
838
+ pivotLookback: config2.HEADSHOULDERS_PIVOT_LOOKBACK,
839
+ shoulderTolerancePct: config2.HEADSHOULDERS_SHOULDER_TOLERANCE_PCT,
840
+ minHeadProminenceRatio: config2.HEADSHOULDERS_MIN_HEAD_PROMINENCE_RATIO,
841
+ targetHeightPct: config2.HEADSHOULDERS_TARGET_HEIGHT_PCT,
842
+ targetHeightPctLong: config2.HEADSHOULDERS_TARGET_HEIGHT_PCT_LONG,
843
+ targetHeightPctShort: config2.HEADSHOULDERS_TARGET_HEIGHT_PCT_SHORT,
844
+ stopBufferHeightPct: config2.HEADSHOULDERS_STOP_BUFFER_HEIGHT_PCT,
845
+ minHeadHeightPct: config2.HEADSHOULDERS_MIN_HEAD_HEIGHT_PCT,
846
+ minHeadHeightAtr: config2.HEADSHOULDERS_MIN_HEAD_HEIGHT_ATR,
847
+ atrPeriod: config2.HEADSHOULDERS_ATR_PERIOD,
848
+ minPatternBars: config2.HEADSHOULDERS_MIN_PATTERN_BARS,
849
+ maxPatternBars: config2.HEADSHOULDERS_MAX_PATTERN_BARS,
850
+ minPatternSymmetryRatio: config2.HEADSHOULDERS_MIN_PATTERN_SYMMETRY_RATIO,
851
+ maxNecklineSlopeRatio: config2.HEADSHOULDERS_MAX_NECKLINE_SLOPE_RATIO,
852
+ maxPatternAgeBars: config2.HEADSHOULDERS_MAX_PATTERN_AGE_BARS,
853
+ priorTrendLookback: config2.HEADSHOULDERS_PRIOR_TREND_LOOKBACK,
854
+ maxPriorMoveAtr: config2.HEADSHOULDERS_MAX_PRIOR_MOVE_ATR,
855
+ minBreakoutDistanceAtr: config2.HEADSHOULDERS_MIN_BREAKOUT_DISTANCE_ATR,
856
+ maxBreakoutDistanceHeightRatio: config2.HEADSHOULDERS_MAX_BREAKOUT_DISTANCE_HEIGHT_RATIO,
857
+ maxBreakoutDistancePct: config2.HEADSHOULDERS_MAX_BREAKOUT_DISTANCE_PCT,
858
+ maxBreakoutDelayBars: config2.HEADSHOULDERS_MAX_BREAKOUT_DELAY_BARS,
859
+ requireBreakoutCross: config2.HEADSHOULDERS_REQUIRE_BREAKOUT_CROSS,
860
+ entryMode: config2.HEADSHOULDERS_ENTRY_MODE,
861
+ confirmationMaxBars: config2.HEADSHOULDERS_CONFIRMATION_MAX_BARS,
862
+ minConfirmationBodyAtr: config2.HEADSHOULDERS_MIN_CONFIRMATION_BODY_ATR,
863
+ maxConfirmationCloseLocation: config2.HEADSHOULDERS_MAX_CONFIRMATION_CLOSE_LOCATION,
864
+ confirmationVolumePeriod: config2.HEADSHOULDERS_CONFIRMATION_VOLUME_PERIOD,
865
+ minConfirmationVolumeRel: config2.HEADSHOULDERS_MIN_CONFIRMATION_VOLUME_REL,
866
+ retestMaxBars: config2.HEADSHOULDERS_RETEST_MAX_BARS,
867
+ retestToleranceAtr: config2.HEADSHOULDERS_RETEST_TOLERANCE_ATR
868
+ });
869
+ var createHeadAndShouldersCore = async ({ config: config2, data: initialData, strategyApi, indicatorsState }) => {
870
+ const detectorState = strategyApi.createStateController(
871
+ "HeadAndShoulders",
872
+ () => ({
873
+ engine: createHeadAndShouldersEngine({
874
+ config: config2,
875
+ initialCandles: initialData
876
+ })
877
+ }),
878
+ {
879
+ configKey: buildHeadAndShouldersStateKey(config2),
880
+ snapshot: (state) => state.engine.getState()
881
+ }
882
+ );
883
+ const lastTradeController = strategyApi.createLastTradeController({
884
+ enabled: true
885
+ });
886
+ const nextDetectorState = (candle) => detectorState.oncePerTimestamp(
887
+ candle.timestamp,
888
+ (state) => state.engine.next(candle)
889
+ );
890
+ return async (candle) => {
891
+ const runtimeState = nextDetectorState(candle);
892
+ const pattern = runtimeState.pattern;
893
+ if (!pattern) return strategyApi.skip("NO_PATTERN");
894
+ const position = await strategyApi.getCurrentPosition();
895
+ if (isOpenPosition(position)) {
896
+ const oppositePattern = position.direction !== pattern.direction;
897
+ if (Boolean(config2.HEADSHOULDERS_EXIT_ON_OPPOSITE_PATTERN) && oppositePattern) {
898
+ return strategyApi.exit({
899
+ code: "HEADSHOULDERS_OPPOSITE_PATTERN_EXIT",
900
+ direction: position.direction
901
+ });
902
+ }
903
+ return strategyApi.skip("POSITION_EXISTS");
904
+ }
905
+ if (lastTradeController.isInCooldown(candle.timestamp)) {
906
+ return strategyApi.skip("DEV_TRADE_COOLDOWN");
907
+ }
908
+ const sideConfig = pattern.direction === "LONG" ? config2.LONG : config2.SHORT;
909
+ if (!sideConfig.enable) return strategyApi.skip("STRATEGY_DISABLED");
910
+ const filterSkipCode = getHeadAndShouldersCoreFilterSkipCode({
911
+ pattern,
912
+ config: config2,
913
+ baseContext: strategyApi.getBaseContext()
914
+ });
915
+ if (filterSkipCode) return strategyApi.skip(filterSkipCode);
916
+ const { timestamp, currentPrice } = await strategyApi.getDecisionPriceContext();
917
+ if (!(0, import_risk.isStopLossOnCorrectSide)({
918
+ direction: pattern.direction,
919
+ currentPrice,
920
+ stopLossPrice: pattern.stopLossPrice
921
+ })) {
922
+ return strategyApi.skip("INVALID_STOP");
923
+ }
924
+ const targetIsValid = pattern.direction === "LONG" ? pattern.targetPrice > currentPrice : pattern.targetPrice < currentPrice;
925
+ if (!targetIsValid) return strategyApi.skip("TARGET_ALREADY_PASSED");
926
+ const economics = (0, import_risk.buildTradeEconomics)({
927
+ entryPrice: currentPrice,
928
+ stopLossPrice: pattern.stopLossPrice,
929
+ takeProfitPrice: pattern.targetPrice,
930
+ feeRate: Number(config2.FEE_PERCENT ?? 0),
931
+ slippageBps: Number(config2.SLIPPAGE_BASE_BPS ?? 0) + Number(config2.SLIPPAGE_MARKET_IMPACT_BPS ?? 0)
932
+ });
933
+ const qty = economics.lossPerUnit > 0 ? Number(config2.MAX_LOSS_VALUE ?? 0) / economics.lossPerUnit : 0;
934
+ const riskRatio = economics.netRiskRatio;
935
+ const signalContext = {
936
+ ...buildHeadAndShouldersSignalContext({
937
+ ...pattern,
938
+ close: currentPrice
939
+ }),
940
+ executionEconomics: {
941
+ grossRiskRatio: economics.grossRiskRatio,
942
+ netRiskRatio: economics.netRiskRatio,
943
+ lossPerUnit: economics.lossPerUnit,
944
+ rewardPerUnit: economics.rewardPerUnit
945
+ }
946
+ };
947
+ if (!qty || !Number.isFinite(qty) || qty <= 0) {
948
+ return strategyApi.skip("INVALID_QTY");
949
+ }
950
+ if (riskRatio <= sideConfig.minRiskRatio) {
951
+ return strategyApi.skip(`RISK_RATIO:${(0, import_math.round)(riskRatio)}`);
952
+ }
953
+ const indicators = indicatorsState.snapshot();
954
+ lastTradeController.markTrade(timestamp);
955
+ return strategyApi.entry({
956
+ code: pattern.direction === "LONG" ? `HEADSHOULDERS_INVERSE_${pattern.entryStage.toUpperCase()}` : `HEADSHOULDERS_CLASSIC_${pattern.entryStage.toUpperCase()}`,
957
+ direction: sideConfig.direction,
958
+ indicators,
959
+ additionalIndicators: {
960
+ headAndShouldersContext: signalContext
961
+ },
962
+ figures: buildHeadAndShouldersFigures({
963
+ pattern,
964
+ entryTimestamp: timestamp,
965
+ entryPrice: currentPrice
966
+ }),
967
+ orderPlan: {
968
+ qty,
969
+ stopLossPrice: pattern.stopLossPrice,
970
+ takeProfits: [{ rate: 1, price: pattern.targetPrice }]
971
+ }
972
+ });
973
+ };
974
+ };
975
+
976
+ // src/HeadAndShoulders/adapters/ai.ts
977
+ var import_strategies = require("@tradejs/core/strategies");
978
+ var import_ai_gate = require("@tradejs/strategy-kit/ai-gate");
979
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
980
+ var getContext = (payload) => asRecord(
981
+ asRecord(payload.additionalIndicators).headAndShouldersContext
982
+ );
983
+ var headAndShouldersBaseAiAdapter = {
984
+ buildPayload: ({ signal, basePayload }) => ({
985
+ ...basePayload,
986
+ additionalIndicators: {
987
+ ...asRecord(basePayload.additionalIndicators),
988
+ headAndShouldersContext: asRecord(signal.additionalIndicators).headAndShouldersContext
989
+ }
990
+ }),
991
+ buildHumanPromptAddon: ({ payload }) => {
992
+ const context = getContext(payload);
993
+ return `
994
+ Additional HeadAndShoulders context:
995
+ - patternKind=${context.patternKind ?? "n/a"}
996
+ - signalDirection=${context.signalDirection ?? "n/a"}
997
+ - entryMode=${context.entryMode ?? "n/a"}
998
+ - entryStage=${context.entryStage ?? "n/a"}
999
+ - neckline=${String(context.neckline ?? "n/a")}
1000
+ - necklineSlopeRatio=${String(context.necklineSlopeRatio ?? "n/a")}
1001
+ - headHeightPct=${String(context.headHeightPct ?? "n/a")}
1002
+ - headHeightAtr=${String(context.headHeightAtr ?? "n/a")}
1003
+ - shoulderDifferencePct=${String(context.shoulderDifferencePct ?? "n/a")}
1004
+ - leftHeadProminenceRatio=${String(context.leftHeadProminenceRatio ?? "n/a")}
1005
+ - rightHeadProminenceRatio=${String(context.rightHeadProminenceRatio ?? "n/a")}
1006
+ - patternSymmetryRatio=${String(context.patternSymmetryRatio ?? "n/a")}
1007
+ - priorMoveAtr=${String(context.priorMoveAtr ?? "n/a")}
1008
+ - breakoutDistanceAtr=${String(context.breakoutDistanceAtr ?? "n/a")}
1009
+ - breakoutDistanceHeightRatio=${String(context.breakoutDistanceHeightRatio ?? "n/a")}
1010
+ - breakoutDelayBars=${String(context.breakoutDelayBars ?? "n/a")}
1011
+ - breakoutCrossedOnSignalBar=${String(context.breakoutCrossedOnSignalBar ?? "n/a")}
1012
+ - confirmationBodyAtr=${String(context.confirmationBodyAtr ?? "n/a")}
1013
+ - confirmationCloseLocation=${String(context.confirmationCloseLocation ?? "n/a")}
1014
+ - confirmationVolumeRel=${String(context.confirmationVolumeRel ?? "n/a")}
1015
+
1016
+ Interpretation rules for HeadAndShoulders:
1017
+ - SHORT is a classic head-and-shoulders neckline breakdown; LONG is the mirrored inverse pattern.
1018
+ - Prefer balanced shoulders, a clearly prominent head, a moderate neckline slope, and a compact breakout.
1019
+ - Treat strongly asymmetric patterns, shallow heads, or overextended breakouts as lower quality.
1020
+ `.trim();
1021
+ },
1022
+ mapEntryRuntimeFromConfig: (config2) => (0, import_strategies.mapAiRuntimeFromConfig)(
1023
+ config2
1024
+ )
1025
+ };
1026
+ var headAndShouldersAiAdapter = (0, import_ai_gate.withStrategyLocalAiGate)(
1027
+ headAndShouldersBaseAiAdapter,
1028
+ {
1029
+ id: "head_and_shoulders_short_wick_breadth_2026_08_12",
1030
+ approves: ({ signal, payload }) => {
1031
+ const upperWickPct = (0, import_ai_gate.getAiPayloadNumber)(
1032
+ payload,
1033
+ "additionalIndicators.baseContext.structure.candleQuality.upperWickPct"
1034
+ );
1035
+ const altBasketReturn24h = (0, import_ai_gate.getAiPayloadNumber)(
1036
+ payload,
1037
+ "additionalIndicators.baseContext.relative.btcAltRegime.altBasketReturn24h"
1038
+ );
1039
+ return signal.direction === "SHORT" && upperWickPct != null && upperWickPct <= 0.3 && altBasketReturn24h != null && altBasketReturn24h >= -5e-3;
1040
+ }
1041
+ }
1042
+ );
1043
+
1044
+ // src/HeadAndShoulders/manifest.ts
1045
+ var headAndShouldersManifest = {
1046
+ name: "HeadAndShoulders",
1047
+ aiAdapter: headAndShouldersAiAdapter
1048
+ };
1049
+
1050
+ // src/HeadAndShoulders/strategy.ts
1051
+ var HeadAndShouldersStrategyDefinition = {
1052
+ defaults: config,
1053
+ createCore: createHeadAndShouldersCore,
1054
+ manifest: headAndShouldersManifest
1055
+ };
1056
+
1057
+ // src/index.ts
1058
+ var strategyEntries = [
1059
+ HeadAndShouldersStrategyDefinition
1060
+ ];
1061
+ var defaultConfigs = {
1062
+ HeadAndShoulders: config
1063
+ };
1064
+ var getBuiltInStrategyDefaultConfig = (strategyName) => defaultConfigs[strategyName];
1065
+ var index_default = (0, import_config4.defineStrategyPlugin)({ strategyEntries });
1066
+ // Annotate the CommonJS export names for ESM import in node:
1067
+ 0 && (module.exports = {
1068
+ HeadAndShouldersStrategyDefinition,
1069
+ getBuiltInStrategyDefaultConfig,
1070
+ headAndShouldersAiAdapter,
1071
+ headAndShouldersDefaultConfig,
1072
+ headAndShouldersManifest,
1073
+ strategyEntries
1074
+ });