@tradejs/strategies 1.0.5 → 1.0.8
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/chunk-37ZWRG3W.mjs +128 -0
- package/dist/chunk-H4MHFD4B.mjs +62 -0
- package/dist/chunk-IMLNXICX.mjs +547 -0
- package/dist/chunk-QVWMBYYM.mjs +836 -0
- package/dist/chunk-SOVTOGY4.mjs +163 -0
- package/dist/chunk-TDUTYEGH.mjs +797 -0
- package/dist/chunk-UK4YHD2E.mjs +683 -0
- package/dist/index.d.mts +325 -4
- package/dist/index.d.ts +325 -4
- package/dist/index.js +5689 -1195
- package/dist/index.mjs +61 -14
- package/dist/{strategy-ZVNTA6UR.mjs → strategy-ABIO65CR.mjs} +3 -46
- package/dist/strategy-D7H5J3C4.mjs +348 -0
- package/dist/strategy-HQIPCUOY.mjs +399 -0
- package/dist/{strategy-Y4SOK6FF.mjs → strategy-JQIJILHQ.mjs} +28 -109
- package/dist/strategy-QEIPAPY4.mjs +373 -0
- package/dist/strategy-WYN4FZ5S.mjs +613 -0
- package/dist/{strategy-UHRWSGZC.mjs → strategy-XTUKPYPM.mjs} +484 -128
- package/package.json +5 -5
- package/dist/chunk-3PN7ZSJJ.mjs +0 -27
- package/dist/chunk-MVIV5ZII.mjs +0 -137
- package/dist/chunk-RDK2JK3K.mjs +0 -28
- package/dist/chunk-RYEPHOGL.mjs +0 -28
- package/dist/chunk-ULLCAH5C.mjs +0 -67
- package/dist/strategy-M3BRWDRR.mjs +0 -377
- package/dist/strategy-UZBWST3G.mjs +0 -156
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
// src/ReverseTrendLine/adapters/ai.ts
|
|
2
|
+
import { mapAiRuntimeFromConfig } from "@tradejs/core/strategies";
|
|
3
|
+
|
|
4
|
+
// src/ReverseTrendLine/guardrails.ts
|
|
5
|
+
var REVERSE_TRENDLINE_NEAR_LINE_PCT = 0.45;
|
|
6
|
+
var REVERSE_TRENDLINE_FAILED_BOUNCE_PCT = 0.35;
|
|
7
|
+
var REVERSE_TRENDLINE_TIMING_WINDOW = 6;
|
|
8
|
+
var MIN_REJECTION_WICK_PCT = 0.12;
|
|
9
|
+
var MIN_REJECTION_STRENGTH_PCT = 0.08;
|
|
10
|
+
var FOLLOW_THROUGH_STRENGTH_PCT = 0.18;
|
|
11
|
+
var toFiniteNumberOrNull = (value) => {
|
|
12
|
+
const num = Number(value);
|
|
13
|
+
return Number.isFinite(num) ? num : null;
|
|
14
|
+
};
|
|
15
|
+
var getLastFiniteNumber = (value) => {
|
|
16
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return toFiniteNumberOrNull(value[value.length - 1]);
|
|
20
|
+
};
|
|
21
|
+
var getBias = (fast, slow) => {
|
|
22
|
+
if (fast == null || slow == null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (fast > slow) {
|
|
26
|
+
return "bullish";
|
|
27
|
+
}
|
|
28
|
+
if (fast < slow) {
|
|
29
|
+
return "bearish";
|
|
30
|
+
}
|
|
31
|
+
return "flat";
|
|
32
|
+
};
|
|
33
|
+
var getSpreadPct = (fast, slow) => {
|
|
34
|
+
if (fast == null || slow == null || slow === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return (fast - slow) / slow * 100;
|
|
38
|
+
};
|
|
39
|
+
var getTrendLineFromPayload = (signal) => signal.figures?.trendLine ?? signal.additionalIndicators?.trendLine ?? null;
|
|
40
|
+
var deriveDirectionFromMode = (mode) => {
|
|
41
|
+
if (mode === "lows") {
|
|
42
|
+
return "LONG";
|
|
43
|
+
}
|
|
44
|
+
if (mode === "highs") {
|
|
45
|
+
return "SHORT";
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
var getSortedTrendLinePoints = (trendLine) => {
|
|
50
|
+
const rawPoints = Array.isArray(trendLine?.points) ? trendLine.points : [];
|
|
51
|
+
return rawPoints.map((point) => {
|
|
52
|
+
if (!point || typeof point !== "object") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const typedPoint = point;
|
|
56
|
+
const timestamp = toFiniteNumberOrNull(typedPoint.timestamp);
|
|
57
|
+
const value = toFiniteNumberOrNull(typedPoint.value);
|
|
58
|
+
if (timestamp == null || value == null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return { timestamp, value };
|
|
62
|
+
}).filter(Boolean).sort((left, right) => left.timestamp - right.timestamp);
|
|
63
|
+
};
|
|
64
|
+
var buildTrendLineEvaluator = (trendLine) => {
|
|
65
|
+
const points = getSortedTrendLinePoints(trendLine);
|
|
66
|
+
if (points.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const firstPoint = points[0];
|
|
70
|
+
const lastPoint = points[points.length - 1];
|
|
71
|
+
const deltaTime = lastPoint.timestamp - firstPoint.timestamp;
|
|
72
|
+
if (deltaTime === 0) {
|
|
73
|
+
return {
|
|
74
|
+
firstPoint,
|
|
75
|
+
lastPoint,
|
|
76
|
+
evaluate: (_timestamp) => lastPoint.value
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const slope = (lastPoint.value - firstPoint.value) / deltaTime;
|
|
80
|
+
return {
|
|
81
|
+
firstPoint,
|
|
82
|
+
lastPoint,
|
|
83
|
+
evaluate: (timestamp) => firstPoint.value + slope * (timestamp - firstPoint.timestamp)
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
var getCurrentCandle = (signal) => {
|
|
87
|
+
const candle = signal.additionalIndicators?.currentCandle;
|
|
88
|
+
return candle && typeof candle === "object" ? candle : null;
|
|
89
|
+
};
|
|
90
|
+
var getLineTouched = ({
|
|
91
|
+
low,
|
|
92
|
+
high,
|
|
93
|
+
linePrice
|
|
94
|
+
}) => {
|
|
95
|
+
if (low == null || high == null || linePrice == null) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return low <= linePrice && high >= linePrice;
|
|
99
|
+
};
|
|
100
|
+
var getCloseOnBounceSide = ({
|
|
101
|
+
direction,
|
|
102
|
+
priceVsLinePct
|
|
103
|
+
}) => {
|
|
104
|
+
if (direction == null || priceVsLinePct == null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return direction === "LONG" ? priceVsLinePct >= 0 : priceVsLinePct <= 0;
|
|
108
|
+
};
|
|
109
|
+
var getFailedBounceBreak = ({
|
|
110
|
+
direction,
|
|
111
|
+
priceVsLinePct
|
|
112
|
+
}) => {
|
|
113
|
+
if (direction == null || priceVsLinePct == null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return direction === "LONG" ? priceVsLinePct <= -REVERSE_TRENDLINE_FAILED_BOUNCE_PCT : priceVsLinePct >= REVERSE_TRENDLINE_FAILED_BOUNCE_PCT;
|
|
117
|
+
};
|
|
118
|
+
var getBodyAligned = ({
|
|
119
|
+
direction,
|
|
120
|
+
open,
|
|
121
|
+
close
|
|
122
|
+
}) => {
|
|
123
|
+
if (direction == null || open == null || close == null) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return direction === "LONG" ? close >= open : close <= open;
|
|
127
|
+
};
|
|
128
|
+
var getRejectionWickPct = ({
|
|
129
|
+
direction,
|
|
130
|
+
open,
|
|
131
|
+
close,
|
|
132
|
+
high,
|
|
133
|
+
low
|
|
134
|
+
}) => {
|
|
135
|
+
if (direction == null || open == null || close == null || high == null || low == null || close <= 0) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const lowerWick = Math.max(0, Math.min(open, close) - low);
|
|
139
|
+
const upperWick = Math.max(0, high - Math.max(open, close));
|
|
140
|
+
return direction === "LONG" ? lowerWick / close * 100 : upperWick / close * 100;
|
|
141
|
+
};
|
|
142
|
+
var getRejectionStrengthPct = ({
|
|
143
|
+
direction,
|
|
144
|
+
close,
|
|
145
|
+
linePrice
|
|
146
|
+
}) => {
|
|
147
|
+
if (direction == null || close == null || linePrice == null || linePrice === 0) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (direction === "LONG") {
|
|
151
|
+
return close >= linePrice ? (close - linePrice) / linePrice * 100 : 0;
|
|
152
|
+
}
|
|
153
|
+
return close <= linePrice ? (linePrice - close) / linePrice * 100 : 0;
|
|
154
|
+
};
|
|
155
|
+
var getRejectionBar = ({
|
|
156
|
+
direction,
|
|
157
|
+
lineTouched,
|
|
158
|
+
closeOnBounceSide,
|
|
159
|
+
bodyAligned,
|
|
160
|
+
rejectionWickPct,
|
|
161
|
+
rejectionStrengthPct
|
|
162
|
+
}) => {
|
|
163
|
+
if (direction == null) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return lineTouched && closeOnBounceSide === true && bodyAligned === true && (rejectionWickPct ?? 0) >= MIN_REJECTION_WICK_PCT && (rejectionStrengthPct ?? 0) >= MIN_REJECTION_STRENGTH_PCT;
|
|
167
|
+
};
|
|
168
|
+
var buildReverseTrendlineStructuralContext = (signal) => {
|
|
169
|
+
const trendLine = getTrendLineFromPayload(signal);
|
|
170
|
+
const evaluator = buildTrendLineEvaluator(trendLine);
|
|
171
|
+
const currentPrice = toFiniteNumberOrNull(signal.prices?.currentPrice);
|
|
172
|
+
const currentCandle = getCurrentCandle(signal);
|
|
173
|
+
const currentTimestamp = toFiniteNumberOrNull(currentCandle?.timestamp);
|
|
174
|
+
const currentOpen = toFiniteNumberOrNull(currentCandle?.open);
|
|
175
|
+
const currentClose = toFiniteNumberOrNull(currentCandle?.close) ?? currentPrice;
|
|
176
|
+
const currentHigh = toFiniteNumberOrNull(currentCandle?.high);
|
|
177
|
+
const currentLow = toFiniteNumberOrNull(currentCandle?.low);
|
|
178
|
+
const signalDirection = signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : deriveDirectionFromMode(trendLine?.mode);
|
|
179
|
+
const currentLinePrice = currentTimestamp != null && evaluator ? evaluator.evaluate(currentTimestamp) : evaluator?.lastPoint.value ?? null;
|
|
180
|
+
const priceVsLinePct = currentClose != null && currentLinePrice != null && currentLinePrice !== 0 ? (currentClose - currentLinePrice) / currentLinePrice * 100 : null;
|
|
181
|
+
const priceVsLinePctAbs = priceVsLinePct == null ? null : Math.abs(priceVsLinePct);
|
|
182
|
+
const priceVsLineSide = priceVsLinePct == null ? null : priceVsLinePct > 0 ? "above" : priceVsLinePct < 0 ? "below" : "at";
|
|
183
|
+
const nearLine = priceVsLinePctAbs == null ? null : priceVsLinePctAbs <= REVERSE_TRENDLINE_NEAR_LINE_PCT;
|
|
184
|
+
const lineTouchedNow = getLineTouched({
|
|
185
|
+
low: currentLow,
|
|
186
|
+
high: currentHigh,
|
|
187
|
+
linePrice: currentLinePrice
|
|
188
|
+
});
|
|
189
|
+
const closeOnBounceSide = getCloseOnBounceSide({
|
|
190
|
+
direction: signalDirection,
|
|
191
|
+
priceVsLinePct
|
|
192
|
+
});
|
|
193
|
+
const failedBounceBreak = getFailedBounceBreak({
|
|
194
|
+
direction: signalDirection,
|
|
195
|
+
priceVsLinePct
|
|
196
|
+
});
|
|
197
|
+
const bodyAligned = getBodyAligned({
|
|
198
|
+
direction: signalDirection,
|
|
199
|
+
open: currentOpen,
|
|
200
|
+
close: currentClose
|
|
201
|
+
});
|
|
202
|
+
const rejectionWickPct = getRejectionWickPct({
|
|
203
|
+
direction: signalDirection,
|
|
204
|
+
open: currentOpen,
|
|
205
|
+
close: currentClose,
|
|
206
|
+
high: currentHigh,
|
|
207
|
+
low: currentLow
|
|
208
|
+
});
|
|
209
|
+
const rejectionStrengthPct = getRejectionStrengthPct({
|
|
210
|
+
direction: signalDirection,
|
|
211
|
+
close: currentClose,
|
|
212
|
+
linePrice: currentLinePrice
|
|
213
|
+
});
|
|
214
|
+
const rejectionBarNow = getRejectionBar({
|
|
215
|
+
direction: signalDirection,
|
|
216
|
+
lineTouched: lineTouchedNow,
|
|
217
|
+
closeOnBounceSide,
|
|
218
|
+
bodyAligned,
|
|
219
|
+
rejectionWickPct,
|
|
220
|
+
rejectionStrengthPct
|
|
221
|
+
});
|
|
222
|
+
const touchesTotal = toFiniteNumberOrNull(
|
|
223
|
+
signal.additionalIndicators?.touches
|
|
224
|
+
);
|
|
225
|
+
const distance = toFiniteNumberOrNull(signal.additionalIndicators?.distance);
|
|
226
|
+
const touches = touchesTotal != null ? touchesTotal : Array.isArray(trendLine?.touches) ? trendLine.touches.length : null;
|
|
227
|
+
const atrPct = getLastFiniteNumber(signal.indicators?.atrPct);
|
|
228
|
+
const breakVsAtrRatio = rejectionStrengthPct != null && atrPct != null && atrPct > 0 ? rejectionStrengthPct / atrPct : null;
|
|
229
|
+
const coinMaFast = getLastFiniteNumber(signal.indicators?.maFast);
|
|
230
|
+
const coinMaSlow = getLastFiniteNumber(signal.indicators?.maSlow);
|
|
231
|
+
const coinMaBias = getBias(coinMaFast, coinMaSlow);
|
|
232
|
+
const coinMaSpreadPct = getSpreadPct(coinMaFast, coinMaSlow);
|
|
233
|
+
const coinBiasAligned = signalDirection == null || coinMaBias == null ? null : signalDirection === "LONG" ? coinMaBias === "bullish" : coinMaBias === "bearish";
|
|
234
|
+
const btcMaFast = getLastFiniteNumber(signal.indicators?.btcMaFast);
|
|
235
|
+
const btcMaSlow = getLastFiniteNumber(signal.indicators?.btcMaSlow);
|
|
236
|
+
const btcMaBias = getBias(btcMaFast, btcMaSlow);
|
|
237
|
+
const btcMaSpreadPct = getSpreadPct(btcMaFast, btcMaSlow);
|
|
238
|
+
const btcBiasAligned = signalDirection == null || btcMaBias == null ? null : signalDirection === "LONG" ? btcMaBias === "bullish" : btcMaBias === "bearish";
|
|
239
|
+
const structuralHardBlockReasons = [];
|
|
240
|
+
if (failedBounceBreak === true) {
|
|
241
|
+
structuralHardBlockReasons.push("failed_bounce_break");
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
signalDirection,
|
|
245
|
+
mode: typeof trendLine?.mode === "string" ? trendLine.mode : null,
|
|
246
|
+
currentPrice,
|
|
247
|
+
currentLinePrice,
|
|
248
|
+
priceVsLinePct,
|
|
249
|
+
priceVsLinePctAbs,
|
|
250
|
+
priceVsLineSide,
|
|
251
|
+
nearLine,
|
|
252
|
+
lineTouchedNow,
|
|
253
|
+
closeOnBounceSide,
|
|
254
|
+
failedBounceBreak,
|
|
255
|
+
bodyAligned,
|
|
256
|
+
rejectionWickPct,
|
|
257
|
+
rejectionStrengthPct,
|
|
258
|
+
rejectionBarNow,
|
|
259
|
+
touches,
|
|
260
|
+
distance,
|
|
261
|
+
atrPct,
|
|
262
|
+
breakVsAtrRatio,
|
|
263
|
+
coinMaFast,
|
|
264
|
+
coinMaSlow,
|
|
265
|
+
coinMaBias,
|
|
266
|
+
coinMaSpreadPct,
|
|
267
|
+
coinBiasAligned,
|
|
268
|
+
btcMaFast,
|
|
269
|
+
btcMaSlow,
|
|
270
|
+
btcMaBias,
|
|
271
|
+
btcMaSpreadPct,
|
|
272
|
+
btcBiasAligned,
|
|
273
|
+
structuralHardBlockReasons
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
var buildReverseTrendlineTimingContext = ({
|
|
277
|
+
signal,
|
|
278
|
+
candles,
|
|
279
|
+
structuralContext
|
|
280
|
+
}) => {
|
|
281
|
+
const structural = structuralContext ?? buildReverseTrendlineStructuralContext(signal);
|
|
282
|
+
const trendLine = getTrendLineFromPayload(signal);
|
|
283
|
+
const evaluator = buildTrendLineEvaluator(trendLine);
|
|
284
|
+
const timingCandles = Array.isArray(candles) ? candles.slice(-REVERSE_TRENDLINE_TIMING_WINDOW) : [];
|
|
285
|
+
const sortedCandles = [...timingCandles].sort(
|
|
286
|
+
(left, right) => Number(left?.timestamp ?? 0) - Number(right?.timestamp ?? 0)
|
|
287
|
+
);
|
|
288
|
+
const recentSamples = evaluator ? sortedCandles.map((candle) => {
|
|
289
|
+
const timestamp = toFiniteNumberOrNull(candle.timestamp);
|
|
290
|
+
const open = toFiniteNumberOrNull(candle.open);
|
|
291
|
+
const close = toFiniteNumberOrNull(candle.close);
|
|
292
|
+
const high = toFiniteNumberOrNull(candle.high);
|
|
293
|
+
const low = toFiniteNumberOrNull(candle.low);
|
|
294
|
+
if (timestamp == null || open == null || close == null || high == null || low == null) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const linePrice = evaluator.evaluate(timestamp);
|
|
298
|
+
const priceVsLinePct = linePrice !== 0 ? (close - linePrice) / linePrice * 100 : null;
|
|
299
|
+
const closeOnBounceSide = getCloseOnBounceSide({
|
|
300
|
+
direction: structural.signalDirection,
|
|
301
|
+
priceVsLinePct
|
|
302
|
+
});
|
|
303
|
+
const failedBounceBreak = getFailedBounceBreak({
|
|
304
|
+
direction: structural.signalDirection,
|
|
305
|
+
priceVsLinePct
|
|
306
|
+
});
|
|
307
|
+
const lineTouched = getLineTouched({
|
|
308
|
+
low,
|
|
309
|
+
high,
|
|
310
|
+
linePrice
|
|
311
|
+
});
|
|
312
|
+
const bodyAligned = getBodyAligned({
|
|
313
|
+
direction: structural.signalDirection,
|
|
314
|
+
open,
|
|
315
|
+
close
|
|
316
|
+
});
|
|
317
|
+
const rejectionWickPct = getRejectionWickPct({
|
|
318
|
+
direction: structural.signalDirection,
|
|
319
|
+
open,
|
|
320
|
+
close,
|
|
321
|
+
high,
|
|
322
|
+
low
|
|
323
|
+
});
|
|
324
|
+
const rejectionStrengthPct = getRejectionStrengthPct({
|
|
325
|
+
direction: structural.signalDirection,
|
|
326
|
+
close,
|
|
327
|
+
linePrice
|
|
328
|
+
});
|
|
329
|
+
const rejectionBar = getRejectionBar({
|
|
330
|
+
direction: structural.signalDirection,
|
|
331
|
+
lineTouched,
|
|
332
|
+
closeOnBounceSide,
|
|
333
|
+
bodyAligned,
|
|
334
|
+
rejectionWickPct,
|
|
335
|
+
rejectionStrengthPct
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
timestamp,
|
|
339
|
+
priceVsLinePct,
|
|
340
|
+
lineTouched,
|
|
341
|
+
closeOnBounceSide,
|
|
342
|
+
failedBounceBreak,
|
|
343
|
+
rejectionWickPct,
|
|
344
|
+
rejectionStrengthPct,
|
|
345
|
+
rejectionBar
|
|
346
|
+
};
|
|
347
|
+
}).filter(Boolean) : [];
|
|
348
|
+
const currentIndex = recentSamples.length - 1;
|
|
349
|
+
const lastSample = currentIndex >= 0 ? recentSamples[currentIndex] : null;
|
|
350
|
+
const prevSample = currentIndex > 0 ? recentSamples[currentIndex - 1] : null;
|
|
351
|
+
let latestRejectionIndex = null;
|
|
352
|
+
for (let index = 0; index < recentSamples.length; index += 1) {
|
|
353
|
+
if (recentSamples[index].rejectionBar === true) {
|
|
354
|
+
latestRejectionIndex = index;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const barsSinceRejection = latestRejectionIndex != null ? currentIndex - latestRejectionIndex : null;
|
|
358
|
+
const rejectionFresh = barsSinceRejection != null && barsSinceRejection >= 0 && barsSinceRejection <= 1;
|
|
359
|
+
const followThroughReady = latestRejectionIndex != null && latestRejectionIndex === currentIndex - 1 && lastSample?.closeOnBounceSide === true && lastSample.failedBounceBreak !== true && lastSample.lineTouched === false && (lastSample.rejectionStrengthPct ?? 0) >= FOLLOW_THROUGH_STRENGTH_PCT;
|
|
360
|
+
const staleReaction = latestRejectionIndex != null && barsSinceRejection != null && barsSinceRejection > 1 && lastSample?.failedBounceBreak !== true;
|
|
361
|
+
let entryTiming = "unknown";
|
|
362
|
+
if (lastSample?.failedBounceBreak === true) {
|
|
363
|
+
entryTiming = "stale_reaction";
|
|
364
|
+
} else if (lastSample?.rejectionBar === true) {
|
|
365
|
+
entryTiming = "ready_rejection";
|
|
366
|
+
} else if (followThroughReady) {
|
|
367
|
+
entryTiming = "ready_follow_through";
|
|
368
|
+
} else if (lastSample?.lineTouched === true && lastSample.closeOnBounceSide === true && !lastSample.rejectionBar) {
|
|
369
|
+
entryTiming = "wait_reaction_confirmation";
|
|
370
|
+
} else if (staleReaction) {
|
|
371
|
+
entryTiming = "stale_reaction";
|
|
372
|
+
} else {
|
|
373
|
+
entryTiming = "wait_touch";
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
rejectionDetected: latestRejectionIndex != null,
|
|
377
|
+
barsSinceRejection,
|
|
378
|
+
rejectionFresh,
|
|
379
|
+
followThroughReady,
|
|
380
|
+
staleReaction,
|
|
381
|
+
entryTiming,
|
|
382
|
+
entryReadyNow: entryTiming === "ready_rejection" || entryTiming === "ready_follow_through",
|
|
383
|
+
currentRejectionStrengthPct: lastSample?.rejectionStrengthPct ?? null,
|
|
384
|
+
previousRejectionStrengthPct: prevSample?.rejectionStrengthPct ?? null
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/ReverseTrendLine/adapters/ai.ts
|
|
389
|
+
var REVERSE_TRENDLINE_CONTEXT_PROMPT = `
|
|
390
|
+
ReverseTrendLine addon:
|
|
391
|
+
- This is a trendline bounce strategy, not a breakout strategy.
|
|
392
|
+
- For LONG on a support line (\`trendline.mode="lows"\`), you need a touch or false break of the line followed by a close back above it.
|
|
393
|
+
- For SHORT on a resistance line (\`trendline.mode="highs"\`), you need a touch or false break of the line followed by a close back below it.
|
|
394
|
+
- If price has already broken through the line with conviction in the opposite direction, this is not a bounce setup: use \`direction=null\` and \`quality <= 2\`.
|
|
395
|
+
- For bounce setups, prioritize candle reaction at the line, rejection wick quality, a close on the correct side, and next-bar follow-through.
|
|
396
|
+
- If \`payload.additionalIndicators.reverseTrendlineContext.failedBounceBreak=true\`, do not treat the signal as structurally confirmed.
|
|
397
|
+
- If \`payload.additionalIndicators.reverseTrendlineContext.entryTiming\` is not \`ready_rejection\` or \`ready_follow_through\`, quality is usually \`<= 3\`.
|
|
398
|
+
- Baseline deterministic approval for same-bar rejection is intentionally strict:
|
|
399
|
+
- a strong conflict-only rejection may qualify for \`quality=4\`;
|
|
400
|
+
- some same-bar rejections with \`conflictState=none\` or \`both\` may reach \`quality=4\` only with a very strong deterministic rejection score.
|
|
401
|
+
- For SHORT bounce setups with \`btc_bias_conflict\`, do not overstate quality; those cases usually stay in watch mode unless the structural confirmation is much stronger.
|
|
402
|
+
- If \`deterministicRejectionScore\` is low or medium, do not assign \`quality=4\` just because the candle visually resembles a rejection.
|
|
403
|
+
`;
|
|
404
|
+
var REVERSE_TRENDLINE_PAYLOAD_PROMPT = `
|
|
405
|
+
- \`payload.figures.trendline\` contains the line geometry.
|
|
406
|
+
- \`payload.additionalIndicators.reverseTrendlineContext\` contains a compact bounce summary: direction, price distance to the line, whether the line was touched, whether there was a rejection candle, rejection strength, timing stage, bias conflicts, and \`deterministicRejectionScore\`.
|
|
407
|
+
`;
|
|
408
|
+
var getReverseTrendlineBiasConflictState = (context) => {
|
|
409
|
+
const coinConflict = context.coinBiasAligned === false;
|
|
410
|
+
const btcConflict = context.btcBiasAligned === false;
|
|
411
|
+
if (coinConflict && btcConflict) {
|
|
412
|
+
return "both";
|
|
413
|
+
}
|
|
414
|
+
if (coinConflict) {
|
|
415
|
+
return "coin_only";
|
|
416
|
+
}
|
|
417
|
+
if (btcConflict) {
|
|
418
|
+
return "btc_only";
|
|
419
|
+
}
|
|
420
|
+
if (context.coinBiasAligned === true && context.btcBiasAligned === true) {
|
|
421
|
+
return "none";
|
|
422
|
+
}
|
|
423
|
+
return "unknown";
|
|
424
|
+
};
|
|
425
|
+
var getDeterministicReverseTrendlineQuality = (context) => {
|
|
426
|
+
if (context.hardBlockReasons.length > 0) {
|
|
427
|
+
return 2;
|
|
428
|
+
}
|
|
429
|
+
if (context.entryTiming !== "ready_rejection" && context.entryTiming !== "ready_follow_through") {
|
|
430
|
+
return 3;
|
|
431
|
+
}
|
|
432
|
+
const rejectionStrengthPct = context.rejectionStrengthPct ?? 0;
|
|
433
|
+
const rejectionWickPct = context.rejectionWickPct ?? 0;
|
|
434
|
+
const touches = context.touches ?? 0;
|
|
435
|
+
const distance = context.distance ?? Number.POSITIVE_INFINITY;
|
|
436
|
+
const biasConflictState = getReverseTrendlineBiasConflictState(context);
|
|
437
|
+
const noConflict = biasConflictState === "none";
|
|
438
|
+
const conflictOnly = biasConflictState === "coin_only" || biasConflictState === "btc_only";
|
|
439
|
+
const quality5 = context.entryTiming === "ready_follow_through" && noConflict && rejectionStrengthPct >= 0.25 && rejectionWickPct >= 0.18 && touches >= 4 && distance < 500;
|
|
440
|
+
if (quality5) {
|
|
441
|
+
return 5;
|
|
442
|
+
}
|
|
443
|
+
const quality4FollowThrough = context.entryTiming === "ready_follow_through" && noConflict && rejectionStrengthPct >= 0.22 && rejectionWickPct >= 0.18 && touches >= 4;
|
|
444
|
+
if (quality4FollowThrough) {
|
|
445
|
+
return 4;
|
|
446
|
+
}
|
|
447
|
+
const quality4ConflictRejection = context.entryTiming === "ready_rejection" && conflictOnly && rejectionStrengthPct >= 0.45 && touches >= 5 && !(context.signalDirection === "SHORT" && biasConflictState === "coin_only" && distance <= 180 && rejectionWickPct <= 0.45) && !(context.signalDirection === "SHORT" && biasConflictState === "btc_only");
|
|
448
|
+
if (quality4ConflictRejection) {
|
|
449
|
+
return 4;
|
|
450
|
+
}
|
|
451
|
+
const rejectionScore = getDeterministicReverseTrendlineRejectionScore(context);
|
|
452
|
+
const quality4EliteShortBtcOnlyRejection = context.entryTiming === "ready_rejection" && context.signalDirection === "SHORT" && biasConflictState === "btc_only" && rejectionScore != null && rejectionScore >= 5 && rejectionWickPct >= 0.6 && touches >= 5 && distance <= 200;
|
|
453
|
+
if (quality4EliteShortBtcOnlyRejection) {
|
|
454
|
+
return 4;
|
|
455
|
+
}
|
|
456
|
+
const quality4ScoredRejection = context.entryTiming === "ready_rejection" && (biasConflictState === "none" || biasConflictState === "both") && rejectionScore != null && rejectionScore >= 7 && !(context.signalDirection === "SHORT" && biasConflictState === "none" && distance <= 150 && (rejectionWickPct >= 0.7 || rejectionStrengthPct >= 1.3));
|
|
457
|
+
if (quality4ScoredRejection) {
|
|
458
|
+
return 4;
|
|
459
|
+
}
|
|
460
|
+
const quality4EliteAlignedRejection = context.entryTiming === "ready_rejection" && noConflict && rejectionStrengthPct >= 0.9 && rejectionWickPct >= 0.15 && touches >= 5 && distance <= 250;
|
|
461
|
+
return quality4EliteAlignedRejection ? 4 : 3;
|
|
462
|
+
};
|
|
463
|
+
var getDeterministicReverseTrendlineRejectionScore = (context) => {
|
|
464
|
+
if (context.entryTiming !== "ready_rejection") {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const biasConflictState = getReverseTrendlineBiasConflictState(context);
|
|
468
|
+
const rejectionStrengthPct = context.rejectionStrengthPct ?? 0;
|
|
469
|
+
const rejectionWickPct = context.rejectionWickPct ?? 0;
|
|
470
|
+
const touches = context.touches ?? 0;
|
|
471
|
+
const distance = context.distance ?? Number.POSITIVE_INFINITY;
|
|
472
|
+
let score = 0;
|
|
473
|
+
if (rejectionStrengthPct >= 0.25) {
|
|
474
|
+
score += 1;
|
|
475
|
+
}
|
|
476
|
+
if (rejectionStrengthPct >= 0.6) {
|
|
477
|
+
score += 1;
|
|
478
|
+
}
|
|
479
|
+
if (rejectionWickPct >= 0.18) {
|
|
480
|
+
score += 1;
|
|
481
|
+
}
|
|
482
|
+
if (touches >= 4) {
|
|
483
|
+
score += 1;
|
|
484
|
+
}
|
|
485
|
+
if (distance <= 250) {
|
|
486
|
+
score += 1;
|
|
487
|
+
}
|
|
488
|
+
if (context.signalDirection === "LONG") {
|
|
489
|
+
if (biasConflictState === "both") {
|
|
490
|
+
score += 1;
|
|
491
|
+
}
|
|
492
|
+
if (rejectionWickPct >= 0.75) {
|
|
493
|
+
score += 1;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (context.signalDirection === "SHORT") {
|
|
497
|
+
if (biasConflictState === "none") {
|
|
498
|
+
score += 1;
|
|
499
|
+
}
|
|
500
|
+
if (distance <= 150) {
|
|
501
|
+
score += 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return score;
|
|
505
|
+
};
|
|
506
|
+
var buildReverseTrendlineAiContext = (signal) => {
|
|
507
|
+
const structural = buildReverseTrendlineStructuralContext(signal);
|
|
508
|
+
const computedTiming = buildReverseTrendlineTimingContext({ signal });
|
|
509
|
+
const timingFromSignal = typeof signal.additionalIndicators?.reverseTrendlineTiming === "object" && signal.additionalIndicators?.reverseTrendlineTiming && typeof signal.additionalIndicators.reverseTrendlineTiming.entryTiming === "string" ? signal.additionalIndicators.reverseTrendlineTiming : null;
|
|
510
|
+
const timing = timingFromSignal ? {
|
|
511
|
+
...computedTiming,
|
|
512
|
+
...timingFromSignal,
|
|
513
|
+
entryReadyNow: timingFromSignal.entryTiming === "ready_rejection" || timingFromSignal.entryTiming === "ready_follow_through"
|
|
514
|
+
} : computedTiming;
|
|
515
|
+
const hardBlockReasons = [...structural.structuralHardBlockReasons];
|
|
516
|
+
const deterministicRejectionScore = getDeterministicReverseTrendlineRejectionScore({
|
|
517
|
+
...structural,
|
|
518
|
+
...timing,
|
|
519
|
+
hardBlockReasons
|
|
520
|
+
});
|
|
521
|
+
const deterministicQuality = getDeterministicReverseTrendlineQuality({
|
|
522
|
+
...structural,
|
|
523
|
+
...timing,
|
|
524
|
+
hardBlockReasons
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
...structural,
|
|
528
|
+
...timing,
|
|
529
|
+
deterministicQuality,
|
|
530
|
+
deterministicRejectionScore,
|
|
531
|
+
approvalAllowedNow: deterministicQuality >= 4,
|
|
532
|
+
hardBlockReasons
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
var getReverseTrendlineContextFromPayload = (payload, signal) => {
|
|
536
|
+
const additional = payload.additionalIndicators;
|
|
537
|
+
const fromPayload = additional?.reverseTrendlineContext;
|
|
538
|
+
return fromPayload ?? buildReverseTrendlineAiContext(signal);
|
|
539
|
+
};
|
|
540
|
+
var getHardBlockReasonText = (reason) => {
|
|
541
|
+
switch (reason) {
|
|
542
|
+
case "failed_bounce_break":
|
|
543
|
+
return "price broke through the line against the intended bounce";
|
|
544
|
+
case "coin_bias_conflict":
|
|
545
|
+
return "coin bias conflicts with the bounce direction";
|
|
546
|
+
case "btc_bias_conflict":
|
|
547
|
+
return "BTC context conflicts with the bounce direction";
|
|
548
|
+
default:
|
|
549
|
+
return reason;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
var reverseTrendLineAiAdapter = {
|
|
553
|
+
buildPayload: ({ signal, basePayload }) => ({
|
|
554
|
+
...basePayload,
|
|
555
|
+
figures: {
|
|
556
|
+
...basePayload.figures,
|
|
557
|
+
trendline: getTrendLineFromPayload(signal)
|
|
558
|
+
},
|
|
559
|
+
additionalIndicators: {
|
|
560
|
+
...basePayload.additionalIndicators,
|
|
561
|
+
reverseTrendlineContext: buildReverseTrendlineAiContext(signal)
|
|
562
|
+
}
|
|
563
|
+
}),
|
|
564
|
+
postProcessAnalysis: ({ signal, payload, analysis }) => {
|
|
565
|
+
const context = getReverseTrendlineContextFromPayload(payload, signal);
|
|
566
|
+
const signalDirection = signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
|
|
567
|
+
if (context.approvalAllowedNow === true && signalDirection != null) {
|
|
568
|
+
return {
|
|
569
|
+
...analysis,
|
|
570
|
+
direction: signalDirection,
|
|
571
|
+
quality: context.deterministicQuality,
|
|
572
|
+
needRetest: false,
|
|
573
|
+
retestPrice: null,
|
|
574
|
+
takeProfitPrice: analysis.takeProfitPrice ?? signal.prices?.takeProfitPrice ?? null,
|
|
575
|
+
stopLossPrice: analysis.stopLossPrice ?? signal.prices?.stopLossPrice ?? null
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
...analysis,
|
|
580
|
+
direction: null,
|
|
581
|
+
quality: context.deterministicQuality,
|
|
582
|
+
needRetest: true,
|
|
583
|
+
retestPrice: context.currentLinePrice ?? null,
|
|
584
|
+
takeProfitPrice: null,
|
|
585
|
+
stopLossPrice: null,
|
|
586
|
+
qualityReason: context.hardBlockReasons.length > 0 ? `ReverseTrendLine guardrail: ${context.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "ReverseTrendLine deterministic quality requires either a strong conflict-only rejection or a confirmed aligned follow-through for a bounce.",
|
|
587
|
+
triggerInvalidation: context.hardBlockReasons.length > 0 ? `Wait for a new bounce setup: ${context.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "Wait for a line touch, a rejection candle, and a close held on the correct side of the line.",
|
|
588
|
+
comment: context.hardBlockReasons.length > 0 ? `ReverseTrendLine guardrail blocked the entry: ${context.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "ReverseTrendLine keeps the setup in watch mode until the bounce is confirmed."
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
buildSystemPromptAddon: () => `${REVERSE_TRENDLINE_CONTEXT_PROMPT}
|
|
592
|
+
${REVERSE_TRENDLINE_PAYLOAD_PROMPT}`,
|
|
593
|
+
buildHumanPromptAddon: ({ signal, payload }) => {
|
|
594
|
+
const context = getReverseTrendlineContextFromPayload(payload, signal);
|
|
595
|
+
return `
|
|
596
|
+
|
|
597
|
+
Additional ReverseTrendLine context:
|
|
598
|
+
- entryTiming=${context.entryTiming}
|
|
599
|
+
- lineTouchedNow=${context.lineTouchedNow}
|
|
600
|
+
- closeOnBounceSide=${context.closeOnBounceSide}
|
|
601
|
+
- failedBounceBreak=${context.failedBounceBreak}
|
|
602
|
+
- rejectionWickPct=${context.rejectionWickPct?.toFixed?.(3) ?? "n/a"}%
|
|
603
|
+
- rejectionStrengthPct=${context.rejectionStrengthPct?.toFixed?.(3) ?? "n/a"}%
|
|
604
|
+
- touches=${context.touches ?? "n/a"}
|
|
605
|
+
- distance=${context.distance ?? "n/a"}
|
|
606
|
+
- coinBiasAligned=${context.coinBiasAligned}
|
|
607
|
+
- btcBiasAligned=${context.btcBiasAligned}
|
|
608
|
+
- deterministicRejectionScore=${context.deterministicRejectionScore ?? "n/a"}
|
|
609
|
+
- approvalAllowedNow=${context.approvalAllowedNow}
|
|
610
|
+
- hardBlockReasons=${context.hardBlockReasons.join(", ") || "none"}
|
|
611
|
+
|
|
612
|
+
Interpretation rules for ReverseTrendLine:
|
|
613
|
+
- look for structural confirmation of a reaction from the line, not a breakout through the line;
|
|
614
|
+
- if \`failedBounceBreak=true\` is already present, do not treat the signal as confirmed;
|
|
615
|
+
- if the setup is still in \`wait_touch\`, \`wait_reaction_confirmation\`, or \`stale_reaction\`, do not overstate quality;
|
|
616
|
+
- if \`deterministicRejectionScore\` is high, use it only as an extra signal together with the proper bounce context, not as a replacement for structure.
|
|
617
|
+
`;
|
|
618
|
+
},
|
|
619
|
+
mapEntryRuntimeFromConfig: (config2) => mapAiRuntimeFromConfig(
|
|
620
|
+
config2
|
|
621
|
+
)
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// src/ReverseTrendLine/manifest.ts
|
|
625
|
+
var reverseTrendLineManifest = {
|
|
626
|
+
name: "ReverseTrendLine",
|
|
627
|
+
aiAdapter: reverseTrendLineAiAdapter
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/ReverseTrendLine/config.ts
|
|
631
|
+
var config = {
|
|
632
|
+
ENV: "BACKTEST",
|
|
633
|
+
INTERVAL: "15",
|
|
634
|
+
MAKE_ORDERS: true,
|
|
635
|
+
CLOSE_OPPOSITE_POSITIONS: false,
|
|
636
|
+
BACKTEST_PRICE_MODE: "mid",
|
|
637
|
+
AI_ENABLED: false,
|
|
638
|
+
AI_MODE: "llm",
|
|
639
|
+
MIN_AI_QUALITY: 3,
|
|
640
|
+
FEE_PERCENT: 5e-3,
|
|
641
|
+
MAX_LOSS_VALUE: 10,
|
|
642
|
+
MA_FAST: 14,
|
|
643
|
+
MA_MEDIUM: 49,
|
|
644
|
+
MA_SLOW: 50,
|
|
645
|
+
OBV_SMA: 10,
|
|
646
|
+
ATR: 14,
|
|
647
|
+
ATR_PCT_SHORT: 7,
|
|
648
|
+
ATR_PCT_LONG: 30,
|
|
649
|
+
BB: 20,
|
|
650
|
+
BB_STD: 2,
|
|
651
|
+
MACD_FAST: 12,
|
|
652
|
+
MACD_SLOW: 26,
|
|
653
|
+
MACD_SIGNAL: 9,
|
|
654
|
+
TRENDLINE: {
|
|
655
|
+
minTouches: 4,
|
|
656
|
+
offset: 3,
|
|
657
|
+
epsilon: 3e-3,
|
|
658
|
+
epsilonOffset: 4e-3
|
|
659
|
+
},
|
|
660
|
+
HIGHS: {
|
|
661
|
+
enable: true,
|
|
662
|
+
direction: "SHORT",
|
|
663
|
+
TP: 3.2,
|
|
664
|
+
SL: 1.1,
|
|
665
|
+
minRiskRatio: 1.6
|
|
666
|
+
},
|
|
667
|
+
LOWS: {
|
|
668
|
+
enable: true,
|
|
669
|
+
direction: "LONG",
|
|
670
|
+
TP: 3.2,
|
|
671
|
+
SL: 1.1,
|
|
672
|
+
minRiskRatio: 1.6
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
export {
|
|
677
|
+
toFiniteNumberOrNull,
|
|
678
|
+
buildReverseTrendlineStructuralContext,
|
|
679
|
+
buildReverseTrendlineTimingContext,
|
|
680
|
+
reverseTrendLineAiAdapter,
|
|
681
|
+
reverseTrendLineManifest,
|
|
682
|
+
config
|
|
683
|
+
};
|