@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.
@@ -0,0 +1,418 @@
1
+ import {
2
+ buildTrendlineStructuralContext,
3
+ buildTrendlineTimingContext,
4
+ config,
5
+ trendLineManifest
6
+ } from "./chunk-H2TU2YMA.mjs";
7
+ import "./chunk-HEBXNMVQ.mjs";
8
+
9
+ // src/TrendLine/strategy.ts
10
+ import { createStrategyRuntime } from "@tradejs/node/strategies";
11
+
12
+ // src/TrendLine/core.ts
13
+ import { round as round2 } from "@tradejs/core/math";
14
+ import { createTrendlineEngine } from "@tradejs/core/indicators";
15
+
16
+ // src/TrendLine/filters.ts
17
+ import { diffRel } from "@tradejs/core/math";
18
+ import { ATR_PCT } from "@tradejs/indicators";
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
+ const isVeryVolatility = diffRel(lastCandle.low, lastCandle.high) > MAX_CANDLE_VOLATILITY || diffRel(prevCandle.low, prevCandle.high) > MAX_CANDLE_VOLATILITY;
24
+ if (isVeryVolatility) {
25
+ return false;
26
+ }
27
+ return true;
28
+ };
29
+
30
+ // src/TrendLine/figures.ts
31
+ var buildTrendLineFigures = (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" ? "#facc15" : "#fb923c",
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/TrendLine/risk.ts
58
+ import { round } from "@tradejs/core/math";
59
+ var MIN_STOP_BUFFER_PCT = 0.15;
60
+ var LINE_BUFFER_ATR_FACTOR = 0.35;
61
+ var LINE_BUFFER_BASE_SL_FACTOR = 0.15;
62
+ var ATR_STOP_FLOOR_FACTOR = 0.8;
63
+ var MIN_STOP_LOSS_FACTOR = 0.75;
64
+ var MAX_STOP_LOSS_FACTOR = 2.25;
65
+ var clampNumber = (value, min, max) => Math.min(Math.max(value, min), max);
66
+ var getTimingStopFactor = (entryTiming) => {
67
+ if (entryTiming === "ready_retest") {
68
+ return 0.9;
69
+ }
70
+ if (entryTiming === "ready_follow_through") {
71
+ return 1.05;
72
+ }
73
+ return 1;
74
+ };
75
+ var getTimingTargetRiskRatio = ({
76
+ direction,
77
+ entryTiming
78
+ }) => {
79
+ if (direction === "LONG") {
80
+ if (entryTiming === "ready_retest") {
81
+ return 2.45;
82
+ }
83
+ if (entryTiming === "ready_follow_through") {
84
+ return 2.3;
85
+ }
86
+ return 2.6;
87
+ }
88
+ if (entryTiming === "ready_retest") {
89
+ return 2.3;
90
+ }
91
+ if (entryTiming === "ready_follow_through") {
92
+ return 2.15;
93
+ }
94
+ return 2.45;
95
+ };
96
+ var buildTrendlineRiskPlan = ({
97
+ direction,
98
+ modeConfig,
99
+ structuralContext,
100
+ timingContext
101
+ }) => {
102
+ const baseStopLossDelta = modeConfig.SL;
103
+ const atrPct = structuralContext.atrPct ?? baseStopLossDelta;
104
+ const priceVsLinePctAbs = structuralContext.priceVsLinePctAbs ?? 0;
105
+ const breakVsAtrRatio = structuralContext.breakVsAtrRatio ?? 0;
106
+ const touches = structuralContext.touches ?? 0;
107
+ const distance = structuralContext.distance ?? null;
108
+ const lineBufferPct = Math.max(
109
+ atrPct * LINE_BUFFER_ATR_FACTOR,
110
+ baseStopLossDelta * LINE_BUFFER_BASE_SL_FACTOR,
111
+ MIN_STOP_BUFFER_PCT
112
+ );
113
+ const lineInvalidationPct = priceVsLinePctAbs + lineBufferPct;
114
+ const volatilityFloorPct = Math.max(
115
+ atrPct * ATR_STOP_FLOOR_FACTOR,
116
+ baseStopLossDelta * MIN_STOP_LOSS_FACTOR
117
+ );
118
+ let stopLossDelta = Math.max(lineInvalidationPct, volatilityFloorPct);
119
+ if (touches >= 6) {
120
+ stopLossDelta *= 0.95;
121
+ } else if (touches > 0 && touches <= 4) {
122
+ stopLossDelta *= 1.05;
123
+ }
124
+ if (distance != null && distance >= 250) {
125
+ stopLossDelta *= 1.08;
126
+ } else if (distance != null && distance <= 120) {
127
+ stopLossDelta *= 0.95;
128
+ }
129
+ stopLossDelta *= getTimingStopFactor(timingContext.entryTiming);
130
+ if (direction === "SHORT") {
131
+ stopLossDelta *= 1.08;
132
+ }
133
+ if (breakVsAtrRatio >= 1.5) {
134
+ stopLossDelta *= 0.95;
135
+ }
136
+ stopLossDelta = clampNumber(
137
+ stopLossDelta,
138
+ baseStopLossDelta * MIN_STOP_LOSS_FACTOR,
139
+ baseStopLossDelta * MAX_STOP_LOSS_FACTOR
140
+ );
141
+ let targetRiskRatio = getTimingTargetRiskRatio({
142
+ direction,
143
+ entryTiming: timingContext.entryTiming
144
+ });
145
+ if (breakVsAtrRatio >= 1.25) {
146
+ targetRiskRatio += 0.2;
147
+ } else if (breakVsAtrRatio > 0 && breakVsAtrRatio < 0.75) {
148
+ targetRiskRatio -= 0.15;
149
+ }
150
+ if (touches >= 6) {
151
+ targetRiskRatio += 0.1;
152
+ }
153
+ if (distance != null && distance >= 120 && distance <= 350) {
154
+ targetRiskRatio += 0.1;
155
+ }
156
+ if (direction === "SHORT" && distance != null && distance > 450) {
157
+ targetRiskRatio -= 0.25;
158
+ }
159
+ if (direction === "LONG" && distance != null && distance > 500) {
160
+ targetRiskRatio -= 0.15;
161
+ }
162
+ const minTargetRiskRatio = modeConfig.minRiskRatio + 0.05;
163
+ const maxTargetRiskRatio = Math.max(modeConfig.TP / modeConfig.SL, minTargetRiskRatio) + 0.4;
164
+ targetRiskRatio = clampNumber(
165
+ targetRiskRatio,
166
+ minTargetRiskRatio,
167
+ maxTargetRiskRatio
168
+ );
169
+ return {
170
+ stopLossDelta: round(stopLossDelta, 3),
171
+ targetRiskRatio: round(targetRiskRatio, 2),
172
+ takeProfitDelta: round(stopLossDelta * targetRiskRatio, 3)
173
+ };
174
+ };
175
+
176
+ // src/TrendLine/core.ts
177
+ var BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
178
+ var buildTrendlineSignalSeed = ({
179
+ direction,
180
+ currentPrice,
181
+ indicators,
182
+ bestLine,
183
+ trendlineTiming
184
+ }) => ({
185
+ direction,
186
+ prices: { currentPrice },
187
+ indicators,
188
+ additionalIndicators: {
189
+ touches: bestLine.touches.length + 2,
190
+ distance: bestLine.distance,
191
+ trendLine: bestLine,
192
+ ...trendlineTiming ? { trendlineTiming } : {}
193
+ },
194
+ figures: {
195
+ trendLine: bestLine
196
+ }
197
+ });
198
+ var isOpenPosition = (position) => Boolean(
199
+ 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")
200
+ );
201
+ var getFavorableMovePct = ({
202
+ direction,
203
+ entryPrice,
204
+ currentPrice
205
+ }) => {
206
+ if (!Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || entryPrice <= 0) {
207
+ return null;
208
+ }
209
+ return direction === "LONG" ? (currentPrice - entryPrice) / entryPrice * 100 : (entryPrice - currentPrice) / entryPrice * 100;
210
+ };
211
+ var getPositionStopLossPrice = (position) => {
212
+ if (!position || typeof position !== "object") {
213
+ return null;
214
+ }
215
+ const slPrice = Number(
216
+ position.slPrice ?? Number.NaN
217
+ );
218
+ if (Number.isFinite(slPrice)) {
219
+ return slPrice;
220
+ }
221
+ const signalStopLossPrice = Number(
222
+ position.signal?.prices?.stopLossPrice ?? Number.NaN
223
+ );
224
+ return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
225
+ };
226
+ var getPositionRiskPct = ({
227
+ direction,
228
+ entryPrice,
229
+ stopLossPrice
230
+ }) => {
231
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice) || entryPrice <= 0) {
232
+ return null;
233
+ }
234
+ return direction === "LONG" ? (entryPrice - stopLossPrice) / entryPrice * 100 : (stopLossPrice - entryPrice) / entryPrice * 100;
235
+ };
236
+ var isBreakEvenStopAlreadyApplied = ({
237
+ direction,
238
+ entryPrice,
239
+ stopLossPrice
240
+ }) => {
241
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice)) {
242
+ return false;
243
+ }
244
+ return direction === "LONG" ? stopLossPrice >= entryPrice : stopLossPrice <= entryPrice;
245
+ };
246
+ var isFailedBreakout = ({
247
+ direction,
248
+ priceVsLinePct
249
+ }) => {
250
+ if (priceVsLinePct == null) {
251
+ return false;
252
+ }
253
+ return direction === "LONG" ? priceVsLinePct < 0 : priceVsLinePct > 0;
254
+ };
255
+ var createTrendLineCore = async ({ config: config2, data: cachedData, strategyApi, indicatorsState }) => {
256
+ const { TRENDLINE, FEE_PERCENT, MAX_LOSS_VALUE, HIGHS, LOWS } = config2;
257
+ const lastTradeController = strategyApi.createLastTradeController();
258
+ const trendlineOptions = {
259
+ bestLines: 1,
260
+ capture: true,
261
+ ...TRENDLINE
262
+ };
263
+ const getLowsTrendlines = createTrendlineEngine(cachedData, {
264
+ mode: "lows",
265
+ ...trendlineOptions
266
+ });
267
+ const getHighsTrendlines = createTrendlineEngine(cachedData, {
268
+ mode: "highs",
269
+ ...trendlineOptions
270
+ });
271
+ return async (candle) => {
272
+ const lowsTrendlines = getLowsTrendlines.next(candle);
273
+ const highsTrendlines = getHighsTrendlines.next(candle);
274
+ indicatorsState.onBar();
275
+ const currentPosition = await strategyApi.getCurrentPosition();
276
+ if (isOpenPosition(currentPosition)) {
277
+ const { currentPrice: currentPrice2 } = await strategyApi.getMarketData();
278
+ const activeLine = currentPosition.direction === "LONG" ? highsTrendlines[0] : lowsTrendlines[0];
279
+ const activeModeConfig = currentPosition.direction === "LONG" ? HIGHS : LOWS;
280
+ const currentStopLossPrice = getPositionStopLossPrice(currentPosition);
281
+ const favorableMovePct = getFavorableMovePct({
282
+ direction: currentPosition.direction,
283
+ entryPrice: currentPosition.price,
284
+ currentPrice: currentPrice2
285
+ });
286
+ const currentPositionRiskPct = getPositionRiskPct({
287
+ direction: currentPosition.direction,
288
+ entryPrice: currentPosition.price,
289
+ stopLossPrice: currentStopLossPrice
290
+ });
291
+ if (activeLine) {
292
+ const indicators2 = indicatorsState.snapshot();
293
+ const manageSignalSeed = buildTrendlineSignalSeed({
294
+ direction: activeModeConfig.direction,
295
+ currentPrice: currentPrice2,
296
+ indicators: indicators2,
297
+ bestLine: activeLine
298
+ });
299
+ const structuralContext2 = buildTrendlineStructuralContext(manageSignalSeed);
300
+ if (isFailedBreakout({
301
+ direction: currentPosition.direction,
302
+ priceVsLinePct: structuralContext2.priceVsLinePct
303
+ })) {
304
+ return strategyApi.exit({
305
+ code: "TRENDLINE_FAILED_BREAKOUT_EXIT",
306
+ direction: currentPosition.direction
307
+ });
308
+ }
309
+ }
310
+ if (!isBreakEvenStopAlreadyApplied({
311
+ direction: currentPosition.direction,
312
+ entryPrice: currentPosition.price,
313
+ stopLossPrice: currentStopLossPrice
314
+ }) && favorableMovePct != null && favorableMovePct >= (currentPositionRiskPct ?? activeModeConfig.SL) * BREAK_EVEN_TRIGGER_RISK_MULTIPLIER) {
315
+ return strategyApi.protect({
316
+ code: "TRENDLINE_MOVE_STOP_TO_BREAK_EVEN",
317
+ protectPlan: {
318
+ direction: currentPosition.direction,
319
+ stopLossPrice: currentPosition.price
320
+ }
321
+ });
322
+ }
323
+ return strategyApi.skip("POSITION_EXISTS");
324
+ }
325
+ const bestLine = lowsTrendlines.length > 0 ? lowsTrendlines[0] : highsTrendlines[0];
326
+ if (!bestLine) {
327
+ return strategyApi.skip("NO_TRENDLINE");
328
+ }
329
+ if (lastTradeController.isInCooldown(candle.timestamp)) {
330
+ return strategyApi.skip("DEV_TRADE_COOLDOWN");
331
+ }
332
+ const modeConfig = bestLine.mode === "highs" ? HIGHS : LOWS;
333
+ const { direction, minRiskRatio, enable } = modeConfig;
334
+ if (!enable) {
335
+ return strategyApi.skip("STRATEGY_DISABLED");
336
+ }
337
+ const { fullData, timestamp, currentPrice } = await strategyApi.getMarketData();
338
+ if (!filterByVeryVolatility(fullData)) {
339
+ return strategyApi.skip("VERY_VOLATILITY");
340
+ }
341
+ const indicators = indicatorsState.snapshot();
342
+ const signalSeed = buildTrendlineSignalSeed({
343
+ direction,
344
+ currentPrice,
345
+ indicators,
346
+ bestLine
347
+ });
348
+ const structuralContext = buildTrendlineStructuralContext(signalSeed);
349
+ if (structuralContext.structuralHardBlockReasons.length > 0) {
350
+ return strategyApi.skip(
351
+ `TRENDLINE_STRUCTURE:${structuralContext.structuralHardBlockReasons[0]}`
352
+ );
353
+ }
354
+ const timingContext = buildTrendlineTimingContext({
355
+ signal: signalSeed,
356
+ candles: fullData,
357
+ structuralContext
358
+ });
359
+ if (!timingContext.entryReadyNow) {
360
+ const timingCode = timingContext.entryTiming === "stale_breakout" ? "STALE_BREAKOUT" : timingContext.entryTiming === "wait_retest_confirmation" ? "WAIT_RETEST_CONFIRMATION" : "WAIT_RETEST";
361
+ return strategyApi.skip(`TRENDLINE_TIMING:${timingCode}`);
362
+ }
363
+ const riskPlan = buildTrendlineRiskPlan({
364
+ direction,
365
+ modeConfig,
366
+ structuralContext,
367
+ timingContext
368
+ });
369
+ const { stopLossPrice, takeProfitPrice, riskRatio, qty } = strategyApi.getDirectionalTpSlPrices({
370
+ price: currentPrice,
371
+ direction,
372
+ takeProfitDelta: riskPlan.takeProfitDelta,
373
+ stopLossDelta: riskPlan.stopLossDelta,
374
+ unit: "percent",
375
+ maxLossValue: MAX_LOSS_VALUE,
376
+ feePercent: Number(FEE_PERCENT ?? 0)
377
+ });
378
+ if (!qty || !Number.isFinite(qty) || qty <= 0) {
379
+ return strategyApi.skip("INVALID_QTY");
380
+ }
381
+ if (riskRatio <= minRiskRatio) {
382
+ return strategyApi.skip(`RISK_RATIO:${round2(riskRatio)}`);
383
+ }
384
+ lastTradeController.markTrade(timestamp);
385
+ return strategyApi.entry({
386
+ code: "TRENDLINE_SIGNAL",
387
+ figures: {
388
+ ...buildTrendLineFigures(bestLine)
389
+ },
390
+ direction,
391
+ indicators,
392
+ additionalIndicators: buildTrendlineSignalSeed({
393
+ direction,
394
+ currentPrice,
395
+ indicators,
396
+ bestLine,
397
+ trendlineTiming: timingContext
398
+ }).additionalIndicators,
399
+ orderPlan: {
400
+ qty,
401
+ stopLossPrice,
402
+ takeProfits: [{ rate: 1, price: takeProfitPrice }]
403
+ }
404
+ });
405
+ };
406
+ };
407
+
408
+ // src/TrendLine/strategy.ts
409
+ var TrendlineStrategyCreator = createStrategyRuntime({
410
+ strategyName: "TrendLine",
411
+ defaults: config,
412
+ createCore: createTrendLineCore,
413
+ manifest: trendLineManifest,
414
+ strategyDirectory: __dirname
415
+ });
416
+ export {
417
+ TrendlineStrategyCreator
418
+ };
@@ -19,7 +19,6 @@ var config = {
19
19
  MIN_AI_QUALITY: 3,
20
20
  FEE_PERCENT: 5e-3,
21
21
  MAX_LOSS_VALUE: 10,
22
- MAX_CORRELATION: 0.45,
23
22
  TRADE_COOLDOWN_MS: 0,
24
23
  MA_FAST: 21,
25
24
  MA_SLOW: 55,
@@ -134,15 +133,7 @@ var detectCross = (maFast, maSlow) => {
134
133
  return null;
135
134
  };
136
135
  var createMaStrategyCore = async ({ config: config2, strategyApi, indicatorsState }) => {
137
- const {
138
- ENV,
139
- FEE_PERCENT,
140
- MAX_LOSS_VALUE,
141
- MAX_CORRELATION,
142
- TRADE_COOLDOWN_MS,
143
- LONG,
144
- SHORT
145
- } = config2;
136
+ const { FEE_PERCENT, MAX_LOSS_VALUE, TRADE_COOLDOWN_MS, LONG, SHORT } = config2;
146
137
  const lastTradeController = strategyApi.createLastTradeController({
147
138
  enabled: Number(TRADE_COOLDOWN_MS ?? 0) > 0,
148
139
  cooldownMs: Number(TRADE_COOLDOWN_MS ?? 0)
@@ -205,9 +196,6 @@ var createMaStrategyCore = async ({ config: config2, strategyApi, indicatorsStat
205
196
  return strategyApi.skip(`RISK_RATIO:${round(riskRatio)}`);
206
197
  }
207
198
  const correlation = indicatorsState.latestNumber("correlation");
208
- if (ENV !== "BACKTEST" && correlation != null && correlation >= MAX_CORRELATION) {
209
- return strategyApi.skip(`MAX_CORRELATION:${round(correlation)}`);
210
- }
211
199
  lastTradeController.markTrade(timestamp);
212
200
  return strategyApi.entry({
213
201
  code: cross.kind === "bullish" ? "MA_BULLISH_CROSS" : "MA_BEARISH_CROSS",