@tradejs/strategies 1.0.5 → 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.
- package/dist/chunk-GNQJ5TVU.mjs +687 -0
- package/dist/chunk-H2TU2YMA.mjs +762 -0
- package/dist/chunk-MOBKL73M.mjs +405 -0
- package/dist/chunk-XMRB45ZO.mjs +789 -0
- package/dist/index.d.mts +43 -3
- package/dist/index.d.ts +43 -3
- package/dist/index.js +4043 -468
- package/dist/index.mjs +21 -7
- package/dist/strategy-6TS2NFSC.mjs +736 -0
- package/dist/strategy-AFIGEHDS.mjs +418 -0
- package/dist/{strategy-ZVNTA6UR.mjs → strategy-FYNNJDOH.mjs} +1 -13
- package/dist/strategy-LC2FSFVN.mjs +470 -0
- package/dist/{strategy-UHRWSGZC.mjs → strategy-OI4WRB3S.mjs} +51 -41
- package/package.json +5 -5
- package/dist/chunk-MVIV5ZII.mjs +0 -137
- package/dist/chunk-RYEPHOGL.mjs +0 -28
- package/dist/chunk-ULLCAH5C.mjs +0 -67
- package/dist/strategy-M3BRWDRR.mjs +0 -377
- package/dist/strategy-UZBWST3G.mjs +0 -156
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildReverseTrendlineStructuralContext,
|
|
3
|
+
buildReverseTrendlineTimingContext,
|
|
4
|
+
config,
|
|
5
|
+
reverseTrendLineManifest,
|
|
6
|
+
toFiniteNumberOrNull
|
|
7
|
+
} from "./chunk-GNQJ5TVU.mjs";
|
|
8
|
+
import "./chunk-HEBXNMVQ.mjs";
|
|
9
|
+
|
|
10
|
+
// src/ReverseTrendLine/strategy.ts
|
|
11
|
+
import { createStrategyRuntime } from "@tradejs/node/strategies";
|
|
12
|
+
|
|
13
|
+
// src/ReverseTrendLine/core.ts
|
|
14
|
+
import { round as round2 } from "@tradejs/core/math";
|
|
15
|
+
import { createTrendlineEngine } from "@tradejs/core/indicators";
|
|
16
|
+
|
|
17
|
+
// src/ReverseTrendLine/filters.ts
|
|
18
|
+
import { diffRel } from "@tradejs/core/math";
|
|
19
|
+
var MAX_CANDLE_VOLATILITY = 0.025;
|
|
20
|
+
var filterByVeryVolatility = (data) => {
|
|
21
|
+
const lastCandle = data[data.length - 1];
|
|
22
|
+
const prevCandle = data[data.length - 2];
|
|
23
|
+
if (!lastCandle || !prevCandle) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const isVeryVolatility = diffRel(lastCandle.low, lastCandle.high) > MAX_CANDLE_VOLATILITY || diffRel(prevCandle.low, prevCandle.high) > MAX_CANDLE_VOLATILITY;
|
|
27
|
+
return !isVeryVolatility;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/ReverseTrendLine/figures.ts
|
|
31
|
+
var buildReverseTrendLineFigures = (bestLine) => ({
|
|
32
|
+
lines: [
|
|
33
|
+
{
|
|
34
|
+
id: bestLine.id,
|
|
35
|
+
kind: "trendline",
|
|
36
|
+
points: [...bestLine.points ?? []].sort(
|
|
37
|
+
(left, right) => left.timestamp - right.timestamp
|
|
38
|
+
),
|
|
39
|
+
color: bestLine.mode === "lows" ? "#22c55e" : "#f97316",
|
|
40
|
+
width: 2,
|
|
41
|
+
style: "solid"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
points: [
|
|
45
|
+
{
|
|
46
|
+
id: `${bestLine.id}-points`,
|
|
47
|
+
kind: "trendline_points",
|
|
48
|
+
points: [...bestLine.points ?? [], ...bestLine.touches ?? []].sort(
|
|
49
|
+
(left, right) => left.timestamp - right.timestamp
|
|
50
|
+
),
|
|
51
|
+
color: "#ef4444",
|
|
52
|
+
radius: 4
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// src/ReverseTrendLine/risk.ts
|
|
58
|
+
import { round } from "@tradejs/core/math";
|
|
59
|
+
var MIN_STOP_BUFFER_PCT = 0.1;
|
|
60
|
+
var LINE_BUFFER_ATR_FACTOR = 0.25;
|
|
61
|
+
var LINE_BUFFER_BASE_SL_FACTOR = 0.1;
|
|
62
|
+
var ATR_STOP_FLOOR_FACTOR = 0.65;
|
|
63
|
+
var MIN_STOP_LOSS_FACTOR = 0.8;
|
|
64
|
+
var MAX_STOP_LOSS_FACTOR = 2;
|
|
65
|
+
var clampNumber = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
66
|
+
var getTimingStopFactor = (entryTiming) => {
|
|
67
|
+
if (entryTiming === "ready_follow_through") {
|
|
68
|
+
return 0.95;
|
|
69
|
+
}
|
|
70
|
+
return 1;
|
|
71
|
+
};
|
|
72
|
+
var getTimingTargetRiskRatio = ({
|
|
73
|
+
direction,
|
|
74
|
+
entryTiming
|
|
75
|
+
}) => {
|
|
76
|
+
if (direction === "LONG") {
|
|
77
|
+
return entryTiming === "ready_follow_through" ? 2.15 : 1.95;
|
|
78
|
+
}
|
|
79
|
+
return entryTiming === "ready_follow_through" ? 2.2 : 2;
|
|
80
|
+
};
|
|
81
|
+
var buildReverseTrendlineRiskPlan = ({
|
|
82
|
+
direction,
|
|
83
|
+
modeConfig,
|
|
84
|
+
structuralContext,
|
|
85
|
+
timingContext
|
|
86
|
+
}) => {
|
|
87
|
+
const baseStopLossDelta = modeConfig.SL;
|
|
88
|
+
const atrPct = structuralContext.atrPct ?? baseStopLossDelta;
|
|
89
|
+
const priceVsLinePctAbs = structuralContext.priceVsLinePctAbs ?? 0;
|
|
90
|
+
const rejectionStrengthPct = structuralContext.rejectionStrengthPct ?? 0;
|
|
91
|
+
const touches = structuralContext.touches ?? 0;
|
|
92
|
+
const distance = structuralContext.distance ?? null;
|
|
93
|
+
const lineBufferPct = Math.max(
|
|
94
|
+
atrPct * LINE_BUFFER_ATR_FACTOR,
|
|
95
|
+
baseStopLossDelta * LINE_BUFFER_BASE_SL_FACTOR,
|
|
96
|
+
MIN_STOP_BUFFER_PCT
|
|
97
|
+
);
|
|
98
|
+
const lineInvalidationPct = priceVsLinePctAbs + lineBufferPct;
|
|
99
|
+
const volatilityFloorPct = Math.max(
|
|
100
|
+
atrPct * ATR_STOP_FLOOR_FACTOR,
|
|
101
|
+
baseStopLossDelta * MIN_STOP_LOSS_FACTOR
|
|
102
|
+
);
|
|
103
|
+
let stopLossDelta = Math.max(lineInvalidationPct, volatilityFloorPct);
|
|
104
|
+
if (touches >= 6) {
|
|
105
|
+
stopLossDelta *= 0.95;
|
|
106
|
+
} else if (touches > 0 && touches <= 4) {
|
|
107
|
+
stopLossDelta *= 1.03;
|
|
108
|
+
}
|
|
109
|
+
if (distance != null && distance >= 400) {
|
|
110
|
+
stopLossDelta *= 1.05;
|
|
111
|
+
} else if (distance != null && distance <= 120) {
|
|
112
|
+
stopLossDelta *= 0.95;
|
|
113
|
+
}
|
|
114
|
+
if (rejectionStrengthPct >= 0.2) {
|
|
115
|
+
stopLossDelta *= 0.95;
|
|
116
|
+
}
|
|
117
|
+
stopLossDelta *= getTimingStopFactor(timingContext.entryTiming);
|
|
118
|
+
stopLossDelta = clampNumber(
|
|
119
|
+
stopLossDelta,
|
|
120
|
+
baseStopLossDelta * MIN_STOP_LOSS_FACTOR,
|
|
121
|
+
baseStopLossDelta * MAX_STOP_LOSS_FACTOR
|
|
122
|
+
);
|
|
123
|
+
let targetRiskRatio = getTimingTargetRiskRatio({
|
|
124
|
+
direction,
|
|
125
|
+
entryTiming: timingContext.entryTiming
|
|
126
|
+
});
|
|
127
|
+
if (touches >= 6) {
|
|
128
|
+
targetRiskRatio += 0.1;
|
|
129
|
+
}
|
|
130
|
+
if (distance != null && distance >= 120 && distance <= 350) {
|
|
131
|
+
targetRiskRatio += 0.05;
|
|
132
|
+
}
|
|
133
|
+
if (direction === "SHORT" && distance != null && distance > 500) {
|
|
134
|
+
targetRiskRatio -= 0.15;
|
|
135
|
+
}
|
|
136
|
+
if (direction === "LONG" && distance != null && distance > 500) {
|
|
137
|
+
targetRiskRatio -= 0.1;
|
|
138
|
+
}
|
|
139
|
+
const minTargetRiskRatio = modeConfig.minRiskRatio + 0.05;
|
|
140
|
+
const maxTargetRiskRatio = Math.max(modeConfig.TP / modeConfig.SL, minTargetRiskRatio) + 0.3;
|
|
141
|
+
targetRiskRatio = clampNumber(
|
|
142
|
+
targetRiskRatio,
|
|
143
|
+
minTargetRiskRatio,
|
|
144
|
+
maxTargetRiskRatio
|
|
145
|
+
);
|
|
146
|
+
return {
|
|
147
|
+
stopLossDelta: round(stopLossDelta, 3),
|
|
148
|
+
targetRiskRatio: round(targetRiskRatio, 2),
|
|
149
|
+
takeProfitDelta: round(stopLossDelta * targetRiskRatio, 3)
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/ReverseTrendLine/core.ts
|
|
154
|
+
var BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
|
|
155
|
+
var buildReverseTrendlineSignalSeed = ({
|
|
156
|
+
direction,
|
|
157
|
+
currentPrice,
|
|
158
|
+
indicators,
|
|
159
|
+
bestLine,
|
|
160
|
+
currentCandle,
|
|
161
|
+
reverseTrendlineTiming
|
|
162
|
+
}) => ({
|
|
163
|
+
direction,
|
|
164
|
+
prices: { currentPrice },
|
|
165
|
+
indicators,
|
|
166
|
+
additionalIndicators: {
|
|
167
|
+
touches: Array.isArray(bestLine.touches) ? bestLine.touches.length + 2 : 2,
|
|
168
|
+
distance: bestLine.distance,
|
|
169
|
+
trendLine: bestLine,
|
|
170
|
+
...currentCandle ? { currentCandle } : {},
|
|
171
|
+
...reverseTrendlineTiming ? { reverseTrendlineTiming } : {}
|
|
172
|
+
},
|
|
173
|
+
figures: {
|
|
174
|
+
trendLine: bestLine
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
var isOpenPosition = (position) => Boolean(
|
|
178
|
+
position && typeof position.price === "number" && Number.isFinite(position.price) && typeof position.qty === "number" && Number.isFinite(position.qty) && position.qty > 0 && (position.direction === "LONG" || position.direction === "SHORT")
|
|
179
|
+
);
|
|
180
|
+
var getFavorableMovePct = ({
|
|
181
|
+
direction,
|
|
182
|
+
entryPrice,
|
|
183
|
+
currentPrice
|
|
184
|
+
}) => {
|
|
185
|
+
if (!Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || entryPrice <= 0) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return direction === "LONG" ? (currentPrice - entryPrice) / entryPrice * 100 : (entryPrice - currentPrice) / entryPrice * 100;
|
|
189
|
+
};
|
|
190
|
+
var getPositionStopLossPrice = (position) => {
|
|
191
|
+
if (!position || typeof position !== "object") {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const slPrice = Number(
|
|
195
|
+
position.slPrice ?? Number.NaN
|
|
196
|
+
);
|
|
197
|
+
if (Number.isFinite(slPrice)) {
|
|
198
|
+
return slPrice;
|
|
199
|
+
}
|
|
200
|
+
const signalStopLossPrice = Number(
|
|
201
|
+
position.signal?.prices?.stopLossPrice ?? Number.NaN
|
|
202
|
+
);
|
|
203
|
+
return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
|
|
204
|
+
};
|
|
205
|
+
var getPositionRiskPct = ({
|
|
206
|
+
direction,
|
|
207
|
+
entryPrice,
|
|
208
|
+
stopLossPrice
|
|
209
|
+
}) => {
|
|
210
|
+
if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice) || entryPrice <= 0) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return direction === "LONG" ? (entryPrice - stopLossPrice) / entryPrice * 100 : (stopLossPrice - entryPrice) / entryPrice * 100;
|
|
214
|
+
};
|
|
215
|
+
var isBreakEvenStopAlreadyApplied = ({
|
|
216
|
+
direction,
|
|
217
|
+
entryPrice,
|
|
218
|
+
stopLossPrice
|
|
219
|
+
}) => {
|
|
220
|
+
if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
return direction === "LONG" ? stopLossPrice >= entryPrice : stopLossPrice <= entryPrice;
|
|
224
|
+
};
|
|
225
|
+
var getLinePriceAtNow = (line, timestamp) => {
|
|
226
|
+
if (!line || !Array.isArray(line.points) || line.points.length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const sortedPoints = [...line.points].sort(
|
|
230
|
+
(left, right) => left.timestamp - right.timestamp
|
|
231
|
+
);
|
|
232
|
+
const first = sortedPoints[0];
|
|
233
|
+
const last = sortedPoints[sortedPoints.length - 1];
|
|
234
|
+
if (first.timestamp === last.timestamp) {
|
|
235
|
+
return last.value;
|
|
236
|
+
}
|
|
237
|
+
const slope = (last.value - first.value) / (last.timestamp - first.timestamp);
|
|
238
|
+
return first.value + slope * (timestamp - first.timestamp);
|
|
239
|
+
};
|
|
240
|
+
var buildReverseTrendlineCandidateContext = ({
|
|
241
|
+
line,
|
|
242
|
+
candle,
|
|
243
|
+
direction
|
|
244
|
+
}) => {
|
|
245
|
+
const currentLinePrice = getLinePriceAtNow(line, candle.timestamp);
|
|
246
|
+
const priceVsLinePct = currentLinePrice != null && currentLinePrice !== 0 ? (candle.close - currentLinePrice) / currentLinePrice * 100 : null;
|
|
247
|
+
const priceVsLinePctAbs = priceVsLinePct == null ? null : Math.abs(priceVsLinePct);
|
|
248
|
+
const lineTouchedNow = currentLinePrice != null && candle.low <= currentLinePrice && candle.high >= currentLinePrice;
|
|
249
|
+
const failedBounceBreak = direction === "LONG" ? priceVsLinePct != null && priceVsLinePct <= -0.35 : priceVsLinePct != null && priceVsLinePct >= 0.35;
|
|
250
|
+
return {
|
|
251
|
+
currentLinePrice,
|
|
252
|
+
priceVsLinePctAbs,
|
|
253
|
+
lineTouchedNow,
|
|
254
|
+
failedBounceBreak,
|
|
255
|
+
distance: toFiniteNumberOrNull(line.distance)
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
var pickBestCandidateLine = ({
|
|
259
|
+
candle,
|
|
260
|
+
lines
|
|
261
|
+
}) => {
|
|
262
|
+
const ranked = lines.map(({ line, direction }) => {
|
|
263
|
+
const candidateContext = buildReverseTrendlineCandidateContext({
|
|
264
|
+
line,
|
|
265
|
+
candle,
|
|
266
|
+
direction
|
|
267
|
+
});
|
|
268
|
+
return { line, direction, candidateContext };
|
|
269
|
+
}).filter(({ candidateContext }) => candidateContext.currentLinePrice != null).sort((left, right) => {
|
|
270
|
+
const leftTouchRank = left.candidateContext.lineTouchedNow ? 0 : 1;
|
|
271
|
+
const rightTouchRank = right.candidateContext.lineTouchedNow ? 0 : 1;
|
|
272
|
+
if (leftTouchRank !== rightTouchRank) {
|
|
273
|
+
return leftTouchRank - rightTouchRank;
|
|
274
|
+
}
|
|
275
|
+
const leftDistance = left.candidateContext.priceVsLinePctAbs ?? Number.POSITIVE_INFINITY;
|
|
276
|
+
const rightDistance = right.candidateContext.priceVsLinePctAbs ?? Number.POSITIVE_INFINITY;
|
|
277
|
+
if (leftDistance !== rightDistance) {
|
|
278
|
+
return leftDistance - rightDistance;
|
|
279
|
+
}
|
|
280
|
+
return (left.candidateContext.distance ?? Number.POSITIVE_INFINITY) - (right.candidateContext.distance ?? Number.POSITIVE_INFINITY);
|
|
281
|
+
});
|
|
282
|
+
return ranked[0] ?? null;
|
|
283
|
+
};
|
|
284
|
+
var createReverseTrendLineCore = async ({ config: config2, data: cachedData, strategyApi, indicatorsState }) => {
|
|
285
|
+
const { TRENDLINE, FEE_PERCENT, MAX_LOSS_VALUE, HIGHS, LOWS } = config2;
|
|
286
|
+
const lastTradeController = strategyApi.createLastTradeController();
|
|
287
|
+
const trendlineOptions = {
|
|
288
|
+
bestLines: 1,
|
|
289
|
+
capture: true,
|
|
290
|
+
...TRENDLINE
|
|
291
|
+
};
|
|
292
|
+
const getLowsTrendlines = createTrendlineEngine(cachedData, {
|
|
293
|
+
mode: "lows",
|
|
294
|
+
...trendlineOptions
|
|
295
|
+
});
|
|
296
|
+
const getHighsTrendlines = createTrendlineEngine(cachedData, {
|
|
297
|
+
mode: "highs",
|
|
298
|
+
...trendlineOptions
|
|
299
|
+
});
|
|
300
|
+
return async (candle) => {
|
|
301
|
+
const lowsTrendlines = getLowsTrendlines.next(candle);
|
|
302
|
+
const highsTrendlines = getHighsTrendlines.next(candle);
|
|
303
|
+
indicatorsState.onBar();
|
|
304
|
+
const currentPosition = await strategyApi.getCurrentPosition();
|
|
305
|
+
if (isOpenPosition(currentPosition)) {
|
|
306
|
+
const activeLine = currentPosition.direction === "LONG" ? lowsTrendlines[0] : highsTrendlines[0];
|
|
307
|
+
const activeModeConfig = currentPosition.direction === "LONG" ? LOWS : HIGHS;
|
|
308
|
+
const activeLinePrice = getLinePriceAtNow(
|
|
309
|
+
activeLine ?? null,
|
|
310
|
+
candle.timestamp
|
|
311
|
+
);
|
|
312
|
+
const priceVsLinePct = activeLinePrice != null && activeLinePrice !== 0 ? (candle.close - activeLinePrice) / activeLinePrice * 100 : null;
|
|
313
|
+
const failedBounceBreak = currentPosition.direction === "LONG" ? priceVsLinePct != null && priceVsLinePct <= -0.35 : priceVsLinePct != null && priceVsLinePct >= 0.35;
|
|
314
|
+
if (failedBounceBreak) {
|
|
315
|
+
return strategyApi.exit({
|
|
316
|
+
code: "REVERSE_TRENDLINE_FAILED_BOUNCE_EXIT",
|
|
317
|
+
direction: currentPosition.direction
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const favorableMovePct = getFavorableMovePct({
|
|
321
|
+
direction: currentPosition.direction,
|
|
322
|
+
entryPrice: currentPosition.price,
|
|
323
|
+
currentPrice: candle.close
|
|
324
|
+
});
|
|
325
|
+
const currentStopLossPrice = getPositionStopLossPrice(currentPosition);
|
|
326
|
+
const currentPositionRiskPct = getPositionRiskPct({
|
|
327
|
+
direction: currentPosition.direction,
|
|
328
|
+
entryPrice: currentPosition.price,
|
|
329
|
+
stopLossPrice: currentStopLossPrice
|
|
330
|
+
});
|
|
331
|
+
if (!isBreakEvenStopAlreadyApplied({
|
|
332
|
+
direction: currentPosition.direction,
|
|
333
|
+
entryPrice: currentPosition.price,
|
|
334
|
+
stopLossPrice: currentStopLossPrice
|
|
335
|
+
}) && favorableMovePct != null && favorableMovePct >= (currentPositionRiskPct ?? activeModeConfig.SL) * BREAK_EVEN_TRIGGER_RISK_MULTIPLIER) {
|
|
336
|
+
return strategyApi.protect({
|
|
337
|
+
code: "REVERSE_TRENDLINE_MOVE_STOP_TO_BREAK_EVEN",
|
|
338
|
+
protectPlan: {
|
|
339
|
+
direction: currentPosition.direction,
|
|
340
|
+
stopLossPrice: currentPosition.price
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return strategyApi.skip("POSITION_EXISTS");
|
|
345
|
+
}
|
|
346
|
+
if (lastTradeController.isInCooldown(candle.timestamp)) {
|
|
347
|
+
return strategyApi.skip("DEV_TRADE_COOLDOWN");
|
|
348
|
+
}
|
|
349
|
+
const candidates = [];
|
|
350
|
+
if (LOWS.enable && lowsTrendlines.length > 0) {
|
|
351
|
+
candidates.push({ line: lowsTrendlines[0], direction: LOWS.direction });
|
|
352
|
+
}
|
|
353
|
+
if (HIGHS.enable && highsTrendlines.length > 0) {
|
|
354
|
+
candidates.push({ line: highsTrendlines[0], direction: HIGHS.direction });
|
|
355
|
+
}
|
|
356
|
+
if (candidates.length === 0) {
|
|
357
|
+
return strategyApi.skip("NO_TRENDLINE");
|
|
358
|
+
}
|
|
359
|
+
const bestCandidate = pickBestCandidateLine({
|
|
360
|
+
candle: {
|
|
361
|
+
timestamp: candle.timestamp,
|
|
362
|
+
open: candle.open,
|
|
363
|
+
close: candle.close,
|
|
364
|
+
high: candle.high,
|
|
365
|
+
low: candle.low
|
|
366
|
+
},
|
|
367
|
+
lines: candidates
|
|
368
|
+
});
|
|
369
|
+
if (!bestCandidate) {
|
|
370
|
+
return strategyApi.skip("NO_TRENDLINE");
|
|
371
|
+
}
|
|
372
|
+
const { line: bestLine, direction, candidateContext } = bestCandidate;
|
|
373
|
+
const modeConfig = direction === "LONG" ? LOWS : HIGHS;
|
|
374
|
+
const { minRiskRatio } = modeConfig;
|
|
375
|
+
if (candidateContext.failedBounceBreak) {
|
|
376
|
+
return strategyApi.skip(
|
|
377
|
+
"REVERSE_TRENDLINE_STRUCTURE:failed_bounce_break"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const { fullData, timestamp, currentPrice } = await strategyApi.getMarketData();
|
|
381
|
+
if (!filterByVeryVolatility(fullData)) {
|
|
382
|
+
return strategyApi.skip("VERY_VOLATILITY");
|
|
383
|
+
}
|
|
384
|
+
const indicators = indicatorsState.snapshot();
|
|
385
|
+
const signalSeed = buildReverseTrendlineSignalSeed({
|
|
386
|
+
direction,
|
|
387
|
+
currentPrice,
|
|
388
|
+
indicators,
|
|
389
|
+
bestLine,
|
|
390
|
+
currentCandle: {
|
|
391
|
+
timestamp: candle.timestamp,
|
|
392
|
+
open: candle.open,
|
|
393
|
+
close: candle.close,
|
|
394
|
+
high: candle.high,
|
|
395
|
+
low: candle.low
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
const structuralContext = buildReverseTrendlineStructuralContext(signalSeed);
|
|
399
|
+
const timingContext = buildReverseTrendlineTimingContext({
|
|
400
|
+
signal: signalSeed,
|
|
401
|
+
candles: fullData,
|
|
402
|
+
structuralContext
|
|
403
|
+
});
|
|
404
|
+
if (!timingContext.entryReadyNow) {
|
|
405
|
+
const timingCode = timingContext.entryTiming === "stale_reaction" ? "STALE_REACTION" : timingContext.entryTiming === "wait_reaction_confirmation" ? "WAIT_REACTION_CONFIRMATION" : "WAIT_TOUCH";
|
|
406
|
+
return strategyApi.skip(`REVERSE_TRENDLINE_TIMING:${timingCode}`);
|
|
407
|
+
}
|
|
408
|
+
const riskPlan = buildReverseTrendlineRiskPlan({
|
|
409
|
+
direction,
|
|
410
|
+
modeConfig,
|
|
411
|
+
structuralContext,
|
|
412
|
+
timingContext
|
|
413
|
+
});
|
|
414
|
+
const { stopLossPrice, takeProfitPrice, riskRatio, qty } = strategyApi.getDirectionalTpSlPrices({
|
|
415
|
+
price: currentPrice,
|
|
416
|
+
direction,
|
|
417
|
+
takeProfitDelta: riskPlan.takeProfitDelta,
|
|
418
|
+
stopLossDelta: riskPlan.stopLossDelta,
|
|
419
|
+
unit: "percent",
|
|
420
|
+
maxLossValue: MAX_LOSS_VALUE,
|
|
421
|
+
feePercent: Number(FEE_PERCENT ?? 0)
|
|
422
|
+
});
|
|
423
|
+
if (!qty || !Number.isFinite(qty) || qty <= 0) {
|
|
424
|
+
return strategyApi.skip("INVALID_QTY");
|
|
425
|
+
}
|
|
426
|
+
if (riskRatio <= minRiskRatio) {
|
|
427
|
+
return strategyApi.skip(`RISK_RATIO:${round2(riskRatio)}`);
|
|
428
|
+
}
|
|
429
|
+
lastTradeController.markTrade(timestamp);
|
|
430
|
+
return strategyApi.entry({
|
|
431
|
+
code: "REVERSE_TRENDLINE_SIGNAL",
|
|
432
|
+
figures: {
|
|
433
|
+
...buildReverseTrendLineFigures(bestLine)
|
|
434
|
+
},
|
|
435
|
+
direction,
|
|
436
|
+
indicators,
|
|
437
|
+
additionalIndicators: buildReverseTrendlineSignalSeed({
|
|
438
|
+
direction,
|
|
439
|
+
currentPrice,
|
|
440
|
+
indicators,
|
|
441
|
+
bestLine,
|
|
442
|
+
currentCandle: {
|
|
443
|
+
timestamp: candle.timestamp,
|
|
444
|
+
open: candle.open,
|
|
445
|
+
close: candle.close,
|
|
446
|
+
high: candle.high,
|
|
447
|
+
low: candle.low
|
|
448
|
+
},
|
|
449
|
+
reverseTrendlineTiming: timingContext
|
|
450
|
+
}).additionalIndicators,
|
|
451
|
+
orderPlan: {
|
|
452
|
+
qty,
|
|
453
|
+
stopLossPrice,
|
|
454
|
+
takeProfits: [{ rate: 1, price: takeProfitPrice }]
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// src/ReverseTrendLine/strategy.ts
|
|
461
|
+
var ReverseTrendLineStrategyCreator = createStrategyRuntime({
|
|
462
|
+
strategyName: "ReverseTrendLine",
|
|
463
|
+
defaults: config,
|
|
464
|
+
createCore: createReverseTrendLineCore,
|
|
465
|
+
manifest: reverseTrendLineManifest,
|
|
466
|
+
strategyDirectory: __dirname
|
|
467
|
+
});
|
|
468
|
+
export {
|
|
469
|
+
ReverseTrendLineStrategyCreator
|
|
470
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
adaptiveMomentumRibbonManifest
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MOBKL73M.mjs";
|
|
4
4
|
import {
|
|
5
5
|
__commonJS,
|
|
6
6
|
__esm,
|
|
@@ -3233,9 +3233,9 @@ var require_buffer_list = __commonJS({
|
|
|
3233
3233
|
}
|
|
3234
3234
|
});
|
|
3235
3235
|
|
|
3236
|
-
// ../../node_modules/safe-buffer/index.js
|
|
3236
|
+
// ../../node_modules/readable-stream/node_modules/safe-buffer/index.js
|
|
3237
3237
|
var require_safe_buffer = __commonJS({
|
|
3238
|
-
"../../node_modules/safe-buffer/index.js"(exports, module) {
|
|
3238
|
+
"../../node_modules/readable-stream/node_modules/safe-buffer/index.js"(exports, module) {
|
|
3239
3239
|
"use strict";
|
|
3240
3240
|
var buffer = __require("buffer");
|
|
3241
3241
|
var Buffer2 = buffer.Buffer;
|
|
@@ -10925,6 +10925,8 @@ var config = {
|
|
|
10925
10925
|
ML_ENABLED: false,
|
|
10926
10926
|
ML_THRESHOLD: 0.1,
|
|
10927
10927
|
MIN_AI_QUALITY: 3,
|
|
10928
|
+
FEE_PERCENT: 5e-3,
|
|
10929
|
+
MAX_LOSS_VALUE: 10,
|
|
10928
10930
|
AMR_LOOKBACK_BARS: 400,
|
|
10929
10931
|
AMR_MOMENTUM_PERIOD: 20,
|
|
10930
10932
|
AMR_BUTTERWORTH_SMOOTHING: 3,
|
|
@@ -10953,9 +10955,8 @@ var config = {
|
|
|
10953
10955
|
|
|
10954
10956
|
// src/AdaptiveMomentumRibbon/core.ts
|
|
10955
10957
|
import {
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
getLatestPinePlotValue,
|
|
10958
|
+
getLatestPineBooleanPlotValues,
|
|
10959
|
+
getLatestPineNumberPlotValues,
|
|
10959
10960
|
runPineScript
|
|
10960
10961
|
} from "@tradejs/node/pine";
|
|
10961
10962
|
import { asPositiveInt, asPositiveNumber } from "@tradejs/core/math";
|
|
@@ -11001,8 +11002,8 @@ var logger = (0, import_winston.createLogger)({
|
|
|
11001
11002
|
|
|
11002
11003
|
// src/AdaptiveMomentumRibbon/figures.ts
|
|
11003
11004
|
import {
|
|
11004
|
-
|
|
11005
|
-
|
|
11005
|
+
getPinePlotSeries,
|
|
11006
|
+
toFiniteNumber
|
|
11006
11007
|
} from "@tradejs/node/pine";
|
|
11007
11008
|
var DEFAULT_COLORS = ["#2962ff", "#f23645", "#089981", "#f59e0b"];
|
|
11008
11009
|
var LINE_STYLE_BY_PLOT = {
|
|
@@ -11032,8 +11033,8 @@ var toFigurePoints = (series, maxPoints) => {
|
|
|
11032
11033
|
const points = [];
|
|
11033
11034
|
for (let i = start; i < series.length; i += 1) {
|
|
11034
11035
|
const item = series[i];
|
|
11035
|
-
const timestamp =
|
|
11036
|
-
const value =
|
|
11036
|
+
const timestamp = toFiniteNumber(item?.time);
|
|
11037
|
+
const value = toFiniteNumber(item?.value);
|
|
11037
11038
|
if (timestamp == null || value == null) continue;
|
|
11038
11039
|
points.push({
|
|
11039
11040
|
timestamp,
|
|
@@ -11085,6 +11086,20 @@ var buildAdaptiveMomentumRibbonFigures = ({
|
|
|
11085
11086
|
|
|
11086
11087
|
// src/AdaptiveMomentumRibbon/core.ts
|
|
11087
11088
|
var AMR_PINE_FILE_NAME = "adaptiveMomentumRibbon.pine";
|
|
11089
|
+
var AMR_BOOLEAN_PLOTS = [
|
|
11090
|
+
"entryLong",
|
|
11091
|
+
"entryShort",
|
|
11092
|
+
"invalidated",
|
|
11093
|
+
"activeBuy",
|
|
11094
|
+
"activeSell"
|
|
11095
|
+
];
|
|
11096
|
+
var AMR_NUMBER_PLOTS = [
|
|
11097
|
+
"signalOsc",
|
|
11098
|
+
"kcMidline",
|
|
11099
|
+
"kcUpper",
|
|
11100
|
+
"kcLower",
|
|
11101
|
+
"invalidationLevel"
|
|
11102
|
+
];
|
|
11088
11103
|
var asKcMaType = (value) => {
|
|
11089
11104
|
if (value === "SMA" || value === "EMA" || value === "SMMA (RMA)" || value === "WMA" || value === "VWMA") {
|
|
11090
11105
|
return value;
|
|
@@ -11108,38 +11123,16 @@ var resolveLinePlots = (value) => {
|
|
|
11108
11123
|
}
|
|
11109
11124
|
return value.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0);
|
|
11110
11125
|
};
|
|
11111
|
-
var getLookbackCandles = (candles, lookbackBars) => {
|
|
11112
|
-
if (lookbackBars <= 0) {
|
|
11113
|
-
return candles;
|
|
11114
|
-
}
|
|
11115
|
-
return candles.slice(-lookbackBars);
|
|
11116
|
-
};
|
|
11117
|
-
var readBooleanPlot = (pineContext, plotName) => asPineBoolean(getLatestPinePlotValue(pineContext, plotName));
|
|
11118
|
-
var readNumericPlot = (pineContext, plotName) => asFiniteNumber2(getLatestPinePlotValue(pineContext, plotName)) ?? null;
|
|
11119
11126
|
var readAmrSnapshot = (pineContext, linePlots) => {
|
|
11120
|
-
const lineValues = Object.fromEntries(
|
|
11121
|
-
linePlots.map((plotName) => [
|
|
11122
|
-
plotName,
|
|
11123
|
-
readNumericPlot(pineContext, plotName)
|
|
11124
|
-
])
|
|
11125
|
-
);
|
|
11126
11127
|
return {
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
activeBuy: readBooleanPlot(pineContext, "activeBuy"),
|
|
11131
|
-
activeSell: readBooleanPlot(pineContext, "activeSell"),
|
|
11132
|
-
signalOsc: readNumericPlot(pineContext, "signalOsc"),
|
|
11133
|
-
kcMidline: readNumericPlot(pineContext, "kcMidline"),
|
|
11134
|
-
kcUpper: readNumericPlot(pineContext, "kcUpper"),
|
|
11135
|
-
kcLower: readNumericPlot(pineContext, "kcLower"),
|
|
11136
|
-
invalidationLevel: readNumericPlot(pineContext, "invalidationLevel"),
|
|
11137
|
-
lineValues
|
|
11128
|
+
...getLatestPineBooleanPlotValues(pineContext, AMR_BOOLEAN_PLOTS),
|
|
11129
|
+
...getLatestPineNumberPlotValues(pineContext, AMR_NUMBER_PLOTS),
|
|
11130
|
+
lineValues: getLatestPineNumberPlotValues(pineContext, linePlots)
|
|
11138
11131
|
};
|
|
11139
11132
|
};
|
|
11140
|
-
var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol,
|
|
11141
|
-
const script =
|
|
11142
|
-
const { LONG, SHORT, AMR_EXIT_ON_INVALIDATION } = config2;
|
|
11133
|
+
var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, loadPineScriptFile, strategyApi }) => {
|
|
11134
|
+
const script = loadPineScriptFile(AMR_PINE_FILE_NAME);
|
|
11135
|
+
const { LONG, SHORT, AMR_EXIT_ON_INVALIDATION, MAX_LOSS_VALUE, FEE_PERCENT } = config2;
|
|
11143
11136
|
const linePlots = resolveLinePlots(config2.AMR_LINE_PLOTS);
|
|
11144
11137
|
const lookbackBars = asPositiveInt(config2.AMR_LOOKBACK_BARS, 0);
|
|
11145
11138
|
const pineInputs = resolveAmrInputs(config2);
|
|
@@ -11156,7 +11149,7 @@ var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, loadPin
|
|
|
11156
11149
|
const positionExists = Boolean(
|
|
11157
11150
|
position && typeof position.qty === "number" && position.qty > 0
|
|
11158
11151
|
);
|
|
11159
|
-
const candles =
|
|
11152
|
+
const candles = lookbackBars > 0 ? fullData.slice(-lookbackBars) : fullData;
|
|
11160
11153
|
let pineContext;
|
|
11161
11154
|
try {
|
|
11162
11155
|
pineContext = await runPineScript({
|
|
@@ -11217,7 +11210,9 @@ var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, loadPin
|
|
|
11217
11210
|
direction: modeConfig.direction,
|
|
11218
11211
|
takeProfitDelta: modeConfig.TP,
|
|
11219
11212
|
stopLossDelta: modeConfig.SL,
|
|
11220
|
-
unit: "percent"
|
|
11213
|
+
unit: "percent",
|
|
11214
|
+
maxLossValue: MAX_LOSS_VALUE,
|
|
11215
|
+
feePercent: Number(FEE_PERCENT ?? 0)
|
|
11221
11216
|
});
|
|
11222
11217
|
if (!qty || !Number.isFinite(qty) || qty <= 0) {
|
|
11223
11218
|
return strategyApi.skip("INVALID_QTY");
|
|
@@ -11233,7 +11228,22 @@ var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, loadPin
|
|
|
11233
11228
|
entryPrice: currentPrice
|
|
11234
11229
|
}),
|
|
11235
11230
|
additionalIndicators: {
|
|
11236
|
-
amr
|
|
11231
|
+
amr,
|
|
11232
|
+
amrSignalTiming: {
|
|
11233
|
+
entryTiming: "zero_cross",
|
|
11234
|
+
waitClose: Boolean(config2.AMR_WAIT_CLOSE),
|
|
11235
|
+
lookbackBars
|
|
11236
|
+
},
|
|
11237
|
+
amrConfigSnapshot: {
|
|
11238
|
+
momentumPeriod: asPositiveInt(config2.AMR_MOMENTUM_PERIOD, 20),
|
|
11239
|
+
butterworthSmoothing: asPositiveInt(
|
|
11240
|
+
config2.AMR_BUTTERWORTH_SMOOTHING,
|
|
11241
|
+
3
|
|
11242
|
+
),
|
|
11243
|
+
kcLength: asPositiveInt(config2.AMR_KC_LENGTH, 20),
|
|
11244
|
+
atrLength: asPositiveInt(config2.AMR_ATR_LENGTH, 14),
|
|
11245
|
+
atrMultiplier: asPositiveNumber(config2.AMR_ATR_MULTIPLIER, 2)
|
|
11246
|
+
}
|
|
11237
11247
|
},
|
|
11238
11248
|
orderPlan: {
|
|
11239
11249
|
qty,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/strategies",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Built-in strategy plugin catalog for the TradeJS open-source framework.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@tradejs/core": "^1.0.
|
|
36
|
-
"@tradejs/indicators": "^1.0.
|
|
37
|
-
"@tradejs/node": "^1.0.
|
|
38
|
-
"@tradejs/types": "^1.0.
|
|
35
|
+
"@tradejs/core": "^1.0.6",
|
|
36
|
+
"@tradejs/indicators": "^1.0.6",
|
|
37
|
+
"@tradejs/node": "^1.0.6",
|
|
38
|
+
"@tradejs/types": "^1.0.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsup": "^8.5.1",
|