@tradejs/strategies 1.0.4 → 1.0.6

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,762 @@
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
+ \u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u0434\u043B\u044F trendline-\u0441\u0435\u0442\u0430\u043F\u043E\u0432:
342
+ - \u042D\u0442\u043E \u0441\u0435\u0442\u0430\u043F \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u043F\u0440\u043E\u0431\u043E\u044F/\u0440\u0435\u0430\u043A\u0446\u0438\u0438 \u043E\u0442 \u0442\u0440\u0435\u043D\u0434\u043E\u0432\u043E\u0439 \u043B\u0438\u043D\u0438\u0438; \u043F\u043E\u043B\u0435 payload.figures.trendline \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u0433\u0435\u043E\u043C\u0435\u0442\u0440\u0438\u044E \u044D\u0442\u043E\u0439 \u043B\u0438\u043D\u0438\u0438, \u0430 payload.additionalIndicators.trendlineContext \u2014 \u043A\u0440\u0430\u0442\u043A\u0443\u044E \u0441\u0432\u043E\u0434\u043A\u0443 \u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0446\u0435\u043D\u044B \u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043B\u0438\u043D\u0438\u0438.
343
+ - \u0414\u043B\u044F TrendLine \u0440\u043E\u043B\u044C \u0433\u0435\u043E\u043C\u0435\u0442\u0440\u0438\u0438/\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B \u0446\u0435\u043D\u044B \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442\u043D\u0435\u0435 \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u043D\u044B\u0445 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0439.
344
+ - \u041A\u0430\u0441\u0430\u043D\u0438\u044F \u0443\u0441\u0438\u043B\u0438\u0432\u0430\u044E\u0442 \u043B\u0438\u043D\u0438\u044E, \u043D\u043E \u0441\u0430\u043C\u0438 \u043F\u043E \u0441\u0435\u0431\u0435 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044E\u0442 \u0441\u0438\u0433\u043D\u0430\u043B. \u0411\u0435\u0437 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u043E\u044F/\u0440\u0435\u0442\u0435\u0441\u0442\u0430 \u043D\u0435 \u043F\u043E\u0432\u044B\u0448\u0430\u0439 quality \u0442\u043E\u043B\u044C\u043A\u043E \u0438\u0437-\u0437\u0430 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u043A\u0430\u0441\u0430\u043D\u0438\u0439.
345
+ - \u0414\u043B\u044F SHORT \u043F\u043E rising support (trendline.mode="lows") \u043E\u0431\u044B\u0447\u043D\u043E \u043D\u0443\u0436\u0435\u043D \u043B\u0438\u0431\u043E \u044F\u0432\u043D\u044B\u0439 \u0443\u0445\u043E\u0434 \u0446\u0435\u043D\u044B \u043D\u0438\u0436\u0435 \u043B\u0438\u043D\u0438\u0438, \u043B\u0438\u0431\u043E \u0440\u0435\u0442\u0435\u0441\u0442 \u043B\u0438\u043D\u0438\u0438 \u0441\u043D\u0438\u0437\u0443 \u0441 \u043E\u0442\u0431\u043E\u0435\u043C. \u0415\u0441\u043B\u0438 \u0446\u0435\u043D\u0430 \u043E\u0441\u0442\u0430\u0435\u0442\u0441\u044F \u043D\u0430\u0434 \u043B\u0438\u043D\u0438\u0435\u0439 \u0438\u043B\u0438 \u043F\u0440\u044F\u043C\u043E \u043D\u0430 \u043D\u0435\u0439, \u043E\u0431\u044B\u0447\u043D\u043E direction=null \u0438 quality <= 2.
346
+ - \u0414\u043B\u044F LONG \u043F\u043E descending resistance (trendline.mode="highs") \u0437\u0435\u0440\u043A\u0430\u043B\u044C\u043D\u043E: \u043D\u0443\u0436\u0435\u043D \u0432\u044B\u0445\u043E\u0434 \u0432\u044B\u0448\u0435 \u043B\u0438\u043D\u0438\u0438 \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442 \u0441\u0432\u0435\u0440\u0445\u0443. \u0415\u0441\u043B\u0438 \u0446\u0435\u043D\u0430 \u043F\u043E\u0434 \u043B\u0438\u043D\u0438\u0435\u0439 \u0438\u043B\u0438 \u043F\u0440\u044F\u043C\u043E \u043D\u0430 \u043D\u0435\u0439, \u043E\u0431\u044B\u0447\u043D\u043E direction=null \u0438 quality <= 2.
347
+ - \u0415\u0441\u043B\u0438 payload.additionalIndicators.trendlineContext.nearLineNoise=true, \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u044D\u0442\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u044B\u043C \u043F\u0440\u043E\u0431\u043E\u0435\u043C: \u0447\u0430\u0449\u0435 quality <= 2-3 \u0438 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0440\u0435\u0442\u0435\u0441\u0442\u0430/\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F.
348
+ - \u0415\u0441\u043B\u0438 payload.additionalIndicators.trendlineContext.coinBiasAligned=false \u0438\u043B\u0438 btcBiasAligned=false, \u0442\u0440\u0430\u043A\u0442\u0443\u0439 \u044D\u0442\u043E \u043A\u0430\u043A \u043F\u0440\u044F\u043C\u043E\u0439 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u0430. \u0412 \u0442\u0430\u043A\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435 \u043E\u0431\u044B\u0447\u043D\u043E \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0441\u0438\u0433\u043D\u0430\u043B \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u044B\u043C, \u0435\u0441\u043B\u0438 \u043D\u0435\u0442 \u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0433\u043E \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E\u0433\u043E \u043F\u0440\u0435\u0438\u043C\u0443\u0449\u0435\u0441\u0442\u0432\u0430.
349
+ - \u0415\u0441\u043B\u0438 payload.additionalIndicators.trendlineContext.clearBreak=false \u0438 \u0446\u0435\u043D\u0430 \u0432\u0441\u0435 \u0435\u0449\u0435 \u043E\u043A\u043E\u043B\u043E \u043B\u0438\u043D\u0438\u0438, \u043D\u0435 \u043E\u043F\u0438\u0441\u044B\u0432\u0430\u0439 \u044D\u0442\u043E \u043A\u0430\u043A "\u0447\u0438\u0441\u0442\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439".
350
+ - \u0415\u0441\u043B\u0438 clearBreak=true, \u043D\u043E trendlineContext.weakCleanBreak=true, \u0442\u0440\u0430\u043A\u0442\u0443\u0439 \u044D\u0442\u043E \u043A\u0430\u043A \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0441\u043B\u0430\u0431\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439: \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u0443\u0436\u0435 \u0437\u0430\u0434\u0435\u043B\u043E, \u043D\u043E \u0437\u0430\u043F\u0430\u0441\u0430 \u043F\u043E displacement \u043F\u043E\u043A\u0430 \u043C\u0430\u043B\u043E. \u041E\u0431\u044B\u0447\u043D\u043E \u0437\u0434\u0435\u0441\u044C \u043D\u0443\u0436\u0435\u043D follow-through \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442, \u0430 \u043D\u0435 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0441\u0438\u0433\u043D\u0430\u043B\u0430.
351
+ - \u0415\u0441\u043B\u0438 clearBreak=true, \u043D\u043E trendlineContext.compressedCleanBreak=true, \u044D\u0442\u043E \u0441\u0436\u0430\u0442\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439 \u043F\u043E\u0441\u043B\u0435 \u0441\u0435\u0440\u0438\u0438 \u0431\u043B\u0438\u0437\u043A\u0438\u0445 \u043A\u0430\u0441\u0430\u043D\u0438\u0439 \u043D\u0430 \u043A\u043E\u0440\u043E\u0442\u043A\u043E\u0439 \u043B\u0438\u043D\u0438\u0438. \u0414\u0430\u0436\u0435 \u043F\u0440\u0438 \u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E\u043C \u0432\u044B\u0445\u043E\u0434\u0435 \u0437\u0430 \u043B\u0438\u043D\u0438\u044E \u0437\u0434\u0435\u0441\u044C \u0447\u0430\u0449\u0435 \u043D\u0443\u0436\u0435\u043D follow-through \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442, \u0430 \u043D\u0435 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0441\u0438\u0433\u043D\u0430\u043B\u0430.
352
+ - \u0415\u0441\u043B\u0438 clearBreak=true, \u043D\u043E trendlineContext.breakVsAtrRatio < 0.5 \u0438 \u043F\u0440\u0438 \u044D\u0442\u043E\u043C trendlineContext.weakBtcLedBreak=true, \u0441\u0447\u0438\u0442\u0430\u0439 \u044D\u0442\u043E \u0441\u043B\u0430\u0431\u044B\u043C BTC-led \u043F\u0440\u043E\u0431\u043E\u0435\u043C \u0431\u0435\u0437 \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0433\u043E follow-through \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435. \u041E\u0431\u044B\u0447\u043D\u043E \u0437\u0434\u0435\u0441\u044C \u043D\u0443\u0436\u0435\u043D \u0440\u0435\u0442\u0435\u0441\u0442/\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435, \u0430 \u043D\u0435 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0441\u0438\u0433\u043D\u0430\u043B\u0430.
353
+ - \u0414\u043B\u044F LONG \u043F\u043E descending resistance, \u0435\u0441\u043B\u0438 \u043B\u0438\u043D\u0438\u044F \u043E\u0447\u0435\u043D\u044C \u0434\u043B\u0438\u043D\u043D\u0430\u044F, \u0430 \u0432\u044B\u0445\u043E\u0434 \u043D\u0430\u0434 \u043D\u0435\u0439 \u043F\u043E\u043A\u0430 \u0443\u043C\u0435\u0440\u0435\u043D\u043D\u044B\u0439 \u0438 BTC \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043F\u0440\u043E\u0431\u043E\u0439 \u0441\u043B\u0430\u0431\u043E, \u0442\u0440\u0430\u043A\u0442\u0443\u0439 \u044D\u0442\u043E \u043A\u0430\u043A \u0440\u0430\u043D\u043D\u0438\u0439 breakout \u0431\u0435\u0437 follow-through. \u0412 \u0442\u0430\u043A\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435 \u0447\u0430\u0449\u0435 \u043D\u0443\u0436\u0435\u043D \u0440\u0435\u0442\u0435\u0441\u0442/\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435, \u0430 \u043D\u0435 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0441\u0438\u0433\u043D\u0430\u043B\u0430.
354
+ - \u0414\u043B\u044F TrendLine quality 4-5 \u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u043E\u0433\u0434\u0430 \u043E\u0434\u043D\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E: clearBreak=true, nearLineNoise=false, coinBiasAligned=true \u0438 btcBiasAligned=true. \u0415\u0441\u043B\u0438 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u043D\u043E \u0438\u0437 \u044D\u0442\u0438\u0445 \u0443\u0441\u043B\u043E\u0432\u0438\u0439 \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043E, \u043D\u0435 \u0441\u0442\u0430\u0432\u044C quality \u0432\u044B\u0448\u0435 3.
355
+ - \u0420\u0435\u0434\u043A\u043E\u0435 \u0438\u0441\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435: \u0435\u0441\u043B\u0438 trendlineContext.aggressivePreBreakPressure=true, \u044D\u0442\u043E \u0430\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439 pre-break pressure \u0441\u0435\u0442\u0430\u043F. \u0412 \u0442\u0430\u043A\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435 \u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C quality=4 \u0434\u0430\u0436\u0435 \u0431\u0435\u0437 clearBreak, \u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u0430\u043A \u0440\u0430\u043D\u043D\u0435\u0435 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0442 coin/BTC bias.
356
+ - \u0415\u0449\u0435 \u043E\u0434\u043D\u043E \u0440\u0435\u0434\u043A\u043E\u0435 \u0438\u0441\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435: \u0435\u0441\u043B\u0438 trendlineContext.strongNearBreakPressure=true, \u044D\u0442\u043E \u0437\u0440\u0435\u043B\u0430\u044F \u043B\u0438\u043D\u0438\u044F \u0441 \u0443\u0436\u0435 \u043D\u0430\u0447\u0430\u0432\u0448\u0438\u043C\u0441\u044F \u043F\u0440\u043E\u0434\u0430\u0432\u043B\u0438\u0432\u0430\u043D\u0438\u0435\u043C \u0432 \u0441\u0442\u043E\u0440\u043E\u043D\u0443 \u0441\u0438\u0433\u043D\u0430\u043B\u0430 \u0438 \u043E\u0447\u0435\u043D\u044C \u0441\u0438\u043B\u044C\u043D\u044B\u043C aligned pressure \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u0438 BTC. \u0412 \u0442\u0430\u043A\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435 \u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C quality=4 \u0434\u0430\u0436\u0435 \u043F\u0440\u0438 nearLineNoise=true, \u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u0430\u043A \u0440\u0430\u043D\u043D\u0435\u0435 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u043E \u0441\u0438\u043B\u044C\u043D\u043E\u0439 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435.
357
+ `;
358
+ var TRENDLINE_PAYLOAD_PROMPT = `
359
+ - \u0412 payload.figures.trendline \u043F\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044F \u043F\u043E\u043B\u043D\u0430\u044F \u0433\u0435\u043E\u043C\u0435\u0442\u0440\u0438\u044F \u0442\u0440\u0435\u043D\u0434\u043E\u0432\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 (\u0431\u0435\u0437 trim), \u0447\u0442\u043E\u0431\u044B \u043C\u043E\u0436\u043D\u043E \u0431\u044B\u043B\u043E \u043E\u0446\u0435\u043D\u0438\u0432\u0430\u0442\u044C \u043A\u0430\u0441\u0430\u043D\u0438\u044F/\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443.
360
+ - \u0412 payload.additionalIndicators.trendlineContext \u043F\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044F mode / touches / distance / currentLinePrice / priceVsLinePct / priceVsLineSide / clearBreak / nearLineNoise / coinMaBias / btcMaBias / maxAllowedQuality / approvalAllowedNow / hardBlockReasons.
361
+ - \u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0432 trendlineContext \u043F\u0435\u0440\u0435\u0434\u0430\u044E\u0442\u0441\u044F atrPct / breakVsAtrRatio / coinMaSpreadPct / btcMaSpreadPct / aggressivePreBreakPressure / strongNearBreakPressure / weakCleanBreak / compressedCleanBreak / weakBtcLedBreak / weakLongFarBreak.
362
+ `;
363
+ var buildTrendlineContext = (signal) => {
364
+ const structural = buildTrendlineStructuralContext(signal);
365
+ const trendLine = getTrendLineFromPayload(signal);
366
+ const coinMaFast = getLastFiniteNumber(signal.indicators?.maFast);
367
+ const coinMaSlow = getLastFiniteNumber(signal.indicators?.maSlow);
368
+ const coinMaBias = getBias(coinMaFast, coinMaSlow);
369
+ const coinMaSpreadPct = getSpreadPct(coinMaFast, coinMaSlow);
370
+ const entryTiming = typeof signal.additionalIndicators?.trendlineTiming === "object" && signal.additionalIndicators?.trendlineTiming && typeof signal.additionalIndicators.trendlineTiming.entryTiming === "string" ? signal.additionalIndicators.trendlineTiming.entryTiming : null;
371
+ const coinBiasAligned = structural.signalDirection == null || coinMaBias == null ? null : structural.signalDirection === "SHORT" ? coinMaBias === "bearish" : coinMaBias === "bullish";
372
+ 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;
373
+ 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;
374
+ 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;
375
+ const hardBlockReasons = [...structural.structuralHardBlockReasons];
376
+ if (coinBiasAligned === false) {
377
+ hardBlockReasons.push("coin_bias_conflict");
378
+ }
379
+ if (structural.btcBiasAligned === false) {
380
+ hardBlockReasons.push("btc_bias_conflict");
381
+ }
382
+ if (weakBtcLedBreak) {
383
+ hardBlockReasons.push("weak_btc_led_break");
384
+ }
385
+ const deterministicQuality = getDeterministicTrendlineQuality({
386
+ signalDirection: structural.signalDirection,
387
+ clearBreak: structural.clearBreak,
388
+ nearLineNoise: structural.nearLineNoise,
389
+ breakVsAtrRatio: structural.breakVsAtrRatio,
390
+ priceVsLinePctAbs: structural.priceVsLinePctAbs,
391
+ touches: structural.touches,
392
+ distance: structural.distance,
393
+ btcMaSpreadPct: structural.btcMaSpreadPct,
394
+ aggressivePreBreakPressure,
395
+ strongNearBreakPressure,
396
+ hardBlockReasons,
397
+ entryTiming,
398
+ coinMaSpreadPct
399
+ });
400
+ const maxAllowedQuality = deterministicQuality;
401
+ const approvalAllowedNow = deterministicQuality >= 4;
402
+ return {
403
+ ...structural,
404
+ entryTiming,
405
+ coinMaFast,
406
+ coinMaSlow,
407
+ coinMaBias,
408
+ coinMaSpreadPct,
409
+ coinBiasAligned,
410
+ aggressivePreBreakPressure,
411
+ strongNearBreakPressure,
412
+ weakBtcLedBreak,
413
+ deterministicQuality,
414
+ maxAllowedQuality,
415
+ approvalAllowedNow,
416
+ hardBlockReasons
417
+ };
418
+ };
419
+ var formatPromptNumber = (value, fractionDigits = 4) => {
420
+ if (value == null) {
421
+ return "n/a";
422
+ }
423
+ return value.toFixed(fractionDigits);
424
+ };
425
+ var getHardBlockReasonText = (reason) => {
426
+ switch (reason) {
427
+ case "no_clear_break":
428
+ return "\u043D\u0435\u0442 \u0447\u0438\u0441\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u043E\u044F \u043B\u0438\u043D\u0438\u0438";
429
+ case "near_line_noise":
430
+ return "\u0446\u0435\u043D\u0430 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043B\u0438\u0437\u043A\u043E \u043A \u043B\u0438\u043D\u0438\u0438 \u0438 \u044D\u0442\u043E \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u0448\u0443\u043C";
431
+ case "coin_bias_conflict":
432
+ return "bias \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435\u043C";
433
+ case "btc_bias_conflict":
434
+ return "BTC-\u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435\u043C";
435
+ case "weak_clean_break":
436
+ return "\u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439 \u0435\u0441\u0442\u044C, \u043D\u043E displacement \u0435\u0449\u0435 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0441\u043B\u0430\u0431\u044B\u0439 \u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u043E ATR";
437
+ case "compressed_clean_break":
438
+ return "\u043F\u0440\u043E\u0431\u043E\u0439 \u0432\u044B\u0433\u043B\u044F\u0434\u0438\u0442 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0441\u0436\u0430\u0442\u044B\u043C: \u0441\u0435\u0440\u0438\u044F \u0431\u043B\u0438\u0437\u043A\u0438\u0445 \u043A\u0430\u0441\u0430\u043D\u0438\u0439 \u043D\u0430 \u043A\u043E\u0440\u043E\u0442\u043A\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 \u0431\u0435\u0437 \u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E\u0433\u043E follow-through";
439
+ case "weak_btc_led_break":
440
+ return "\u043F\u0440\u043E\u0431\u043E\u0439 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u0435\u043B\u043A\u0438\u0439 \u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u043E ATR \u0438 \u0431\u043E\u043B\u044C\u0448\u0435 \u043F\u043E\u0445\u043E\u0436 \u043D\u0430 BTC-led \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u0435 \u0431\u0435\u0437 follow-through \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435";
441
+ case "weak_long_far_break":
442
+ return "\u0434\u043B\u044F LONG \u043F\u0440\u043E\u0431\u043E\u0439 \u043E\u0447\u0435\u043D\u044C \u0434\u043B\u0438\u043D\u043D\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 \u043F\u043E\u043A\u0430 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0443\u043C\u0435\u0440\u0435\u043D\u043D\u044B\u0439, \u0430 BTC \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0435\u0433\u043E \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0441\u043B\u0430\u0431\u043E";
443
+ default:
444
+ return reason;
445
+ }
446
+ };
447
+ var mergeShortText = (primary, fallback, maxLength) => {
448
+ const value = primary.trim() || fallback;
449
+ return value.slice(0, maxLength);
450
+ };
451
+ var getDeterministicTrendlineQuality = (trendlineContext) => {
452
+ if (trendlineContext.aggressivePreBreakPressure === true || trendlineContext.strongNearBreakPressure === true) {
453
+ return 4;
454
+ }
455
+ if (trendlineContext.hardBlockReasons.length > 0) {
456
+ return trendlineContext.clearBreak === true ? 3 : 2;
457
+ }
458
+ if (trendlineContext.clearBreak !== true || trendlineContext.nearLineNoise !== false || trendlineContext.signalDirection == null) {
459
+ return 2;
460
+ }
461
+ const breakVsAtrRatio = trendlineContext.breakVsAtrRatio ?? 0;
462
+ const priceVsLinePctAbs = trendlineContext.priceVsLinePctAbs ?? 0;
463
+ const touches = trendlineContext.touches ?? 0;
464
+ const distance = trendlineContext.distance ?? Number.POSITIVE_INFINITY;
465
+ const btcMaSpreadPct = trendlineContext.btcMaSpreadPct ?? 0;
466
+ const coinMaSpreadPct = trendlineContext.coinMaSpreadPct ?? 0;
467
+ const entryTiming = trendlineContext.entryTiming;
468
+ if (trendlineContext.signalDirection === "LONG") {
469
+ const quality52 = breakVsAtrRatio >= 1.1 && priceVsLinePctAbs >= 1 && touches >= 5 && distance < 250 && btcMaSpreadPct >= 0.5;
470
+ if (quality52) {
471
+ return 5;
472
+ }
473
+ const compactBreakoutQuality4 = breakVsAtrRatio >= 0.75 && priceVsLinePctAbs >= 0.7 && distance < 150 && (btcMaSpreadPct >= 0.4 || breakVsAtrRatio >= 1) && (touches >= 5 || breakVsAtrRatio >= 0.85);
474
+ const shortLineStrengthQuality4 = breakVsAtrRatio >= 0.6 && priceVsLinePctAbs >= 0.65 && touches >= 6 && distance < 120 && btcMaSpreadPct >= 0.75;
475
+ const matureLineQuality4 = breakVsAtrRatio >= 0.8 && priceVsLinePctAbs >= 0.7 && touches >= 5 && distance < 350 && btcMaSpreadPct >= 0.4;
476
+ const extendedHighConvictionQuality4 = breakVsAtrRatio >= 0.75 && priceVsLinePctAbs >= 0.65 && touches >= 5 && distance < 600 && btcMaSpreadPct >= 0.9;
477
+ return compactBreakoutQuality4 || shortLineStrengthQuality4 || matureLineQuality4 || extendedHighConvictionQuality4 ? 4 : 3;
478
+ }
479
+ const quality5 = breakVsAtrRatio >= 5 && priceVsLinePctAbs >= 10 && touches >= 5 && distance >= 240 && distance <= 400 && btcMaSpreadPct <= -1;
480
+ if (quality5) {
481
+ return 5;
482
+ }
483
+ const quality4 = breakVsAtrRatio >= 1 && breakVsAtrRatio < 2.5 && priceVsLinePctAbs >= 1 && touches >= 5 && distance < 300 && btcMaSpreadPct <= -0.5;
484
+ const strongReadyBreakoutQuality4 = entryTiming === "ready_breakout" && breakVsAtrRatio >= 2 && priceVsLinePctAbs >= 1.8 && touches >= 5 && btcMaSpreadPct <= -1 && (coinMaSpreadPct <= -1 || breakVsAtrRatio >= 3);
485
+ return quality4 || strongReadyBreakoutQuality4 ? 4 : 3;
486
+ };
487
+ var getDeterministicTrendlineQualityReason = (trendlineContext) => {
488
+ if (trendlineContext.hardBlockReasons.length > 0) {
489
+ return `TrendLine guardrail: \u0432\u0445\u043E\u0434 \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D, \u043F\u043E\u0442\u043E\u043C\u0443 \u0447\u0442\u043E ${trendlineContext.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.`;
490
+ }
491
+ if (trendlineContext.signalDirection === "LONG") {
492
+ return "TrendLine deterministic quality: \u043F\u0440\u043E\u0431\u043E\u0439 \u0435\u0441\u0442\u044C, \u043D\u043E \u0434\u043B\u044F LONG \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 displacement, \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u043A\u0438 BTC \u0438\u043B\u0438 \u043B\u0438\u043D\u0438\u044F \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043B\u0438\u043D\u043D\u0430\u044F \u0434\u043B\u044F \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u0445\u043E\u0434\u0430.";
493
+ }
494
+ if (trendlineContext.signalDirection === "SHORT") {
495
+ return "TrendLine deterministic quality: \u043F\u0440\u043E\u0431\u043E\u0439 \u0435\u0441\u0442\u044C, \u043D\u043E \u0434\u043B\u044F SHORT \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 bearish displacement \u0438\u043B\u0438 follow-through, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u0432\u0445\u043E\u0434 \u043F\u043E\u043A\u0430 \u0440\u0430\u043D\u043E \u043E\u0434\u043E\u0431\u0440\u044F\u0442\u044C.";
496
+ }
497
+ return "TrendLine deterministic quality: \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0435\u0449\u0435 \u043D\u0435 \u0434\u043E\u0442\u044F\u0433\u0438\u0432\u0430\u0435\u0442 \u0434\u043E \u0432\u0445\u043E\u0434\u0430 \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441.";
498
+ };
499
+ var getTrendlineContextFromPayload = (payload, signal) => {
500
+ const additional = payload.additionalIndicators;
501
+ const fromPayload = additional?.trendlineContext;
502
+ return fromPayload ?? buildTrendlineContext(signal);
503
+ };
504
+ var trendLineAiAdapter = {
505
+ // Shared builder trims nested series/figures; TrendLine keeps trendline geometry untrimmed on purpose.
506
+ buildPayload: ({ signal, basePayload }) => ({
507
+ ...basePayload,
508
+ figures: {
509
+ ...basePayload.figures,
510
+ // Keep raw line geometry available exactly where the shared prompt expects it.
511
+ trendline: getTrendLineFromPayload(signal)
512
+ },
513
+ additionalIndicators: {
514
+ ...basePayload.additionalIndicators,
515
+ trendlineContext: buildTrendlineContext(signal)
516
+ }
517
+ }),
518
+ postProcessAnalysis: ({ signal, payload, analysis }) => {
519
+ const trendlineContext = getTrendlineContextFromPayload(payload, signal);
520
+ const quality = trendlineContext.deterministicQuality;
521
+ const signalDirection = signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
522
+ if ((trendlineContext.aggressivePreBreakPressure === true || trendlineContext.strongNearBreakPressure === true) && signalDirection != null) {
523
+ const fallbackReason = trendlineContext.strongNearBreakPressure === true ? "TrendLine strong near-break pressure: \u0437\u0440\u0435\u043B\u0430\u044F \u043B\u0438\u043D\u0438\u044F \u0443\u0436\u0435 \u043F\u0440\u043E\u0434\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0432 \u0441\u0442\u043E\u0440\u043E\u043D\u0443 \u0441\u0434\u0435\u043B\u043A\u0438, \u0440\u0430\u043D\u043D\u0438\u0439 \u0432\u0445\u043E\u0434 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D \u043A\u043E\u0434\u043E\u043C \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0438." : "TrendLine aggressive pre-break pressure: \u0440\u0430\u043D\u043D\u0435\u0435 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E \u043F\u0440\u0438 \u0441\u0438\u043B\u044C\u043D\u043E\u043C bearish pressure.";
524
+ const fallbackComment = trendlineContext.strongNearBreakPressure === true ? "TrendLine strong near-break pressure: \u0440\u0430\u043D\u043D\u0438\u0439 \u0432\u0445\u043E\u0434 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D \u043A\u043E\u0434\u043E\u043C \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0438." : "TrendLine aggressive pre-break pressure: \u0440\u0430\u043D\u043D\u0438\u0439 \u0432\u0445\u043E\u0434 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D \u043A\u043E\u0434\u043E\u043C \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0438.";
525
+ return {
526
+ ...analysis,
527
+ direction: signalDirection,
528
+ quality: 4,
529
+ needRetest: false,
530
+ retestPrice: null,
531
+ takeProfitPrice: analysis.takeProfitPrice ?? signal.prices?.takeProfitPrice ?? null,
532
+ stopLossPrice: analysis.stopLossPrice ?? signal.prices?.stopLossPrice ?? null,
533
+ qualityReason: mergeShortText(
534
+ analysis.qualityReason ?? "",
535
+ fallbackReason,
536
+ 400
537
+ ),
538
+ comment: mergeShortText(analysis.comment ?? "", fallbackComment, 1024)
539
+ };
540
+ }
541
+ if (trendlineContext.approvalAllowedNow === true && signalDirection != null) {
542
+ return {
543
+ ...analysis,
544
+ direction: signalDirection,
545
+ quality,
546
+ needRetest: false,
547
+ retestPrice: null,
548
+ takeProfitPrice: analysis.takeProfitPrice ?? signal.prices?.takeProfitPrice ?? null,
549
+ stopLossPrice: analysis.stopLossPrice ?? signal.prices?.stopLossPrice ?? null
550
+ };
551
+ }
552
+ const retestPrice = trendlineContext.currentLinePrice ?? analysis.retestPrice ?? null;
553
+ const qualityReason = mergeShortText(
554
+ getDeterministicTrendlineQualityReason(trendlineContext),
555
+ "TrendLine guardrail: \u0432\u0445\u043E\u0434 \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D \u0434\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B.",
556
+ 400
557
+ );
558
+ const triggerInvalidation = mergeShortText(
559
+ trendlineContext.hardBlockReasons.length > 0 ? `\u0416\u0434\u0430\u0442\u044C \u0447\u0438\u0441\u0442\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439/\u0440\u0435\u0442\u0435\u0441\u0442 \u043B\u0438\u043D\u0438\u0438 \u0438 \u0443\u0431\u0440\u0430\u0442\u044C \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u044B: ${trendlineContext.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "\u0416\u0434\u0430\u0442\u044C \u0431\u043E\u043B\u0435\u0435 \u0441\u0438\u043B\u044C\u043D\u044B\u0439 breakout/follow-through \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442 \u043B\u0438\u043D\u0438\u0438 \u0441 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435\u043C \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u0438 BTC.",
560
+ "\u0416\u0434\u0430\u0442\u044C \u0447\u0438\u0441\u0442\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439/\u0440\u0435\u0442\u0435\u0441\u0442 \u043B\u0438\u043D\u0438\u0438 \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u0438 BTC.",
561
+ 400
562
+ );
563
+ const comment = mergeShortText(
564
+ trendlineContext.hardBlockReasons.length > 0 ? `TrendLine guardrail \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043B \u0432\u0445\u043E\u0434: ${trendlineContext.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "TrendLine deterministic quality \u043E\u043F\u0443\u0441\u0442\u0438\u043B \u0432\u0445\u043E\u0434 \u0432 watch/reject \u0434\u043E \u043F\u043E\u044F\u0432\u043B\u0435\u043D\u0438\u044F \u0431\u043E\u043B\u0435\u0435 \u0441\u0438\u043B\u044C\u043D\u043E\u0439 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B.",
565
+ "TrendLine guardrail \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043B \u0432\u0445\u043E\u0434 \u0434\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B.",
566
+ 1024
567
+ );
568
+ return {
569
+ ...analysis,
570
+ direction: null,
571
+ quality,
572
+ needRetest: true,
573
+ retestPrice,
574
+ takeProfitPrice: null,
575
+ stopLossPrice: null,
576
+ setup: mergeShortText(
577
+ analysis.setup ?? "",
578
+ "\u0421\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u043E\u044F/\u0440\u0435\u0442\u0435\u0441\u0442\u0430 \u0442\u0440\u0435\u043D\u0434\u043E\u0432\u043E\u0439 \u0434\u043B\u044F \u0432\u0445\u043E\u0434\u0430.",
579
+ 400
580
+ ),
581
+ retestPlan: mergeShortText(
582
+ analysis.retestPlan ?? "",
583
+ "\u0416\u0434\u0430\u0442\u044C \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043A \u043B\u0438\u043D\u0438\u0438 \u0438 \u0440\u0435\u0430\u043A\u0446\u0438\u044E \u0432 \u0441\u0442\u043E\u0440\u043E\u043D\u0443 \u0441\u0434\u0435\u043B\u043A\u0438 \u043F\u0435\u0440\u0435\u0434 \u043D\u043E\u0432\u044B\u043C \u0432\u0445\u043E\u0434\u043E\u043C.",
584
+ 400
585
+ ),
586
+ qualityReason,
587
+ triggerInvalidation,
588
+ comment
589
+ };
590
+ },
591
+ buildSystemPromptAddon: () => `
592
+ ${TRENDLINE_CONTEXT_PROMPT}
593
+ ${TRENDLINE_PAYLOAD_PROMPT}
594
+ `,
595
+ buildHumanPromptAddon: ({ payload }) => {
596
+ const additional = payload.additionalIndicators;
597
+ const trendlineContext = additional?.trendlineContext;
598
+ if (!trendlineContext) {
599
+ return "";
600
+ }
601
+ return `
602
+
603
+ \u0414\u043E\u043F. \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 TrendLine:
604
+ - trendline.mode=${trendlineContext.mode ?? "n/a"}
605
+ - trendline.touches=${formatPromptNumber(trendlineContext.touches, 0)}
606
+ - trendline.distance=${formatPromptNumber(trendlineContext.distance, 0)}
607
+ - trendline.currentLinePrice=${formatPromptNumber(trendlineContext.currentLinePrice, 6)}
608
+ - trendline.currentPrice=${formatPromptNumber(trendlineContext.currentPrice, 6)}
609
+ - trendline.priceVsLinePct=${formatPromptNumber(trendlineContext.priceVsLinePct, 3)}%
610
+ - trendline.priceVsLineSide=${trendlineContext.priceVsLineSide ?? "n/a"}
611
+ - trendline.clearBreak=${String(trendlineContext.clearBreak)}
612
+ - trendline.nearLineNoise=${String(trendlineContext.nearLineNoise)}
613
+ - trendline.atrPct=${formatPromptNumber(trendlineContext.atrPct, 3)}%
614
+ - trendline.breakVsAtrRatio=${formatPromptNumber(trendlineContext.breakVsAtrRatio, 3)}
615
+ - trendline.aggressivePreBreakPressure=${String(trendlineContext.aggressivePreBreakPressure)}
616
+ - trendline.strongNearBreakPressure=${String(trendlineContext.strongNearBreakPressure)}
617
+ - trendline.weakCleanBreak=${String(trendlineContext.weakCleanBreak)}
618
+ - trendline.compressedCleanBreak=${String(trendlineContext.compressedCleanBreak)}
619
+ - trendline.weakBtcLedBreak=${String(trendlineContext.weakBtcLedBreak)}
620
+ - trendline.weakLongFarBreak=${String(trendlineContext.weakLongFarBreak)}
621
+ - trendline.entryTiming=${trendlineContext.entryTiming ?? "n/a"}
622
+ - trendline.deterministicQuality=${String(trendlineContext.deterministicQuality)}
623
+ - trendline.maxAllowedQuality=${String(trendlineContext.maxAllowedQuality)}
624
+ - trendline.approvalAllowedNow=${String(trendlineContext.approvalAllowedNow)}
625
+ - trendline.hardBlockReasons=${JSON.stringify(trendlineContext.hardBlockReasons)}
626
+ - coin.maFastLast=${formatPromptNumber(trendlineContext.coinMaFast, 6)}
627
+ - coin.maSlowLast=${formatPromptNumber(trendlineContext.coinMaSlow, 6)}
628
+ - coin.maBias=${trendlineContext.coinMaBias ?? "n/a"}
629
+ - coin.maSpreadPct=${formatPromptNumber(trendlineContext.coinMaSpreadPct, 3)}%
630
+ - coin.biasAligned=${String(trendlineContext.coinBiasAligned)}
631
+ - btc.maFastLast=${formatPromptNumber(trendlineContext.btcMaFast, 2)}
632
+ - btc.maSlowLast=${formatPromptNumber(trendlineContext.btcMaSlow, 2)}
633
+ - btc.maBias=${trendlineContext.btcMaBias ?? "n/a"}
634
+ - btc.maSpreadPct=${formatPromptNumber(trendlineContext.btcMaSpreadPct, 3)}%
635
+ - btc.biasAligned=${String(trendlineContext.btcBiasAligned)}
636
+
637
+ \u041F\u0440\u0430\u0432\u0438\u043B\u043E \u0438\u043D\u0442\u0435\u0440\u043F\u0440\u0435\u0442\u0430\u0446\u0438\u0438 \u0434\u043B\u044F TrendLine:
638
+ - SHORT \u043E\u0442 \u043B\u0438\u043D\u0438\u0438 lows \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u044F\u0432\u043D\u044B\u043C \u0443\u0445\u043E\u0434\u043E\u043C \u043D\u0438\u0436\u0435 \u043B\u0438\u043D\u0438\u0438 \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442\u043E\u043C \u0441\u043D\u0438\u0437\u0443 \u0441 \u043E\u0442\u0431\u043E\u0435\u043C.
639
+ - LONG \u043E\u0442 \u043B\u0438\u043D\u0438\u0438 highs \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u044F\u0432\u043D\u044B\u043C \u0443\u0445\u043E\u0434\u043E\u043C \u0432\u044B\u0448\u0435 \u043B\u0438\u043D\u0438\u0438 \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442\u043E\u043C \u0441\u0432\u0435\u0440\u0445\u0443 \u0441 \u043E\u0442\u0431\u043E\u0435\u043C.
640
+ - \u0415\u0441\u043B\u0438 trendline.nearLineNoise=true \u0438\u043B\u0438 biasAligned=false, \u043B\u0443\u0447\u0448\u0435 \u0432\u0435\u0440\u043D\u0443\u0442\u044C direction=null \u0438 quality 1-3, \u0447\u0435\u043C \u0441\u0447\u0438\u0442\u0430\u0442\u044C \u0441\u0438\u0433\u043D\u0430\u043B \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u044B\u043C \u0431\u0435\u0437 \u0437\u0430\u043F\u0430\u0441\u0430.
641
+ - \u0415\u0441\u043B\u0438 trendline.weakCleanBreak=true, \u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C, \u043D\u043E \u043E\u043D \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0441\u043B\u0430\u0431\u044B\u0439 \u043F\u043E displacement: \u043D\u0443\u0436\u0435\u043D follow-through \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442, \u0430 \u043D\u0435 quality 4-5.
642
+ - \u0415\u0441\u043B\u0438 trendline.compressedCleanBreak=true, \u043F\u0440\u043E\u0431\u043E\u0439 \u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E \u0435\u0441\u0442\u044C, \u043D\u043E \u043B\u0438\u043D\u0438\u044F \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u043A\u043E\u0440\u043E\u0442\u043A\u0430\u044F \u0438 \u0441\u0436\u0430\u0442\u0430\u044F \u043F\u043E\u0441\u043B\u0435 \u0441\u0435\u0440\u0438\u0438 \u0431\u043B\u0438\u0437\u043A\u0438\u0445 \u043A\u0430\u0441\u0430\u043D\u0438\u0439: \u043E\u0431\u044B\u0447\u043D\u043E \u0437\u0434\u0435\u0441\u044C \u043D\u0443\u0436\u0435\u043D follow-through \u0438\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442, \u0430 \u043D\u0435 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0441\u0438\u0433\u043D\u0430\u043B\u0430.
643
+ - \u0415\u0441\u043B\u0438 trendline.weakBtcLedBreak=true, \u0442\u0440\u0430\u043A\u0442\u0443\u0439 \u044D\u0442\u043E \u043A\u0430\u043A \u043C\u0435\u043B\u043A\u0438\u0439 \u043F\u0440\u043E\u0431\u043E\u0439, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0441\u0438\u043B\u044C\u043D\u0435\u0435 \u0442\u044F\u043D\u0435\u0442 BTC, \u0447\u0435\u043C \u0441\u0430\u043C\u0430 \u043C\u043E\u043D\u0435\u0442\u0430: \u0437\u0434\u0435\u0441\u044C \u043E\u0431\u044B\u0447\u043D\u043E \u043D\u0443\u0436\u0435\u043D \u0440\u0435\u0442\u0435\u0441\u0442 \u0438 quality 1-3.
644
+ - \u0415\u0441\u043B\u0438 clearBreak=false \u0438\u043B\u0438 \u043B\u044E\u0431\u043E\u0439 alignment=false, \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0439 quality \u0432\u044B\u0448\u0435 3.
645
+ - \u0415\u0441\u043B\u0438 trendline.aggressivePreBreakPressure=true, \u043C\u043E\u0436\u043D\u043E \u0440\u0430\u0441\u0441\u043C\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044C \u0440\u0430\u043D\u043D\u0438\u0439 SHORT \u0434\u043E \u044F\u0432\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u043E\u044F, \u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u0430\u043A \u0438\u0441\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435: quality \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C 4 \u0438 \u044F\u0432\u043D\u043E\u0435 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435, \u0447\u0442\u043E \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u043E\u043A\u0430 \u0430\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u043E\u0435.
646
+ - \u0415\u0441\u043B\u0438 trendline.strongNearBreakPressure=true, \u043C\u043E\u0436\u043D\u043E \u0440\u0430\u0441\u0441\u043C\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044C \u0440\u0430\u043D\u043D\u0438\u0439 SHORT \u043F\u0440\u0438 \u0441\u0438\u043B\u044C\u043D\u043E\u043C \u0434\u0430\u0432\u043B\u0435\u043D\u0438\u0438 \u0443\u0436\u0435 \u043F\u043E \u043D\u0443\u0436\u043D\u0443\u044E \u0441\u0442\u043E\u0440\u043E\u043D\u0443 \u043B\u0438\u043D\u0438\u0438, \u0434\u0430\u0436\u0435 \u0435\u0441\u043B\u0438 \u043F\u0440\u043E\u0431\u043E\u0439 \u0435\u0449\u0435 \u043D\u0435 \u0434\u043E\u0442\u044F\u0433\u0438\u0432\u0430\u0435\u0442 \u0434\u043E clearBreak-\u043F\u043E\u0440\u043E\u0433\u0430: quality \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C 4.
647
+ - \u0421\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044F \u0434\u0435\u0442\u0435\u0440\u043C\u0438\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0443\u0435\u0442 \u0438\u0442\u043E\u0433\u043E\u0432\u044B\u0439 quality \u0434\u043E trendline.deterministicQuality; \u0442\u0432\u043E\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u2014 \u043E\u0431\u044A\u044F\u0441\u043D\u0438\u0442\u044C \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0432 \u044D\u0442\u0438\u0445 \u0440\u0430\u043C\u043A\u0430\u0445, \u0430 \u043D\u0435 \u0441\u043F\u043E\u0440\u0438\u0442\u044C \u0441 tier.
648
+ - \u0415\u0441\u043B\u0438 trendline.approvalAllowedNow=false, \u043D\u0435 \u043E\u043F\u0438\u0441\u044B\u0432\u0430\u0439 \u044D\u0442\u043E \u043A\u0430\u043A \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u044B\u0439 \u0441\u0438\u0433\u043D\u0430\u043B \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441: \u043E\u0431\u044A\u044F\u0441\u043D\u044F\u0439, \u0447\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 \u0434\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F.
649
+ `;
650
+ },
651
+ mapEntryRuntimeFromConfig: (config2) => mapAiRuntimeFromConfig(
652
+ config2
653
+ )
654
+ };
655
+
656
+ // src/TrendLine/adapters/ml.ts
657
+ import { mapMlRuntimeFromConfig } from "@tradejs/core/strategies";
658
+ var toTrendLineMlStrategyConfig = (input) => {
659
+ if (!input) return void 0;
660
+ return {
661
+ ...input,
662
+ TRENDLINE_CONFIG: input.TRENDLINE_CONFIG ?? input.TRENDLINE ?? {}
663
+ };
664
+ };
665
+ var trendLineMlAdapter = {
666
+ normalizeSignal: (signal) => {
667
+ const nextSignal = {
668
+ ...signal,
669
+ indicators: {
670
+ ...signal.indicators ?? {}
671
+ }
672
+ };
673
+ const additional = signal.additionalIndicators ?? {};
674
+ if (nextSignal.indicators.touches == null && additional.touches != null) {
675
+ nextSignal.indicators.touches = additional.touches;
676
+ }
677
+ if (nextSignal.indicators.distance == null && additional.distance != null) {
678
+ nextSignal.indicators.distance = additional.distance;
679
+ }
680
+ return nextSignal;
681
+ },
682
+ normalizeStrategyConfig: (strategyConfig) => {
683
+ return toTrendLineMlStrategyConfig(strategyConfig);
684
+ },
685
+ mapEntryRuntimeFromConfig: (config2) => mapMlRuntimeFromConfig(config2, {
686
+ strategyConfig: toTrendLineMlStrategyConfig(
687
+ config2
688
+ )
689
+ })
690
+ };
691
+
692
+ // src/TrendLine/hooks.ts
693
+ import { createCloseOppositeBeforePlaceOrderHook } from "@tradejs/node/strategies";
694
+ var trendLineBeforePlaceOrderHook = createCloseOppositeBeforePlaceOrderHook({
695
+ isEnabled: (config2) => Boolean(config2.CLOSE_OPPOSITE_POSITIONS)
696
+ });
697
+
698
+ // src/TrendLine/manifest.ts
699
+ var trendLineManifest = {
700
+ name: "TrendLine",
701
+ hooks: {
702
+ beforePlaceOrder: trendLineBeforePlaceOrderHook
703
+ },
704
+ aiAdapter: trendLineAiAdapter,
705
+ mlAdapter: trendLineMlAdapter
706
+ };
707
+
708
+ // src/TrendLine/config.ts
709
+ var config = {
710
+ ENV: "BACKTEST",
711
+ INTERVAL: "15",
712
+ MAKE_ORDERS: true,
713
+ CLOSE_OPPOSITE_POSITIONS: false,
714
+ BACKTEST_PRICE_MODE: "mid",
715
+ AI_ENABLED: false,
716
+ ML_ENABLED: false,
717
+ ML_THRESHOLD: 0.1,
718
+ MIN_AI_QUALITY: 3,
719
+ FEE_PERCENT: 5e-3,
720
+ MAX_LOSS_VALUE: 10,
721
+ MA_FAST: 14,
722
+ MA_MEDIUM: 49,
723
+ MA_SLOW: 50,
724
+ OBV_SMA: 10,
725
+ ATR: 14,
726
+ ATR_PCT_SHORT: 7,
727
+ ATR_PCT_LONG: 30,
728
+ BB: 20,
729
+ BB_STD: 2,
730
+ MACD_FAST: 12,
731
+ MACD_SLOW: 26,
732
+ MACD_SIGNAL: 9,
733
+ LEVEL_LOOKBACK: 20,
734
+ LEVEL_DELAY: 2,
735
+ TRENDLINE: {
736
+ minTouches: 4,
737
+ offset: 3,
738
+ epsilon: 3e-3,
739
+ epsilonOffset: 4e-3
740
+ },
741
+ HIGHS: {
742
+ enable: true,
743
+ direction: "LONG",
744
+ TP: 4,
745
+ SL: 1.3,
746
+ minRiskRatio: 2
747
+ },
748
+ LOWS: {
749
+ enable: true,
750
+ direction: "SHORT",
751
+ TP: 4,
752
+ SL: 1.3,
753
+ minRiskRatio: 2
754
+ }
755
+ };
756
+
757
+ export {
758
+ buildTrendlineStructuralContext,
759
+ buildTrendlineTimingContext,
760
+ trendLineManifest,
761
+ config
762
+ };