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