@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.
@@ -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
+ };