@tradejs/strategy-trend-follow 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,1275 @@
1
+ // src/index.ts
2
+ import { defineStrategyPlugin } from "@tradejs/core/config";
3
+
4
+ // src/TrendFollow/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: 3,
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
+ TRENDFOLLOW_PIVOT_LENGTH: 10,
32
+ TRENDFOLLOW_MIN_BARS_BETWEEN_SIGNALS: 0,
33
+ TRENDFOLLOW_ATR_LENGTH: 14,
34
+ TRENDFOLLOW_ATR_MULT: 4,
35
+ TRENDFOLLOW_SIGNAL_OFFSET_ATR: 0.35,
36
+ TRENDFOLLOW_REQUIRE_STRUCTURE_BREAKOUT: false,
37
+ TRENDFOLLOW_REQUIRE_TREND_ALIGNMENT: false,
38
+ TRENDFOLLOW_REQUIRE_BENCHMARK_ALIGNMENT: false,
39
+ TRENDFOLLOW_MIN_VOLUME_REL20: 0,
40
+ TRENDFOLLOW_MIN_STRUCTURE_ACCEPTANCE_CLOSES: 0,
41
+ TRENDFOLLOW_MIN_BREAKOUT_BODY_ATR: 0,
42
+ TRENDFOLLOW_MIN_TREND_PERSISTENCE: 0,
43
+ TRENDFOLLOW_MIN_TREND_PERSISTENCE_LONG: 0.5,
44
+ TRENDFOLLOW_MIN_TREND_PERSISTENCE_SHORT: 0.5,
45
+ TRENDFOLLOW_MAX_RSI: 0,
46
+ TRENDFOLLOW_MAX_RSI_LONG: 75,
47
+ TRENDFOLLOW_MAX_RSI_SHORT: 0,
48
+ TRENDFOLLOW_MAX_BB_WIDTH_PCT: 0,
49
+ TRENDFOLLOW_MAX_BB_WIDTH_PCT_LONG: 0,
50
+ TRENDFOLLOW_MAX_BB_WIDTH_PCT_SHORT: 8,
51
+ TRENDFOLLOW_MIN_BREAKOUT_DISTANCE_PCT: 0,
52
+ TRENDFOLLOW_MIN_BREAKOUT_DISTANCE_PCT_LONG: 3,
53
+ TRENDFOLLOW_MIN_BREAKOUT_DISTANCE_PCT_SHORT: 2,
54
+ TRENDFOLLOW_MAX_BREAKOUT_DISTANCE_PCT: 0,
55
+ TRENDFOLLOW_TARGET_R_MULT: 2,
56
+ TRENDFOLLOW_TARGET_R_MULT_LONG: 1.4,
57
+ TRENDFOLLOW_TARGET_R_MULT_SHORT: 1.2,
58
+ TRENDFOLLOW_EXIT_ON_TRAIL_STOP: true,
59
+ TRENDFOLLOW_EXIT_ON_OPPOSITE_SIGNAL: true,
60
+ TRENDFOLLOW_MAX_FIGURE_POINTS: 180,
61
+ LONG: {
62
+ enable: true,
63
+ direction: "LONG",
64
+ minRiskRatio: 0.8
65
+ },
66
+ SHORT: {
67
+ enable: true,
68
+ direction: "SHORT",
69
+ minRiskRatio: 0.8
70
+ }
71
+ };
72
+
73
+ // src/TrendFollow/core.ts
74
+ import { round } from "@tradejs/core/math";
75
+
76
+ // src/TrendFollow/engine.ts
77
+ var asFiniteNumber = (value) => {
78
+ const parsed = Number(value);
79
+ return Number.isFinite(parsed) ? parsed : null;
80
+ };
81
+ var clampPositive = (value, fallback) => Number.isFinite(value) && value > 0 ? value : fallback;
82
+ var calculateTrueRange = (candle, prevClose) => {
83
+ const high = asFiniteNumber(candle.high);
84
+ const low = asFiniteNumber(candle.low);
85
+ const close = asFiniteNumber(candle.close);
86
+ if (high == null || low == null || close == null) {
87
+ return 0;
88
+ }
89
+ if (prevClose == null || !Number.isFinite(prevClose)) {
90
+ return Math.max(high - low, 0);
91
+ }
92
+ return Math.max(
93
+ high - low,
94
+ Math.abs(high - prevClose),
95
+ Math.abs(low - prevClose)
96
+ );
97
+ };
98
+ var updateAtrState = ({
99
+ atrState,
100
+ tr,
101
+ period
102
+ }) => {
103
+ const safeTr = Number.isFinite(tr) ? Math.max(tr, 0) : 0;
104
+ const safePeriod = Math.max(1, Math.floor(period));
105
+ if (atrState.value == null) {
106
+ return { value: safeTr, count: 1 };
107
+ }
108
+ if (atrState.count < safePeriod) {
109
+ const nextCount = atrState.count + 1;
110
+ return {
111
+ value: (atrState.value * atrState.count + safeTr) / nextCount,
112
+ count: nextCount
113
+ };
114
+ }
115
+ return {
116
+ value: (atrState.value * (safePeriod - 1) + safeTr) / safePeriod,
117
+ count: atrState.count + 1
118
+ };
119
+ };
120
+ var pushBoundedPoint = (series, point, maxPoints) => {
121
+ series.push(point);
122
+ if (series.length > maxPoints) {
123
+ series.splice(0, series.length - maxPoints);
124
+ }
125
+ };
126
+ var pushBoundedCandle = (state, candle, maxCandles) => {
127
+ state.currentIndex += 1;
128
+ state.candles.push(candle);
129
+ if (state.candles.length > maxCandles) {
130
+ const overflow = state.candles.length - maxCandles;
131
+ state.candles.splice(0, overflow);
132
+ state.candleStartIndex += overflow;
133
+ }
134
+ return state.currentIndex;
135
+ };
136
+ var getBufferedCandle = (state, absoluteIndex) => state.candles[absoluteIndex - state.candleStartIndex] ?? null;
137
+ var getConfigNumbers = (config2) => ({
138
+ pivotLength: Math.max(2, Math.floor(config2.TRENDFOLLOW_PIVOT_LENGTH ?? 10)),
139
+ minBarsBetween: Math.max(
140
+ 0,
141
+ Math.floor(config2.TRENDFOLLOW_MIN_BARS_BETWEEN_SIGNALS ?? 0)
142
+ ),
143
+ atrLength: Math.max(1, Math.floor(config2.TRENDFOLLOW_ATR_LENGTH ?? 14)),
144
+ atrMult: clampPositive(config2.TRENDFOLLOW_ATR_MULT, 4),
145
+ signalOffsetAtr: Math.max(
146
+ 0,
147
+ Number(config2.TRENDFOLLOW_SIGNAL_OFFSET_ATR ?? 0)
148
+ ),
149
+ maxFigurePoints: Math.max(
150
+ 20,
151
+ Math.floor(config2.TRENDFOLLOW_MAX_FIGURE_POINTS ?? 180)
152
+ )
153
+ });
154
+ var getWindow = (state, candidateIndex, lookback) => {
155
+ const window = [];
156
+ for (let index = candidateIndex - lookback; index <= candidateIndex + lookback; index += 1) {
157
+ const candle = getBufferedCandle(state, index);
158
+ if (!candle) {
159
+ return [];
160
+ }
161
+ window.push(candle);
162
+ }
163
+ return window;
164
+ };
165
+ var isPivotHigh = (state, candidateIndex, lookback) => {
166
+ const candidate = getBufferedCandle(state, candidateIndex);
167
+ const candidateHigh = asFiniteNumber(candidate?.high);
168
+ if (candidateHigh == null) {
169
+ return false;
170
+ }
171
+ const window = getWindow(state, candidateIndex, lookback);
172
+ return window.length === lookback * 2 + 1 && window.every((candle) => candidateHigh >= Number(candle.high));
173
+ };
174
+ var isPivotLow = (state, candidateIndex, lookback) => {
175
+ const candidate = getBufferedCandle(state, candidateIndex);
176
+ const candidateLow = asFiniteNumber(candidate?.low);
177
+ if (candidateLow == null) {
178
+ return false;
179
+ }
180
+ const window = getWindow(state, candidateIndex, lookback);
181
+ return window.length === lookback * 2 + 1 && window.every((candle) => candidateLow <= Number(candle.low));
182
+ };
183
+ var buildTrendFollowSignalContext = (signal) => ({
184
+ signalDirection: signal.direction,
185
+ entryLevel: signal.entryLevel,
186
+ trailStop: signal.trailStop,
187
+ atr: signal.atr,
188
+ pivotKind: signal.pivot.kind,
189
+ pivotTimestamp: signal.pivot.timestamp,
190
+ pivotValue: signal.pivot.value,
191
+ barsSinceSignal: signal.barsSinceSignal,
192
+ breakoutDistancePct: signal.breakoutDistancePct,
193
+ distanceToStopPct: signal.distanceToStopPct,
194
+ currentPrice: signal.close
195
+ });
196
+ var createTrendFollowEngine = ({
197
+ config: config2,
198
+ initialCandles = []
199
+ }) => {
200
+ const {
201
+ pivotLength,
202
+ minBarsBetween,
203
+ atrLength,
204
+ atrMult,
205
+ signalOffsetAtr,
206
+ maxFigurePoints
207
+ } = getConfigNumbers(config2);
208
+ const maxCandles = pivotLength * 2 + 1;
209
+ const state = {
210
+ candles: [],
211
+ candleStartIndex: 0,
212
+ currentIndex: -1,
213
+ atrState: { value: null, count: 0 },
214
+ prevClose: null,
215
+ trendState: 0,
216
+ lastPivotHigh: null,
217
+ lastPivotLow: null,
218
+ lastSignalIndex: null,
219
+ trailStop: null,
220
+ entryLevel: null,
221
+ signal: null,
222
+ snapshot: null,
223
+ series: { trailStop: [] }
224
+ };
225
+ const apply = (candle) => {
226
+ state.signal = null;
227
+ const prevClose = state.prevClose;
228
+ const close = Number(candle.close);
229
+ const tr = calculateTrueRange(candle, prevClose);
230
+ state.atrState = updateAtrState({
231
+ atrState: state.atrState,
232
+ tr,
233
+ period: atrLength
234
+ });
235
+ const atr = state.atrState.value ?? 0;
236
+ const currentIndex = pushBoundedCandle(state, candle, maxCandles);
237
+ const candidateIndex = currentIndex - pivotLength;
238
+ const candidate = candidateIndex >= pivotLength ? getBufferedCandle(state, candidateIndex) : null;
239
+ if (candidate && isPivotHigh(state, candidateIndex, pivotLength)) {
240
+ state.lastPivotHigh = {
241
+ timestamp: candidate.timestamp,
242
+ index: candidateIndex,
243
+ value: Number(candidate.high),
244
+ kind: "high"
245
+ };
246
+ }
247
+ if (candidate && isPivotLow(state, candidateIndex, pivotLength)) {
248
+ state.lastPivotLow = {
249
+ timestamp: candidate.timestamp,
250
+ index: candidateIndex,
251
+ value: Number(candidate.low),
252
+ kind: "low"
253
+ };
254
+ }
255
+ const barsSinceLastSignal = state.lastSignalIndex == null ? 999999 : currentIndex - state.lastSignalIndex;
256
+ const filterPass = minBarsBetween === 0 || barsSinceLastSignal >= minBarsBetween;
257
+ const bullConfirmationLevel = state.lastPivotHigh != null ? state.lastPivotHigh.value + atr * signalOffsetAtr : null;
258
+ const bearConfirmationLevel = state.lastPivotLow != null ? state.lastPivotLow.value - atr * signalOffsetAtr : null;
259
+ const bullCross = state.lastPivotHigh != null && bullConfirmationLevel != null && prevClose != null && prevClose <= bullConfirmationLevel && close > bullConfirmationLevel && state.trendState !== 1 && filterPass;
260
+ const bearCross = state.lastPivotLow != null && bearConfirmationLevel != null && prevClose != null && prevClose >= bearConfirmationLevel && close < bearConfirmationLevel && state.trendState !== -1 && filterPass;
261
+ if (bullCross && state.lastPivotHigh) {
262
+ state.trendState = 1;
263
+ state.entryLevel = state.lastPivotHigh.value;
264
+ state.trailStop = close - atr * atrMult;
265
+ state.lastSignalIndex = currentIndex;
266
+ state.signal = {
267
+ direction: "LONG",
268
+ entryLevel: state.entryLevel,
269
+ trailStop: state.trailStop,
270
+ atr,
271
+ pivot: state.lastPivotHigh,
272
+ barsSinceSignal: 0,
273
+ breakoutDistancePct: state.entryLevel !== 0 ? (close - state.entryLevel) / Math.abs(state.entryLevel) * 100 : 0,
274
+ distanceToStopPct: close !== 0 ? Math.abs(close - state.trailStop) / close * 100 : 0,
275
+ timestamp: candle.timestamp,
276
+ close
277
+ };
278
+ } else if (bearCross && state.lastPivotLow) {
279
+ state.trendState = -1;
280
+ state.entryLevel = state.lastPivotLow.value;
281
+ state.trailStop = close + atr * atrMult;
282
+ state.lastSignalIndex = currentIndex;
283
+ state.signal = {
284
+ direction: "SHORT",
285
+ entryLevel: state.entryLevel,
286
+ trailStop: state.trailStop,
287
+ atr,
288
+ pivot: state.lastPivotLow,
289
+ barsSinceSignal: 0,
290
+ breakoutDistancePct: state.entryLevel !== 0 ? (state.entryLevel - close) / Math.abs(state.entryLevel) * 100 : 0,
291
+ distanceToStopPct: close !== 0 ? Math.abs(close - state.trailStop) / close * 100 : 0,
292
+ timestamp: candle.timestamp,
293
+ close
294
+ };
295
+ } else if (state.trendState === 1) {
296
+ const newStop = close - atr * atrMult;
297
+ state.trailStop = state.trailStop == null ? newStop : Math.max(state.trailStop, newStop);
298
+ } else if (state.trendState === -1) {
299
+ const newStop = close + atr * atrMult;
300
+ state.trailStop = state.trailStop == null ? newStop : Math.min(state.trailStop, newStop);
301
+ }
302
+ const barsSinceSignal = state.lastSignalIndex == null ? null : currentIndex - state.lastSignalIndex;
303
+ const distanceToStopPct = state.trailStop != null && close !== 0 ? Math.abs(close - state.trailStop) / close * 100 : null;
304
+ const breakoutDistancePct = state.entryLevel != null && state.entryLevel !== 0 ? state.trendState === 1 ? (close - state.entryLevel) / Math.abs(state.entryLevel) * 100 : state.trendState === -1 ? (state.entryLevel - close) / Math.abs(state.entryLevel) * 100 : null : null;
305
+ if (state.trailStop != null && state.trendState !== 0) {
306
+ pushBoundedPoint(
307
+ state.series.trailStop,
308
+ { timestamp: candle.timestamp, value: state.trailStop },
309
+ maxFigurePoints
310
+ );
311
+ }
312
+ state.snapshot = {
313
+ trendState: state.trendState,
314
+ signalDirection: state.signal?.direction ?? null,
315
+ bullCross,
316
+ bearCross,
317
+ entryLevel: state.entryLevel,
318
+ trailStop: state.trailStop,
319
+ atr,
320
+ barsSinceSignal,
321
+ lastPivotHigh: state.lastPivotHigh,
322
+ lastPivotLow: state.lastPivotLow,
323
+ distanceToStopPct,
324
+ breakoutDistancePct,
325
+ timestamp: candle.timestamp,
326
+ close
327
+ };
328
+ state.prevClose = close;
329
+ return {
330
+ signal: state.signal,
331
+ snapshot: state.snapshot,
332
+ series: state.series
333
+ };
334
+ };
335
+ for (const candle of initialCandles) {
336
+ apply(candle);
337
+ }
338
+ return {
339
+ next: apply,
340
+ getState: () => ({
341
+ signal: state.signal,
342
+ snapshot: state.snapshot,
343
+ series: state.series
344
+ })
345
+ };
346
+ };
347
+
348
+ // src/TrendFollow/figures.ts
349
+ var buildTrendFollowFigures = ({
350
+ signal,
351
+ series,
352
+ entryTimestamp,
353
+ entryPrice,
354
+ stopLossPrice,
355
+ takeProfitPrice
356
+ }) => {
357
+ const color = signal.direction === "LONG" ? "#00c853" : "#ef5350";
358
+ const lines = [
359
+ {
360
+ id: `trendfollow-trail-${entryTimestamp}`,
361
+ kind: "trendfollow_trailing_stop",
362
+ points: series.trailStop.slice(),
363
+ color,
364
+ width: 2,
365
+ style: "solid"
366
+ },
367
+ {
368
+ id: `trendfollow-entry-level-${entryTimestamp}`,
369
+ kind: "trendfollow_entry_level",
370
+ points: [
371
+ { timestamp: signal.pivot.timestamp, value: signal.entryLevel },
372
+ { timestamp: entryTimestamp, value: signal.entryLevel }
373
+ ],
374
+ color: "#f59e0b",
375
+ width: 2,
376
+ style: "dashed"
377
+ },
378
+ {
379
+ id: `trendfollow-target-${entryTimestamp}`,
380
+ kind: "trendfollow_target",
381
+ points: [
382
+ { timestamp: signal.pivot.timestamp, value: takeProfitPrice },
383
+ { timestamp: entryTimestamp, value: takeProfitPrice }
384
+ ],
385
+ color: "#22c55e",
386
+ width: 1,
387
+ style: "dashed"
388
+ },
389
+ {
390
+ id: `trendfollow-stop-${entryTimestamp}`,
391
+ kind: "trendfollow_stop",
392
+ points: [
393
+ { timestamp: signal.pivot.timestamp, value: stopLossPrice },
394
+ { timestamp: entryTimestamp, value: stopLossPrice }
395
+ ],
396
+ color: "#ef4444",
397
+ width: 1,
398
+ style: "dashed"
399
+ }
400
+ ].filter((line) => line.points.length > 0);
401
+ const points = [
402
+ {
403
+ id: `trendfollow-pivot-${entryTimestamp}`,
404
+ kind: `trendfollow_${signal.pivot.kind}_pivot`,
405
+ points: [
406
+ { timestamp: signal.pivot.timestamp, value: signal.pivot.value }
407
+ ],
408
+ color: "#f59e0b",
409
+ radius: 4
410
+ },
411
+ {
412
+ id: `trendfollow-entry-${entryTimestamp}`,
413
+ kind: "trendfollow_entry",
414
+ points: [{ timestamp: entryTimestamp, value: entryPrice }],
415
+ color,
416
+ radius: 5
417
+ }
418
+ ];
419
+ return { lines, points };
420
+ };
421
+
422
+ // src/TrendFollow/filters.ts
423
+ import { resolveDirectionalConfigNumber } from "@tradejs/strategy-kit/config";
424
+ var asPositiveThreshold = (value) => {
425
+ const parsed = Number(value);
426
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
427
+ };
428
+ var isDirectionAligned = ({
429
+ direction,
430
+ bullishValue,
431
+ bearishValue,
432
+ value
433
+ }) => direction === "LONG" ? value === bullishValue : value === bearishValue;
434
+ var getTrendFollowCoreFilterSkipCode = ({
435
+ signal,
436
+ config: config2,
437
+ baseContext
438
+ }) => {
439
+ const minBreakoutDistancePct = asPositiveThreshold(
440
+ resolveDirectionalConfigNumber({
441
+ config: config2,
442
+ key: "TRENDFOLLOW_MIN_BREAKOUT_DISTANCE_PCT",
443
+ direction: signal.direction,
444
+ fallback: 0
445
+ })
446
+ );
447
+ if (minBreakoutDistancePct != null && signal.breakoutDistancePct < minBreakoutDistancePct) {
448
+ return "TRENDFOLLOW_BREAKOUT_DISTANCE_TOO_SMALL";
449
+ }
450
+ const maxBreakoutDistancePct = asPositiveThreshold(
451
+ config2.TRENDFOLLOW_MAX_BREAKOUT_DISTANCE_PCT
452
+ );
453
+ if (maxBreakoutDistancePct != null && signal.breakoutDistancePct > maxBreakoutDistancePct) {
454
+ return "TRENDFOLLOW_BREAKOUT_DISTANCE_TOO_EXTENDED";
455
+ }
456
+ const breakoutState = baseContext?.structure?.localRange?.breakoutState;
457
+ if (config2.TRENDFOLLOW_REQUIRE_STRUCTURE_BREAKOUT && !isDirectionAligned({
458
+ direction: signal.direction,
459
+ bullishValue: "above_high_level",
460
+ bearishValue: "below_low_level",
461
+ value: breakoutState
462
+ })) {
463
+ return "TRENDFOLLOW_STRUCTURE_BREAKOUT_NOT_CONFIRMED";
464
+ }
465
+ const trendBias = baseContext?.regime?.trend?.bias;
466
+ if (config2.TRENDFOLLOW_REQUIRE_TREND_ALIGNMENT && !isDirectionAligned({
467
+ direction: signal.direction,
468
+ bullishValue: "bull",
469
+ bearishValue: "bear",
470
+ value: trendBias
471
+ })) {
472
+ return "TRENDFOLLOW_TREND_NOT_ALIGNED";
473
+ }
474
+ const benchmarkAlignment = baseContext?.relative?.benchmark?.trendAlignment;
475
+ if (config2.TRENDFOLLOW_REQUIRE_BENCHMARK_ALIGNMENT && !isDirectionAligned({
476
+ direction: signal.direction,
477
+ bullishValue: "aligned_bull",
478
+ bearishValue: "aligned_bear",
479
+ value: benchmarkAlignment
480
+ })) {
481
+ return "TRENDFOLLOW_BENCHMARK_NOT_ALIGNED";
482
+ }
483
+ const minVolumeRel20 = asPositiveThreshold(
484
+ config2.TRENDFOLLOW_MIN_VOLUME_REL20
485
+ );
486
+ if (minVolumeRel20 != null) {
487
+ const volumeRel20 = Number(baseContext?.participation?.volume?.volumeRel20);
488
+ if (!Number.isFinite(volumeRel20) || volumeRel20 < minVolumeRel20) {
489
+ return "TRENDFOLLOW_VOLUME_TOO_THIN";
490
+ }
491
+ }
492
+ const minAcceptanceCloses = asPositiveThreshold(
493
+ config2.TRENDFOLLOW_MIN_STRUCTURE_ACCEPTANCE_CLOSES
494
+ );
495
+ if (minAcceptanceCloses != null) {
496
+ const acceptance = baseContext?.structure?.acceptance;
497
+ const continuationCloses = Number(
498
+ signal.direction === "LONG" ? acceptance?.closesAboveHighLevel3 : acceptance?.closesBelowLowLevel3
499
+ );
500
+ if (!Number.isFinite(continuationCloses) || continuationCloses < minAcceptanceCloses) {
501
+ return "TRENDFOLLOW_CLOSE_ACCEPTANCE_TOO_WEAK";
502
+ }
503
+ }
504
+ const minBreakoutBodyAtr = asPositiveThreshold(
505
+ config2.TRENDFOLLOW_MIN_BREAKOUT_BODY_ATR
506
+ );
507
+ if (minBreakoutBodyAtr != null) {
508
+ const breakoutBodyAtr = Number(
509
+ baseContext?.structure?.acceptance?.breakoutBodyAtr
510
+ );
511
+ if (!Number.isFinite(breakoutBodyAtr) || breakoutBodyAtr < minBreakoutBodyAtr) {
512
+ return "TRENDFOLLOW_BREAKOUT_BODY_TOO_SMALL";
513
+ }
514
+ }
515
+ const minTrendPersistence = asPositiveThreshold(
516
+ resolveDirectionalConfigNumber({
517
+ config: config2,
518
+ key: "TRENDFOLLOW_MIN_TREND_PERSISTENCE",
519
+ direction: signal.direction,
520
+ fallback: 0
521
+ })
522
+ );
523
+ if (minTrendPersistence != null) {
524
+ const persistence = Number(baseContext?.regime?.trend?.persistence);
525
+ if (!Number.isFinite(persistence) || persistence < minTrendPersistence) {
526
+ return "TRENDFOLLOW_TREND_PERSISTENCE_TOO_LOW";
527
+ }
528
+ }
529
+ const maxRsi = asPositiveThreshold(
530
+ resolveDirectionalConfigNumber({
531
+ config: config2,
532
+ key: "TRENDFOLLOW_MAX_RSI",
533
+ direction: signal.direction,
534
+ fallback: 0
535
+ })
536
+ );
537
+ if (maxRsi != null) {
538
+ const rsi = Number(baseContext?.regime?.momentum?.rsi);
539
+ if (!Number.isFinite(rsi) || rsi > maxRsi) {
540
+ return "TRENDFOLLOW_RSI_TOO_EXTENDED";
541
+ }
542
+ }
543
+ const maxBbWidthPct = asPositiveThreshold(
544
+ resolveDirectionalConfigNumber({
545
+ config: config2,
546
+ key: "TRENDFOLLOW_MAX_BB_WIDTH_PCT",
547
+ direction: signal.direction,
548
+ fallback: 0
549
+ })
550
+ );
551
+ if (maxBbWidthPct != null) {
552
+ const bbWidthPct = Number(baseContext?.raw?.volatility?.bbWidthPct);
553
+ if (!Number.isFinite(bbWidthPct) || bbWidthPct > maxBbWidthPct) {
554
+ return "TRENDFOLLOW_VOLATILITY_TOO_WIDE";
555
+ }
556
+ }
557
+ return null;
558
+ };
559
+
560
+ // src/TrendFollow/core.ts
561
+ import {
562
+ buildStructureRiskPlan,
563
+ isStopLossOnCorrectSide
564
+ } from "@tradejs/strategy-kit/risk";
565
+ import { resolveDirectionalConfigNumber as resolveDirectionalConfigNumber2 } from "@tradejs/strategy-kit/config";
566
+ var isOpenPosition = (position) => Boolean(
567
+ 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")
568
+ );
569
+ var buildTrendFollowStateKey = (config2) => JSON.stringify({
570
+ pivotLength: config2.TRENDFOLLOW_PIVOT_LENGTH,
571
+ minBarsBetween: config2.TRENDFOLLOW_MIN_BARS_BETWEEN_SIGNALS,
572
+ atrLength: config2.TRENDFOLLOW_ATR_LENGTH,
573
+ atrMult: config2.TRENDFOLLOW_ATR_MULT,
574
+ signalOffsetAtr: config2.TRENDFOLLOW_SIGNAL_OFFSET_ATR,
575
+ maxFigurePoints: config2.TRENDFOLLOW_MAX_FIGURE_POINTS
576
+ });
577
+ var createTrendFollowCore = async ({ config: config2, data: initialData, strategyApi, indicatorsState }) => {
578
+ const detectorState = strategyApi.createStateController(
579
+ "TrendFollow",
580
+ () => ({
581
+ engine: createTrendFollowEngine({
582
+ config: config2,
583
+ initialCandles: initialData
584
+ })
585
+ }),
586
+ {
587
+ configKey: buildTrendFollowStateKey(config2),
588
+ snapshot: (state) => state.engine.getState()
589
+ }
590
+ );
591
+ const lastTradeController = strategyApi.createLastTradeController({
592
+ enabled: true
593
+ });
594
+ const nextDetectorState = (candle) => detectorState.oncePerTimestamp(
595
+ candle.timestamp,
596
+ (state) => state.engine.next(candle)
597
+ );
598
+ return async (candle) => {
599
+ const runtimeState = nextDetectorState(candle);
600
+ const signal = runtimeState.signal;
601
+ const snapshot = runtimeState.snapshot;
602
+ const position = await strategyApi.getCurrentPosition();
603
+ if (isOpenPosition(position)) {
604
+ const trailStop = snapshot?.trailStop;
605
+ const close = Number(candle.close);
606
+ const trailStopHit = trailStop != null && (position.direction === "LONG" && close <= trailStop || position.direction === "SHORT" && close >= trailStop);
607
+ const oppositeSignal = signal != null && (position.direction === "LONG" ? signal.direction === "SHORT" : signal.direction === "LONG");
608
+ if (Boolean(config2.TRENDFOLLOW_EXIT_ON_TRAIL_STOP) && trailStopHit) {
609
+ return strategyApi.exit({
610
+ code: "TRENDFOLLOW_TRAIL_STOP_EXIT",
611
+ direction: position.direction
612
+ });
613
+ }
614
+ if (Boolean(config2.TRENDFOLLOW_EXIT_ON_OPPOSITE_SIGNAL) && oppositeSignal) {
615
+ return strategyApi.exit({
616
+ code: "TRENDFOLLOW_OPPOSITE_SIGNAL_EXIT",
617
+ direction: position.direction
618
+ });
619
+ }
620
+ return strategyApi.skip("POSITION_EXISTS");
621
+ }
622
+ if (!signal) {
623
+ return strategyApi.skip("NO_TREND_FOLLOW_SIGNAL");
624
+ }
625
+ if (lastTradeController.isInCooldown(candle.timestamp)) {
626
+ return strategyApi.skip("DEV_TRADE_COOLDOWN");
627
+ }
628
+ const modeConfig = signal.direction === "LONG" ? config2.LONG : config2.SHORT;
629
+ if (!modeConfig.enable) {
630
+ return strategyApi.skip("STRATEGY_DISABLED");
631
+ }
632
+ const baseContext = strategyApi.getBaseContext();
633
+ const coreFilterSkipCode = getTrendFollowCoreFilterSkipCode({
634
+ signal,
635
+ config: config2,
636
+ baseContext
637
+ });
638
+ if (coreFilterSkipCode) {
639
+ return strategyApi.skip(coreFilterSkipCode);
640
+ }
641
+ const { timestamp, currentPrice } = await strategyApi.getDecisionPriceContext();
642
+ const stopLossPrice = signal.trailStop;
643
+ if (!isStopLossOnCorrectSide({
644
+ direction: signal.direction,
645
+ currentPrice,
646
+ stopLossPrice
647
+ })) {
648
+ return strategyApi.skip("INVALID_STOP");
649
+ }
650
+ const { takeProfitPrice, riskRatio, qty } = buildStructureRiskPlan({
651
+ currentPrice,
652
+ direction: signal.direction,
653
+ stopLossPrice,
654
+ targetR: resolveDirectionalConfigNumber2({
655
+ config: config2,
656
+ key: "TRENDFOLLOW_TARGET_R_MULT",
657
+ direction: signal.direction,
658
+ fallback: 2
659
+ }),
660
+ maxLossValue: config2.MAX_LOSS_VALUE,
661
+ feeRate: Number(config2.FEE_PERCENT ?? 0),
662
+ slippageBps: Number(config2.SLIPPAGE_BASE_BPS ?? 0) + Number(config2.SLIPPAGE_MARKET_IMPACT_BPS ?? 0)
663
+ });
664
+ if (!qty || !Number.isFinite(qty) || qty <= 0) {
665
+ return strategyApi.skip("INVALID_QTY");
666
+ }
667
+ if (riskRatio <= modeConfig.minRiskRatio) {
668
+ return strategyApi.skip(`RISK_RATIO:${round(riskRatio)}`);
669
+ }
670
+ const indicators = indicatorsState.snapshot();
671
+ lastTradeController.markTrade(timestamp);
672
+ return strategyApi.entry({
673
+ code: signal.direction === "LONG" ? "TRENDFOLLOW_BULL_TREND" : "TRENDFOLLOW_BEAR_TREND",
674
+ direction: modeConfig.direction,
675
+ indicators,
676
+ additionalIndicators: {
677
+ trendFollowContext: buildTrendFollowSignalContext({
678
+ ...signal,
679
+ close: currentPrice
680
+ })
681
+ },
682
+ figures: buildTrendFollowFigures({
683
+ signal,
684
+ series: runtimeState.series,
685
+ entryTimestamp: timestamp,
686
+ entryPrice: currentPrice,
687
+ stopLossPrice,
688
+ takeProfitPrice
689
+ }),
690
+ orderPlan: {
691
+ qty,
692
+ stopLossPrice,
693
+ takeProfits: [{ rate: 1, price: takeProfitPrice }]
694
+ }
695
+ });
696
+ };
697
+ };
698
+
699
+ // src/TrendFollow/adapters/ai.ts
700
+ import { mapAiRuntimeFromConfig } from "@tradejs/core/strategies";
701
+
702
+ // src/TrendFollow/guardrails.ts
703
+ var TREND_FOLLOW_SHORT_FLUSH_OI_MIN_CHANGE_24H = 2.1;
704
+ var TREND_FOLLOW_SHORT_FLUSH_OI_MIN_LIQ_LONG = 12;
705
+ var TREND_FOLLOW_SHORT_FLUSH_OI_MAX_LIQ_IMBALANCE = -0.75;
706
+ var TREND_FOLLOW_SHORT_MARKET_MAX_BTC_TURNOVER_SHARE_24H = 0.416874;
707
+ var TREND_FOLLOW_SHORT_MARKET_MIN_ALT_BASKET_RETURN_24H = -0.020269;
708
+ var TREND_FOLLOW_SHORT_MARKET_MAX_ALT_BASKET_RETURN_24H = 0.052359;
709
+ var TREND_FOLLOW_SHORT_MIN_TARGET_BTC_BETA_20 = 0.627393;
710
+ var TREND_FOLLOW_SHORT_MAX_SHARED_PARTICIPATION_SCORE = 86;
711
+ var TREND_FOLLOW_REF_HIGH_XRP_OI_MIN = 273e6;
712
+ var TREND_FOLLOW_REF_HIGH_SOL_OI_MIN = 103e5;
713
+ var TREND_FOLLOW_REF_BNB_OI_CHANGE_24H_MAX = -2.7;
714
+ var TREND_FOLLOW_REF_XRP_OI_CHANGE_24H_MIN = 3.8;
715
+ var TREND_FOLLOW_REF_TRX_FUNDING_RATE_MAX = -0.025;
716
+ var TREND_FOLLOW_REF_SOL_FLUSH_OI_MIN = 99e5;
717
+ var TREND_FOLLOW_REF_SOL_OI_CHANGE_4H_MIN = 1.31;
718
+ var TREND_FOLLOW_REF_LOSS_XRP_OI_MIN = 272e6;
719
+ var TREND_FOLLOW_REF_LOSS_ETH_CROWDING_MIN = 90;
720
+ var TREND_FOLLOW_REF_LOSS_SOL_FUNDING_Z_MAX = -1.6;
721
+ var TREND_FOLLOW_OPENING_SESSION_MAX_MINUTES_FROM_OPEN = 75;
722
+ var TREND_FOLLOW_OPENING_REF_XRP_OI_MAX = 324e6;
723
+ var TREND_FOLLOW_OPENING_REF_XRP_OI_CHANGE_1H_MIN = 0.4;
724
+ var TREND_FOLLOW_OPENING_REF_BNB_OI_MIN = 56e4;
725
+ var TREND_FOLLOW_ALLOWED_VOLATILITY_STATE = "normal";
726
+ var asFiniteNumber2 = (value) => {
727
+ const parsed = Number(value);
728
+ return Number.isFinite(parsed) ? parsed : null;
729
+ };
730
+ var asStringArray = (value) => Array.isArray(value) ? value.filter(
731
+ (entry) => typeof entry === "string" && entry.trim().length > 0
732
+ ) : [];
733
+ var getReferenceInterval = (baseContext, symbol, interval) => baseContext?.derivatives?.referenceContexts?.[symbol]?.intervals?.[interval];
734
+ var getReferenceSummary = (baseContext, symbol) => baseContext?.derivatives?.referenceContexts?.[symbol]?.summary;
735
+ var isDirectionAligned2 = ({
736
+ direction,
737
+ bullishValue,
738
+ bearishValue,
739
+ value
740
+ }) => direction === "LONG" ? value === bullishValue : direction === "SHORT" ? value === bearishValue : false;
741
+ var calculateDistanceAtr = ({
742
+ atr,
743
+ currentPrice,
744
+ targetPrice,
745
+ direction,
746
+ longDistance
747
+ }) => {
748
+ if (atr == null || atr <= 0 || currentPrice == null || targetPrice == null || direction !== "LONG" && direction !== "SHORT") {
749
+ return null;
750
+ }
751
+ const distance = direction === "LONG" ? longDistance === "target_above" ? targetPrice - currentPrice : currentPrice - targetPrice : longDistance === "target_above" ? currentPrice - targetPrice : targetPrice - currentPrice;
752
+ return Number.isFinite(distance) ? distance / atr : null;
753
+ };
754
+ var toTrendFollowContinuationAlignment = ({
755
+ direction,
756
+ bullishValue,
757
+ bearishValue,
758
+ value
759
+ }) => {
760
+ if (value == null || direction !== "LONG" && direction !== "SHORT") {
761
+ return null;
762
+ }
763
+ return direction === "LONG" ? value === bullishValue : value === bearishValue;
764
+ };
765
+ var buildTrendFollowGateFeatures = ({
766
+ signalContext,
767
+ baseContext,
768
+ prices,
769
+ volumeStructureDirectionAligned,
770
+ flushSupport,
771
+ directionalCrowding
772
+ }) => {
773
+ const direction = signalContext.signalDirection;
774
+ const atr = asFiniteNumber2(baseContext?.raw?.volatility?.atr) ?? asFiniteNumber2(signalContext.atr);
775
+ const currentPrice = asFiniteNumber2(prices?.currentPrice) ?? asFiniteNumber2(signalContext.currentPrice);
776
+ const setupStopDistanceAtr = calculateDistanceAtr({
777
+ atr,
778
+ currentPrice,
779
+ targetPrice: asFiniteNumber2(prices?.stopLossPrice) ?? asFiniteNumber2(signalContext.trailStop),
780
+ direction,
781
+ longDistance: "target_below"
782
+ }) ?? asFiniteNumber2(baseContext?.gateFeatures?.setup?.stopDistanceAtr);
783
+ const setupTpDistanceAtr = calculateDistanceAtr({
784
+ atr,
785
+ currentPrice,
786
+ targetPrice: asFiniteNumber2(prices?.takeProfitPrice),
787
+ direction,
788
+ longDistance: "target_above"
789
+ }) ?? asFiniteNumber2(baseContext?.gateFeatures?.setup?.tpDistanceAtr);
790
+ const setupRewardToVolatility = setupTpDistanceAtr ?? asFiniteNumber2(baseContext?.gateFeatures?.setup?.rewardToVolatility);
791
+ const setupRiskShape = setupStopDistanceAtr == null ? "unknown" : setupStopDistanceAtr < 0.8 ? "too_tight" : setupStopDistanceAtr < 1.15 ? "tight" : setupStopDistanceAtr <= 3.5 ? "balanced" : "wide";
792
+ const breakoutBodyAtr = asFiniteNumber2(
793
+ baseContext?.structure?.acceptance?.breakoutBodyAtr
794
+ );
795
+ const closesAboveHighLevel3 = asFiniteNumber2(
796
+ baseContext?.structure?.acceptance?.closesAboveHighLevel3
797
+ );
798
+ const closesBelowLowLevel3 = asFiniteNumber2(
799
+ baseContext?.structure?.acceptance?.closesBelowLowLevel3
800
+ );
801
+ const continuationCloses = direction === "LONG" ? closesAboveHighLevel3 : direction === "SHORT" ? closesBelowLowLevel3 : null;
802
+ const breakoutAcceptance = continuationCloses == null ? "unknown" : continuationCloses >= 2 ? "confirmed" : continuationCloses >= 1 ? "single_close" : "failed_acceptance";
803
+ const breakoutState = baseContext?.structure?.localRange?.breakoutState ?? null;
804
+ const breakoutWithDirection = toTrendFollowContinuationAlignment({
805
+ direction,
806
+ bullishValue: "above_high_level",
807
+ bearishValue: "below_low_level",
808
+ value: breakoutState
809
+ });
810
+ const failedBreakoutForDirection = toTrendFollowContinuationAlignment({
811
+ direction,
812
+ bullishValue: "failed_low_breakout",
813
+ bearishValue: "failed_high_breakout",
814
+ value: breakoutState
815
+ });
816
+ const continuationState = breakoutWithDirection === true ? "breakout_confirmed" : failedBreakoutForDirection === true ? "failed_breakout" : breakoutState === "inside_range" ? "inside_range" : "unknown";
817
+ const volumeRel20 = asFiniteNumber2(
818
+ baseContext?.participation?.volume?.volumeRel20
819
+ );
820
+ const participationState = volumeRel20 == null ? "unknown" : volumeRel20 < 0.8 ? "thin" : volumeRel20 < 1.5 ? "weak" : "confirmed";
821
+ const derivativesDirectionAligned = typeof baseContext?.derivatives?.summary?.directionAligned === "boolean" ? baseContext.derivatives.summary.directionAligned : null;
822
+ const derivativesContinuation = flushSupport ? "flush_support" : derivativesDirectionAligned === true ? "aligned" : derivativesDirectionAligned === false ? "conflict" : directionalCrowding ? "crowded" : baseContext?.derivatives?.summary == null ? "unknown" : "neutral";
823
+ const benchmarkTrendAlignment = baseContext?.relative?.benchmark?.trendAlignment ?? null;
824
+ const benchmarkAligned = benchmarkTrendAlignment === "against_benchmark" ? false : toTrendFollowContinuationAlignment({
825
+ direction,
826
+ bullishValue: "aligned_bull",
827
+ bearishValue: "aligned_bear",
828
+ value: benchmarkTrendAlignment
829
+ });
830
+ const relativeContinuation = benchmarkAligned === true ? "aligned" : benchmarkAligned === false ? "against" : benchmarkTrendAlignment == null ? "unknown" : "neutral";
831
+ const marketBreadth = baseContext?.relative?.marketBreadth;
832
+ const marketBreadthReturn = asFiniteNumber2(
833
+ marketBreadth?.equalWeightedReturn
834
+ );
835
+ const marketBreadthAligned = marketBreadthReturn == null || marketBreadth?.stale ? null : direction === "LONG" ? marketBreadthReturn >= 0 : direction === "SHORT" ? marketBreadthReturn <= 0 : null;
836
+ const marketBreadthContinuation = marketBreadth?.stale === true ? "stale" : marketBreadthAligned === true ? "aligned" : marketBreadthAligned === false ? "against" : "unknown";
837
+ const marketVolatilityState = typeof baseContext?.regime?.volatility?.state === "string" ? baseContext.regime.volatility.state : null;
838
+ const targetVsBtcBeta20 = asFiniteNumber2(
839
+ baseContext?.relative?.targetVsBtc?.betaToBtc20
840
+ );
841
+ const sharedParticipationScore = asFiniteNumber2(
842
+ baseContext?.gateFeatures?.scores?.participation
843
+ );
844
+ const btcAltRegime = baseContext?.relative?.btcAltRegime;
845
+ const btcAltRegimeBtcTurnoverShare24h = asFiniteNumber2(
846
+ btcAltRegime?.btcTurnoverShare24h
847
+ );
848
+ const btcAltRegimeAltBasketReturn24h = asFiniteNumber2(
849
+ btcAltRegime?.altBasketReturn24h
850
+ );
851
+ const derivatives1h = baseContext?.derivatives?.intervals?.["1h"];
852
+ const derivatives1hOiChangePct24h = asFiniteNumber2(
853
+ derivatives1h?.oiChangePct24h
854
+ );
855
+ const derivatives1hLiqLong = asFiniteNumber2(derivatives1h?.liqLong);
856
+ const derivatives1hLiqImbalance = asFiniteNumber2(derivatives1h?.liqImbalance);
857
+ const xrp15m = getReferenceInterval(baseContext, "XRPUSDT", "15m");
858
+ const xrp1h = getReferenceInterval(baseContext, "XRPUSDT", "1h");
859
+ const sol15m = getReferenceInterval(baseContext, "SOLUSDT", "15m");
860
+ const bnb15m = getReferenceInterval(baseContext, "BNBUSDT", "15m");
861
+ const trx15m = getReferenceInterval(baseContext, "TRXUSDT", "15m");
862
+ const ethSummary = getReferenceSummary(baseContext, "ETHUSDT");
863
+ const minutesFromSessionOpen = asFiniteNumber2(
864
+ baseContext?.regime?.session?.minutesFromSessionOpen
865
+ );
866
+ const referenceXrp15mOpenInterest = asFiniteNumber2(xrp15m?.openInterest);
867
+ const referenceXrp15mOiChangePct24h = asFiniteNumber2(xrp15m?.oiChangePct24h);
868
+ const referenceXrp1hOiChangePct1h = asFiniteNumber2(xrp1h?.oiChangePct1h);
869
+ const referenceSol15mOpenInterest = asFiniteNumber2(sol15m?.openInterest);
870
+ const referenceSol15mOiChangePct4h = asFiniteNumber2(sol15m?.oiChangePct4h);
871
+ const referenceSol15mFundingZScore = asFiniteNumber2(sol15m?.fundingZScore);
872
+ const referenceBnb15mOpenInterest = asFiniteNumber2(bnb15m?.openInterest);
873
+ const referenceBnb15mOiChangePct24h = asFiniteNumber2(bnb15m?.oiChangePct24h);
874
+ const referenceTrx15mFundingRate = asFiniteNumber2(trx15m?.fundingRate);
875
+ const referenceEthCrowdingPersistenceBars = asFiniteNumber2(
876
+ ethSummary?.crowdingPersistenceBars
877
+ );
878
+ const derivativesShortFlushOiPocket = direction === "SHORT" && derivatives1hOiChangePct24h != null && derivatives1hOiChangePct24h >= TREND_FOLLOW_SHORT_FLUSH_OI_MIN_CHANGE_24H && derivatives1hLiqLong != null && derivatives1hLiqLong >= TREND_FOLLOW_SHORT_FLUSH_OI_MIN_LIQ_LONG && derivatives1hLiqImbalance != null && derivatives1hLiqImbalance <= TREND_FOLLOW_SHORT_FLUSH_OI_MAX_LIQ_IMBALANCE;
879
+ const marketRegimeCadencePocket = btcAltRegimeBtcTurnoverShare24h != null && btcAltRegimeBtcTurnoverShare24h < TREND_FOLLOW_SHORT_MARKET_MAX_BTC_TURNOVER_SHARE_24H && btcAltRegimeAltBasketReturn24h != null && btcAltRegimeAltBasketReturn24h >= TREND_FOLLOW_SHORT_MARKET_MIN_ALT_BASKET_RETURN_24H && btcAltRegimeAltBasketReturn24h < TREND_FOLLOW_SHORT_MARKET_MAX_ALT_BASKET_RETURN_24H;
880
+ const relativeCadencePocket = targetVsBtcBeta20 != null && targetVsBtcBeta20 > TREND_FOLLOW_SHORT_MIN_TARGET_BTC_BETA_20;
881
+ const participationCadencePocket = sharedParticipationScore != null && sharedParticipationScore <= TREND_FOLLOW_SHORT_MAX_SHARED_PARTICIPATION_SCORE;
882
+ const legacyCadencePocket = derivativesShortFlushOiPocket && marketRegimeCadencePocket && relativeCadencePocket && participationCadencePocket;
883
+ const referenceDerivativesOiCompressionPocket = direction === "SHORT" && referenceXrp15mOpenInterest != null && referenceXrp15mOpenInterest >= TREND_FOLLOW_REF_HIGH_XRP_OI_MIN && referenceSol15mOpenInterest != null && referenceSol15mOpenInterest >= TREND_FOLLOW_REF_HIGH_SOL_OI_MIN && referenceBnb15mOiChangePct24h != null && referenceBnb15mOiChangePct24h <= TREND_FOLLOW_REF_BNB_OI_CHANGE_24H_MAX;
884
+ const referenceDerivativesXrpFundingPocket = direction === "SHORT" && referenceXrp15mOpenInterest != null && referenceXrp15mOpenInterest >= TREND_FOLLOW_REF_HIGH_XRP_OI_MIN && referenceXrp15mOiChangePct24h != null && referenceXrp15mOiChangePct24h >= TREND_FOLLOW_REF_XRP_OI_CHANGE_24H_MIN && referenceTrx15mFundingRate != null && referenceTrx15mFundingRate <= TREND_FOLLOW_REF_TRX_FUNDING_RATE_MAX;
885
+ const referenceDerivativesSolFlushPocket = direction === "SHORT" && referenceSol15mOpenInterest != null && referenceSol15mOpenInterest >= TREND_FOLLOW_REF_SOL_FLUSH_OI_MIN && derivativesShortFlushOiPocket && referenceSol15mOiChangePct4h != null && referenceSol15mOiChangePct4h >= TREND_FOLLOW_REF_SOL_OI_CHANGE_4H_MIN;
886
+ const referenceDerivativesLossBlock = direction === "SHORT" && referenceXrp15mOpenInterest != null && referenceXrp15mOpenInterest >= TREND_FOLLOW_REF_LOSS_XRP_OI_MIN && referenceEthCrowdingPersistenceBars != null && referenceEthCrowdingPersistenceBars >= TREND_FOLLOW_REF_LOSS_ETH_CROWDING_MIN && referenceSol15mFundingZScore != null && referenceSol15mFundingZScore <= TREND_FOLLOW_REF_LOSS_SOL_FUNDING_Z_MAX;
887
+ const referenceDerivativesCadencePocket = !referenceDerivativesLossBlock && (legacyCadencePocket || referenceDerivativesOiCompressionPocket || referenceDerivativesXrpFundingPocket || referenceDerivativesSolFlushPocket);
888
+ const referenceDerivativesOpeningPocket = direction === "SHORT" && minutesFromSessionOpen != null && minutesFromSessionOpen <= TREND_FOLLOW_OPENING_SESSION_MAX_MINUTES_FROM_OPEN && referenceXrp15mOpenInterest != null && referenceXrp15mOpenInterest <= TREND_FOLLOW_OPENING_REF_XRP_OI_MAX && referenceXrp1hOiChangePct1h != null && referenceXrp1hOiChangePct1h >= TREND_FOLLOW_OPENING_REF_XRP_OI_CHANGE_1H_MIN && referenceBnb15mOpenInterest != null && referenceBnb15mOpenInterest >= TREND_FOLLOW_OPENING_REF_BNB_OI_MIN;
889
+ const referenceDerivativesCleanCadencePocket = referenceDerivativesCadencePocket && !derivativesShortFlushOiPocket;
890
+ const normalVolatilityCadencePocket = marketVolatilityState === TREND_FOLLOW_ALLOWED_VOLATILITY_STATE;
891
+ return {
892
+ setupStopDistanceAtr,
893
+ setupTpDistanceAtr,
894
+ setupRewardToVolatility,
895
+ setupRiskShape,
896
+ breakoutBodyAtr,
897
+ breakoutAcceptance,
898
+ continuationState,
899
+ participationState,
900
+ directionalVolumeAligned: volumeStructureDirectionAligned,
901
+ derivativesContinuation,
902
+ relativeContinuation,
903
+ marketBreadthContinuation,
904
+ marketBreadthDispersion: asFiniteNumber2(marketBreadth?.dispersion),
905
+ marketVolatilityState,
906
+ targetVsBtcBeta20,
907
+ btcAltRegimeBtcTurnoverShare24h,
908
+ btcAltRegimeAltBasketReturn24h,
909
+ derivatives1hOiChangePct24h,
910
+ derivatives1hLiqLong,
911
+ derivatives1hLiqImbalance,
912
+ sharedParticipationScore,
913
+ minutesFromSessionOpen,
914
+ referenceXrp15mOpenInterest,
915
+ referenceXrp15mOiChangePct24h,
916
+ referenceXrp1hOiChangePct1h,
917
+ referenceSol15mOpenInterest,
918
+ referenceSol15mOiChangePct4h,
919
+ referenceSol15mFundingZScore,
920
+ referenceBnb15mOpenInterest,
921
+ referenceBnb15mOiChangePct24h,
922
+ referenceTrx15mFundingRate,
923
+ referenceEthCrowdingPersistenceBars,
924
+ derivativesShortFlushOiPocket,
925
+ marketRegimeCadencePocket,
926
+ participationCadencePocket,
927
+ referenceDerivativesOiCompressionPocket,
928
+ referenceDerivativesXrpFundingPocket,
929
+ referenceDerivativesSolFlushPocket,
930
+ referenceDerivativesLossBlock,
931
+ referenceDerivativesCadencePocket,
932
+ referenceDerivativesCleanCadencePocket,
933
+ referenceDerivativesOpeningPocket,
934
+ normalVolatilityCadencePocket,
935
+ highQualityCadencePocket: normalVolatilityCadencePocket && referenceDerivativesCleanCadencePocket
936
+ };
937
+ };
938
+ var buildTrendFollowGuardrailContext = ({
939
+ signalContext,
940
+ baseContext,
941
+ prices
942
+ }) => {
943
+ const derivativesSummary = baseContext?.derivatives?.summary ?? null;
944
+ const primarySession = baseContext?.regime?.session?.sessionPhase ?? null;
945
+ const trendBias = baseContext?.regime?.trend?.bias ?? null;
946
+ const trendFollowState = signalContext.signalDirection === "LONG" ? "bull" : signalContext.signalDirection === "SHORT" ? "bear" : null;
947
+ const breakoutState = baseContext?.structure?.localRange?.breakoutState ?? null;
948
+ const momentumRsi = asFiniteNumber2(baseContext?.regime?.momentum?.rsi);
949
+ const volumeRel20 = asFiniteNumber2(
950
+ baseContext?.participation?.volume?.volumeRel20
951
+ );
952
+ const deltaDivergenceVsPrice = baseContext?.participation?.delta?.deltaDivergenceVsPrice ?? null;
953
+ const totalUpVolumeShare = asFiniteNumber2(
954
+ baseContext?.participation?.volumeStructure?.totalUpVolumeShare
955
+ );
956
+ const totalDownVolumeShare = asFiniteNumber2(
957
+ baseContext?.participation?.volumeStructure?.totalDownVolumeShare
958
+ );
959
+ const benchmarkTrendAlignment = baseContext?.relative?.benchmark?.trendAlignment ?? null;
960
+ const derivativesPressure = typeof derivativesSummary?.pressure === "string" ? derivativesSummary.pressure : null;
961
+ const derivativesDirectionAligned = typeof derivativesSummary?.directionAligned === "boolean" ? derivativesSummary.directionAligned : null;
962
+ const derivativesRiskFlags = asStringArray(derivativesSummary?.riskFlags);
963
+ const hardBlockReasons = [];
964
+ const softBlockReasons = [];
965
+ if (signalContext.signalDirection !== "LONG" && signalContext.signalDirection !== "SHORT") {
966
+ hardBlockReasons.push("missing_direction");
967
+ }
968
+ if ((signalContext.atr ?? 0) <= 0 || signalContext.trailStop == null) {
969
+ hardBlockReasons.push("missing_trailing_stop");
970
+ }
971
+ if ((signalContext.breakoutDistancePct ?? 0) <= 0) {
972
+ hardBlockReasons.push("missing_breakout");
973
+ }
974
+ if ((signalContext.distanceToStopPct ?? 0) <= 0) {
975
+ hardBlockReasons.push("invalid_stop_distance");
976
+ }
977
+ const direction = signalContext.signalDirection;
978
+ const trendAligned = isDirectionAligned2({
979
+ direction,
980
+ bullishValue: "bull",
981
+ bearishValue: "bear",
982
+ value: trendBias
983
+ });
984
+ const benchmarkAligned = isDirectionAligned2({
985
+ direction,
986
+ bullishValue: "aligned_bull",
987
+ bearishValue: "aligned_bear",
988
+ value: benchmarkTrendAlignment
989
+ });
990
+ const breakoutAligned = isDirectionAligned2({
991
+ direction,
992
+ bullishValue: "above_high_level",
993
+ bearishValue: "below_low_level",
994
+ value: breakoutState
995
+ });
996
+ const strategyTrendFollowAligned = isDirectionAligned2({
997
+ direction,
998
+ bullishValue: "bull",
999
+ bearishValue: "bear",
1000
+ value: trendFollowState
1001
+ });
1002
+ const flushSupport = direction === "LONG" ? derivativesRiskFlags.includes("short_liquidation_spike") || derivativesPressure === "short_flush" : direction === "SHORT" ? derivativesRiskFlags.includes("long_liquidation_spike") || derivativesPressure === "long_flush" : false;
1003
+ const directionalCrowding = direction === "LONG" ? derivativesRiskFlags.includes("crowded_long") : direction === "SHORT" ? derivativesRiskFlags.includes("crowded_short") : false;
1004
+ const adverseDeltaDivergence = direction === "LONG" ? deltaDivergenceVsPrice === "bearish" : direction === "SHORT" ? deltaDivergenceVsPrice === "bullish" : false;
1005
+ const volumeStructureDirectionalShare = direction === "LONG" ? totalUpVolumeShare : direction === "SHORT" ? totalDownVolumeShare : null;
1006
+ const volumeStructureDirectionAligned = volumeStructureDirectionalShare == null ? null : volumeStructureDirectionalShare >= 0.48;
1007
+ const breakoutDistancePct = signalContext.breakoutDistancePct ?? 0;
1008
+ const distanceToStopPct = signalContext.distanceToStopPct ?? 0;
1009
+ const highConvictionApprovalPocket = direction === "SHORT" && (primarySession === "off_hours" || primarySession === "asia") && breakoutDistancePct >= 0.5 && breakoutDistancePct <= 2 && distanceToStopPct >= 0.5 && distanceToStopPct <= 3;
1010
+ const trendFollowGateFeatures = buildTrendFollowGateFeatures({
1011
+ signalContext,
1012
+ baseContext,
1013
+ prices,
1014
+ volumeStructureDirectionAligned,
1015
+ flushSupport,
1016
+ directionalCrowding
1017
+ });
1018
+ if (volumeRel20 != null && volumeRel20 < 0.8) {
1019
+ softBlockReasons.push("thin_participation");
1020
+ } else if (volumeRel20 != null && volumeRel20 < 1.5) {
1021
+ softBlockReasons.push("weak_relative_volume");
1022
+ }
1023
+ if (trendFollowGateFeatures.setupRiskShape === "unknown") {
1024
+ softBlockReasons.push("missing_setup_stop_distance_atr");
1025
+ } else if (trendFollowGateFeatures.setupStopDistanceAtr != null && trendFollowGateFeatures.setupStopDistanceAtr < 1.15) {
1026
+ softBlockReasons.push("tight_setup_stop_distance_atr");
1027
+ }
1028
+ if (trendFollowGateFeatures.setupTpDistanceAtr != null && trendFollowGateFeatures.setupTpDistanceAtr < 1.5) {
1029
+ softBlockReasons.push("weak_setup_tp_distance_atr");
1030
+ }
1031
+ if (direction === "SHORT" && momentumRsi != null && momentumRsi > 36.35) {
1032
+ softBlockReasons.push("weak_downside_momentum");
1033
+ }
1034
+ if (directionalCrowding && !flushSupport) {
1035
+ softBlockReasons.push("directional_crowding");
1036
+ }
1037
+ if (derivativesDirectionAligned === false && !flushSupport) {
1038
+ softBlockReasons.push("derivatives_not_aligned");
1039
+ }
1040
+ if (breakoutState === "inside_range") {
1041
+ softBlockReasons.push("inside_range_breakout");
1042
+ }
1043
+ if (adverseDeltaDivergence) {
1044
+ softBlockReasons.push("adverse_delta_divergence");
1045
+ }
1046
+ if (volumeStructureDirectionAligned === false) {
1047
+ softBlockReasons.push("weak_volume_structure");
1048
+ }
1049
+ if (!trendFollowGateFeatures.highQualityCadencePocket) {
1050
+ softBlockReasons.push("outside_high_conviction_cadence_pocket");
1051
+ }
1052
+ let deterministicQuality = 3;
1053
+ if (hardBlockReasons.length > 0) {
1054
+ deterministicQuality = 1;
1055
+ } else if (breakoutDistancePct >= 0.25 && breakoutDistancePct <= 2.5 && distanceToStopPct >= 0.25 && (trendAligned || strategyTrendFollowAligned || benchmarkAligned || breakoutAligned || flushSupport)) {
1056
+ deterministicQuality = flushSupport || breakoutAligned || strategyTrendFollowAligned ? 5 : 4;
1057
+ } else if (breakoutDistancePct > 0 && distanceToStopPct > 0) {
1058
+ deterministicQuality = 4;
1059
+ }
1060
+ if (deterministicQuality >= 5 && softBlockReasons.length > 0) {
1061
+ deterministicQuality = 4;
1062
+ }
1063
+ return {
1064
+ ...signalContext,
1065
+ baseContextAvailable: Boolean(baseContext),
1066
+ primarySession,
1067
+ trendBias,
1068
+ trendFollowState,
1069
+ breakoutState,
1070
+ momentumRsi,
1071
+ volumeRel20,
1072
+ deltaDivergenceVsPrice,
1073
+ volumeStructureDirectionalShare,
1074
+ volumeStructureDirectionAligned,
1075
+ highConvictionApprovalPocket,
1076
+ benchmarkTrendAlignment,
1077
+ derivativesPressure,
1078
+ derivativesDirectionAligned,
1079
+ derivativesRiskFlags,
1080
+ trendFollowGateFeatures,
1081
+ hardBlockReasons,
1082
+ softBlockReasons,
1083
+ deterministicQuality,
1084
+ approvalAllowedNow: deterministicQuality >= 4 && hardBlockReasons.length === 0 && trendFollowGateFeatures.highQualityCadencePocket
1085
+ };
1086
+ };
1087
+
1088
+ // src/TrendFollow/adapters/ai.ts
1089
+ import {
1090
+ getAiPayloadNumber,
1091
+ withStrategyLocalAiGate
1092
+ } from "@tradejs/strategy-kit/ai-gate";
1093
+ var asRecord = (value) => typeof value === "object" && value != null && !Array.isArray(value) ? value : null;
1094
+ var getTrendFollowContext = (payload) => {
1095
+ const additional = asRecord(payload.additionalIndicators);
1096
+ const signalContext = (additional?.trendFollowContext ?? {}) || {};
1097
+ const baseContext = additional?.baseContext ?? null;
1098
+ return buildTrendFollowGuardrailContext({
1099
+ signalContext,
1100
+ baseContext,
1101
+ prices: payload.signal?.prices
1102
+ });
1103
+ };
1104
+ var trendFollowBaseAiAdapter = {
1105
+ buildPayload: ({ signal, basePayload }) => {
1106
+ const payload = {
1107
+ ...basePayload,
1108
+ additionalIndicators: {
1109
+ ...basePayload.additionalIndicators,
1110
+ trendFollowContext: signal.additionalIndicators?.trendFollowContext
1111
+ }
1112
+ };
1113
+ return {
1114
+ ...payload,
1115
+ additionalIndicators: {
1116
+ ...payload.additionalIndicators,
1117
+ trendFollowContext: getTrendFollowContext(payload)
1118
+ }
1119
+ };
1120
+ },
1121
+ postProcessAnalysis: ({ payload, analysis }) => {
1122
+ const context = getTrendFollowContext(payload);
1123
+ const requestedDirection = analysis.direction === "LONG" || analysis.direction === "SHORT" ? analysis.direction : context.signalDirection;
1124
+ const approved = context.approvalAllowedNow === true && requestedDirection != null;
1125
+ return {
1126
+ ...analysis,
1127
+ direction: approved ? requestedDirection : null,
1128
+ quality: context.deterministicQuality,
1129
+ approved,
1130
+ rejectReason: approved ? void 0 : [...context.hardBlockReasons, ...context.softBlockReasons].join(
1131
+ "; "
1132
+ ) || "Trend Follow breakout lacks confirmation."
1133
+ };
1134
+ },
1135
+ buildHumanPromptAddon: ({ payload }) => {
1136
+ const context = getTrendFollowContext(payload);
1137
+ return `
1138
+ Additional TrendFollow context:
1139
+ - signalDirection=${context.signalDirection ?? "n/a"}
1140
+ - entryLevel=${String(context.entryLevel ?? "n/a")}
1141
+ - trailStop=${String(context.trailStop ?? "n/a")}
1142
+ - atr=${String(context.atr ?? "n/a")}
1143
+ - pivotKind=${context.pivotKind ?? "n/a"}
1144
+ - pivotTimestamp=${String(context.pivotTimestamp ?? "n/a")}
1145
+ - pivotValue=${String(context.pivotValue ?? "n/a")}
1146
+ - barsSinceSignal=${String(context.barsSinceSignal ?? "n/a")}
1147
+ - breakoutDistancePct=${String(context.breakoutDistancePct ?? "n/a")}
1148
+ - distanceToStopPct=${String(context.distanceToStopPct ?? "n/a")}
1149
+ - currentPrice=${String(context.currentPrice ?? "n/a")}
1150
+ - primarySession=${context.primarySession ?? "n/a"}
1151
+ - trendBias=${context.trendBias ?? "n/a"}
1152
+ - breakoutState=${context.breakoutState ?? "n/a"}
1153
+ - momentumRsi=${String(context.momentumRsi ?? "n/a")}
1154
+ - volumeRel20=${String(context.volumeRel20 ?? "n/a")}
1155
+ - deltaDivergenceVsPrice=${context.deltaDivergenceVsPrice ?? "n/a"}
1156
+ - volumeStructureDirectionalShare=${String(context.volumeStructureDirectionalShare ?? "n/a")}
1157
+ - volumeStructureDirectionAligned=${String(context.volumeStructureDirectionAligned ?? "n/a")}
1158
+ - highConvictionApprovalPocket=${String(context.highConvictionApprovalPocket)}
1159
+ - trendFollowGateSetupStopDistanceAtr=${String(context.trendFollowGateFeatures.setupStopDistanceAtr ?? "n/a")}
1160
+ - trendFollowGateSetupTpDistanceAtr=${String(context.trendFollowGateFeatures.setupTpDistanceAtr ?? "n/a")}
1161
+ - trendFollowGateSetupRewardToVolatility=${String(context.trendFollowGateFeatures.setupRewardToVolatility ?? "n/a")}
1162
+ - trendFollowGateSetupRiskShape=${context.trendFollowGateFeatures.setupRiskShape}
1163
+ - trendFollowGateBreakoutBodyAtr=${String(context.trendFollowGateFeatures.breakoutBodyAtr ?? "n/a")}
1164
+ - trendFollowGateBreakoutAcceptance=${context.trendFollowGateFeatures.breakoutAcceptance}
1165
+ - trendFollowGateContinuationState=${context.trendFollowGateFeatures.continuationState}
1166
+ - trendFollowGateParticipationState=${context.trendFollowGateFeatures.participationState}
1167
+ - trendFollowGateDirectionalVolumeAligned=${String(context.trendFollowGateFeatures.directionalVolumeAligned ?? "n/a")}
1168
+ - trendFollowGateDerivativesContinuation=${context.trendFollowGateFeatures.derivativesContinuation}
1169
+ - trendFollowGateRelativeContinuation=${context.trendFollowGateFeatures.relativeContinuation}
1170
+ - trendFollowGateMarketBreadthContinuation=${context.trendFollowGateFeatures.marketBreadthContinuation}
1171
+ - trendFollowGateMarketBreadthDispersion=${String(context.trendFollowGateFeatures.marketBreadthDispersion ?? "n/a")}
1172
+ - trendFollowGateMarketVolatilityState=${String(context.trendFollowGateFeatures.marketVolatilityState ?? "n/a")}
1173
+ - trendFollowGateTargetVsBtcBeta20=${String(context.trendFollowGateFeatures.targetVsBtcBeta20 ?? "n/a")}
1174
+ - trendFollowGateBtcAltRegimeBtcTurnoverShare24h=${String(context.trendFollowGateFeatures.btcAltRegimeBtcTurnoverShare24h ?? "n/a")}
1175
+ - trendFollowGateBtcAltRegimeAltBasketReturn24h=${String(context.trendFollowGateFeatures.btcAltRegimeAltBasketReturn24h ?? "n/a")}
1176
+ - trendFollowGateDerivatives1hOiChangePct24h=${String(context.trendFollowGateFeatures.derivatives1hOiChangePct24h ?? "n/a")}
1177
+ - trendFollowGateDerivatives1hLiqLong=${String(context.trendFollowGateFeatures.derivatives1hLiqLong ?? "n/a")}
1178
+ - trendFollowGateDerivatives1hLiqImbalance=${String(context.trendFollowGateFeatures.derivatives1hLiqImbalance ?? "n/a")}
1179
+ - trendFollowGateSharedParticipationScore=${String(context.trendFollowGateFeatures.sharedParticipationScore ?? "n/a")}
1180
+ - trendFollowGateMinutesFromSessionOpen=${String(context.trendFollowGateFeatures.minutesFromSessionOpen ?? "n/a")}
1181
+ - trendFollowGateReferenceXrp15mOpenInterest=${String(context.trendFollowGateFeatures.referenceXrp15mOpenInterest ?? "n/a")}
1182
+ - trendFollowGateReferenceXrp15mOiChangePct24h=${String(context.trendFollowGateFeatures.referenceXrp15mOiChangePct24h ?? "n/a")}
1183
+ - trendFollowGateReferenceXrp1hOiChangePct1h=${String(context.trendFollowGateFeatures.referenceXrp1hOiChangePct1h ?? "n/a")}
1184
+ - trendFollowGateReferenceSol15mOpenInterest=${String(context.trendFollowGateFeatures.referenceSol15mOpenInterest ?? "n/a")}
1185
+ - trendFollowGateReferenceSol15mOiChangePct4h=${String(context.trendFollowGateFeatures.referenceSol15mOiChangePct4h ?? "n/a")}
1186
+ - trendFollowGateReferenceSol15mFundingZScore=${String(context.trendFollowGateFeatures.referenceSol15mFundingZScore ?? "n/a")}
1187
+ - trendFollowGateReferenceBnb15mOpenInterest=${String(context.trendFollowGateFeatures.referenceBnb15mOpenInterest ?? "n/a")}
1188
+ - trendFollowGateReferenceBnb15mOiChangePct24h=${String(context.trendFollowGateFeatures.referenceBnb15mOiChangePct24h ?? "n/a")}
1189
+ - trendFollowGateReferenceTrx15mFundingRate=${String(context.trendFollowGateFeatures.referenceTrx15mFundingRate ?? "n/a")}
1190
+ - trendFollowGateReferenceEthCrowdingPersistenceBars=${String(context.trendFollowGateFeatures.referenceEthCrowdingPersistenceBars ?? "n/a")}
1191
+ - trendFollowGateDerivativesShortFlushOiPocket=${String(context.trendFollowGateFeatures.derivativesShortFlushOiPocket)}
1192
+ - trendFollowGateMarketRegimeCadencePocket=${String(context.trendFollowGateFeatures.marketRegimeCadencePocket)}
1193
+ - trendFollowGateParticipationCadencePocket=${String(context.trendFollowGateFeatures.participationCadencePocket)}
1194
+ - trendFollowGateReferenceDerivativesOiCompressionPocket=${String(context.trendFollowGateFeatures.referenceDerivativesOiCompressionPocket)}
1195
+ - trendFollowGateReferenceDerivativesXrpFundingPocket=${String(context.trendFollowGateFeatures.referenceDerivativesXrpFundingPocket)}
1196
+ - trendFollowGateReferenceDerivativesSolFlushPocket=${String(context.trendFollowGateFeatures.referenceDerivativesSolFlushPocket)}
1197
+ - trendFollowGateReferenceDerivativesLossBlock=${String(context.trendFollowGateFeatures.referenceDerivativesLossBlock)}
1198
+ - trendFollowGateReferenceDerivativesCadencePocket=${String(context.trendFollowGateFeatures.referenceDerivativesCadencePocket)}
1199
+ - trendFollowGateReferenceDerivativesCleanCadencePocket=${String(context.trendFollowGateFeatures.referenceDerivativesCleanCadencePocket)}
1200
+ - trendFollowGateReferenceDerivativesOpeningPocket=${String(context.trendFollowGateFeatures.referenceDerivativesOpeningPocket)}
1201
+ - trendFollowGateNormalVolatilityCadencePocket=${String(context.trendFollowGateFeatures.normalVolatilityCadencePocket)}
1202
+ - trendFollowGateHighQualityCadencePocket=${String(context.trendFollowGateFeatures.highQualityCadencePocket)}
1203
+ - benchmarkTrendAlignment=${context.benchmarkTrendAlignment ?? "n/a"}
1204
+ - derivativesPressure=${context.derivativesPressure ?? "n/a"}
1205
+ - derivativesDirectionAligned=${String(context.derivativesDirectionAligned ?? "n/a")}
1206
+ - derivativesRiskFlags=${JSON.stringify(context.derivativesRiskFlags)}
1207
+ - deterministicQuality=${context.deterministicQuality}
1208
+ - approvalAllowedNow=${String(context.approvalAllowedNow)}
1209
+ - hardBlockReasons=${JSON.stringify(context.hardBlockReasons)}
1210
+ - softBlockReasons=${JSON.stringify(context.softBlockReasons)}
1211
+
1212
+ Interpretation rules for TrendFollow:
1213
+ - This is a market-structure trend-following strategy, not a mean-reversion setup.
1214
+ - LONG appears when close crosses above the latest confirmed pivot high.
1215
+ - SHORT appears when close crosses below the latest confirmed pivot low.
1216
+ - The ATR trailing stop is the structural invalidation line and also updates while a position is open.
1217
+ - Prefer breakouts aligned with shared market context and backed by participation.
1218
+ - Late, thin, crowded, inside-range, adverse-delta, weak-momentum, or weak-volume-structure breakouts should be downgraded even if the pivot cross is valid.
1219
+ - Live approval is reserved for calibrated SHORT extra-reference derivatives pockets in normal volatility when the BTC benchmark short-flush/OI pocket is not active; legacy BTC benchmark flush and opening-session recovery remain watch mode.
1220
+ - Treat deterministicQuality and approvalAllowedNow as the local normalized gate result.
1221
+ `.trim();
1222
+ },
1223
+ mapEntryRuntimeFromConfig: (config2) => mapAiRuntimeFromConfig(
1224
+ config2
1225
+ )
1226
+ };
1227
+ var trendFollowAiAdapter = withStrategyLocalAiGate(
1228
+ trendFollowBaseAiAdapter,
1229
+ {
1230
+ id: "trend_follow_short_breadth_2026_08_12",
1231
+ approves: ({ signal, payload }) => {
1232
+ const advancers = getAiPayloadNumber(
1233
+ payload,
1234
+ "additionalIndicators.baseContext.relative.marketBreadth.advancers"
1235
+ );
1236
+ const top5Unchanged = getAiPayloadNumber(
1237
+ payload,
1238
+ "additionalIndicators.baseContext.relative.marketBreadths.top5.unchanged"
1239
+ );
1240
+ return signal.direction === "SHORT" && advancers != null && advancers >= 2 && top5Unchanged != null && top5Unchanged <= 0;
1241
+ }
1242
+ }
1243
+ );
1244
+
1245
+ // src/TrendFollow/manifest.ts
1246
+ var trendFollowManifest = {
1247
+ name: "TrendFollow",
1248
+ aiAdapter: trendFollowAiAdapter
1249
+ };
1250
+
1251
+ // src/TrendFollow/strategy.ts
1252
+ var TrendFollowStrategyDefinition = {
1253
+ defaults: config,
1254
+ createCore: createTrendFollowCore,
1255
+ manifest: trendFollowManifest
1256
+ };
1257
+
1258
+ // src/index.ts
1259
+ var strategyEntries = [
1260
+ TrendFollowStrategyDefinition
1261
+ ];
1262
+ var defaultConfigs = {
1263
+ TrendFollow: config
1264
+ };
1265
+ var getBuiltInStrategyDefaultConfig = (strategyName) => defaultConfigs[strategyName];
1266
+ var index_default = defineStrategyPlugin({ strategyEntries });
1267
+ export {
1268
+ TrendFollowStrategyDefinition,
1269
+ index_default as default,
1270
+ getBuiltInStrategyDefaultConfig,
1271
+ strategyEntries,
1272
+ trendFollowAiAdapter,
1273
+ config as trendFollowDefaultConfig,
1274
+ trendFollowManifest
1275
+ };