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