@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/LICENSE +96 -0
- package/README.md +34 -0
- package/dist/index.cjs +1299 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +1275 -0
- package/package.json +65 -0
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
|
+
});
|