@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,797 @@
1
+ // src/TrendLine/adapters/ai.ts
2
+ import { mapAiRuntimeFromConfig } from "@tradejs/core/strategies";
3
+
4
+ // src/TrendLine/guardrails.ts
5
+ var TRENDLINE_CLEAR_BREAK_PCT = 0.35;
6
+ var TRENDLINE_TIMING_WINDOW = 6;
7
+ var WEAK_CLEAN_BREAK_ATR_RATIO_MAX = 0.45;
8
+ var COMPRESSED_CLEAN_BREAK_ATR_RATIO_MAX = 0.6;
9
+ var COMPRESSED_CLEAN_BREAK_DISTANCE_MAX = 120;
10
+ var COMPRESSED_CLEAN_BREAK_TOUCHES_MIN = 5;
11
+ var WEAK_LONG_FAR_BREAK_ATR_RATIO_MAX = 0.6;
12
+ var WEAK_LONG_FAR_BREAK_DISTANCE_MIN = 1e3;
13
+ var WEAK_LONG_FAR_BREAK_BTC_SPREAD_MAX = 0.35;
14
+ var toFiniteNumberOrNull = (value) => {
15
+ const num = Number(value);
16
+ return Number.isFinite(num) ? num : null;
17
+ };
18
+ var getLastFiniteNumber = (value) => {
19
+ if (!Array.isArray(value) || value.length === 0) {
20
+ return null;
21
+ }
22
+ return toFiniteNumberOrNull(value[value.length - 1]);
23
+ };
24
+ var getFiniteTailNumbers = (value, count) => {
25
+ if (!Array.isArray(value) || count <= 0) {
26
+ return [];
27
+ }
28
+ const tail = value.slice(-count);
29
+ return tail.map((item) => toFiniteNumberOrNull(item));
30
+ };
31
+ var getBias = (fast, slow) => {
32
+ if (fast == null || slow == null) {
33
+ return null;
34
+ }
35
+ if (fast > slow) {
36
+ return "bullish";
37
+ }
38
+ if (fast < slow) {
39
+ return "bearish";
40
+ }
41
+ return "flat";
42
+ };
43
+ var getSpreadPct = (fast, slow) => {
44
+ if (fast == null || slow == null || slow === 0) {
45
+ return null;
46
+ }
47
+ return (fast - slow) / slow * 100;
48
+ };
49
+ var getTrendLineFromPayload = (signal) => signal.figures?.trendLine ?? signal.additionalIndicators?.trendLine ?? null;
50
+ var getSortedTrendLinePoints = (trendLine) => {
51
+ const rawPoints = Array.isArray(trendLine?.points) ? trendLine.points : [];
52
+ return rawPoints.map((point) => {
53
+ if (!point || typeof point !== "object") {
54
+ return null;
55
+ }
56
+ const typedPoint = point;
57
+ const timestamp = toFiniteNumberOrNull(typedPoint.timestamp);
58
+ const value = toFiniteNumberOrNull(typedPoint.value);
59
+ if (timestamp == null || value == null) {
60
+ return null;
61
+ }
62
+ return { timestamp, value };
63
+ }).filter(Boolean).sort((left, right) => left.timestamp - right.timestamp);
64
+ };
65
+ var buildTrendLineEvaluator = (trendLine) => {
66
+ const points = getSortedTrendLinePoints(trendLine);
67
+ if (points.length === 0) {
68
+ return null;
69
+ }
70
+ const firstPoint = points[0];
71
+ const lastPoint = points[points.length - 1];
72
+ const deltaTime = lastPoint.timestamp - firstPoint.timestamp;
73
+ if (deltaTime === 0) {
74
+ return {
75
+ firstPoint,
76
+ lastPoint,
77
+ evaluate: (_timestamp) => lastPoint.value
78
+ };
79
+ }
80
+ const slope = (lastPoint.value - firstPoint.value) / deltaTime;
81
+ return {
82
+ firstPoint,
83
+ lastPoint,
84
+ evaluate: (timestamp) => firstPoint.value + slope * (timestamp - firstPoint.timestamp)
85
+ };
86
+ };
87
+ var getBreakoutSide = ({
88
+ direction,
89
+ priceVsLinePct
90
+ }) => {
91
+ if (direction == null || priceVsLinePct == null) {
92
+ return null;
93
+ }
94
+ return direction === "SHORT" ? priceVsLinePct < 0 : priceVsLinePct > 0;
95
+ };
96
+ var getClearBreakAtPct = ({
97
+ direction,
98
+ priceVsLinePct
99
+ }) => {
100
+ if (direction == null || priceVsLinePct == null) {
101
+ return null;
102
+ }
103
+ return direction === "SHORT" ? priceVsLinePct <= -TRENDLINE_CLEAR_BREAK_PCT : priceVsLinePct >= TRENDLINE_CLEAR_BREAK_PCT;
104
+ };
105
+ var getLineSlopeDirection = (value) => {
106
+ if (value == null) {
107
+ return null;
108
+ }
109
+ if (value > 0) {
110
+ return "rising";
111
+ }
112
+ if (value < 0) {
113
+ return "falling";
114
+ }
115
+ return "flat";
116
+ };
117
+ var buildTrendlineStructuralContext = (signal) => {
118
+ const trendLine = getTrendLineFromPayload(signal);
119
+ const currentPrice = toFiniteNumberOrNull(signal.prices?.currentPrice);
120
+ const signalDirection = signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
121
+ const points = Array.isArray(trendLine?.points) ? trendLine.points : [];
122
+ const latestPoint = points.length ? points[points.length - 1] : null;
123
+ const currentLinePrice = toFiniteNumberOrNull(
124
+ latestPoint && typeof latestPoint === "object" ? latestPoint.value : null
125
+ );
126
+ const priceVsLinePct = currentPrice != null && currentLinePrice != null && currentLinePrice !== 0 ? (currentPrice - currentLinePrice) / currentLinePrice * 100 : null;
127
+ const priceVsLineSide = priceVsLinePct == null ? null : priceVsLinePct > 0 ? "above" : priceVsLinePct < 0 ? "below" : "at";
128
+ const priceVsLinePctAbs = priceVsLinePct == null ? null : Math.abs(priceVsLinePct);
129
+ const touchesTotal = toFiniteNumberOrNull(
130
+ signal.additionalIndicators?.touches
131
+ );
132
+ const distance = toFiniteNumberOrNull(signal.additionalIndicators?.distance);
133
+ const touches = touchesTotal != null ? touchesTotal : Array.isArray(trendLine?.touches) ? trendLine.touches.length : null;
134
+ const atrPct = getLastFiniteNumber(signal.indicators?.atrPct);
135
+ const btcMaFast = getLastFiniteNumber(signal.indicators?.btcMaFast);
136
+ const btcMaSlow = getLastFiniteNumber(signal.indicators?.btcMaSlow);
137
+ const btcMaBias = getBias(btcMaFast, btcMaSlow);
138
+ const btcMaSpreadPct = getSpreadPct(btcMaFast, btcMaSlow);
139
+ const btcBiasAligned = signalDirection == null || btcMaBias == null ? null : signalDirection === "SHORT" ? btcMaBias === "bearish" : btcMaBias === "bullish";
140
+ const clearBreak = getClearBreakAtPct({
141
+ direction: signalDirection,
142
+ priceVsLinePct
143
+ });
144
+ const nearLineNoise = priceVsLinePctAbs == null ? null : priceVsLinePctAbs < TRENDLINE_CLEAR_BREAK_PCT;
145
+ const breakVsAtrRatio = priceVsLinePctAbs != null && atrPct != null && atrPct > 0 ? priceVsLinePctAbs / atrPct : null;
146
+ const weakCleanBreak = clearBreak === true && nearLineNoise === false && breakVsAtrRatio != null && breakVsAtrRatio < WEAK_CLEAN_BREAK_ATR_RATIO_MAX;
147
+ const compressedCleanBreak = clearBreak === true && nearLineNoise === false && breakVsAtrRatio != null && breakVsAtrRatio < COMPRESSED_CLEAN_BREAK_ATR_RATIO_MAX && (touches ?? 0) >= COMPRESSED_CLEAN_BREAK_TOUCHES_MIN && distance != null && distance < COMPRESSED_CLEAN_BREAK_DISTANCE_MAX;
148
+ const weakLongFarBreak = signalDirection === "LONG" && trendLine?.mode === "highs" && clearBreak === true && nearLineNoise === false && breakVsAtrRatio != null && breakVsAtrRatio < WEAK_LONG_FAR_BREAK_ATR_RATIO_MAX && distance != null && distance > WEAK_LONG_FAR_BREAK_DISTANCE_MIN && btcBiasAligned === true && btcMaSpreadPct != null && btcMaSpreadPct < WEAK_LONG_FAR_BREAK_BTC_SPREAD_MAX;
149
+ const structuralHardBlockReasons = [];
150
+ if (clearBreak === false) {
151
+ structuralHardBlockReasons.push("no_clear_break");
152
+ }
153
+ if (nearLineNoise === true) {
154
+ structuralHardBlockReasons.push("near_line_noise");
155
+ }
156
+ if (weakCleanBreak) {
157
+ structuralHardBlockReasons.push("weak_clean_break");
158
+ }
159
+ if (compressedCleanBreak) {
160
+ structuralHardBlockReasons.push("compressed_clean_break");
161
+ }
162
+ if (weakLongFarBreak) {
163
+ structuralHardBlockReasons.push("weak_long_far_break");
164
+ }
165
+ return {
166
+ signalDirection,
167
+ mode: typeof trendLine?.mode === "string" ? trendLine.mode : null,
168
+ touches,
169
+ distance,
170
+ currentLinePrice,
171
+ currentPrice,
172
+ priceVsLinePct,
173
+ priceVsLineSide,
174
+ priceVsLinePctAbs,
175
+ clearBreak,
176
+ nearLineNoise,
177
+ atrPct,
178
+ breakVsAtrRatio,
179
+ btcMaFast,
180
+ btcMaSlow,
181
+ btcMaBias,
182
+ btcMaSpreadPct,
183
+ btcBiasAligned,
184
+ weakCleanBreak,
185
+ compressedCleanBreak,
186
+ weakLongFarBreak,
187
+ structuralHardBlockReasons
188
+ };
189
+ };
190
+ var buildTrendlineTimingContext = ({
191
+ signal,
192
+ candles,
193
+ structuralContext
194
+ }) => {
195
+ const structural = structuralContext ?? buildTrendlineStructuralContext(signal);
196
+ const trendLine = getTrendLineFromPayload(signal);
197
+ const evaluator = buildTrendLineEvaluator(trendLine);
198
+ const timingCandles = Array.isArray(candles) ? candles.slice(-TRENDLINE_TIMING_WINDOW) : [];
199
+ const sortedCandles = [...timingCandles].sort(
200
+ (left, right) => Number(left?.timestamp ?? 0) - Number(right?.timestamp ?? 0)
201
+ );
202
+ const atrTail = getFiniteTailNumbers(
203
+ signal.indicators?.atrPct,
204
+ sortedCandles.length
205
+ );
206
+ const atrValues = Array.from({ length: sortedCandles.length }, (_, index) => {
207
+ const offset = index - (sortedCandles.length - atrTail.length);
208
+ return offset >= 0 ? atrTail[offset] : null;
209
+ });
210
+ const recentSamples = evaluator ? sortedCandles.map((candle, index) => {
211
+ const timestamp = toFiniteNumberOrNull(candle.timestamp);
212
+ const close = toFiniteNumberOrNull(candle.close);
213
+ const high = toFiniteNumberOrNull(candle.high);
214
+ const low = toFiniteNumberOrNull(candle.low);
215
+ if (timestamp == null || close == null || high == null || low == null) {
216
+ return null;
217
+ }
218
+ const linePrice = evaluator.evaluate(timestamp);
219
+ const priceVsLinePct = linePrice !== 0 ? (close - linePrice) / linePrice * 100 : null;
220
+ const priceVsLinePctAbs = priceVsLinePct == null ? null : Math.abs(priceVsLinePct);
221
+ const breakoutSideClose = getBreakoutSide({
222
+ direction: structural.signalDirection,
223
+ priceVsLinePct
224
+ });
225
+ const clearBreakClose = getClearBreakAtPct({
226
+ direction: structural.signalDirection,
227
+ priceVsLinePct
228
+ });
229
+ const nearLineClose = priceVsLinePctAbs == null ? null : priceVsLinePctAbs < TRENDLINE_CLEAR_BREAK_PCT;
230
+ const lineTouched = low <= linePrice && high >= linePrice;
231
+ const distanceAtrRatio = priceVsLinePctAbs != null && atrValues[index] != null && atrValues[index] > 0 ? priceVsLinePctAbs / atrValues[index] : null;
232
+ return {
233
+ timestamp,
234
+ linePrice,
235
+ priceVsLinePct,
236
+ priceVsLinePctAbs,
237
+ breakoutSideClose,
238
+ clearBreakClose,
239
+ nearLineClose,
240
+ lineTouched,
241
+ distanceAtrRatio
242
+ };
243
+ }).filter(Boolean) : [];
244
+ const lastSample = recentSamples.length > 0 ? recentSamples[recentSamples.length - 1] : null;
245
+ const prevSample = recentSamples.length > 1 ? recentSamples[recentSamples.length - 2] : null;
246
+ const prevPrevSample = recentSamples.length > 2 ? recentSamples[recentSamples.length - 3] : null;
247
+ let latestLineCrossIndex = null;
248
+ let latestClearBreakIndex = null;
249
+ for (let index = 0; index < recentSamples.length; index += 1) {
250
+ const sample = recentSamples[index];
251
+ const prev = index > 0 ? recentSamples[index - 1] : null;
252
+ if (sample.breakoutSideClose === true && (prev == null || prev.breakoutSideClose !== true)) {
253
+ latestLineCrossIndex = index;
254
+ }
255
+ if (sample.clearBreakClose === true && (prev == null || prev.clearBreakClose !== true)) {
256
+ latestClearBreakIndex = index;
257
+ }
258
+ }
259
+ let latestRetestIndex = null;
260
+ if (latestLineCrossIndex != null) {
261
+ for (let index = latestLineCrossIndex + 1; index < recentSamples.length - 1; index += 1) {
262
+ const sample = recentSamples[index];
263
+ if (sample.lineTouched || sample.nearLineClose === true) {
264
+ latestRetestIndex = index;
265
+ }
266
+ }
267
+ }
268
+ const currentIndex = recentSamples.length - 1;
269
+ const barsSinceLineCross = latestLineCrossIndex != null ? currentIndex - latestLineCrossIndex : null;
270
+ const barsSinceClearBreak = latestClearBreakIndex != null ? currentIndex - latestClearBreakIndex : null;
271
+ const barsSinceRetest = latestRetestIndex != null ? currentIndex - latestRetestIndex : null;
272
+ const retestHappened = latestRetestIndex != null;
273
+ const retestConfirmed = retestHappened === true && barsSinceRetest != null && barsSinceRetest > 0 && lastSample?.clearBreakClose === true;
274
+ const breakoutFresh = barsSinceLineCross != null && barsSinceLineCross >= 0 && barsSinceLineCross <= 1;
275
+ const staleBreakout = lastSample?.clearBreakClose === true && barsSinceLineCross != null && barsSinceLineCross > 1 && retestConfirmed !== true;
276
+ const currentDistanceAtrRatio = lastSample?.distanceAtrRatio ?? null;
277
+ const previousDistanceAtrRatio = prevSample?.distanceAtrRatio ?? null;
278
+ const distanceAtrVelocity = currentDistanceAtrRatio != null && previousDistanceAtrRatio != null ? currentDistanceAtrRatio - previousDistanceAtrRatio : null;
279
+ const distanceAtrAcceleration = currentDistanceAtrRatio != null && previousDistanceAtrRatio != null && prevPrevSample?.distanceAtrRatio != null ? currentDistanceAtrRatio - 2 * previousDistanceAtrRatio + prevPrevSample.distanceAtrRatio : null;
280
+ const distanceAtrRecent = recentSamples.map((sample) => sample.distanceAtrRatio).filter((value) => value != null);
281
+ const maxDistanceAtrRatioRecent = distanceAtrRecent.length > 0 ? Math.max(...distanceAtrRecent) : null;
282
+ const minDistanceAtrRatioRecent = distanceAtrRecent.length > 0 ? Math.min(...distanceAtrRecent) : null;
283
+ const firstPoint = evaluator?.firstPoint ?? null;
284
+ const lastPoint = evaluator?.lastPoint ?? null;
285
+ const intervalMs = sortedCandles.length > 1 ? toFiniteNumberOrNull(
286
+ sortedCandles[sortedCandles.length - 1].timestamp
287
+ ) - toFiniteNumberOrNull(
288
+ sortedCandles[sortedCandles.length - 2].timestamp
289
+ ) : null;
290
+ const lineBarsSpan = firstPoint != null && lastPoint != null && intervalMs != null && intervalMs > 0 ? Math.max(
291
+ 1,
292
+ Math.round((lastPoint.timestamp - firstPoint.timestamp) / intervalMs)
293
+ ) : null;
294
+ const lineSlopePct = firstPoint != null && lastPoint != null && firstPoint.value !== 0 ? (lastPoint.value - firstPoint.value) / firstPoint.value * 100 : null;
295
+ const lineSlopePctPerBar = lineSlopePct != null && lineBarsSpan != null && lineBarsSpan > 0 ? lineSlopePct / lineBarsSpan : null;
296
+ const lineSlopeDirection = getLineSlopeDirection(lineSlopePctPerBar);
297
+ const lineSlopeAligned = lineSlopeDirection == null || structural.mode == null ? null : structural.mode === "lows" ? lineSlopeDirection === "rising" : structural.mode === "highs" ? lineSlopeDirection === "falling" : null;
298
+ let entryTiming = "unknown";
299
+ if (lastSample?.clearBreakClose === true) {
300
+ if (retestConfirmed) {
301
+ entryTiming = "ready_retest";
302
+ } else if (barsSinceLineCross === 0) {
303
+ entryTiming = "ready_breakout";
304
+ } else if (barsSinceLineCross === 1 && (distanceAtrVelocity == null || distanceAtrVelocity >= 0)) {
305
+ entryTiming = "ready_follow_through";
306
+ } else if (retestHappened) {
307
+ entryTiming = "wait_retest_confirmation";
308
+ } else if (staleBreakout) {
309
+ entryTiming = "stale_breakout";
310
+ } else {
311
+ entryTiming = "wait_retest";
312
+ }
313
+ }
314
+ return {
315
+ lineCrossDetected: latestLineCrossIndex != null,
316
+ clearBreakDetected: latestClearBreakIndex != null,
317
+ barsSinceLineCross,
318
+ barsSinceClearBreak,
319
+ barsSinceRetest,
320
+ breakoutFresh,
321
+ retestHappened,
322
+ retestConfirmed,
323
+ staleBreakout,
324
+ entryTiming,
325
+ entryReadyNow: entryTiming === "ready_breakout" || entryTiming === "ready_follow_through" || entryTiming === "ready_retest",
326
+ lineSlopePct,
327
+ lineSlopePctPerBar,
328
+ lineSlopeDirection,
329
+ lineSlopeAligned,
330
+ currentDistanceAtrRatio,
331
+ previousDistanceAtrRatio,
332
+ distanceAtrVelocity,
333
+ distanceAtrAcceleration,
334
+ maxDistanceAtrRatioRecent,
335
+ minDistanceAtrRatioRecent
336
+ };
337
+ };
338
+
339
+ // src/TrendLine/adapters/ai.ts
340
+ var TRENDLINE_CONTEXT_PROMPT = `
341
+ TrendLine addon:
342
+ - This setup is based on breakout or reaction around a trendline. 'payload.figures.trendline' contains the line geometry, and 'payload.additionalIndicators.trendlineContext' contains a compact summary of price location versus the line.
343
+ - For TrendLine, geometry and price structure have higher priority than indicator confirmation.
344
+ - Touches strengthen a line but do not confirm the signal by themselves. Without a confirmed breakout or retest, do not raise quality only because there were many touches.
345
+ - For SHORT on rising support ('trendline.mode="lows"'), you usually need either a clear move below the line or a retest from below with rejection. If price remains above the line or directly on it, use 'direction=null' and usually 'quality <= 2'.
346
+ - For LONG on descending resistance ('trendline.mode="highs"'), the mirror logic applies: you usually need a move above the line or a retest from above. If price remains below the line or directly on it, use 'direction=null' and usually 'quality <= 2'.
347
+ - If 'payload.additionalIndicators.trendlineContext.nearLineNoise=true', do not treat that as a confirmed breakout. Quality is usually '<= 2-3', and a retest or confirmation is still needed.
348
+ - If 'payload.additionalIndicators.trendlineContext.coinBiasAligned=false' or 'btcBiasAligned=false', treat it as a direct conflict with the signal direction. In that case, the signal is usually not confirmed unless the structural edge is exceptional.
349
+ - If 'payload.additionalIndicators.derivativesContext' exists, use it as Coinalyze-based breakout confirmation or conflict: open interest should support the move, funding should not be extremely crowded against the entry quality, and liquidation spikes can indicate flush, squeeze, or exhaustion.
350
+ - If 'derivativesContext.summary.riskFlags' contains 'oi_not_confirming', treat it as a direct sign that open interest does not confirm the breakout yet. Without very strong follow-through, do not elevate the signal to immediate entry.
351
+ - For SHORT during 'off_hours' or session overlap, require cleaner structural follow-through than during normal hours; those windows are noisier and less suitable for immediate approval.
352
+ - If 'payload.additionalIndicators.trendlineContext.clearBreak=false' and price is still near the line, do not describe it as a clean breakout.
353
+ - If 'clearBreak=true' but 'trendlineContext.weakCleanBreak=true', treat it as a formally valid but too-weak breakout: structure has been touched, but displacement margin is still limited. This usually calls for follow-through or retest, not immediate confirmation.
354
+ - If 'clearBreak=true' but 'trendlineContext.compressedCleanBreak=true', treat it as a compressed breakout after a cluster of close touches on a short line. Even with a formal line exit, this still usually calls for follow-through or retest rather than immediate confirmation.
355
+ - If 'clearBreak=true', 'trendlineContext.breakVsAtrRatio < 0.5', and 'trendlineContext.weakBtcLedBreak=true', treat it as a weak BTC-led break without enough coin-specific follow-through. This usually calls for retest or extra confirmation rather than immediate confirmation.
356
+ - For LONG on descending resistance, if the line is very long, the move above it is still modest, and BTC only weakly supports the break, treat it as an early breakout without sufficient follow-through. That usually needs retest or confirmation rather than immediate confirmation.
357
+ - For TrendLine, quality 4-5 is only allowed when all of the following are true at once: 'clearBreak=true', 'nearLineNoise=false', 'coinBiasAligned=true', and 'btcBiasAligned=true'. If any one of these conditions is not met, do not set quality above 3.
358
+ - Rare exception: if 'trendlineContext.aggressivePreBreakPressure=true', this is an aggressive pre-break-pressure setup. In that case 'quality=4' is allowed even without 'clearBreak', but only as early structural confirmation and only when coin/BTC bias is not conflicting.
359
+ - Another rare exception: if 'trendlineContext.strongNearBreakPressure=true', this is a mature line with pressure already building in the signal direction and very strong aligned pressure from both the coin and BTC. In that case 'quality=4' is allowed even when 'nearLineNoise=true', but only as early structural confirmation on strong structure.
360
+ `;
361
+ var TRENDLINE_PAYLOAD_PROMPT = `
362
+ - 'payload.figures.trendline' contains the full trendline geometry without trimming so touches and structure can be evaluated.
363
+ - 'payload.additionalIndicators.trendlineContext' contains 'mode / touches / distance / currentLinePrice / priceVsLinePct / priceVsLineSide / clearBreak / nearLineNoise / coinMaBias / btcMaBias / maxAllowedQuality / approvalAllowedNow / hardBlockReasons'.
364
+ - It also includes 'atrPct / breakVsAtrRatio / coinMaSpreadPct / btcMaSpreadPct / aggressivePreBreakPressure / strongNearBreakPressure / weakCleanBreak / compressedCleanBreak / weakBtcLedBreak / weakLongFarBreak'.
365
+ - If 'payload.additionalIndicators.derivativesContext' exists, it contains Coinalyze-derived open interest, funding, and liquidation fields for the signal moment; do not treat 'stale' or 'missing_derivatives' as confirmation or conflict.
366
+ `;
367
+ var buildTrendlineContext = (signal) => {
368
+ const marketContext = signal.additionalIndicators && typeof signal.additionalIndicators.marketContext === "object" && signal.additionalIndicators.marketContext && !Array.isArray(signal.additionalIndicators.marketContext) ? signal.additionalIndicators.marketContext : null;
369
+ const tradingSession = marketContext && typeof marketContext.tradingSession === "object" && marketContext.tradingSession && !Array.isArray(marketContext.tradingSession) ? marketContext.tradingSession : null;
370
+ const sessionPrimary = typeof tradingSession?.primarySession === "string" ? tradingSession.primarySession : null;
371
+ const sessionIsOverlap = tradingSession?.isOverlap === true;
372
+ const derivativesContext = signal.additionalIndicators && typeof signal.additionalIndicators.derivativesContext === "object" && signal.additionalIndicators.derivativesContext && !Array.isArray(signal.additionalIndicators.derivativesContext) ? signal.additionalIndicators.derivativesContext : null;
373
+ const derivativesSummary = derivativesContext && typeof derivativesContext.summary === "object" && derivativesContext.summary && !Array.isArray(derivativesContext.summary) ? derivativesContext.summary : null;
374
+ const derivativesRiskFlags = Array.isArray(derivativesSummary?.riskFlags) ? derivativesSummary.riskFlags.filter(
375
+ (flag) => typeof flag === "string" && flag.length > 0
376
+ ) : [];
377
+ const oiNotConfirming = derivativesRiskFlags.includes("oi_not_confirming");
378
+ const structural = buildTrendlineStructuralContext(signal);
379
+ const trendLine = getTrendLineFromPayload(signal);
380
+ const coinMaFast = getLastFiniteNumber(signal.indicators?.maFast);
381
+ const coinMaSlow = getLastFiniteNumber(signal.indicators?.maSlow);
382
+ const coinMaBias = getBias(coinMaFast, coinMaSlow);
383
+ const coinMaSpreadPct = getSpreadPct(coinMaFast, coinMaSlow);
384
+ const entryTiming = typeof signal.additionalIndicators?.trendlineTiming === "object" && signal.additionalIndicators?.trendlineTiming && typeof signal.additionalIndicators.trendlineTiming.entryTiming === "string" ? signal.additionalIndicators.trendlineTiming.entryTiming : null;
385
+ const coinBiasAligned = structural.signalDirection == null || coinMaBias == null ? null : structural.signalDirection === "SHORT" ? coinMaBias === "bearish" : coinMaBias === "bullish";
386
+ const aggressivePreBreakPressure = structural.signalDirection === "SHORT" && trendLine?.mode === "lows" && structural.priceVsLinePct != null && structural.priceVsLinePct > 0 && structural.priceVsLinePct <= 0.15 && (structural.touches ?? 0) >= 5 && structural.distance != null && structural.distance >= 90 && structural.distance <= 120 && coinBiasAligned === true && structural.btcBiasAligned === true && coinMaSpreadPct != null && coinMaSpreadPct <= -1 && structural.btcMaSpreadPct != null && structural.btcMaSpreadPct <= -0.3;
387
+ const strongNearBreakPressure = structural.signalDirection === "SHORT" && trendLine?.mode === "lows" && structural.clearBreak === false && structural.nearLineNoise === true && structural.priceVsLinePct != null && structural.priceVsLinePct < 0 && structural.breakVsAtrRatio != null && structural.breakVsAtrRatio >= 0.25 && structural.breakVsAtrRatio <= 0.35 && coinBiasAligned === true && structural.btcBiasAligned === true && coinMaSpreadPct != null && coinMaSpreadPct <= -1.5 && structural.btcMaSpreadPct != null && structural.btcMaSpreadPct <= -0.5 && (structural.touches ?? 0) >= 5 && structural.distance != null && structural.distance >= 300;
388
+ const weakBtcLedBreak = structural.signalDirection === "SHORT" ? structural.clearBreak === true && structural.breakVsAtrRatio != null && structural.breakVsAtrRatio < 0.5 && coinBiasAligned === true && structural.btcBiasAligned === true && coinMaSpreadPct != null && coinMaSpreadPct > -0.6 && structural.btcMaSpreadPct != null && structural.btcMaSpreadPct <= -0.3 : structural.signalDirection === "LONG" ? structural.clearBreak === true && structural.breakVsAtrRatio != null && structural.breakVsAtrRatio < 0.5 && coinBiasAligned === true && structural.btcBiasAligned === true && coinMaSpreadPct != null && coinMaSpreadPct < 0.6 && structural.btcMaSpreadPct != null && structural.btcMaSpreadPct >= 0.3 : false;
389
+ const hardBlockReasons = [...structural.structuralHardBlockReasons];
390
+ if (coinBiasAligned === false) {
391
+ hardBlockReasons.push("coin_bias_conflict");
392
+ }
393
+ if (structural.btcBiasAligned === false) {
394
+ hardBlockReasons.push("btc_bias_conflict");
395
+ }
396
+ if (weakBtcLedBreak) {
397
+ hardBlockReasons.push("weak_btc_led_break");
398
+ }
399
+ if (oiNotConfirming && !hardBlockReasons.includes("oi_not_confirming")) {
400
+ hardBlockReasons.push("oi_not_confirming");
401
+ }
402
+ if (structural.signalDirection === "SHORT" && (entryTiming === "ready_follow_through" || entryTiming === "ready_retest") && (sessionPrimary === "off_hours" || sessionIsOverlap) && !hardBlockReasons.includes("short_session_risk")) {
403
+ hardBlockReasons.push("short_session_risk");
404
+ }
405
+ const deterministicQuality = getDeterministicTrendlineQuality({
406
+ signalDirection: structural.signalDirection,
407
+ clearBreak: structural.clearBreak,
408
+ nearLineNoise: structural.nearLineNoise,
409
+ breakVsAtrRatio: structural.breakVsAtrRatio,
410
+ priceVsLinePctAbs: structural.priceVsLinePctAbs,
411
+ touches: structural.touches,
412
+ distance: structural.distance,
413
+ btcMaSpreadPct: structural.btcMaSpreadPct,
414
+ aggressivePreBreakPressure,
415
+ strongNearBreakPressure,
416
+ hardBlockReasons,
417
+ entryTiming,
418
+ coinMaSpreadPct
419
+ });
420
+ const maxAllowedQuality = deterministicQuality;
421
+ const approvalAllowedNow = deterministicQuality >= 4;
422
+ return {
423
+ ...structural,
424
+ entryTiming,
425
+ coinMaFast,
426
+ coinMaSlow,
427
+ coinMaBias,
428
+ coinMaSpreadPct,
429
+ coinBiasAligned,
430
+ aggressivePreBreakPressure,
431
+ strongNearBreakPressure,
432
+ weakBtcLedBreak,
433
+ sessionPrimary,
434
+ sessionIsOverlap,
435
+ derivativesRiskFlags,
436
+ oiNotConfirming,
437
+ deterministicQuality,
438
+ maxAllowedQuality,
439
+ approvalAllowedNow,
440
+ hardBlockReasons
441
+ };
442
+ };
443
+ var formatPromptNumber = (value, fractionDigits = 4) => {
444
+ if (value == null) {
445
+ return "n/a";
446
+ }
447
+ return value.toFixed(fractionDigits);
448
+ };
449
+ var getHardBlockReasonText = (reason) => {
450
+ switch (reason) {
451
+ case "no_clear_break":
452
+ return "there is no clean breakout of the line";
453
+ case "near_line_noise":
454
+ return "price is too close to the line and looks like noise";
455
+ case "coin_bias_conflict":
456
+ return "coin bias conflicts with the direction";
457
+ case "btc_bias_conflict":
458
+ return "BTC context conflicts with the direction";
459
+ case "weak_clean_break":
460
+ return "the formal breakout exists, but displacement is still too weak relative to ATR";
461
+ case "compressed_clean_break":
462
+ return "the breakout looks too compressed: clustered close touches on a short line without enough follow-through";
463
+ case "weak_btc_led_break":
464
+ return "the breakout is too small relative to ATR and looks more like a BTC-led move without enough coin follow-through";
465
+ case "weak_long_far_break":
466
+ return "for LONG, the breakout of the very long line is still too modest and BTC support is too weak";
467
+ case "oi_not_confirming":
468
+ return "open interest does not confirm the move, so the breakout still looks unconfirmed on derivatives context";
469
+ case "short_session_risk":
470
+ return "for SHORT, the current session is too noisy or thin (off-hours or overlap), so clearer follow-through is required";
471
+ default:
472
+ return reason;
473
+ }
474
+ };
475
+ var mergeShortText = (primary, fallback, maxLength) => {
476
+ const value = primary.trim() || fallback;
477
+ return value.slice(0, maxLength);
478
+ };
479
+ var getDeterministicTrendlineQuality = (trendlineContext) => {
480
+ if (trendlineContext.aggressivePreBreakPressure === true || trendlineContext.strongNearBreakPressure === true) {
481
+ return 4;
482
+ }
483
+ if (trendlineContext.hardBlockReasons.length > 0) {
484
+ return trendlineContext.clearBreak === true ? 3 : 2;
485
+ }
486
+ if (trendlineContext.clearBreak !== true || trendlineContext.nearLineNoise !== false || trendlineContext.signalDirection == null) {
487
+ return 2;
488
+ }
489
+ const breakVsAtrRatio = trendlineContext.breakVsAtrRatio ?? 0;
490
+ const priceVsLinePctAbs = trendlineContext.priceVsLinePctAbs ?? 0;
491
+ const touches = trendlineContext.touches ?? 0;
492
+ const distance = trendlineContext.distance ?? Number.POSITIVE_INFINITY;
493
+ const btcMaSpreadPct = trendlineContext.btcMaSpreadPct ?? 0;
494
+ const coinMaSpreadPct = trendlineContext.coinMaSpreadPct ?? 0;
495
+ const entryTiming = trendlineContext.entryTiming;
496
+ if (trendlineContext.signalDirection === "LONG") {
497
+ const quality52 = breakVsAtrRatio >= 1.1 && priceVsLinePctAbs >= 1 && touches >= 5 && distance < 250 && btcMaSpreadPct >= 0.5;
498
+ if (quality52) {
499
+ return 5;
500
+ }
501
+ const compactBreakoutQuality4 = breakVsAtrRatio >= 0.75 && priceVsLinePctAbs >= 0.7 && distance < 150 && (btcMaSpreadPct >= 0.4 || breakVsAtrRatio >= 1) && (touches >= 5 || breakVsAtrRatio >= 0.85);
502
+ const shortLineStrengthQuality4 = breakVsAtrRatio >= 0.6 && priceVsLinePctAbs >= 0.65 && touches >= 6 && distance < 120 && btcMaSpreadPct >= 0.75;
503
+ const matureLineQuality4 = breakVsAtrRatio >= 0.8 && priceVsLinePctAbs >= 0.7 && touches >= 5 && distance < 350 && btcMaSpreadPct >= 0.4;
504
+ const extendedHighConvictionQuality4 = breakVsAtrRatio >= 0.75 && priceVsLinePctAbs >= 0.65 && touches >= 5 && distance < 600 && btcMaSpreadPct >= 0.9;
505
+ const alignedRecentFollowThroughQuality4 = (entryTiming === "ready_follow_through" || entryTiming === "ready_retest") && breakVsAtrRatio >= 0.58 && breakVsAtrRatio <= 0.72 && priceVsLinePctAbs >= 0.48 && priceVsLinePctAbs <= 0.7 && touches >= 5 && distance <= 420 && btcMaSpreadPct >= 0.45 && coinMaSpreadPct >= 0.25;
506
+ return compactBreakoutQuality4 || shortLineStrengthQuality4 || matureLineQuality4 || extendedHighConvictionQuality4 || alignedRecentFollowThroughQuality4 ? 4 : 3;
507
+ }
508
+ const quality5 = breakVsAtrRatio >= 5 && priceVsLinePctAbs >= 10 && touches >= 5 && distance >= 240 && distance <= 400 && btcMaSpreadPct <= -1;
509
+ if (quality5) {
510
+ return 5;
511
+ }
512
+ const quality4 = breakVsAtrRatio >= 1 && breakVsAtrRatio < 2.5 && priceVsLinePctAbs >= 1 && touches >= 5 && distance < 300 && btcMaSpreadPct <= -0.5;
513
+ const strongReadyBreakoutQuality4 = entryTiming === "ready_breakout" && breakVsAtrRatio >= 2 && priceVsLinePctAbs >= 1.8 && touches >= 5 && btcMaSpreadPct <= -1 && (coinMaSpreadPct <= -1 || breakVsAtrRatio >= 3);
514
+ const moderateReadyBreakoutQuality4 = entryTiming === "ready_breakout" && breakVsAtrRatio >= 0.65 && breakVsAtrRatio <= 1.2 && priceVsLinePctAbs >= 0.65 && priceVsLinePctAbs <= 1 && touches >= 5 && distance >= 150 && distance <= 320 && btcMaSpreadPct <= -0.05 && coinMaSpreadPct <= -0.25;
515
+ if ((quality4 || moderateReadyBreakoutQuality4) && entryTiming === "ready_breakout") {
516
+ return 4;
517
+ }
518
+ return strongReadyBreakoutQuality4 ? 4 : 3;
519
+ };
520
+ var getDeterministicTrendlineQualityReason = (trendlineContext) => {
521
+ if (trendlineContext.hardBlockReasons.length > 0) {
522
+ return `TrendLine guardrail: entry is blocked because ${trendlineContext.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.`;
523
+ }
524
+ if (trendlineContext.signalDirection === "LONG") {
525
+ return "TrendLine deterministic quality: the breakout exists, but LONG still lacks enough displacement, BTC support, or a compact enough line for immediate entry.";
526
+ }
527
+ if (trendlineContext.signalDirection === "SHORT") {
528
+ return "TrendLine deterministic quality: the breakout exists, but SHORT still lacks enough bearish displacement or follow-through, so entry is still too early.";
529
+ }
530
+ return "TrendLine deterministic quality: the structure is still not strong enough for entry right now.";
531
+ };
532
+ var getTrendlineContextFromPayload = (payload, signal) => {
533
+ const additional = payload.additionalIndicators;
534
+ const fromPayload = additional?.trendlineContext;
535
+ return fromPayload ?? buildTrendlineContext(signal);
536
+ };
537
+ var trendLineAiAdapter = {
538
+ // Shared builder trims nested series/figures; TrendLine keeps trendline geometry untrimmed on purpose.
539
+ buildPayload: ({ signal, basePayload }) => {
540
+ const mergedAdditionalIndicators = {
541
+ ...signal.additionalIndicators ?? {},
542
+ ...basePayload.additionalIndicators ?? {}
543
+ };
544
+ const trendlineContext = buildTrendlineContext({
545
+ ...signal,
546
+ additionalIndicators: mergedAdditionalIndicators
547
+ });
548
+ return {
549
+ ...basePayload,
550
+ figures: {
551
+ ...basePayload.figures,
552
+ // Keep raw line geometry available exactly where the shared prompt expects it.
553
+ trendline: getTrendLineFromPayload(signal)
554
+ },
555
+ additionalIndicators: {
556
+ ...mergedAdditionalIndicators,
557
+ trendlineContext
558
+ }
559
+ };
560
+ },
561
+ postProcessAnalysis: ({ signal, payload, analysis }) => {
562
+ const trendlineContext = getTrendlineContextFromPayload(payload, signal);
563
+ const quality = trendlineContext.deterministicQuality;
564
+ const signalDirection = signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
565
+ if ((trendlineContext.aggressivePreBreakPressure === true || trendlineContext.strongNearBreakPressure === true) && signalDirection != null) {
566
+ const fallbackReason = trendlineContext.strongNearBreakPressure === true ? "TrendLine strong near-break pressure: a mature line is already compressing in the trade direction, so early entry is allowed by strategy code." : "TrendLine aggressive pre-break pressure: early structural confirmation is allowed under strong bearish pressure.";
567
+ const fallbackComment = trendlineContext.strongNearBreakPressure === true ? "TrendLine strong near-break pressure: early entry is allowed by strategy code." : "TrendLine aggressive pre-break pressure: early entry is allowed by strategy code.";
568
+ return {
569
+ ...analysis,
570
+ direction: signalDirection,
571
+ quality: 4,
572
+ needRetest: false,
573
+ retestPrice: null,
574
+ takeProfitPrice: analysis.takeProfitPrice ?? signal.prices?.takeProfitPrice ?? null,
575
+ stopLossPrice: analysis.stopLossPrice ?? signal.prices?.stopLossPrice ?? null,
576
+ qualityReason: mergeShortText(
577
+ analysis.qualityReason ?? "",
578
+ fallbackReason,
579
+ 400
580
+ ),
581
+ comment: mergeShortText(analysis.comment ?? "", fallbackComment, 1024)
582
+ };
583
+ }
584
+ if (trendlineContext.approvalAllowedNow === true && signalDirection != null) {
585
+ return {
586
+ ...analysis,
587
+ direction: signalDirection,
588
+ quality,
589
+ needRetest: false,
590
+ retestPrice: null,
591
+ takeProfitPrice: analysis.takeProfitPrice ?? signal.prices?.takeProfitPrice ?? null,
592
+ stopLossPrice: analysis.stopLossPrice ?? signal.prices?.stopLossPrice ?? null
593
+ };
594
+ }
595
+ const retestPrice = trendlineContext.currentLinePrice ?? analysis.retestPrice ?? null;
596
+ const qualityReason = mergeShortText(
597
+ getDeterministicTrendlineQualityReason(trendlineContext),
598
+ "TrendLine guardrail: entry is blocked until the structure is confirmed.",
599
+ 400
600
+ );
601
+ const triggerInvalidation = mergeShortText(
602
+ trendlineContext.hardBlockReasons.length > 0 ? `Wait for a clean breakout or retest of the line and resolve the conflicts: ${trendlineContext.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "Wait for stronger breakout follow-through or a line retest confirmed by both the coin and BTC.",
603
+ "Wait for a clean line breakout or retest plus confirmation from the coin and BTC.",
604
+ 400
605
+ );
606
+ const comment = mergeShortText(
607
+ trendlineContext.hardBlockReasons.length > 0 ? `TrendLine guardrail blocked the entry: ${trendlineContext.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "TrendLine deterministic quality downgraded the entry to watch or reject until stronger structure appears.",
608
+ "TrendLine guardrail blocked the entry until the structure is confirmed.",
609
+ 1024
610
+ );
611
+ return {
612
+ ...analysis,
613
+ direction: null,
614
+ quality,
615
+ needRetest: true,
616
+ retestPrice,
617
+ takeProfitPrice: null,
618
+ stopLossPrice: null,
619
+ setup: mergeShortText(
620
+ analysis.setup ?? "",
621
+ "There is no confirmed trendline breakout or retest for entry right now.",
622
+ 400
623
+ ),
624
+ retestPlan: mergeShortText(
625
+ analysis.retestPlan ?? "",
626
+ "Wait for a return to the line and a reaction in the trade direction before a new entry.",
627
+ 400
628
+ ),
629
+ qualityReason,
630
+ triggerInvalidation,
631
+ comment
632
+ };
633
+ },
634
+ buildSystemPromptAddon: () => `
635
+ ${TRENDLINE_CONTEXT_PROMPT}
636
+ ${TRENDLINE_PAYLOAD_PROMPT}
637
+ `,
638
+ buildHumanPromptAddon: ({ payload }) => {
639
+ const additional = payload.additionalIndicators;
640
+ const trendlineContext = additional?.trendlineContext;
641
+ if (!trendlineContext) {
642
+ return "";
643
+ }
644
+ return `
645
+
646
+ Additional TrendLine context:
647
+ - trendline.mode=${trendlineContext.mode ?? "n/a"}
648
+ - trendline.touches=${formatPromptNumber(trendlineContext.touches, 0)}
649
+ - trendline.distance=${formatPromptNumber(trendlineContext.distance, 0)}
650
+ - trendline.currentLinePrice=${formatPromptNumber(trendlineContext.currentLinePrice, 6)}
651
+ - trendline.currentPrice=${formatPromptNumber(trendlineContext.currentPrice, 6)}
652
+ - trendline.priceVsLinePct=${formatPromptNumber(trendlineContext.priceVsLinePct, 3)}%
653
+ - trendline.priceVsLineSide=${trendlineContext.priceVsLineSide ?? "n/a"}
654
+ - trendline.clearBreak=${String(trendlineContext.clearBreak)}
655
+ - trendline.nearLineNoise=${String(trendlineContext.nearLineNoise)}
656
+ - trendline.atrPct=${formatPromptNumber(trendlineContext.atrPct, 3)}%
657
+ - trendline.breakVsAtrRatio=${formatPromptNumber(trendlineContext.breakVsAtrRatio, 3)}
658
+ - trendline.aggressivePreBreakPressure=${String(trendlineContext.aggressivePreBreakPressure)}
659
+ - trendline.strongNearBreakPressure=${String(trendlineContext.strongNearBreakPressure)}
660
+ - trendline.weakCleanBreak=${String(trendlineContext.weakCleanBreak)}
661
+ - trendline.compressedCleanBreak=${String(trendlineContext.compressedCleanBreak)}
662
+ - trendline.weakBtcLedBreak=${String(trendlineContext.weakBtcLedBreak)}
663
+ - trendline.weakLongFarBreak=${String(trendlineContext.weakLongFarBreak)}
664
+ - trendline.entryTiming=${trendlineContext.entryTiming ?? "n/a"}
665
+ - trendline.deterministicQuality=${String(trendlineContext.deterministicQuality)}
666
+ - trendline.maxAllowedQuality=${String(trendlineContext.maxAllowedQuality)}
667
+ - trendline.approvalAllowedNow=${String(trendlineContext.approvalAllowedNow)}
668
+ - trendline.hardBlockReasons=${JSON.stringify(trendlineContext.hardBlockReasons)}
669
+ - coin.maFastLast=${formatPromptNumber(trendlineContext.coinMaFast, 6)}
670
+ - coin.maSlowLast=${formatPromptNumber(trendlineContext.coinMaSlow, 6)}
671
+ - coin.maBias=${trendlineContext.coinMaBias ?? "n/a"}
672
+ - coin.maSpreadPct=${formatPromptNumber(trendlineContext.coinMaSpreadPct, 3)}%
673
+ - coin.biasAligned=${String(trendlineContext.coinBiasAligned)}
674
+ - btc.maFastLast=${formatPromptNumber(trendlineContext.btcMaFast, 2)}
675
+ - btc.maSlowLast=${formatPromptNumber(trendlineContext.btcMaSlow, 2)}
676
+ - btc.maBias=${trendlineContext.btcMaBias ?? "n/a"}
677
+ - btc.maSpreadPct=${formatPromptNumber(trendlineContext.btcMaSpreadPct, 3)}%
678
+ - btc.biasAligned=${String(trendlineContext.btcBiasAligned)}
679
+
680
+ Interpretation rules for TrendLine:
681
+ - SHORT from a 'lows' line is confirmed only by a clear move below the line or a retest from below with rejection.
682
+ - LONG from a 'highs' line is confirmed only by a clear move above the line or a retest from above with rejection.
683
+ - If 'trendline.nearLineNoise=true' or any bias alignment is false, it is better to return 'direction=null' and quality 1-3 than to describe the signal as confirmed without margin.
684
+ - If 'trendline.weakCleanBreak=true', the formal breakout exists but is too weak in displacement; it needs follow-through or retest rather than quality 4-5.
685
+ - If 'trendline.compressedCleanBreak=true', the breakout exists formally, but the line is too short and compressed after clustered touches; this usually needs follow-through or retest rather than immediate confirmation.
686
+ - If 'trendline.weakBtcLedBreak=true', treat it as a small breakout driven more by BTC than by the coin itself; this usually calls for retest and quality 1-3.
687
+ - If 'clearBreak=false' or any alignment is false, do not raise quality above 3.
688
+ - If 'trendline.aggressivePreBreakPressure=true', an early SHORT before a clear breakout may be considered only as an exception: quality can be at most 4, and the explanation must clearly state that the structural confirmation is still aggressive.
689
+ - If 'trendline.strongNearBreakPressure=true', an early SHORT may be considered when pressure is already strong on the correct side of the line even if the breakout still falls short of the 'clearBreak' threshold: quality can be at most 4.
690
+ - The strategy deterministically normalizes final quality to 'trendline.deterministicQuality'; your job is to explain the decision within that frame, not to argue with the tier.
691
+ - If 'trendline.approvalAllowedNow=false', do not describe the signal as fully confirmed right now; explain what is still missing for confirmation.
692
+ `;
693
+ },
694
+ mapEntryRuntimeFromConfig: (config2) => mapAiRuntimeFromConfig(
695
+ config2
696
+ )
697
+ };
698
+
699
+ // src/TrendLine/adapters/ml.ts
700
+ import { mapMlRuntimeFromConfig } from "@tradejs/core/strategies";
701
+ var toTrendLineMlStrategyConfig = (input) => {
702
+ if (!input) return void 0;
703
+ return {
704
+ ...input,
705
+ TRENDLINE_CONFIG: input.TRENDLINE_CONFIG ?? input.TRENDLINE ?? {}
706
+ };
707
+ };
708
+ var trendLineMlAdapter = {
709
+ normalizeSignal: (signal) => {
710
+ const nextSignal = {
711
+ ...signal,
712
+ indicators: {
713
+ ...signal.indicators ?? {}
714
+ }
715
+ };
716
+ const additional = signal.additionalIndicators ?? {};
717
+ if (nextSignal.indicators.touches == null && additional.touches != null) {
718
+ nextSignal.indicators.touches = additional.touches;
719
+ }
720
+ if (nextSignal.indicators.distance == null && additional.distance != null) {
721
+ nextSignal.indicators.distance = additional.distance;
722
+ }
723
+ return nextSignal;
724
+ },
725
+ normalizeStrategyConfig: (strategyConfig) => {
726
+ return toTrendLineMlStrategyConfig(strategyConfig);
727
+ },
728
+ mapEntryRuntimeFromConfig: (config2) => mapMlRuntimeFromConfig(config2, {
729
+ strategyConfig: toTrendLineMlStrategyConfig(
730
+ config2
731
+ )
732
+ })
733
+ };
734
+
735
+ // src/TrendLine/manifest.ts
736
+ var trendLineManifest = {
737
+ name: "TrendLine",
738
+ aiAdapter: trendLineAiAdapter,
739
+ mlAdapter: trendLineMlAdapter
740
+ };
741
+
742
+ // src/TrendLine/config.ts
743
+ var config = {
744
+ ENV: "BACKTEST",
745
+ INTERVAL: "15",
746
+ MAKE_ORDERS: true,
747
+ CLOSE_OPPOSITE_POSITIONS: false,
748
+ BACKTEST_PRICE_MODE: "mid",
749
+ AI_ENABLED: false,
750
+ AI_MODE: "llm",
751
+ ML_ENABLED: false,
752
+ ML_THRESHOLD: 0.1,
753
+ MIN_AI_QUALITY: 3,
754
+ FEE_PERCENT: 5e-3,
755
+ MAX_LOSS_VALUE: 10,
756
+ MA_FAST: 14,
757
+ MA_MEDIUM: 49,
758
+ MA_SLOW: 50,
759
+ OBV_SMA: 10,
760
+ ATR: 14,
761
+ ATR_PCT_SHORT: 7,
762
+ ATR_PCT_LONG: 30,
763
+ BB: 20,
764
+ BB_STD: 2,
765
+ MACD_FAST: 12,
766
+ MACD_SLOW: 26,
767
+ MACD_SIGNAL: 9,
768
+ LEVEL_LOOKBACK: 20,
769
+ LEVEL_DELAY: 2,
770
+ TRENDLINE: {
771
+ minTouches: 4,
772
+ offset: 3,
773
+ epsilon: 3e-3,
774
+ epsilonOffset: 4e-3
775
+ },
776
+ HIGHS: {
777
+ enable: true,
778
+ direction: "LONG",
779
+ TP: 4,
780
+ SL: 1.3,
781
+ minRiskRatio: 2
782
+ },
783
+ LOWS: {
784
+ enable: true,
785
+ direction: "SHORT",
786
+ TP: 4,
787
+ SL: 1.3,
788
+ minRiskRatio: 2
789
+ }
790
+ };
791
+
792
+ export {
793
+ buildTrendlineStructuralContext,
794
+ buildTrendlineTimingContext,
795
+ trendLineManifest,
796
+ config
797
+ };