@tradejs/strategy-double-tap 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,1653 @@
1
+ // src/index.ts
2
+ import { defineStrategyPlugin } from "@tradejs/core/config";
3
+
4
+ // src/DoubleTap/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
+ DOUBLETAP_PIVOT_LENGTH: 35,
32
+ DOUBLETAP_PIVOT_TOLERANCE_PCT: 12,
33
+ DOUBLETAP_TARGET_FIB_PCT: 180,
34
+ DOUBLETAP_STOP_FIB_PCT: 0,
35
+ DOUBLETAP_MIN_PATTERN_HEIGHT_PCT: 0.2,
36
+ DOUBLETAP_MIN_PATTERN_HEIGHT_ATR: 1,
37
+ DOUBLETAP_ATR_PERIOD: 14,
38
+ DOUBLETAP_MIN_TAP_SPACING_BARS: 3,
39
+ DOUBLETAP_MAX_PATTERN_AGE_BARS: 180,
40
+ DOUBLETAP_MIN_LEG_SYMMETRY_RATIO: 0.25,
41
+ DOUBLETAP_MIN_BREAKOUT_DISTANCE_ATR: 0.05,
42
+ DOUBLETAP_MAX_BREAKOUT_DISTANCE_HEIGHT_RATIO: 0.8,
43
+ DOUBLETAP_MAX_BREAKOUT_DISTANCE_PCT: 0.8,
44
+ DOUBLETAP_ENTRY_MODE: "close_acceptance",
45
+ DOUBLETAP_CONFIRMATION_MAX_BARS: 2,
46
+ DOUBLETAP_MAX_ENTRY_CONFIRMATION_BARS: 0,
47
+ DOUBLETAP_MAX_ENTRY_CONFIRMATION_BARS_LONG: 1,
48
+ DOUBLETAP_MAX_ENTRY_CONFIRMATION_BARS_SHORT: 0,
49
+ DOUBLETAP_MAX_BB_WIDTH_PCT: 0,
50
+ DOUBLETAP_MAX_BB_WIDTH_PCT_LONG: 0,
51
+ DOUBLETAP_MAX_BB_WIDTH_PCT_SHORT: 6,
52
+ DOUBLETAP_RETEST_MAX_BARS: 4,
53
+ DOUBLETAP_RETEST_TOLERANCE_ATR: 0.25,
54
+ DOUBLETAP_EXIT_ON_OPPOSITE_PATTERN: true,
55
+ LONG: {
56
+ enable: true,
57
+ direction: "LONG",
58
+ minRiskRatio: 0.7
59
+ },
60
+ SHORT: {
61
+ enable: true,
62
+ direction: "SHORT",
63
+ minRiskRatio: 0.7
64
+ }
65
+ };
66
+
67
+ // src/DoubleTap/core.ts
68
+ import { round } from "@tradejs/core/math";
69
+
70
+ // src/DoubleTap/engine.ts
71
+ var asNumber = (value) => {
72
+ const numeric = Number(value);
73
+ return Number.isFinite(numeric) ? numeric : null;
74
+ };
75
+ var clampPositive = (value, fallback) => Number.isFinite(value) && value > 0 ? value : fallback;
76
+ var getConfigNumbers = (config2) => ({
77
+ pivotLength: Math.max(1, Math.floor(config2.DOUBLETAP_PIVOT_LENGTH ?? 50)),
78
+ tolerancePct: clampPositive(config2.DOUBLETAP_PIVOT_TOLERANCE_PCT, 15),
79
+ targetFibPct: Math.max(0, Number(config2.DOUBLETAP_TARGET_FIB_PCT ?? 100)),
80
+ stopFibPct: Number(config2.DOUBLETAP_STOP_FIB_PCT ?? 0),
81
+ minPatternHeightPct: Math.max(
82
+ 0,
83
+ Number(config2.DOUBLETAP_MIN_PATTERN_HEIGHT_PCT ?? 0)
84
+ ),
85
+ minPatternHeightAtr: Math.max(
86
+ 0,
87
+ Number(config2.DOUBLETAP_MIN_PATTERN_HEIGHT_ATR ?? 0)
88
+ ),
89
+ atrPeriod: Math.max(2, Math.floor(config2.DOUBLETAP_ATR_PERIOD ?? 14)),
90
+ minTapSpacingBars: Math.max(
91
+ 1,
92
+ Math.floor(config2.DOUBLETAP_MIN_TAP_SPACING_BARS ?? 1)
93
+ ),
94
+ maxPatternAgeBars: Math.max(
95
+ 1,
96
+ Math.floor(config2.DOUBLETAP_MAX_PATTERN_AGE_BARS ?? 180)
97
+ ),
98
+ minLegSymmetryRatio: Math.min(
99
+ 1,
100
+ Math.max(0, Number(config2.DOUBLETAP_MIN_LEG_SYMMETRY_RATIO ?? 0))
101
+ ),
102
+ minBreakoutDistanceAtr: Math.max(
103
+ 0,
104
+ Number(config2.DOUBLETAP_MIN_BREAKOUT_DISTANCE_ATR ?? 0)
105
+ ),
106
+ maxBreakoutDistanceHeightRatio: Math.max(
107
+ 0,
108
+ Number(config2.DOUBLETAP_MAX_BREAKOUT_DISTANCE_HEIGHT_RATIO ?? 0)
109
+ ),
110
+ maxBreakoutDistancePct: Math.max(
111
+ 0,
112
+ Number(config2.DOUBLETAP_MAX_BREAKOUT_DISTANCE_PCT ?? 0)
113
+ ),
114
+ entryMode: config2.DOUBLETAP_ENTRY_MODE ?? "close_acceptance",
115
+ confirmationMaxBars: Math.max(
116
+ 1,
117
+ Math.floor(config2.DOUBLETAP_CONFIRMATION_MAX_BARS ?? 2)
118
+ ),
119
+ retestMaxBars: Math.max(1, Math.floor(config2.DOUBLETAP_RETEST_MAX_BARS ?? 4)),
120
+ retestToleranceAtr: Math.max(
121
+ 0,
122
+ Number(config2.DOUBLETAP_RETEST_TOLERANCE_ATR ?? 0.25)
123
+ )
124
+ });
125
+ var highest = (candles) => candles.reduce(
126
+ (max, candle) => Math.max(max, Number(candle.high)),
127
+ -Infinity
128
+ );
129
+ var lowest = (candles) => candles.reduce((min, candle) => Math.min(min, Number(candle.low)), Infinity);
130
+ var calculateAtr = (candles, period) => {
131
+ const relevant = candles.slice(-(period + 1));
132
+ if (relevant.length < 2) return null;
133
+ const trueRanges = [];
134
+ for (let index = 1; index < relevant.length; index += 1) {
135
+ const candle = relevant[index];
136
+ const previous = relevant[index - 1];
137
+ const high = asNumber(candle?.high);
138
+ const low = asNumber(candle?.low);
139
+ const previousClose = asNumber(previous?.close);
140
+ if (high == null || low == null || previousClose == null) continue;
141
+ trueRanges.push(
142
+ Math.max(
143
+ high - low,
144
+ Math.abs(high - previousClose),
145
+ Math.abs(low - previousClose)
146
+ )
147
+ );
148
+ }
149
+ if (trueRanges.length === 0) return null;
150
+ return trueRanges.reduce((sum, value) => sum + value, 0) / trueRanges.length;
151
+ };
152
+ var pushBoundedCandle = (state, candle, maxCandles) => {
153
+ state.currentIndex += 1;
154
+ state.candles.push(candle);
155
+ if (state.candles.length > maxCandles) {
156
+ state.candles.splice(0, state.candles.length - maxCandles);
157
+ }
158
+ return state.currentIndex;
159
+ };
160
+ var pushPivot = ({
161
+ pivots,
162
+ candle,
163
+ index,
164
+ kind,
165
+ value
166
+ }) => {
167
+ pivots.push({
168
+ timestamp: candle.timestamp,
169
+ index,
170
+ value,
171
+ kind,
172
+ traded: false
173
+ });
174
+ if (pivots.length > 12) pivots.shift();
175
+ };
176
+ var updateLatestPivot = ({
177
+ pivots,
178
+ candle,
179
+ index,
180
+ kind,
181
+ value
182
+ }) => {
183
+ const latest = pivots[pivots.length - 1];
184
+ if (!latest || latest.kind !== kind) return;
185
+ const isMoreExtreme = kind === "high" ? value > latest.value : value < latest.value;
186
+ if (!isMoreExtreme) return;
187
+ latest.timestamp = candle.timestamp;
188
+ latest.index = index;
189
+ latest.value = value;
190
+ };
191
+ var markTerminal = (state, setup) => {
192
+ state.consumedSetupIds.add(setup.setupId);
193
+ const terminalPivot = state.pivots.find(
194
+ (pivot) => pivot.timestamp === setup.pattern.pivots[3].timestamp
195
+ );
196
+ if (terminalPivot) terminalPivot.traded = true;
197
+ };
198
+ var buildBreakoutPattern = ({
199
+ state,
200
+ candle,
201
+ prevClose,
202
+ atr,
203
+ multiplier,
204
+ tolerancePct,
205
+ targetFibPct,
206
+ stopFibPct,
207
+ minPatternHeightPct,
208
+ minPatternHeightAtr,
209
+ minTapSpacingBars,
210
+ maxPatternAgeBars,
211
+ minLegSymmetryRatio,
212
+ minBreakoutDistanceAtr,
213
+ maxBreakoutDistanceHeightRatio,
214
+ maxBreakoutDistancePct,
215
+ entryMode
216
+ }) => {
217
+ const { pivots } = state;
218
+ if (pivots.length < 5 || prevClose == null) return null;
219
+ const rows = pivots.length;
220
+ const p1 = pivots[rows - 5];
221
+ const p2 = pivots[rows - 4];
222
+ const p3 = pivots[rows - 3];
223
+ const p4 = pivots[rows - 2];
224
+ if (!p1 || !p2 || !p3 || !p4 || p4.traded) return null;
225
+ const close = asNumber(candle.close);
226
+ if (close == null) return null;
227
+ const signedHeight = (p2.value + p4.value) / 2 - p3.value;
228
+ const height = Math.abs(signedHeight);
229
+ if (height <= 0) return null;
230
+ const heightPct = p3.value !== 0 ? height / Math.abs(p3.value) * 100 : 0;
231
+ const patternHeightAtr = atr != null && atr > 0 ? height / atr : 0;
232
+ if (heightPct < minPatternHeightPct || patternHeightAtr < minPatternHeightAtr) {
233
+ return null;
234
+ }
235
+ const upper = p2.value + signedHeight * (tolerancePct / 100);
236
+ const lower = p2.value - signedHeight * (tolerancePct / 100);
237
+ const crossedNeckline = close * multiplier < p3.value * multiplier && !(prevClose * multiplier < p3.value * multiplier);
238
+ const pivotsAligned = p1.value * multiplier < p3.value * multiplier && p4.value * multiplier <= upper * multiplier && p4.value * multiplier >= lower * multiplier;
239
+ if (!crossedNeckline || !pivotsAligned) return null;
240
+ const tapSpacingBars = p4.index - p2.index;
241
+ const firstLegBars = p3.index - p2.index;
242
+ const secondLegBars = p4.index - p3.index;
243
+ const legDurationSymmetryRatio = Math.max(firstLegBars, secondLegBars) > 0 ? Math.min(firstLegBars, secondLegBars) / Math.max(firstLegBars, secondLegBars) : 0;
244
+ const patternAgeBars = state.currentIndex - p2.index;
245
+ if (tapSpacingBars < minTapSpacingBars || patternAgeBars > maxPatternAgeBars || legDurationSymmetryRatio < minLegSymmetryRatio) {
246
+ return null;
247
+ }
248
+ const breakoutDistance = Math.abs(close - p3.value);
249
+ const breakoutDistancePct = p3.value !== 0 ? breakoutDistance / Math.abs(p3.value) * 100 : 0;
250
+ const breakoutDistanceAtr = atr != null && atr > 0 ? breakoutDistance / atr : 0;
251
+ const breakoutDistanceHeightRatio = breakoutDistance / height;
252
+ if (breakoutDistanceAtr < minBreakoutDistanceAtr || maxBreakoutDistanceHeightRatio > 0 && breakoutDistanceHeightRatio > maxBreakoutDistanceHeightRatio || maxBreakoutDistancePct > 0 && breakoutDistancePct > maxBreakoutDistancePct) {
253
+ return null;
254
+ }
255
+ const kind = multiplier === -1 ? "double_bottom" : "double_top";
256
+ const setupId = `${kind}:${p2.timestamp}:${p3.timestamp}:${p4.timestamp}`;
257
+ if (state.consumedSetupIds.has(setupId)) return null;
258
+ const lowerInvalidation = Math.min(p2.value, p4.value);
259
+ const upperInvalidation = Math.max(p2.value, p4.value);
260
+ const stopLossPrice = multiplier === -1 ? lowerInvalidation + height * (stopFibPct / 100) : upperInvalidation - height * (stopFibPct / 100);
261
+ return {
262
+ setupId,
263
+ kind,
264
+ direction: multiplier === -1 ? "LONG" : "SHORT",
265
+ entryMode,
266
+ entryStage: "breakout",
267
+ pivots: [p1, p2, p3, p4],
268
+ neckline: p3.value,
269
+ targetPrice: p3.value - signedHeight * (targetFibPct / 100),
270
+ stopLossPrice,
271
+ height,
272
+ patternHeightAtr,
273
+ patternAgeBars,
274
+ tapSpacingBars,
275
+ legDurationSymmetryRatio,
276
+ pivotTolerancePct: tolerancePct,
277
+ breakoutDistancePct,
278
+ breakoutDistanceAtr,
279
+ breakoutDistanceHeightRatio,
280
+ breakoutTimestamp: candle.timestamp,
281
+ confirmationBars: 0,
282
+ timestamp: candle.timestamp,
283
+ close
284
+ };
285
+ };
286
+ var isBeyondNeckline = (direction, close, neckline, minimumDistance) => direction === "LONG" ? close >= neckline + minimumDistance : close <= neckline - minimumDistance;
287
+ var resolvePending = ({
288
+ state,
289
+ candle,
290
+ atr,
291
+ confirmationMaxBars,
292
+ retestMaxBars,
293
+ retestToleranceAtr,
294
+ minBreakoutDistanceAtr
295
+ }) => {
296
+ const pending = state.pending;
297
+ if (!pending) return null;
298
+ const confirmationBars = state.currentIndex - pending.breakoutIndex;
299
+ if (confirmationBars < 1) return null;
300
+ const close = asNumber(candle.close);
301
+ const high = asNumber(candle.high);
302
+ const low = asNumber(candle.low);
303
+ if (close == null || high == null || low == null) return null;
304
+ const pattern = pending.pattern;
305
+ const invalidated = pattern.direction === "LONG" ? low <= pattern.stopLossPrice : high >= pattern.stopLossPrice;
306
+ const maxBars = pending.mode === "retest" ? retestMaxBars : confirmationMaxBars;
307
+ if (invalidated || confirmationBars > maxBars) {
308
+ markTerminal(state, pending);
309
+ state.pending = null;
310
+ return null;
311
+ }
312
+ const effectiveAtr = atr != null && atr > 0 ? atr : pattern.height;
313
+ const minimumDistance = effectiveAtr * minBreakoutDistanceAtr;
314
+ const closeAccepted = isBeyondNeckline(
315
+ pattern.direction,
316
+ close,
317
+ pattern.neckline,
318
+ minimumDistance
319
+ );
320
+ let entryStage = null;
321
+ if (pending.mode === "close_acceptance") {
322
+ if (closeAccepted) entryStage = "close_accepted";
323
+ } else {
324
+ const tolerance = effectiveAtr * retestToleranceAtr;
325
+ const touched = pattern.direction === "LONG" ? low <= pattern.neckline + tolerance && low >= pattern.neckline - tolerance : high >= pattern.neckline - tolerance && high <= pattern.neckline + tolerance;
326
+ if (touched && closeAccepted) entryStage = "retest_held";
327
+ }
328
+ if (!entryStage) return null;
329
+ markTerminal(state, pending);
330
+ state.pending = null;
331
+ return {
332
+ ...pattern,
333
+ entryStage,
334
+ confirmationBars,
335
+ timestamp: candle.timestamp,
336
+ close
337
+ };
338
+ };
339
+ var clonePending = (pending) => pending ? {
340
+ ...pending,
341
+ pattern: { ...pending.pattern, pivots: [...pending.pattern.pivots] }
342
+ } : null;
343
+ var buildDoubleTapSignalContext = (pattern) => ({
344
+ setupId: pattern.setupId,
345
+ patternKind: pattern.kind,
346
+ signalDirection: pattern.direction,
347
+ entryMode: pattern.entryMode,
348
+ entryStage: pattern.entryStage,
349
+ neckline: pattern.neckline,
350
+ targetPrice: pattern.targetPrice,
351
+ stopLossPrice: pattern.stopLossPrice,
352
+ height: pattern.height,
353
+ patternHeightAtr: pattern.patternHeightAtr,
354
+ patternAgeBars: pattern.patternAgeBars,
355
+ tapSpacingBars: pattern.tapSpacingBars,
356
+ legDurationSymmetryRatio: pattern.legDurationSymmetryRatio,
357
+ pivotTolerancePct: pattern.pivotTolerancePct,
358
+ breakoutDistancePct: pattern.breakoutDistancePct,
359
+ breakoutDistanceAtr: pattern.breakoutDistanceAtr,
360
+ breakoutDistanceHeightRatio: pattern.breakoutDistanceHeightRatio,
361
+ breakoutTimestamp: pattern.breakoutTimestamp,
362
+ confirmationBars: pattern.confirmationBars,
363
+ currentPrice: pattern.close,
364
+ pivots: pattern.pivots.map(({ timestamp, value, kind }) => ({
365
+ timestamp,
366
+ value,
367
+ kind
368
+ }))
369
+ });
370
+ var createDoubleTapEngine = ({
371
+ config: config2,
372
+ initialCandles = []
373
+ }) => {
374
+ const options = getConfigNumbers(config2);
375
+ const state = {
376
+ candles: [],
377
+ currentIndex: -1,
378
+ pivots: [],
379
+ dir: null,
380
+ pattern: null,
381
+ pending: null,
382
+ consumedSetupIds: /* @__PURE__ */ new Set(),
383
+ lastTimestamp: null
384
+ };
385
+ const snapshot = () => ({
386
+ pattern: state.pattern ? { ...state.pattern, pivots: [...state.pattern.pivots] } : null,
387
+ pending: clonePending(state.pending),
388
+ pivots: state.pivots.map((pivot) => ({ ...pivot }))
389
+ });
390
+ const apply = (candle) => {
391
+ if (state.lastTimestamp === candle.timestamp) return snapshot();
392
+ state.lastTimestamp = candle.timestamp;
393
+ state.pattern = null;
394
+ const previous = state.candles[state.candles.length - 1];
395
+ const prevClose = previous ? asNumber(previous.close) : null;
396
+ const maxCandles = Math.max(options.pivotLength + 1, options.atrPeriod + 1);
397
+ const currentIndex = pushBoundedCandle(state, candle, maxCandles);
398
+ const atr = calculateAtr(state.candles, options.atrPeriod);
399
+ const pendingPattern = resolvePending({
400
+ state,
401
+ candle,
402
+ atr,
403
+ confirmationMaxBars: options.confirmationMaxBars,
404
+ retestMaxBars: options.retestMaxBars,
405
+ retestToleranceAtr: options.retestToleranceAtr,
406
+ minBreakoutDistanceAtr: options.minBreakoutDistanceAtr
407
+ });
408
+ const window = state.candles.slice(-options.pivotLength);
409
+ const high = asNumber(candle.high);
410
+ const low = asNumber(candle.low);
411
+ const currentIsHigh = high != null && high >= highest(window);
412
+ const currentIsLow = low != null && low <= lowest(window);
413
+ const nextDir = currentIsHigh ? 1 : currentIsLow ? 0 : state.dir;
414
+ const dirChanged = state.dir != null && nextDir !== state.dir;
415
+ if (dirChanged && nextDir === 1 && high != null) {
416
+ pushPivot({
417
+ pivots: state.pivots,
418
+ candle,
419
+ index: currentIndex,
420
+ kind: "high",
421
+ value: high
422
+ });
423
+ }
424
+ if (dirChanged && nextDir === 0 && low != null) {
425
+ pushPivot({
426
+ pivots: state.pivots,
427
+ candle,
428
+ index: currentIndex,
429
+ kind: "low",
430
+ value: low
431
+ });
432
+ }
433
+ if (!dirChanged && nextDir === 1 && high != null) {
434
+ updateLatestPivot({
435
+ pivots: state.pivots,
436
+ candle,
437
+ index: currentIndex,
438
+ kind: "high",
439
+ value: high
440
+ });
441
+ }
442
+ if (!dirChanged && nextDir === 0 && low != null) {
443
+ updateLatestPivot({
444
+ pivots: state.pivots,
445
+ candle,
446
+ index: currentIndex,
447
+ kind: "low",
448
+ value: low
449
+ });
450
+ }
451
+ state.dir = nextDir;
452
+ if (pendingPattern) {
453
+ state.pattern = pendingPattern;
454
+ return snapshot();
455
+ }
456
+ if (state.pending) return snapshot();
457
+ const common = {
458
+ state,
459
+ candle,
460
+ prevClose,
461
+ atr,
462
+ tolerancePct: options.tolerancePct,
463
+ targetFibPct: options.targetFibPct,
464
+ stopFibPct: options.stopFibPct,
465
+ minPatternHeightPct: options.minPatternHeightPct,
466
+ minPatternHeightAtr: options.minPatternHeightAtr,
467
+ minTapSpacingBars: options.minTapSpacingBars,
468
+ maxPatternAgeBars: options.maxPatternAgeBars,
469
+ minLegSymmetryRatio: options.minLegSymmetryRatio,
470
+ minBreakoutDistanceAtr: options.minBreakoutDistanceAtr,
471
+ maxBreakoutDistanceHeightRatio: options.maxBreakoutDistanceHeightRatio,
472
+ maxBreakoutDistancePct: options.maxBreakoutDistancePct,
473
+ entryMode: options.entryMode
474
+ };
475
+ const breakout = buildBreakoutPattern({ ...common, multiplier: 1 }) ?? buildBreakoutPattern({ ...common, multiplier: -1 });
476
+ if (!breakout) return snapshot();
477
+ if (options.entryMode === "breakout") {
478
+ const terminal = {
479
+ setupId: breakout.setupId,
480
+ mode: "close_acceptance",
481
+ stage: "neckline_crossed",
482
+ breakoutIndex: currentIndex,
483
+ pattern: breakout
484
+ };
485
+ markTerminal(state, terminal);
486
+ state.pattern = breakout;
487
+ return snapshot();
488
+ }
489
+ state.pending = {
490
+ setupId: breakout.setupId,
491
+ mode: options.entryMode,
492
+ stage: options.entryMode === "retest" ? "retest_pending" : "neckline_crossed",
493
+ breakoutIndex: currentIndex,
494
+ pattern: breakout
495
+ };
496
+ return snapshot();
497
+ };
498
+ for (const candle of initialCandles) apply(candle);
499
+ return { next: apply, getState: snapshot };
500
+ };
501
+
502
+ // src/DoubleTap/figures.ts
503
+ var buildDoubleTapFigures = ({
504
+ pattern,
505
+ entryTimestamp,
506
+ entryPrice
507
+ }) => {
508
+ const color = pattern.direction === "LONG" ? "#22c55e" : "#ef4444";
509
+ const [p1, p2, p3, p4] = pattern.pivots;
510
+ const patternPoints = [
511
+ { timestamp: p1.timestamp, value: p1.value },
512
+ { timestamp: p2.timestamp, value: p2.value },
513
+ { timestamp: p3.timestamp, value: p3.value },
514
+ { timestamp: p4.timestamp, value: p4.value },
515
+ { timestamp: entryTimestamp, value: entryPrice }
516
+ ];
517
+ const lines = [
518
+ {
519
+ id: `doubletap-pattern-${entryTimestamp}`,
520
+ kind: `doubletap_${pattern.kind}_pattern`,
521
+ points: patternPoints,
522
+ color,
523
+ width: 2,
524
+ style: "solid"
525
+ },
526
+ {
527
+ id: `doubletap-neckline-${entryTimestamp}`,
528
+ kind: "doubletap_neckline",
529
+ points: [
530
+ { timestamp: p3.timestamp, value: pattern.neckline },
531
+ { timestamp: entryTimestamp, value: pattern.neckline }
532
+ ],
533
+ color: "#f59e0b",
534
+ width: 2,
535
+ style: "dashed"
536
+ },
537
+ {
538
+ id: `doubletap-target-${entryTimestamp}`,
539
+ kind: "doubletap_target",
540
+ points: [
541
+ { timestamp: p3.timestamp, value: pattern.targetPrice },
542
+ { timestamp: entryTimestamp, value: pattern.targetPrice }
543
+ ],
544
+ color: "#22c55e",
545
+ width: 1,
546
+ style: "dashed"
547
+ },
548
+ {
549
+ id: `doubletap-stop-${entryTimestamp}`,
550
+ kind: "doubletap_stop",
551
+ points: [
552
+ { timestamp: p3.timestamp, value: pattern.stopLossPrice },
553
+ { timestamp: entryTimestamp, value: pattern.stopLossPrice }
554
+ ],
555
+ color: "#ef4444",
556
+ width: 1,
557
+ style: "dashed"
558
+ }
559
+ ];
560
+ const points = [
561
+ {
562
+ id: `doubletap-pivots-${entryTimestamp}`,
563
+ kind: `doubletap_${pattern.kind}_pivots`,
564
+ points: patternPoints.slice(0, 4),
565
+ color,
566
+ radius: 4
567
+ },
568
+ {
569
+ id: `doubletap-entry-${entryTimestamp}`,
570
+ kind: "doubletap_entry",
571
+ points: [{ timestamp: entryTimestamp, value: entryPrice }],
572
+ color,
573
+ radius: 5
574
+ }
575
+ ];
576
+ return { lines, points };
577
+ };
578
+
579
+ // src/DoubleTap/filters.ts
580
+ import { resolveDirectionalConfigNumber } from "@tradejs/strategy-kit/config";
581
+ var asPositiveThreshold = (value) => {
582
+ const parsed = Number(value);
583
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
584
+ };
585
+ var getDoubleTapCoreFilterSkipCode = ({
586
+ config: config2,
587
+ pattern,
588
+ baseContext
589
+ }) => {
590
+ const maxConfirmationBars = asPositiveThreshold(
591
+ resolveDirectionalConfigNumber({
592
+ config: config2,
593
+ key: "DOUBLETAP_MAX_ENTRY_CONFIRMATION_BARS",
594
+ direction: pattern.direction,
595
+ fallback: 0
596
+ })
597
+ );
598
+ if (maxConfirmationBars != null && pattern.confirmationBars > maxConfirmationBars) {
599
+ return "DOUBLETAP_CONFIRMATION_TOO_LATE";
600
+ }
601
+ const maxBbWidthPct = asPositiveThreshold(
602
+ resolveDirectionalConfigNumber({
603
+ config: config2,
604
+ key: "DOUBLETAP_MAX_BB_WIDTH_PCT",
605
+ direction: pattern.direction,
606
+ fallback: 0
607
+ })
608
+ );
609
+ if (maxBbWidthPct != null) {
610
+ const bbWidthPct = Number(baseContext?.raw?.volatility?.bbWidthPct);
611
+ if (!Number.isFinite(bbWidthPct) || bbWidthPct > maxBbWidthPct) {
612
+ return "DOUBLETAP_VOLATILITY_TOO_WIDE";
613
+ }
614
+ }
615
+ return null;
616
+ };
617
+
618
+ // src/DoubleTap/core.ts
619
+ import {
620
+ buildTradeEconomics,
621
+ isStopLossOnCorrectSide
622
+ } from "@tradejs/strategy-kit/risk";
623
+ var isOpenPosition = (position) => Boolean(
624
+ 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")
625
+ );
626
+ var buildDoubleTapStateKey = (config2) => JSON.stringify({
627
+ pivotLength: config2.DOUBLETAP_PIVOT_LENGTH,
628
+ pivotTolerancePct: config2.DOUBLETAP_PIVOT_TOLERANCE_PCT,
629
+ targetFibPct: config2.DOUBLETAP_TARGET_FIB_PCT,
630
+ stopFibPct: config2.DOUBLETAP_STOP_FIB_PCT,
631
+ minPatternHeightPct: config2.DOUBLETAP_MIN_PATTERN_HEIGHT_PCT,
632
+ minPatternHeightAtr: config2.DOUBLETAP_MIN_PATTERN_HEIGHT_ATR,
633
+ atrPeriod: config2.DOUBLETAP_ATR_PERIOD,
634
+ minTapSpacingBars: config2.DOUBLETAP_MIN_TAP_SPACING_BARS,
635
+ maxPatternAgeBars: config2.DOUBLETAP_MAX_PATTERN_AGE_BARS,
636
+ minLegSymmetryRatio: config2.DOUBLETAP_MIN_LEG_SYMMETRY_RATIO,
637
+ minBreakoutDistanceAtr: config2.DOUBLETAP_MIN_BREAKOUT_DISTANCE_ATR,
638
+ maxBreakoutDistanceHeightRatio: config2.DOUBLETAP_MAX_BREAKOUT_DISTANCE_HEIGHT_RATIO,
639
+ maxBreakoutDistancePct: config2.DOUBLETAP_MAX_BREAKOUT_DISTANCE_PCT,
640
+ entryMode: config2.DOUBLETAP_ENTRY_MODE,
641
+ confirmationMaxBars: config2.DOUBLETAP_CONFIRMATION_MAX_BARS,
642
+ retestMaxBars: config2.DOUBLETAP_RETEST_MAX_BARS,
643
+ retestToleranceAtr: config2.DOUBLETAP_RETEST_TOLERANCE_ATR
644
+ });
645
+ var createDoubleTapCore = async ({ config: config2, data: initialData, strategyApi, indicatorsState }) => {
646
+ const detectorState = strategyApi.createStateController(
647
+ "DoubleTap",
648
+ () => ({
649
+ engine: createDoubleTapEngine({
650
+ config: config2,
651
+ initialCandles: initialData
652
+ })
653
+ }),
654
+ {
655
+ configKey: buildDoubleTapStateKey(config2),
656
+ snapshot: (state) => state.engine.getState()
657
+ }
658
+ );
659
+ const lastTradeController = strategyApi.createLastTradeController({
660
+ enabled: true
661
+ });
662
+ const nextDetectorState = (candle) => detectorState.oncePerTimestamp(
663
+ candle.timestamp,
664
+ (state) => state.engine.next(candle)
665
+ );
666
+ return async (candle) => {
667
+ const runtimeState = nextDetectorState(candle);
668
+ const pattern = runtimeState.pattern;
669
+ if (!pattern) {
670
+ return strategyApi.skip("NO_PATTERN");
671
+ }
672
+ const position = await strategyApi.getCurrentPosition();
673
+ if (isOpenPosition(position)) {
674
+ const oppositePattern = position.direction === "LONG" ? pattern.direction === "SHORT" : pattern.direction === "LONG";
675
+ if (Boolean(config2.DOUBLETAP_EXIT_ON_OPPOSITE_PATTERN) && oppositePattern) {
676
+ return strategyApi.exit({
677
+ code: "DOUBLETAP_OPPOSITE_PATTERN_EXIT",
678
+ direction: position.direction
679
+ });
680
+ }
681
+ return strategyApi.skip("POSITION_EXISTS");
682
+ }
683
+ if (lastTradeController.isInCooldown(candle.timestamp)) {
684
+ return strategyApi.skip("DEV_TRADE_COOLDOWN");
685
+ }
686
+ const modeConfig = pattern.direction === "LONG" ? config2.LONG : config2.SHORT;
687
+ if (!modeConfig.enable) {
688
+ return strategyApi.skip("STRATEGY_DISABLED");
689
+ }
690
+ const coreFilterSkipCode = getDoubleTapCoreFilterSkipCode({
691
+ config: config2,
692
+ pattern,
693
+ baseContext: strategyApi.getBaseContext()
694
+ });
695
+ if (coreFilterSkipCode) {
696
+ return strategyApi.skip(coreFilterSkipCode);
697
+ }
698
+ const { timestamp, currentPrice } = await strategyApi.getDecisionPriceContext();
699
+ if (!isStopLossOnCorrectSide({
700
+ direction: pattern.direction,
701
+ currentPrice,
702
+ stopLossPrice: pattern.stopLossPrice
703
+ })) {
704
+ return strategyApi.skip("INVALID_STOP");
705
+ }
706
+ const targetIsValid = pattern.direction === "LONG" ? pattern.targetPrice > currentPrice : pattern.targetPrice < currentPrice;
707
+ if (!targetIsValid) {
708
+ return strategyApi.skip("TARGET_ALREADY_PASSED");
709
+ }
710
+ const economics = buildTradeEconomics({
711
+ entryPrice: currentPrice,
712
+ stopLossPrice: pattern.stopLossPrice,
713
+ takeProfitPrice: pattern.targetPrice,
714
+ feeRate: Number(config2.FEE_PERCENT ?? 0),
715
+ slippageBps: Number(config2.SLIPPAGE_BASE_BPS ?? 0) + Number(config2.SLIPPAGE_MARKET_IMPACT_BPS ?? 0)
716
+ });
717
+ const qty = economics.lossPerUnit > 0 ? Number(config2.MAX_LOSS_VALUE ?? 0) / economics.lossPerUnit : 0;
718
+ const riskRatio = economics.netRiskRatio;
719
+ const signalContext = {
720
+ ...buildDoubleTapSignalContext({ ...pattern, close: currentPrice }),
721
+ executionEconomics: {
722
+ grossRiskRatio: economics.grossRiskRatio,
723
+ netRiskRatio: economics.netRiskRatio,
724
+ lossPerUnit: economics.lossPerUnit,
725
+ rewardPerUnit: economics.rewardPerUnit
726
+ }
727
+ };
728
+ if (!qty || !Number.isFinite(qty) || qty <= 0) {
729
+ return strategyApi.skip("INVALID_QTY");
730
+ }
731
+ if (riskRatio <= modeConfig.minRiskRatio) {
732
+ return strategyApi.skip(`RISK_RATIO:${round(riskRatio)}`);
733
+ }
734
+ const indicators = indicatorsState.snapshot();
735
+ lastTradeController.markTrade(timestamp);
736
+ return strategyApi.entry({
737
+ code: pattern.direction === "LONG" ? `DOUBLETAP_DOUBLE_BOTTOM_${pattern.entryStage.toUpperCase()}` : `DOUBLETAP_DOUBLE_TOP_${pattern.entryStage.toUpperCase()}`,
738
+ direction: modeConfig.direction,
739
+ indicators,
740
+ additionalIndicators: {
741
+ doubleTapContext: signalContext
742
+ },
743
+ figures: buildDoubleTapFigures({
744
+ pattern,
745
+ entryTimestamp: timestamp,
746
+ entryPrice: currentPrice
747
+ }),
748
+ orderPlan: {
749
+ qty,
750
+ stopLossPrice: pattern.stopLossPrice,
751
+ takeProfits: [{ rate: 1, price: pattern.targetPrice }]
752
+ }
753
+ });
754
+ };
755
+ };
756
+
757
+ // src/DoubleTap/adapters/ai.ts
758
+ import { intervalToMs } from "@tradejs/core/data";
759
+ import { mapAiRuntimeFromConfig } from "@tradejs/core/strategies";
760
+ import {
761
+ getAiPayloadNumber,
762
+ withStrategyLocalAiGate
763
+ } from "@tradejs/strategy-kit/ai-gate";
764
+ var MIN_EXECUTION_SCORE_FOR_AI_GATE = 35;
765
+ var MIN_LOW_TOUCH_COUNT20_FOR_AI_GATE = 1;
766
+ var HIGH_PRECISION_CMC_ALT_VOLUME_CHANGE_MAX = 0.5;
767
+ var Q4_CMC_BTC_DOMINANCE_CHANGE_MIN = -0.3;
768
+ var Q4_CMC_BTC_DOMINANCE_CHANGE_MAX = -0.05;
769
+ var Q4_ALT_DISPERSION_24H_MAX = 0.06;
770
+ var Q4_DERIVATIVES_BTC_VS_ALT_RETURN_24H_MAX = -9e-3;
771
+ var Q4_DERIVATIVES_ETH_CROWDING_PERSISTENCE_MIN = 140;
772
+ var Q4_DERIVATIVES_SOL_FUNDING_Z_SCORE_15M_MAX = 0.2;
773
+ var Q4_DERIVATIVES_BAD_BTC_VS_ALT_RETURN_24H_MAX = -0.014;
774
+ var Q4_DERIVATIVES_BAD_CMC20_TO_CMC100_CHANGE_24H_MAX = -7e-4;
775
+ var BNB_OI_ROTATION_CHANGE_24H_15M_MIN = 0.65;
776
+ var BNB_OI_ROTATION_CHANGE_1H_1H_MAX = -0.28;
777
+ var XRP_OI_SHORT_NO_HTF_CHANGE_1H_15M_MIN = 0.32;
778
+ var ETH_VOLUME_BREADTH_ETH_VS_BTC_VOLUME_RATIO_MAX = 0.39;
779
+ var ETH_VOLUME_BREADTH_TOP5_DISPERSION_MAX = 7e-4;
780
+ var ETH_VOLUME_BREADTH_ETH_DOMINANCE_CHANGE_MIN = -0.05;
781
+ var APPROVAL_ETH_VS_BTC_VOLUME_RATIO_MIN = 0.34;
782
+ var STRICT_MOMENTUM_ROC1D_MIN = -5.25;
783
+ var PROTECTIVE_RELATIVE_STRENGTH_4H_MAX = 7.77009;
784
+ var PROTECTIVE_CMC_EXCHANGE_LIQUIDITY_CHANGE_24H_MIN = -0.378662;
785
+ var PROTECTIVE_BNB_REFERENCE_PRESSURE_BLOCK = "crowded_long";
786
+ var asRecord = (value) => typeof value === "object" && value != null && !Array.isArray(value) ? value : null;
787
+ var asNumber2 = (value) => {
788
+ if (typeof value === "number") {
789
+ return Number.isFinite(value) ? value : null;
790
+ }
791
+ if (typeof value === "string" && value.trim().length > 0) {
792
+ const numeric = Number(value);
793
+ return Number.isFinite(numeric) ? numeric : null;
794
+ }
795
+ return null;
796
+ };
797
+ var divideFinite = (numerator, denominator) => numerator != null && denominator != null && Number.isFinite(numerator) && Number.isFinite(denominator) && Math.abs(denominator) > Number.EPSILON ? numerator / Math.abs(denominator) : null;
798
+ var resolveIntervalMs = (interval) => {
799
+ if (typeof interval !== "string" || interval.length === 0) {
800
+ return null;
801
+ }
802
+ try {
803
+ const value = intervalToMs(interval);
804
+ return Number.isFinite(value) && value > 0 ? value : null;
805
+ } catch {
806
+ return null;
807
+ }
808
+ };
809
+ var resolveElapsedBars = ({
810
+ from,
811
+ to,
812
+ intervalMs
813
+ }) => {
814
+ if (from == null || to == null || intervalMs == null || to < from) {
815
+ return null;
816
+ }
817
+ const bars = (to - from) / intervalMs;
818
+ return Number.isFinite(bars) ? bars : null;
819
+ };
820
+ var resolveSlopePctPerBar = ({
821
+ fromValue,
822
+ toValue,
823
+ bars
824
+ }) => {
825
+ if (fromValue == null || toValue == null || bars == null || bars <= 0 || Math.abs(fromValue) <= Number.EPSILON) {
826
+ return null;
827
+ }
828
+ return Math.abs(toValue - fromValue) / Math.abs(fromValue) / bars * 100;
829
+ };
830
+ var buildDoubleTapSetupFeatures = ({
831
+ context,
832
+ interval,
833
+ signalTimestamp
834
+ }) => {
835
+ const pivots = Array.isArray(context.pivots) ? context.pivots : [];
836
+ const readPivotNumber = (index, key) => asNumber2(asRecord(pivots[index])?.[key]);
837
+ const p1Timestamp = readPivotNumber(0, "timestamp");
838
+ const p2Timestamp = readPivotNumber(1, "timestamp");
839
+ const p3Timestamp = readPivotNumber(2, "timestamp");
840
+ const p4Timestamp = readPivotNumber(3, "timestamp");
841
+ const p2Value = readPivotNumber(1, "value");
842
+ const p3Value = readPivotNumber(2, "value");
843
+ const p4Value = readPivotNumber(3, "value");
844
+ const intervalMs = resolveIntervalMs(interval);
845
+ const leadInBars = resolveElapsedBars({
846
+ from: p1Timestamp,
847
+ to: p2Timestamp,
848
+ intervalMs
849
+ });
850
+ const firstLegBars = resolveElapsedBars({
851
+ from: p2Timestamp,
852
+ to: p3Timestamp,
853
+ intervalMs
854
+ });
855
+ const secondLegBars = resolveElapsedBars({
856
+ from: p3Timestamp,
857
+ to: p4Timestamp,
858
+ intervalMs
859
+ });
860
+ const tapSpacingBars = resolveElapsedBars({
861
+ from: p2Timestamp,
862
+ to: p4Timestamp,
863
+ intervalMs
864
+ });
865
+ const breakoutLagBars = resolveElapsedBars({
866
+ from: p4Timestamp,
867
+ to: asNumber2(signalTimestamp),
868
+ intervalMs
869
+ });
870
+ const heightValue = asNumber2(context.height);
871
+ const height = heightValue == null ? null : Math.abs(heightValue);
872
+ const neckline = asNumber2(context.neckline);
873
+ const currentPrice = asNumber2(context.currentPrice);
874
+ const stopLossPrice = asNumber2(context.stopLossPrice);
875
+ const targetPrice = asNumber2(context.targetPrice);
876
+ const breakoutDistance = currentPrice == null || neckline == null ? null : Math.abs(currentPrice - neckline);
877
+ const breakoutDistanceHeightRatio = divideFinite(breakoutDistance, height);
878
+ const tapPriceDeviation = p2Value == null || p4Value == null ? null : Math.abs(p2Value - p4Value);
879
+ const averageTapPrice = p2Value == null || p4Value == null ? null : (Math.abs(p2Value) + Math.abs(p4Value)) / 2;
880
+ const patternHeightPriceRatio = divideFinite(height, neckline);
881
+ const tapPriceDeviationRatio = divideFinite(
882
+ tapPriceDeviation,
883
+ averageTapPrice
884
+ );
885
+ const firstLegSlopePctPerBar = resolveSlopePctPerBar({
886
+ fromValue: p2Value,
887
+ toValue: p3Value,
888
+ bars: firstLegBars
889
+ });
890
+ const secondLegSlopePctPerBar = resolveSlopePctPerBar({
891
+ fromValue: p3Value,
892
+ toValue: p4Value,
893
+ bars: secondLegBars
894
+ });
895
+ return {
896
+ geometry: {
897
+ patternHeightPct: patternHeightPriceRatio == null ? null : patternHeightPriceRatio * 100,
898
+ breakoutDistanceHeightRatio,
899
+ tapPriceDeviationPct: tapPriceDeviationRatio == null ? null : tapPriceDeviationRatio * 100,
900
+ tapDeviationHeightRatio: divideFinite(tapPriceDeviation, height),
901
+ stopDistanceHeightRatio: currentPrice == null || stopLossPrice == null ? null : divideFinite(Math.abs(currentPrice - stopLossPrice), height),
902
+ targetDistanceHeightRatio: currentPrice == null || targetPrice == null ? null : divideFinite(Math.abs(targetPrice - currentPrice), height)
903
+ },
904
+ path: {
905
+ leadInBars,
906
+ firstLegBars,
907
+ secondLegBars,
908
+ tapSpacingBars,
909
+ breakoutLagBars,
910
+ legDurationSymmetryRatio: firstLegBars == null || secondLegBars == null ? null : divideFinite(
911
+ Math.min(firstLegBars, secondLegBars),
912
+ Math.max(firstLegBars, secondLegBars)
913
+ ),
914
+ firstLegSlopePctPerBar,
915
+ secondLegSlopePctPerBar,
916
+ legSlopeSymmetryRatio: firstLegSlopePctPerBar == null || secondLegSlopePctPerBar == null ? null : divideFinite(
917
+ Math.min(firstLegSlopePctPerBar, secondLegSlopePctPerBar),
918
+ Math.max(firstLegSlopePctPerBar, secondLegSlopePctPerBar)
919
+ ),
920
+ breakoutSpeedHeightRatioPerBar: breakoutDistanceHeightRatio == null || breakoutLagBars == null ? null : breakoutDistanceHeightRatio / Math.max(1, breakoutLagBars)
921
+ }
922
+ };
923
+ };
924
+ var getDoubleTapContext = (payload) => {
925
+ const additional = asRecord(payload.additionalIndicators);
926
+ return (additional?.doubleTapContext ?? {}) || {};
927
+ };
928
+ var getNestedRecord = (record, path) => path.reduce(
929
+ (current, key) => asRecord(current?.[key]),
930
+ record
931
+ );
932
+ var getNestedNumber = (record, path) => asNumber2(
933
+ path.reduce((current, key) => {
934
+ return asRecord(current)?.[key];
935
+ }, record)
936
+ );
937
+ var getNestedString = (record, path) => {
938
+ const value = path.reduce((current, key) => {
939
+ return asRecord(current)?.[key];
940
+ }, record);
941
+ return typeof value === "string" && value.trim() ? value : null;
942
+ };
943
+ var getNestedBoolean = (record, path) => {
944
+ const value = path.reduce((current, key) => {
945
+ return asRecord(current)?.[key];
946
+ }, record);
947
+ return typeof value === "boolean" ? value : null;
948
+ };
949
+ var getStringArray = (value) => Array.isArray(value) ? value.filter(
950
+ (entry) => typeof entry === "string" && entry.trim().length > 0
951
+ ) : [];
952
+ var resolveGeometryQuality = (context) => {
953
+ const breakoutDistancePct = asNumber2(context.breakoutDistancePct) ?? 999;
954
+ const height = asNumber2(context.height) ?? 0;
955
+ if (height <= 0) {
956
+ return 1;
957
+ }
958
+ if (breakoutDistancePct <= 0.8) {
959
+ return 4;
960
+ }
961
+ if (breakoutDistancePct <= 1.4) {
962
+ return 3;
963
+ }
964
+ if (breakoutDistancePct <= 2.5) {
965
+ return 2;
966
+ }
967
+ return 1;
968
+ };
969
+ var isDirectionalCrowding = (direction, riskFlags) => direction === "LONG" ? riskFlags.includes("crowded_long") : direction === "SHORT" ? riskFlags.includes("crowded_short") : false;
970
+ var isBenchmarkAligned = ({
971
+ direction,
972
+ trendAlignment,
973
+ benchmarkBias
974
+ }) => direction === "LONG" ? trendAlignment === "aligned_bull" || benchmarkBias === "bull" : direction === "SHORT" ? trendAlignment === "aligned_bear" || benchmarkBias === "bear" : false;
975
+ var buildDoubleTapGateFeatures = ({
976
+ setupFeatures,
977
+ signalDirection,
978
+ height,
979
+ breakoutDistancePct,
980
+ trendAligned,
981
+ benchmarkAligned,
982
+ volumeRel20,
983
+ derivativesDirectionAligned,
984
+ derivativesRiskFlags,
985
+ venueSpreadZScore,
986
+ directionalCrowding,
987
+ approvalPocket,
988
+ bnbOiRotationPocket,
989
+ bnbOiRotationBlocked,
990
+ xrpOiShortNoHtfPocket,
991
+ xrpOiShortNoHtfBlocked,
992
+ ethVolumeBreadthCompressionPocket,
993
+ ethVolumeBreadthCompressionBlocked,
994
+ highPrecisionPocket,
995
+ highPrecisionApprovalBlocked,
996
+ q4DerivativesPocket,
997
+ q4DerivativesApprovalBlocked,
998
+ q4ApprovalBlocked,
999
+ defaultApprovalAllowed,
1000
+ q4AltDispersionOk,
1001
+ q4DerivativesCmcRiskOk,
1002
+ q4DerivativesDirectionSessionOk,
1003
+ bnbOiRotationContextAvailable,
1004
+ bnbOiRotationMomentumOk,
1005
+ xrpOiShortNoHtfContextAvailable,
1006
+ ethVolumeBreadthContextAvailable,
1007
+ ethVsBtcVolumeRatioApprovalOk,
1008
+ strictMomentumApproved,
1009
+ strictMomentumRoc1dOk,
1010
+ protectiveApprovalContextAvailable,
1011
+ protectiveApprovalContextOk
1012
+ }) => {
1013
+ const patternGeometry = height == null ? "unknown" : height <= 0 ? "invalid" : breakoutDistancePct != null && breakoutDistancePct <= 1.4 ? "compact" : "extended";
1014
+ const necklineBreakout = breakoutDistancePct == null ? "missing" : breakoutDistancePct <= 0.25 ? "early_noise" : breakoutDistancePct <= 0.8 ? "compact" : breakoutDistancePct <= 1.4 ? "confirmed" : "extended";
1015
+ const trendContext = signalDirection == null ? "unknown" : trendAligned || benchmarkAligned ? "aligned" : "neutral";
1016
+ const participationState = volumeRel20 == null ? "unknown" : volumeRel20 < 0.8 ? "thin" : volumeRel20 >= 2 ? "strong" : "normal";
1017
+ const derivativesState = derivativesDirectionAligned === true ? "aligned" : derivativesDirectionAligned === false ? "conflict" : directionalCrowding ? "crowded" : derivativesRiskFlags.length > 0 ? "neutral" : "unknown";
1018
+ const executionSpreadState = venueSpreadZScore == null ? "unknown" : venueSpreadZScore >= 1 ? "supportive" : venueSpreadZScore <= -1 ? "adverse" : "neutral";
1019
+ return {
1020
+ ...setupFeatures,
1021
+ patternGeometry,
1022
+ necklineBreakout,
1023
+ trendContext,
1024
+ participationState,
1025
+ derivativesState,
1026
+ executionSpreadState,
1027
+ approvalPocket: bnbOiRotationMomentumOk ? bnbOiRotationBlocked ? "bnb_oi_rotation_blocked" : "bnb_oi_rotation" : xrpOiShortNoHtfPocket ? xrpOiShortNoHtfBlocked ? "xrp_oi_short_no_htf_blocked" : "xrp_oi_short_no_htf" : ethVolumeBreadthCompressionPocket ? ethVolumeBreadthCompressionBlocked ? "eth_volume_breadth_blocked" : "eth_volume_breadth" : bnbOiRotationPocket ? bnbOiRotationBlocked ? "bnb_oi_rotation_blocked" : "bnb_oi_rotation" : highPrecisionPocket ? highPrecisionApprovalBlocked ? "high_precision_blocked" : "high_precision" : q4DerivativesPocket ? q4DerivativesApprovalBlocked ? "q4_derivatives_blocked" : "q4_derivatives" : approvalPocket && q4ApprovalBlocked ? "q4_blocked" : approvalPocket ? "q4" : "watch",
1028
+ highQualityCadencePocket: bnbOiRotationMomentumOk && !bnbOiRotationBlocked || xrpOiShortNoHtfPocket && !xrpOiShortNoHtfBlocked,
1029
+ defaultApprovalAllowed,
1030
+ q4AltDispersionOk,
1031
+ q4DerivativesPocket,
1032
+ q4DerivativesCmcRiskOk,
1033
+ q4DerivativesDirectionSessionOk,
1034
+ bnbOiRotationPocket,
1035
+ bnbOiRotationContextAvailable,
1036
+ bnbOiRotationMomentumOk,
1037
+ xrpOiShortNoHtfPocket,
1038
+ xrpOiShortNoHtfContextAvailable,
1039
+ ethVolumeBreadthContextAvailable,
1040
+ ethVolumeBreadthCompressionPocket,
1041
+ ethVsBtcVolumeRatioApprovalOk,
1042
+ strictMomentumApproved,
1043
+ strictMomentumRoc1dOk,
1044
+ protectiveApprovalContextAvailable,
1045
+ protectiveApprovalContextOk
1046
+ };
1047
+ };
1048
+ var buildDoubleTapAiContext = (payload) => {
1049
+ const context = getDoubleTapContext(payload);
1050
+ const additional = asRecord(payload.additionalIndicators);
1051
+ const baseContext = asRecord(additional?.baseContext);
1052
+ const derivativesSummary = getNestedRecord(baseContext, [
1053
+ "derivatives",
1054
+ "summary"
1055
+ ]);
1056
+ const signalDirection = context.signalDirection === "LONG" || context.signalDirection === "SHORT" ? context.signalDirection : null;
1057
+ const breakoutDistancePct = asNumber2(context.breakoutDistancePct);
1058
+ const height = asNumber2(context.height);
1059
+ const baseContextAvailable = Boolean(baseContext);
1060
+ const primarySession = getNestedString(baseContext, [
1061
+ "regime",
1062
+ "session",
1063
+ "sessionPhase"
1064
+ ]);
1065
+ const sessionWindowPhase = getNestedString(baseContext, [
1066
+ "regime",
1067
+ "session",
1068
+ "sessionWindowPhase"
1069
+ ]);
1070
+ const trendBias = getNestedString(baseContext, ["regime", "trend", "bias"]);
1071
+ const swingBias = getNestedString(baseContext, [
1072
+ "structure",
1073
+ "swing",
1074
+ "bias"
1075
+ ]);
1076
+ const breakoutState = getNestedString(baseContext, [
1077
+ "structure",
1078
+ "localRange",
1079
+ "breakoutState"
1080
+ ]);
1081
+ const barsSinceBreakout = getNestedNumber(baseContext, [
1082
+ "structure",
1083
+ "localRange",
1084
+ "barsSinceBreakout"
1085
+ ]);
1086
+ const lowTouchCount20 = getNestedNumber(baseContext, [
1087
+ "structure",
1088
+ "levels",
1089
+ "lowTouchCount20"
1090
+ ]);
1091
+ const volumeRel20 = getNestedNumber(baseContext, [
1092
+ "participation",
1093
+ "volume",
1094
+ "volumeRel20"
1095
+ ]);
1096
+ const benchmarkTrendAlignment = getNestedString(baseContext, [
1097
+ "relative",
1098
+ "benchmark",
1099
+ "trendAlignment"
1100
+ ]);
1101
+ const benchmarkRelativeStrength4h = getNestedNumber(baseContext, [
1102
+ "relative",
1103
+ "benchmark",
1104
+ "relativeStrength4h"
1105
+ ]);
1106
+ const benchmarkBias = getNestedString(baseContext, [
1107
+ "relative",
1108
+ "benchmark",
1109
+ "bias"
1110
+ ]);
1111
+ const derivativesDirectionAligned = typeof derivativesSummary?.directionAligned === "boolean" ? derivativesSummary.directionAligned : null;
1112
+ const derivativesRiskFlags = getStringArray(derivativesSummary?.riskFlags);
1113
+ const bodyStrength = getNestedNumber(baseContext, [
1114
+ "regime",
1115
+ "momentum",
1116
+ "bodyStrength"
1117
+ ]);
1118
+ const roc1d = getNestedNumber(baseContext, ["regime", "momentum", "roc1d"]);
1119
+ const venueSpreadZScore = getNestedNumber(baseContext, [
1120
+ "relative",
1121
+ "execution",
1122
+ "venueSpreadZScore"
1123
+ ]);
1124
+ const rewardToVolatility = getNestedNumber(baseContext, [
1125
+ "gateFeatures",
1126
+ "setup",
1127
+ "rewardToVolatility"
1128
+ ]);
1129
+ const executionScore = getNestedNumber(baseContext, [
1130
+ "gateFeatures",
1131
+ "scores",
1132
+ "execution"
1133
+ ]);
1134
+ const volumeStructureAligned = getNestedBoolean(baseContext, [
1135
+ "gateFeatures",
1136
+ "participation",
1137
+ "volumeStructureAligned"
1138
+ ]);
1139
+ const benchmarkConflict = getNestedBoolean(baseContext, [
1140
+ "gateFeatures",
1141
+ "relative",
1142
+ "benchmarkConflict"
1143
+ ]);
1144
+ const cmcAltVolumeChange24hPct = getNestedNumber(baseContext, [
1145
+ "relative",
1146
+ "cmcGlobal",
1147
+ "altVolumeChange24hPct"
1148
+ ]);
1149
+ const cmcExchangeLiquidityVolumeChange24hPct = getNestedNumber(baseContext, [
1150
+ "gateFeatures",
1151
+ "relative",
1152
+ "cmcExchangeLiquidityVolumeChange24hPct"
1153
+ ]) ?? getNestedNumber(baseContext, [
1154
+ "relative",
1155
+ "cmcExchangeLiquidity",
1156
+ "totalVolumeChange24hPct"
1157
+ ]);
1158
+ const cmcBtcDominanceChange24hPct = getNestedNumber(baseContext, [
1159
+ "relative",
1160
+ "cmcGlobal",
1161
+ "btcDominanceChange24hPct"
1162
+ ]);
1163
+ const cmcEthDominanceChange24hPct = getNestedNumber(baseContext, [
1164
+ "relative",
1165
+ "cmcGlobal",
1166
+ "ethDominanceChange24hPct"
1167
+ ]);
1168
+ const cmc20ToCmc100RatioChange24hPct = getNestedNumber(baseContext, [
1169
+ "relative",
1170
+ "cmcIndexes",
1171
+ "cmc20ToCmc100RatioChange24hPct"
1172
+ ]) ?? getNestedNumber(baseContext, [
1173
+ "gateFeatures",
1174
+ "relative",
1175
+ "cmc20ToCmc100RatioChange24hPct"
1176
+ ]);
1177
+ const ethVsBtcVolumeRatio = getNestedNumber(baseContext, [
1178
+ "relative",
1179
+ "cmcReferenceAssets",
1180
+ "ethVsBtcVolumeRatio"
1181
+ ]);
1182
+ const marketBreadthTop5Dispersion = getNestedNumber(baseContext, [
1183
+ "relative",
1184
+ "marketBreadths",
1185
+ "top5",
1186
+ "dispersion"
1187
+ ]);
1188
+ const btcVsAltReturn24h = getNestedNumber(baseContext, [
1189
+ "relative",
1190
+ "btcAltRegime",
1191
+ "btcVsAltReturn24h"
1192
+ ]) ?? getNestedNumber(baseContext, [
1193
+ "gateFeatures",
1194
+ "relative",
1195
+ "btcVsAltReturn24h"
1196
+ ]);
1197
+ const ethCrowdingPersistenceBars = getNestedNumber(baseContext, [
1198
+ "derivatives",
1199
+ "referenceContexts",
1200
+ "ETHUSDT",
1201
+ "summary",
1202
+ "crowdingPersistenceBars"
1203
+ ]);
1204
+ const solFundingZScore15m = getNestedNumber(baseContext, [
1205
+ "derivatives",
1206
+ "referenceContexts",
1207
+ "SOLUSDT",
1208
+ "intervals",
1209
+ "15m",
1210
+ "fundingZScore"
1211
+ ]);
1212
+ const bnbOiChangePct24h15m = getNestedNumber(baseContext, [
1213
+ "derivatives",
1214
+ "referenceContexts",
1215
+ "BNBUSDT",
1216
+ "intervals",
1217
+ "15m",
1218
+ "oiChangePct24h"
1219
+ ]);
1220
+ const bnbOiChangePct1h1h = getNestedNumber(baseContext, [
1221
+ "derivatives",
1222
+ "referenceContexts",
1223
+ "BNBUSDT",
1224
+ "intervals",
1225
+ "1h",
1226
+ "oiChangePct1h"
1227
+ ]);
1228
+ const bnbReferencePressure = getNestedString(baseContext, [
1229
+ "derivatives",
1230
+ "referenceContexts",
1231
+ "BNBUSDT",
1232
+ "summary",
1233
+ "pressure"
1234
+ ]);
1235
+ const xrpOiChangePct1h15m = getNestedNumber(baseContext, [
1236
+ "derivatives",
1237
+ "referenceContexts",
1238
+ "XRPUSDT",
1239
+ "intervals",
1240
+ "15m",
1241
+ "oiChangePct1h"
1242
+ ]);
1243
+ const higherTimeframeConflict = getNestedBoolean(baseContext, [
1244
+ "gateFeatures",
1245
+ "mtf",
1246
+ "higherTimeframeConflict"
1247
+ ]);
1248
+ const altDispersion24h = getNestedNumber(baseContext, [
1249
+ "relative",
1250
+ "btcAltRegime",
1251
+ "altDispersion24h"
1252
+ ]);
1253
+ const structuralHardBlockReasons = [];
1254
+ if (!baseContextAvailable) {
1255
+ structuralHardBlockReasons.push("missing_base_context");
1256
+ }
1257
+ if ((height ?? 0) <= 0) {
1258
+ structuralHardBlockReasons.push("invalid_pattern_height");
1259
+ }
1260
+ if (breakoutDistancePct == null || breakoutDistancePct > 1.4) {
1261
+ structuralHardBlockReasons.push("extended_or_missing_breakout");
1262
+ }
1263
+ const geometryQuality = resolveGeometryQuality(context);
1264
+ const compactButNotTooEarly = breakoutDistancePct != null && breakoutDistancePct > 0.35 && breakoutDistancePct <= 0.8;
1265
+ const directionalCrowding = isDirectionalCrowding(
1266
+ signalDirection,
1267
+ derivativesRiskFlags
1268
+ );
1269
+ const benchmarkAligned = isBenchmarkAligned({
1270
+ direction: signalDirection,
1271
+ trendAlignment: benchmarkTrendAlignment,
1272
+ benchmarkBias
1273
+ });
1274
+ const trendAligned = signalDirection === "LONG" ? trendBias === "bull" : signalDirection === "SHORT" ? trendBias === "bear" : false;
1275
+ const breakoutAligned = signalDirection === "LONG" ? breakoutState === "above_high_level" : signalDirection === "SHORT" ? breakoutState === "below_low_level" : false;
1276
+ const legacyLongHighPrecisionShapeCandidate = signalDirection === "LONG" && breakoutDistancePct != null && breakoutDistancePct > 0.5 && breakoutDistancePct <= 1.4 && primarySession !== "us" && volumeRel20 != null && volumeRel20 > 2 && barsSinceBreakout != null && barsSinceBreakout <= 1;
1277
+ const legacyShortHighPrecisionShapeCandidate = signalDirection === "SHORT" && breakoutDistancePct != null && breakoutDistancePct > 0.25 && breakoutDistancePct <= 0.8 && (primarySession === "europe" || primarySession === "off_hours") && volumeRel20 != null && volumeRel20 > 0.8 && !directionalCrowding && trendAligned;
1278
+ const legacyHighPrecisionShapeCandidate = legacyLongHighPrecisionShapeCandidate || legacyShortHighPrecisionShapeCandidate;
1279
+ const legacyLongStructuralShapeCandidate = signalDirection === "LONG" && breakoutDistancePct != null && breakoutDistancePct > 0.25 && breakoutDistancePct <= 0.8 && primarySession !== "us" && volumeRel20 != null && volumeRel20 > 0.8 && breakoutAligned;
1280
+ const legacyShortStructuralShapeCandidate = signalDirection === "SHORT" && compactButNotTooEarly && !directionalCrowding && (primarySession === "europe" || primarySession === "off_hours" || volumeRel20 != null && volumeRel20 > 1.2 || benchmarkAligned) && (primarySession !== "us" || trendAligned || volumeRel20 != null && volumeRel20 > 2);
1281
+ const legacyStructuralShapeCandidate = legacyLongStructuralShapeCandidate || legacyShortStructuralShapeCandidate;
1282
+ const legacyShapeCandidate = baseContextAvailable && (legacyStructuralShapeCandidate || legacyHighPrecisionShapeCandidate);
1283
+ const baseIndicatorCandidate = sessionWindowPhase === "active" && executionScore != null && executionScore >= MIN_EXECUTION_SCORE_FOR_AI_GATE && lowTouchCount20 != null && lowTouchCount20 >= MIN_LOW_TOUCH_COUNT20_FOR_AI_GATE;
1284
+ const q4CmcApproval = legacyShapeCandidate && !legacyHighPrecisionShapeCandidate && baseIndicatorCandidate && cmcBtcDominanceChange24hPct != null && cmcBtcDominanceChange24hPct > Q4_CMC_BTC_DOMINANCE_CHANGE_MIN && cmcBtcDominanceChange24hPct <= Q4_CMC_BTC_DOMINANCE_CHANGE_MAX && altDispersion24h != null && altDispersion24h < Q4_ALT_DISPERSION_24H_MAX;
1285
+ const q4DerivativesBadCmcPocket = btcVsAltReturn24h != null && cmc20ToCmc100RatioChange24hPct != null && btcVsAltReturn24h <= Q4_DERIVATIVES_BAD_BTC_VS_ALT_RETURN_24H_MAX && cmc20ToCmc100RatioChange24hPct <= Q4_DERIVATIVES_BAD_CMC20_TO_CMC100_CHANGE_24H_MAX;
1286
+ const q4DerivativesPocket = baseContextAvailable && btcVsAltReturn24h != null && ethCrowdingPersistenceBars != null && solFundingZScore15m != null && btcVsAltReturn24h <= Q4_DERIVATIVES_BTC_VS_ALT_RETURN_24H_MAX && ethCrowdingPersistenceBars >= Q4_DERIVATIVES_ETH_CROWDING_PERSISTENCE_MIN && solFundingZScore15m <= Q4_DERIVATIVES_SOL_FUNDING_Z_SCORE_15M_MAX;
1287
+ const q4DerivativesDirectionSessionOk = q4DerivativesPocket ? signalDirection === "LONG" ? primarySession === "europe" : signalDirection === "SHORT" ? true : false : null;
1288
+ const bnbOiRotationContextAvailable = bnbOiChangePct24h15m != null && bnbOiChangePct1h1h != null;
1289
+ const bnbOiRotationPocket = baseContextAvailable && bnbOiChangePct24h15m != null && bnbOiChangePct1h1h != null && bnbOiChangePct24h15m >= BNB_OI_ROTATION_CHANGE_24H_15M_MIN && bnbOiChangePct1h1h <= BNB_OI_ROTATION_CHANGE_1H_1H_MAX;
1290
+ const strictMomentumRoc1dOk = roc1d == null ? null : roc1d >= STRICT_MOMENTUM_ROC1D_MIN;
1291
+ const bnbOiRotationMomentumOk = bnbOiRotationPocket && strictMomentumRoc1dOk === true;
1292
+ const xrpOiShortNoHtfContextAvailable = xrpOiChangePct1h15m != null && higherTimeframeConflict != null;
1293
+ const xrpOiShortNoHtfPocket = baseContextAvailable && signalDirection === "SHORT" && xrpOiChangePct1h15m != null && xrpOiChangePct1h15m >= XRP_OI_SHORT_NO_HTF_CHANGE_1H_15M_MIN && higherTimeframeConflict === false;
1294
+ const ethVolumeBreadthContextAvailable = ethVsBtcVolumeRatio != null && marketBreadthTop5Dispersion != null && cmcEthDominanceChange24hPct != null;
1295
+ const ethVolumeBreadthCompressionPocket = baseContextAvailable && ethVsBtcVolumeRatio != null && ethVsBtcVolumeRatio <= ETH_VOLUME_BREADTH_ETH_VS_BTC_VOLUME_RATIO_MAX && marketBreadthTop5Dispersion != null && marketBreadthTop5Dispersion <= ETH_VOLUME_BREADTH_TOP5_DISPERSION_MAX && cmcEthDominanceChange24hPct != null && cmcEthDominanceChange24hPct > ETH_VOLUME_BREADTH_ETH_DOMINANCE_CHANGE_MIN;
1296
+ const ethVsBtcVolumeRatioApprovalOk = ethVsBtcVolumeRatio == null ? null : ethVsBtcVolumeRatio >= APPROVAL_ETH_VS_BTC_VOLUME_RATIO_MIN;
1297
+ const protectiveApprovalContextAvailable = benchmarkRelativeStrength4h != null && cmcExchangeLiquidityVolumeChange24hPct != null && bnbReferencePressure != null;
1298
+ const protectiveApprovalContextOk = benchmarkRelativeStrength4h != null && benchmarkRelativeStrength4h <= PROTECTIVE_RELATIVE_STRENGTH_4H_MAX && cmcExchangeLiquidityVolumeChange24hPct != null && cmcExchangeLiquidityVolumeChange24hPct >= PROTECTIVE_CMC_EXCHANGE_LIQUIDITY_CHANGE_24H_MIN && bnbReferencePressure != null && bnbReferencePressure !== PROTECTIVE_BNB_REFERENCE_PRESSURE_BLOCK;
1299
+ const protectiveApprovalSourceAllowed = bnbOiRotationMomentumOk || xrpOiShortNoHtfPocket;
1300
+ const approvalSourceAllowed = protectiveApprovalSourceAllowed;
1301
+ const xrpOiShortNoHtfMissingContext = !bnbOiRotationMomentumOk && signalDirection === "SHORT" && !xrpOiShortNoHtfContextAvailable;
1302
+ const bnbOiRotationMissingContext = !xrpOiShortNoHtfPocket && !bnbOiRotationContextAvailable;
1303
+ const bnbOiRotationOutsideGate = !xrpOiShortNoHtfPocket && bnbOiRotationContextAvailable && !bnbOiRotationPocket;
1304
+ const softBlockReasons = [
1305
+ ...bnbOiRotationMissingContext ? ["missing_bnb_oi_rotation_context"] : [],
1306
+ ...bnbOiRotationOutsideGate ? ["bnb_oi_rotation_outside_gate"] : [],
1307
+ ...bnbOiRotationPocket && strictMomentumRoc1dOk === false ? ["bnb_oi_rotation_roc1d_below_gate"] : [],
1308
+ ...bnbOiRotationPocket && strictMomentumRoc1dOk == null ? ["missing_roc1d_for_bnb_oi_rotation"] : [],
1309
+ ...xrpOiShortNoHtfMissingContext ? ["missing_xrp_oi_short_no_htf_context"] : [],
1310
+ ...xrpOiChangePct1h15m != null && xrpOiChangePct1h15m < XRP_OI_SHORT_NO_HTF_CHANGE_1H_15M_MIN ? ["xrp_oi_short_no_htf_outside_gate"] : [],
1311
+ ...xrpOiChangePct1h15m != null && xrpOiChangePct1h15m >= XRP_OI_SHORT_NO_HTF_CHANGE_1H_15M_MIN && signalDirection !== "SHORT" ? ["xrp_oi_short_no_htf_requires_short_signal"] : [],
1312
+ ...xrpOiChangePct1h15m != null && xrpOiChangePct1h15m >= XRP_OI_SHORT_NO_HTF_CHANGE_1H_15M_MIN && signalDirection === "SHORT" && higherTimeframeConflict !== false ? ["xrp_oi_short_no_htf_higher_timeframe_conflict"] : [],
1313
+ ...protectiveApprovalSourceAllowed && benchmarkRelativeStrength4h == null ? ["missing_relative_strength_4h_for_protective_gate"] : [],
1314
+ ...protectiveApprovalSourceAllowed && benchmarkRelativeStrength4h != null && benchmarkRelativeStrength4h > PROTECTIVE_RELATIVE_STRENGTH_4H_MAX ? ["relative_strength_4h_above_protective_gate"] : [],
1315
+ ...protectiveApprovalSourceAllowed && cmcExchangeLiquidityVolumeChange24hPct == null ? ["missing_cmc_exchange_liquidity_change_for_protective_gate"] : [],
1316
+ ...protectiveApprovalSourceAllowed && cmcExchangeLiquidityVolumeChange24hPct != null && cmcExchangeLiquidityVolumeChange24hPct < PROTECTIVE_CMC_EXCHANGE_LIQUIDITY_CHANGE_24H_MIN ? ["cmc_exchange_liquidity_change_below_protective_gate"] : [],
1317
+ ...protectiveApprovalSourceAllowed && bnbReferencePressure == null ? ["missing_bnb_reference_pressure_for_protective_gate"] : [],
1318
+ ...protectiveApprovalSourceAllowed && bnbReferencePressure === PROTECTIVE_BNB_REFERENCE_PRESSURE_BLOCK ? ["bnb_reference_pressure_crowded_long_protective_gate"] : [],
1319
+ ...protectiveApprovalSourceAllowed && ethVsBtcVolumeRatio == null ? ["missing_eth_vs_btc_volume_ratio_for_approval"] : [],
1320
+ ...protectiveApprovalSourceAllowed && ethVsBtcVolumeRatio != null && ethVsBtcVolumeRatio < APPROVAL_ETH_VS_BTC_VOLUME_RATIO_MIN ? ["eth_vs_btc_volume_ratio_below_approval_gate"] : [],
1321
+ ...ethVolumeBreadthCompressionPocket ? ["eth_volume_breadth_observation_only"] : [],
1322
+ ...legacyShapeCandidate && sessionWindowPhase !== "active" ? ["inactive_session_window"] : [],
1323
+ ...legacyShapeCandidate && (executionScore == null || executionScore < MIN_EXECUTION_SCORE_FOR_AI_GATE) ? ["low_or_missing_execution_score"] : [],
1324
+ ...legacyShapeCandidate && (lowTouchCount20 == null || lowTouchCount20 < MIN_LOW_TOUCH_COUNT20_FOR_AI_GATE) ? ["low_touch_count_below_gate"] : [],
1325
+ ...legacyHighPrecisionShapeCandidate && volumeStructureAligned !== true ? ["high_precision_volume_structure_not_aligned"] : [],
1326
+ ...legacyHighPrecisionShapeCandidate && benchmarkConflict !== false ? ["high_precision_benchmark_conflict_or_missing"] : [],
1327
+ ...legacyHighPrecisionShapeCandidate && cmcAltVolumeChange24hPct == null ? ["missing_cmc_alt_volume_change"] : [],
1328
+ ...legacyHighPrecisionShapeCandidate && cmcAltVolumeChange24hPct != null && cmcAltVolumeChange24hPct > HIGH_PRECISION_CMC_ALT_VOLUME_CHANGE_MAX ? ["cmc_alt_volume_change_too_hot"] : [],
1329
+ ...legacyShapeCandidate && !legacyHighPrecisionShapeCandidate && cmcBtcDominanceChange24hPct == null ? ["missing_cmc_btc_dominance_change"] : [],
1330
+ ...legacyShapeCandidate && !legacyHighPrecisionShapeCandidate && cmcBtcDominanceChange24hPct != null && (cmcBtcDominanceChange24hPct <= Q4_CMC_BTC_DOMINANCE_CHANGE_MIN || cmcBtcDominanceChange24hPct > Q4_CMC_BTC_DOMINANCE_CHANGE_MAX) ? ["cmc_btc_dominance_change_outside_band"] : [],
1331
+ ...legacyShapeCandidate && !legacyHighPrecisionShapeCandidate && cmcBtcDominanceChange24hPct != null && cmcBtcDominanceChange24hPct > Q4_CMC_BTC_DOMINANCE_CHANGE_MIN && cmcBtcDominanceChange24hPct <= Q4_CMC_BTC_DOMINANCE_CHANGE_MAX && altDispersion24h == null ? ["missing_alt_dispersion_24h_for_q4"] : [],
1332
+ ...legacyShapeCandidate && !legacyHighPrecisionShapeCandidate && altDispersion24h != null && altDispersion24h >= Q4_ALT_DISPERSION_24H_MAX ? ["alt_dispersion_24h_too_high_for_q4"] : [],
1333
+ ...q4CmcApproval ? ["legacy_q4_structural_observation_only"] : [],
1334
+ ...q4DerivativesPocket && signalDirection === "LONG" && q4DerivativesDirectionSessionOk === false ? ["long_q4_derivatives_outside_europe_session"] : [],
1335
+ ...q4DerivativesPocket && signalDirection == null ? ["missing_signal_direction_for_q4_derivatives"] : []
1336
+ ];
1337
+ const highPrecisionApprovalBlocked = legacyHighPrecisionShapeCandidate && !bnbOiRotationMomentumOk && !xrpOiShortNoHtfPocket;
1338
+ const q4DerivativesApprovalBlocked = q4DerivativesPocket && !bnbOiRotationMomentumOk && !xrpOiShortNoHtfPocket;
1339
+ const q4ApprovalBlocked = legacyShapeCandidate && !legacyHighPrecisionShapeCandidate && !bnbOiRotationMomentumOk && !xrpOiShortNoHtfPocket;
1340
+ const protectiveApprovalBlocked = protectiveApprovalSourceAllowed && !protectiveApprovalContextOk;
1341
+ const approvalLiquidityBlocked = protectiveApprovalSourceAllowed && ethVsBtcVolumeRatioApprovalOk !== true;
1342
+ const approvalBlocked = !approvalSourceAllowed && (highPrecisionApprovalBlocked || q4ApprovalBlocked || q4DerivativesApprovalBlocked);
1343
+ const approvalSourcePassesOwnGuards = protectiveApprovalSourceAllowed && protectiveApprovalContextOk && ethVsBtcVolumeRatioApprovalOk === true;
1344
+ const defaultApprovalAllowed = structuralHardBlockReasons.length === 0 && approvalSourcePassesOwnGuards && !approvalBlocked;
1345
+ const bnbOiRotationBlocked = bnbOiRotationPocket && (structuralHardBlockReasons.length > 0 || !bnbOiRotationMomentumOk || protectiveApprovalBlocked || approvalLiquidityBlocked);
1346
+ const xrpOiShortNoHtfBlocked = xrpOiShortNoHtfPocket && (structuralHardBlockReasons.length > 0 || protectiveApprovalBlocked || approvalLiquidityBlocked);
1347
+ const ethVolumeBreadthCompressionBlocked = ethVolumeBreadthCompressionPocket;
1348
+ const strictMomentumBlockReasons = [];
1349
+ const strictMomentumApprovalAllowedNow = bnbOiRotationMomentumOk && structuralHardBlockReasons.length === 0 && protectiveApprovalContextOk && strictMomentumRoc1dOk === true && ethVsBtcVolumeRatioApprovalOk === true;
1350
+ const approvalAllowedNow = defaultApprovalAllowed;
1351
+ const setupFeatures = buildDoubleTapSetupFeatures({
1352
+ context,
1353
+ interval: payload.signal?.interval,
1354
+ signalTimestamp: payload.signal?.timestamp
1355
+ });
1356
+ const doubleTapGateFeatures = buildDoubleTapGateFeatures({
1357
+ setupFeatures,
1358
+ signalDirection,
1359
+ height,
1360
+ breakoutDistancePct,
1361
+ trendAligned,
1362
+ benchmarkAligned,
1363
+ volumeRel20,
1364
+ derivativesDirectionAligned,
1365
+ derivativesRiskFlags,
1366
+ venueSpreadZScore,
1367
+ directionalCrowding,
1368
+ approvalPocket: legacyShapeCandidate,
1369
+ bnbOiRotationPocket,
1370
+ bnbOiRotationBlocked,
1371
+ xrpOiShortNoHtfPocket,
1372
+ xrpOiShortNoHtfBlocked,
1373
+ ethVolumeBreadthCompressionPocket,
1374
+ ethVolumeBreadthCompressionBlocked,
1375
+ highPrecisionPocket: legacyHighPrecisionShapeCandidate,
1376
+ highPrecisionApprovalBlocked,
1377
+ q4DerivativesPocket,
1378
+ q4DerivativesApprovalBlocked,
1379
+ q4ApprovalBlocked,
1380
+ defaultApprovalAllowed,
1381
+ q4AltDispersionOk: altDispersion24h == null ? null : altDispersion24h < Q4_ALT_DISPERSION_24H_MAX,
1382
+ q4DerivativesCmcRiskOk: btcVsAltReturn24h == null || cmc20ToCmc100RatioChange24hPct == null ? null : !q4DerivativesBadCmcPocket,
1383
+ q4DerivativesDirectionSessionOk,
1384
+ bnbOiRotationContextAvailable,
1385
+ bnbOiRotationMomentumOk,
1386
+ xrpOiShortNoHtfContextAvailable,
1387
+ ethVolumeBreadthContextAvailable,
1388
+ ethVsBtcVolumeRatioApprovalOk,
1389
+ strictMomentumApproved: strictMomentumApprovalAllowedNow,
1390
+ strictMomentumRoc1dOk,
1391
+ protectiveApprovalContextAvailable,
1392
+ protectiveApprovalContextOk
1393
+ });
1394
+ const deterministicQuality = structuralHardBlockReasons.length > 0 ? Math.min(geometryQuality, 2) : approvalSourcePassesOwnGuards ? 4 : Math.min(geometryQuality, 3);
1395
+ return {
1396
+ ...context,
1397
+ baseContextAvailable,
1398
+ primarySession,
1399
+ sessionWindowPhase,
1400
+ trendBias,
1401
+ swingBias,
1402
+ breakoutState,
1403
+ barsSinceBreakout,
1404
+ lowTouchCount20,
1405
+ volumeRel20,
1406
+ benchmarkTrendAlignment,
1407
+ benchmarkRelativeStrength4h,
1408
+ benchmarkBias,
1409
+ derivativesDirectionAligned,
1410
+ derivativesRiskFlags,
1411
+ bodyStrength,
1412
+ roc1d,
1413
+ venueSpreadZScore,
1414
+ rewardToVolatility,
1415
+ executionScore,
1416
+ volumeStructureAligned,
1417
+ benchmarkConflict,
1418
+ cmcAltVolumeChange24hPct,
1419
+ cmcExchangeLiquidityVolumeChange24hPct,
1420
+ cmcBtcDominanceChange24hPct,
1421
+ cmcEthDominanceChange24hPct,
1422
+ cmc20ToCmc100RatioChange24hPct,
1423
+ ethVsBtcVolumeRatio,
1424
+ marketBreadthTop5Dispersion,
1425
+ btcVsAltReturn24h,
1426
+ ethCrowdingPersistenceBars,
1427
+ solFundingZScore15m,
1428
+ bnbOiChangePct24h15m,
1429
+ bnbOiChangePct1h1h,
1430
+ bnbReferencePressure,
1431
+ xrpOiChangePct1h15m,
1432
+ ethVolumeBreadthContextAvailable,
1433
+ ethVolumeBreadthCompressionPocket,
1434
+ ethVsBtcVolumeRatioApprovalOk,
1435
+ higherTimeframeConflict,
1436
+ altDispersion24h,
1437
+ q4DerivativesDirectionSessionOk,
1438
+ doubleTapGateFeatures,
1439
+ structuralHardBlockReasons,
1440
+ softBlockReasons,
1441
+ strictMomentumBlockReasons,
1442
+ deterministicQuality,
1443
+ approvalAllowedNow: deterministicQuality >= 4 && approvalAllowedNow,
1444
+ protectiveApprovalContextAvailable,
1445
+ protectiveApprovalContextOk,
1446
+ strictMomentumApprovalAllowedNow,
1447
+ maxAllowedQuality: deterministicQuality
1448
+ };
1449
+ };
1450
+ var withDoubleTapGateFeatures = ({
1451
+ baseContext,
1452
+ context
1453
+ }) => baseContext == null ? baseContext : {
1454
+ ...baseContext,
1455
+ doubleTapGateFeatures: context.doubleTapGateFeatures
1456
+ };
1457
+ var doubleTapBaseAiAdapter = {
1458
+ buildPayload: ({ signal, basePayload }) => {
1459
+ const baseAdditional = basePayload.additionalIndicators ?? {};
1460
+ const payload = {
1461
+ ...basePayload,
1462
+ additionalIndicators: {
1463
+ ...baseAdditional,
1464
+ doubleTapContext: signal.additionalIndicators?.doubleTapContext
1465
+ }
1466
+ };
1467
+ const context = buildDoubleTapAiContext(payload);
1468
+ const baseContext = baseAdditional.baseContext ?? null;
1469
+ return {
1470
+ ...payload,
1471
+ additionalIndicators: {
1472
+ ...payload.additionalIndicators,
1473
+ baseContext: withDoubleTapGateFeatures({
1474
+ baseContext,
1475
+ context
1476
+ }),
1477
+ doubleTapContext: context
1478
+ }
1479
+ };
1480
+ },
1481
+ postProcessAnalysis: ({ payload, analysis }) => {
1482
+ const context = buildDoubleTapAiContext(payload);
1483
+ const direction = analysis.direction === "LONG" || analysis.direction === "SHORT" ? analysis.direction : context.signalDirection;
1484
+ const quality = context.deterministicQuality;
1485
+ const approved = context.approvalAllowedNow && Boolean(direction);
1486
+ return {
1487
+ ...analysis,
1488
+ direction: approved ? direction ?? null : null,
1489
+ quality,
1490
+ qualityReason: approved ? analysis.qualityReason : "DoubleTap breakout is not in a strict baseContext-supported approval pocket."
1491
+ };
1492
+ },
1493
+ buildHumanPromptAddon: ({ payload }) => {
1494
+ const context = buildDoubleTapAiContext(payload);
1495
+ return `
1496
+ Additional DoubleTap context:
1497
+ - patternKind=${context.patternKind ?? "n/a"}
1498
+ - signalDirection=${context.signalDirection ?? "n/a"}
1499
+ - neckline=${String(context.neckline ?? "n/a")}
1500
+ - targetPrice=${String(context.targetPrice ?? "n/a")}
1501
+ - stopLossPrice=${String(context.stopLossPrice ?? "n/a")}
1502
+ - height=${String(context.height ?? "n/a")}
1503
+ - pivotTolerancePct=${String(context.pivotTolerancePct ?? "n/a")}
1504
+ - breakoutDistancePct=${String(context.breakoutDistancePct ?? "n/a")}
1505
+ - currentPrice=${String(context.currentPrice ?? "n/a")}
1506
+ - baseContextAvailable=${String(context.baseContextAvailable)}
1507
+ - primarySession=${context.primarySession ?? "n/a"}
1508
+ - sessionWindowPhase=${context.sessionWindowPhase ?? "n/a"}
1509
+ - trendBias=${context.trendBias ?? "n/a"}
1510
+ - swingBias=${context.swingBias ?? "n/a"}
1511
+ - breakoutState=${context.breakoutState ?? "n/a"}
1512
+ - barsSinceBreakout=${String(context.barsSinceBreakout ?? "n/a")}
1513
+ - lowTouchCount20=${String(context.lowTouchCount20 ?? "n/a")}
1514
+ - volumeRel20=${String(context.volumeRel20 ?? "n/a")}
1515
+ - benchmarkTrendAlignment=${context.benchmarkTrendAlignment ?? "n/a"}
1516
+ - benchmarkRelativeStrength4h=${String(context.benchmarkRelativeStrength4h ?? "n/a")}
1517
+ - derivativesDirectionAligned=${String(context.derivativesDirectionAligned ?? "n/a")}
1518
+ - derivativesRiskFlags=${JSON.stringify(context.derivativesRiskFlags)}
1519
+ - bodyStrength=${String(context.bodyStrength ?? "n/a")}
1520
+ - roc1d=${String(context.roc1d ?? "n/a")}
1521
+ - venueSpreadZScore=${String(context.venueSpreadZScore ?? "n/a")}
1522
+ - rewardToVolatility=${String(context.rewardToVolatility ?? "n/a")}
1523
+ - executionScore=${String(context.executionScore ?? "n/a")}
1524
+ - volumeStructureAligned=${String(context.volumeStructureAligned ?? "n/a")}
1525
+ - benchmarkConflict=${String(context.benchmarkConflict ?? "n/a")}
1526
+ - cmcAltVolumeChange24hPct=${String(context.cmcAltVolumeChange24hPct ?? "n/a")}
1527
+ - cmcExchangeLiquidityVolumeChange24hPct=${String(context.cmcExchangeLiquidityVolumeChange24hPct ?? "n/a")}
1528
+ - cmcBtcDominanceChange24hPct=${String(context.cmcBtcDominanceChange24hPct ?? "n/a")}
1529
+ - cmcEthDominanceChange24hPct=${String(context.cmcEthDominanceChange24hPct ?? "n/a")}
1530
+ - cmc20ToCmc100RatioChange24hPct=${String(context.cmc20ToCmc100RatioChange24hPct ?? "n/a")}
1531
+ - ethVsBtcVolumeRatio=${String(context.ethVsBtcVolumeRatio ?? "n/a")}
1532
+ - marketBreadthTop5Dispersion=${String(context.marketBreadthTop5Dispersion ?? "n/a")}
1533
+ - btcVsAltReturn24h=${String(context.btcVsAltReturn24h ?? "n/a")}
1534
+ - ethCrowdingPersistenceBars=${String(context.ethCrowdingPersistenceBars ?? "n/a")}
1535
+ - solFundingZScore15m=${String(context.solFundingZScore15m ?? "n/a")}
1536
+ - bnbOiChangePct24h15m=${String(context.bnbOiChangePct24h15m ?? "n/a")}
1537
+ - bnbOiChangePct1h1h=${String(context.bnbOiChangePct1h1h ?? "n/a")}
1538
+ - bnbReferencePressure=${context.bnbReferencePressure ?? "n/a"}
1539
+ - xrpOiChangePct1h15m=${String(context.xrpOiChangePct1h15m ?? "n/a")}
1540
+ - ethVolumeBreadthContextAvailable=${String(context.ethVolumeBreadthContextAvailable)}
1541
+ - ethVolumeBreadthCompressionPocket=${String(context.ethVolumeBreadthCompressionPocket)}
1542
+ - higherTimeframeConflict=${String(context.higherTimeframeConflict ?? "n/a")}
1543
+ - altDispersion24h=${String(context.altDispersion24h ?? "n/a")}
1544
+ - doubleTapGatePatternGeometry=${context.doubleTapGateFeatures.patternGeometry}
1545
+ - doubleTapGateNecklineBreakout=${context.doubleTapGateFeatures.necklineBreakout}
1546
+ - doubleTapGateTrendContext=${context.doubleTapGateFeatures.trendContext}
1547
+ - doubleTapGateParticipationState=${context.doubleTapGateFeatures.participationState}
1548
+ - doubleTapGateDerivativesState=${context.doubleTapGateFeatures.derivativesState}
1549
+ - doubleTapGateExecutionSpreadState=${context.doubleTapGateFeatures.executionSpreadState}
1550
+ - doubleTapGateGeometry=${JSON.stringify(context.doubleTapGateFeatures.geometry)}
1551
+ - doubleTapGatePath=${JSON.stringify(context.doubleTapGateFeatures.path)}
1552
+ - doubleTapGateApprovalPocket=${context.doubleTapGateFeatures.approvalPocket}
1553
+ - doubleTapGateHighQualityCadencePocket=${String(context.doubleTapGateFeatures.highQualityCadencePocket)}
1554
+ - doubleTapGateDefaultApprovalAllowed=${String(context.doubleTapGateFeatures.defaultApprovalAllowed)}
1555
+ - doubleTapGateQ4AltDispersionOk=${String(context.doubleTapGateFeatures.q4AltDispersionOk ?? "n/a")}
1556
+ - doubleTapGateQ4DerivativesPocket=${String(context.doubleTapGateFeatures.q4DerivativesPocket)}
1557
+ - doubleTapGateQ4DerivativesCmcRiskOk=${String(context.doubleTapGateFeatures.q4DerivativesCmcRiskOk ?? "n/a")}
1558
+ - doubleTapGateQ4DerivativesDirectionSessionOk=${String(context.doubleTapGateFeatures.q4DerivativesDirectionSessionOk ?? "n/a")}
1559
+ - doubleTapGateBnbOiRotationPocket=${String(context.doubleTapGateFeatures.bnbOiRotationPocket)}
1560
+ - doubleTapGateBnbOiRotationContextAvailable=${String(context.doubleTapGateFeatures.bnbOiRotationContextAvailable)}
1561
+ - doubleTapGateBnbOiRotationMomentumOk=${String(context.doubleTapGateFeatures.bnbOiRotationMomentumOk)}
1562
+ - doubleTapGateXrpOiShortNoHtfPocket=${String(context.doubleTapGateFeatures.xrpOiShortNoHtfPocket)}
1563
+ - doubleTapGateXrpOiShortNoHtfContextAvailable=${String(context.doubleTapGateFeatures.xrpOiShortNoHtfContextAvailable)}
1564
+ - doubleTapGateEthVolumeBreadthContextAvailable=${String(context.doubleTapGateFeatures.ethVolumeBreadthContextAvailable)}
1565
+ - doubleTapGateEthVolumeBreadthCompressionPocket=${String(context.doubleTapGateFeatures.ethVolumeBreadthCompressionPocket)}
1566
+ - doubleTapGateEthVsBtcVolumeRatioApprovalOk=${String(context.doubleTapGateFeatures.ethVsBtcVolumeRatioApprovalOk ?? "n/a")}
1567
+ - doubleTapGateStrictMomentumApproved=${String(context.doubleTapGateFeatures.strictMomentumApproved)}
1568
+ - doubleTapGateStrictMomentumRoc1dOk=${String(context.doubleTapGateFeatures.strictMomentumRoc1dOk ?? "n/a")}
1569
+ - doubleTapGateProtectiveApprovalContextAvailable=${String(context.doubleTapGateFeatures.protectiveApprovalContextAvailable)}
1570
+ - doubleTapGateProtectiveApprovalContextOk=${String(context.doubleTapGateFeatures.protectiveApprovalContextOk)}
1571
+ - deterministicQuality=${String(context.deterministicQuality)}
1572
+ - approvalAllowedNow=${String(context.approvalAllowedNow)}
1573
+ - protectiveApprovalContextAvailable=${String(context.protectiveApprovalContextAvailable)}
1574
+ - protectiveApprovalContextOk=${String(context.protectiveApprovalContextOk)}
1575
+ - strictMomentumApprovalAllowedNow=${String(context.strictMomentumApprovalAllowedNow)}
1576
+ - structuralHardBlockReasons=${JSON.stringify(context.structuralHardBlockReasons)}
1577
+ - softBlockReasons=${JSON.stringify(context.softBlockReasons)}
1578
+ - strictMomentumBlockReasons=${JSON.stringify(context.strictMomentumBlockReasons)}
1579
+ - pivots=${JSON.stringify(context.pivots ?? [])}
1580
+
1581
+ Interpretation rules for DoubleTap:
1582
+ - This strategy enters only after a confirmed neckline break of a double bottom or double top.
1583
+ - Prefer compact breaks close to the neckline; late/extended breaks should be downgraded.
1584
+ - Extremely tiny breaks can still be early noise; live approval needs support from baseContext.
1585
+ - Treat deterministicQuality and approvalAllowedNow as the normalized local gate result.
1586
+ - Local q4 approval comes from BNB reference OI rotation with ROC1D confirmation or an XRP OI short/no-HTF-conflict pocket.
1587
+ - BNB approval requires BNB 15m oiChangePct24h >= 0.65, BNB 1h oiChangePct1h <= -0.28, roc1d >= -5.25, and ethVsBtcVolumeRatio >= 0.34.
1588
+ - XRP approval requires a SHORT signal, XRP 15m oiChangePct1h >= 0.32, higherTimeframeConflict=false, and ethVsBtcVolumeRatio >= 0.34.
1589
+ - The BNB/XRP protective gate requires benchmark relativeStrength4h <= 7.77009, CMC exchange liquidity volume change >= -0.378662, and BNB reference pressure not crowded_long.
1590
+ - The ETH volume/breadth pocket requires ethVsBtcVolumeRatio <= 0.39, top5 market-breadth dispersion <= 0.0007, and CMC ETH dominance change > -0.05; it is retained only as observation context and should not be treated as local approval.
1591
+ - Legacy high-precision CMC, structural q4 CMC, and old BTC/ETH/SOL derivatives pockets are retained only as observation context and should not be treated as local approval.
1592
+ - A good long has two comparable lows and a clean close above the neckline.
1593
+ - A good short has two comparable highs and a clean close below the neckline.
1594
+ - Venue spread, trend bias, body strength, and reward-to-volatility are diagnostics for this gate, not strict local blockers.
1595
+ `.trim();
1596
+ },
1597
+ mapEntryRuntimeFromConfig: (config2) => mapAiRuntimeFromConfig(
1598
+ config2
1599
+ )
1600
+ };
1601
+ var doubleTapAiAdapter = withStrategyLocalAiGate(
1602
+ doubleTapBaseAiAdapter,
1603
+ {
1604
+ id: "double_tap_direction_aware_release_candidate",
1605
+ approves: ({ signal, payload }) => {
1606
+ const altDispersion24h = getAiPayloadNumber(
1607
+ payload,
1608
+ "additionalIndicators.doubleTapContext.altDispersion24h"
1609
+ );
1610
+ const pricePositionInChannel = getAiPayloadNumber(
1611
+ payload,
1612
+ "additionalIndicators.baseContext.regime.trend.adaptiveChannel.pricePositionInChannel"
1613
+ );
1614
+ const barsSinceSwingHigh = getAiPayloadNumber(
1615
+ payload,
1616
+ "additionalIndicators.baseContext.structure.pivots.barsSinceSwingHigh"
1617
+ );
1618
+ return signal.direction === "LONG" && altDispersion24h != null && altDispersion24h >= 0.0285 && pricePositionInChannel != null && pricePositionInChannel >= 0.757 || signal.direction === "SHORT" && barsSinceSwingHigh != null && barsSinceSwingHigh <= 47;
1619
+ }
1620
+ }
1621
+ );
1622
+
1623
+ // src/DoubleTap/manifest.ts
1624
+ var doubleTapManifest = {
1625
+ name: "DoubleTap",
1626
+ aiAdapter: doubleTapAiAdapter
1627
+ };
1628
+
1629
+ // src/DoubleTap/strategy.ts
1630
+ var DoubleTapStrategyDefinition = {
1631
+ defaults: config,
1632
+ createCore: createDoubleTapCore,
1633
+ manifest: doubleTapManifest
1634
+ };
1635
+
1636
+ // src/index.ts
1637
+ var strategyEntries = [
1638
+ DoubleTapStrategyDefinition
1639
+ ];
1640
+ var defaultConfigs = {
1641
+ DoubleTap: config
1642
+ };
1643
+ var getBuiltInStrategyDefaultConfig = (strategyName) => defaultConfigs[strategyName];
1644
+ var index_default = defineStrategyPlugin({ strategyEntries });
1645
+ export {
1646
+ DoubleTapStrategyDefinition,
1647
+ index_default as default,
1648
+ doubleTapAiAdapter,
1649
+ config as doubleTapDefaultConfig,
1650
+ doubleTapManifest,
1651
+ getBuiltInStrategyDefaultConfig,
1652
+ strategyEntries
1653
+ };