@tradejs/strategies 1.0.9 → 1.0.10

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.
@@ -1,399 +0,0 @@
1
- import {
2
- buildReverseTrendlineStructuralContext,
3
- buildReverseTrendlineTimingContext,
4
- config,
5
- reverseTrendLineManifest,
6
- toFiniteNumberOrNull
7
- } from "./chunk-UK4YHD2E.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 buildReverseTrendlineSignalSeed = ({
155
- direction,
156
- currentPrice,
157
- indicators,
158
- bestLine,
159
- currentCandle,
160
- reverseTrendlineTiming
161
- }) => ({
162
- direction,
163
- prices: { currentPrice },
164
- indicators,
165
- additionalIndicators: {
166
- touches: Array.isArray(bestLine.touches) ? bestLine.touches.length + 2 : 2,
167
- distance: bestLine.distance,
168
- trendLine: bestLine,
169
- ...currentCandle ? { currentCandle } : {},
170
- ...reverseTrendlineTiming ? { reverseTrendlineTiming } : {}
171
- },
172
- figures: {
173
- trendLine: bestLine
174
- }
175
- });
176
- var isOpenPosition = (position) => Boolean(
177
- 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")
178
- );
179
- var getLinePriceAtNow = (line, timestamp) => {
180
- if (!line || !Array.isArray(line.points) || line.points.length === 0) {
181
- return null;
182
- }
183
- const sortedPoints = [...line.points].sort(
184
- (left, right) => left.timestamp - right.timestamp
185
- );
186
- const first = sortedPoints[0];
187
- const last = sortedPoints[sortedPoints.length - 1];
188
- if (first.timestamp === last.timestamp) {
189
- return last.value;
190
- }
191
- const slope = (last.value - first.value) / (last.timestamp - first.timestamp);
192
- return first.value + slope * (timestamp - first.timestamp);
193
- };
194
- var buildReverseTrendlineCandidateContext = ({
195
- line,
196
- candle,
197
- direction
198
- }) => {
199
- const currentLinePrice = getLinePriceAtNow(line, candle.timestamp);
200
- const priceVsLinePct = currentLinePrice != null && currentLinePrice !== 0 ? (candle.close - currentLinePrice) / currentLinePrice * 100 : null;
201
- const priceVsLinePctAbs = priceVsLinePct == null ? null : Math.abs(priceVsLinePct);
202
- const lineTouchedNow = currentLinePrice != null && candle.low <= currentLinePrice && candle.high >= currentLinePrice;
203
- const failedBounceBreak = direction === "LONG" ? priceVsLinePct != null && priceVsLinePct <= -0.35 : priceVsLinePct != null && priceVsLinePct >= 0.35;
204
- return {
205
- currentLinePrice,
206
- priceVsLinePctAbs,
207
- lineTouchedNow,
208
- failedBounceBreak,
209
- distance: toFiniteNumberOrNull(line.distance)
210
- };
211
- };
212
- var pickBestCandidateLine = ({
213
- candle,
214
- lines
215
- }) => {
216
- const ranked = lines.map(({ line, direction }) => {
217
- const candidateContext = buildReverseTrendlineCandidateContext({
218
- line,
219
- candle,
220
- direction
221
- });
222
- return { line, direction, candidateContext };
223
- }).filter(({ candidateContext }) => candidateContext.currentLinePrice != null).sort((left, right) => {
224
- const leftTouchRank = left.candidateContext.lineTouchedNow ? 0 : 1;
225
- const rightTouchRank = right.candidateContext.lineTouchedNow ? 0 : 1;
226
- if (leftTouchRank !== rightTouchRank) {
227
- return leftTouchRank - rightTouchRank;
228
- }
229
- const leftDistance = left.candidateContext.priceVsLinePctAbs ?? Number.POSITIVE_INFINITY;
230
- const rightDistance = right.candidateContext.priceVsLinePctAbs ?? Number.POSITIVE_INFINITY;
231
- if (leftDistance !== rightDistance) {
232
- return leftDistance - rightDistance;
233
- }
234
- return (left.candidateContext.distance ?? Number.POSITIVE_INFINITY) - (right.candidateContext.distance ?? Number.POSITIVE_INFINITY);
235
- });
236
- return ranked[0] ?? null;
237
- };
238
- var createReverseTrendLineCore = async ({ config: config2, data: cachedData, strategyApi, indicatorsState }) => {
239
- const { TRENDLINE, FEE_PERCENT, MAX_LOSS_VALUE, HIGHS, LOWS } = config2;
240
- const lastTradeController = strategyApi.createLastTradeController();
241
- const trendlineOptions = {
242
- bestLines: 1,
243
- capture: true,
244
- ...TRENDLINE
245
- };
246
- const getLowsTrendlines = createTrendlineEngine(cachedData, {
247
- mode: "lows",
248
- ...trendlineOptions
249
- });
250
- const getHighsTrendlines = createTrendlineEngine(cachedData, {
251
- mode: "highs",
252
- ...trendlineOptions
253
- });
254
- return async (candle) => {
255
- const lowsTrendlines = getLowsTrendlines.next(candle);
256
- const highsTrendlines = getHighsTrendlines.next(candle);
257
- indicatorsState.onBar();
258
- const currentPosition = await strategyApi.getCurrentPosition();
259
- if (isOpenPosition(currentPosition)) {
260
- const activeLine = currentPosition.direction === "LONG" ? lowsTrendlines[0] : highsTrendlines[0];
261
- const activeLinePrice = getLinePriceAtNow(
262
- activeLine ?? null,
263
- candle.timestamp
264
- );
265
- const priceVsLinePct = activeLinePrice != null && activeLinePrice !== 0 ? (candle.close - activeLinePrice) / activeLinePrice * 100 : null;
266
- const failedBounceBreak = currentPosition.direction === "LONG" ? priceVsLinePct != null && priceVsLinePct <= -0.35 : priceVsLinePct != null && priceVsLinePct >= 0.35;
267
- if (failedBounceBreak) {
268
- return strategyApi.exit({
269
- code: "REVERSE_TRENDLINE_FAILED_BOUNCE_EXIT",
270
- direction: currentPosition.direction
271
- });
272
- }
273
- return strategyApi.skip("POSITION_EXISTS");
274
- }
275
- if (lastTradeController.isInCooldown(candle.timestamp)) {
276
- return strategyApi.skip("DEV_TRADE_COOLDOWN");
277
- }
278
- const candidates = [];
279
- if (LOWS.enable && lowsTrendlines.length > 0) {
280
- candidates.push({ line: lowsTrendlines[0], direction: LOWS.direction });
281
- }
282
- if (HIGHS.enable && highsTrendlines.length > 0) {
283
- candidates.push({ line: highsTrendlines[0], direction: HIGHS.direction });
284
- }
285
- if (candidates.length === 0) {
286
- return strategyApi.skip("NO_TRENDLINE");
287
- }
288
- const bestCandidate = pickBestCandidateLine({
289
- candle: {
290
- timestamp: candle.timestamp,
291
- open: candle.open,
292
- close: candle.close,
293
- high: candle.high,
294
- low: candle.low
295
- },
296
- lines: candidates
297
- });
298
- if (!bestCandidate) {
299
- return strategyApi.skip("NO_TRENDLINE");
300
- }
301
- const { line: bestLine, direction, candidateContext } = bestCandidate;
302
- const modeConfig = direction === "LONG" ? LOWS : HIGHS;
303
- const { minRiskRatio } = modeConfig;
304
- if (candidateContext.failedBounceBreak) {
305
- return strategyApi.skip(
306
- "REVERSE_TRENDLINE_STRUCTURE:failed_bounce_break"
307
- );
308
- }
309
- const { fullData, timestamp, currentPrice } = await strategyApi.getMarketData();
310
- if (!filterByVeryVolatility(fullData)) {
311
- return strategyApi.skip("VERY_VOLATILITY");
312
- }
313
- const indicators = indicatorsState.snapshot();
314
- const signalSeed = buildReverseTrendlineSignalSeed({
315
- direction,
316
- currentPrice,
317
- indicators,
318
- bestLine,
319
- currentCandle: {
320
- timestamp: candle.timestamp,
321
- open: candle.open,
322
- close: candle.close,
323
- high: candle.high,
324
- low: candle.low
325
- }
326
- });
327
- const structuralContext = buildReverseTrendlineStructuralContext(signalSeed);
328
- const timingContext = buildReverseTrendlineTimingContext({
329
- signal: signalSeed,
330
- candles: fullData,
331
- structuralContext
332
- });
333
- if (!timingContext.entryReadyNow) {
334
- const timingCode = timingContext.entryTiming === "stale_reaction" ? "STALE_REACTION" : timingContext.entryTiming === "wait_reaction_confirmation" ? "WAIT_REACTION_CONFIRMATION" : "WAIT_TOUCH";
335
- return strategyApi.skip(`REVERSE_TRENDLINE_TIMING:${timingCode}`);
336
- }
337
- const riskPlan = buildReverseTrendlineRiskPlan({
338
- direction,
339
- modeConfig,
340
- structuralContext,
341
- timingContext
342
- });
343
- const { stopLossPrice, takeProfitPrice, riskRatio, qty } = strategyApi.getDirectionalTpSlPrices({
344
- price: currentPrice,
345
- direction,
346
- takeProfitDelta: riskPlan.takeProfitDelta,
347
- stopLossDelta: riskPlan.stopLossDelta,
348
- unit: "percent",
349
- maxLossValue: MAX_LOSS_VALUE,
350
- feePercent: Number(FEE_PERCENT ?? 0)
351
- });
352
- if (!qty || !Number.isFinite(qty) || qty <= 0) {
353
- return strategyApi.skip("INVALID_QTY");
354
- }
355
- if (riskRatio <= minRiskRatio) {
356
- return strategyApi.skip(`RISK_RATIO:${round2(riskRatio)}`);
357
- }
358
- lastTradeController.markTrade(timestamp);
359
- return strategyApi.entry({
360
- code: "REVERSE_TRENDLINE_SIGNAL",
361
- figures: {
362
- ...buildReverseTrendLineFigures(bestLine)
363
- },
364
- direction,
365
- indicators,
366
- additionalIndicators: buildReverseTrendlineSignalSeed({
367
- direction,
368
- currentPrice,
369
- indicators,
370
- bestLine,
371
- currentCandle: {
372
- timestamp: candle.timestamp,
373
- open: candle.open,
374
- close: candle.close,
375
- high: candle.high,
376
- low: candle.low
377
- },
378
- reverseTrendlineTiming: timingContext
379
- }).additionalIndicators,
380
- orderPlan: {
381
- qty,
382
- stopLossPrice,
383
- takeProfits: [{ rate: 1, price: takeProfitPrice }]
384
- }
385
- });
386
- };
387
- };
388
-
389
- // src/ReverseTrendLine/strategy.ts
390
- var ReverseTrendLineStrategyCreator = createStrategyRuntime({
391
- strategyName: "ReverseTrendLine",
392
- defaults: config,
393
- createCore: createReverseTrendLineCore,
394
- manifest: reverseTrendLineManifest,
395
- strategyDirectory: __dirname
396
- });
397
- export {
398
- ReverseTrendLineStrategyCreator
399
- };