@tradejs/strategies 1.0.6 → 1.0.9

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,373 @@
1
+ import {
2
+ config,
3
+ trendShiftManifest
4
+ } from "./chunk-SOVTOGY4.mjs";
5
+ import "./chunk-HEBXNMVQ.mjs";
6
+
7
+ // src/TrendShift/strategy.ts
8
+ import { createStrategyRuntime } from "@tradejs/node/strategies";
9
+
10
+ // src/TrendShift/core.ts
11
+ import { round } from "@tradejs/core/math";
12
+
13
+ // src/TrendShift/engine.ts
14
+ var clampPositive = (value, fallback) => Number.isFinite(value) && value > 0 ? value : fallback;
15
+ var calculateTrueRange = (candle, prevClose) => {
16
+ const high = Number(candle.high);
17
+ const low = Number(candle.low);
18
+ const close = Number(candle.close);
19
+ if (!Number.isFinite(high) || !Number.isFinite(low) || !Number.isFinite(close)) {
20
+ return 0;
21
+ }
22
+ if (prevClose == null || !Number.isFinite(prevClose)) {
23
+ return Math.max(high - low, 0);
24
+ }
25
+ return Math.max(
26
+ high - low,
27
+ Math.abs(high - prevClose),
28
+ Math.abs(low - prevClose)
29
+ );
30
+ };
31
+ var updateAtrState = ({
32
+ atrState,
33
+ tr,
34
+ period
35
+ }) => {
36
+ const safeTr = Number.isFinite(tr) ? Math.max(tr, 0) : 0;
37
+ const safePeriod = Math.max(1, Math.floor(period));
38
+ if (atrState.value == null) {
39
+ return {
40
+ value: safeTr,
41
+ count: 1
42
+ };
43
+ }
44
+ if (atrState.count < safePeriod) {
45
+ const nextCount = atrState.count + 1;
46
+ return {
47
+ value: (atrState.value * atrState.count + safeTr) / nextCount,
48
+ count: nextCount
49
+ };
50
+ }
51
+ return {
52
+ value: (atrState.value * (safePeriod - 1) + safeTr) / safePeriod,
53
+ count: atrState.count + 1
54
+ };
55
+ };
56
+ var trimSeries = (series, maxPoints) => series.length <= maxPoints ? series : series.slice(series.length - maxPoints);
57
+ var asDirection = (trendState) => trendState === 1 ? "LONG" : "SHORT";
58
+ var buildTrendShiftSignalContext = ({
59
+ snapshot,
60
+ indicators
61
+ }) => {
62
+ const maFastSeries = Array.isArray(indicators?.maFast) ? indicators?.maFast : [];
63
+ const maSlowSeries = Array.isArray(indicators?.maSlow) ? indicators?.maSlow : [];
64
+ const maFast = maFastSeries[maFastSeries.length - 1];
65
+ const maSlow = maSlowSeries[maSlowSeries.length - 1];
66
+ const coinBias = Number.isFinite(maFast) && Number.isFinite(maSlow) ? maFast > maSlow ? "bullish" : maFast < maSlow ? "bearish" : "neutral" : "unknown";
67
+ const signalDirection = snapshot.bullFlip ? "LONG" : snapshot.bearFlip ? "SHORT" : asDirection(snapshot.trendState);
68
+ const coinBiasAligned = coinBias === "unknown" || coinBias === "neutral" ? null : signalDirection === "LONG" ? coinBias === "bullish" : coinBias === "bearish";
69
+ return {
70
+ signalDirection,
71
+ trendState: snapshot.trendState,
72
+ rawTrend: snapshot.rawTrend,
73
+ confirmedFlip: snapshot.bullFlip || snapshot.bearFlip,
74
+ bullFlip: snapshot.bullFlip,
75
+ bearFlip: snapshot.bearFlip,
76
+ bullFlipRaw: snapshot.bullFlipRaw,
77
+ bearFlipRaw: snapshot.bearFlipRaw,
78
+ flipDistanceOk: snapshot.flipDistanceOk,
79
+ closeVsAvgPct: snapshot.closeVsAvgPct,
80
+ bandWidthPct: snapshot.bandWidthPct,
81
+ avgSlopePct: snapshot.avgSlopePct,
82
+ distanceAtrRatio: snapshot.distanceAtrRatio,
83
+ adaptiveAtr: snapshot.adaptiveAtr,
84
+ avg: snapshot.avg,
85
+ upper: snapshot.upper,
86
+ lower: snapshot.lower,
87
+ hold: snapshot.hold,
88
+ currentPrice: snapshot.close,
89
+ coinMaFast: Number.isFinite(maFast) ? maFast : null,
90
+ coinMaSlow: Number.isFinite(maSlow) ? maSlow : null,
91
+ coinBias,
92
+ coinBiasAligned
93
+ };
94
+ };
95
+ var getConfigNumbers = (config2) => ({
96
+ mult: clampPositive(config2.TRENDSHIFT_MULTIPLICATIVE_FACTOR, 4),
97
+ slope: clampPositive(config2.TRENDSHIFT_SLOPE, 12),
98
+ atrLength: Math.max(1, Math.floor(config2.TRENDSHIFT_ATR_LENGTH ?? 150)),
99
+ widthPct: clampPositive(config2.TRENDSHIFT_WIDTH_PCT, 75) / 100,
100
+ minFlipAtr: Math.max(0, Number(config2.TRENDSHIFT_MIN_FLIP_DISTANCE_ATR ?? 0)),
101
+ confirmFlipWithClose: Boolean(config2.TRENDSHIFT_CONFIRM_FLIP_WITH_CLOSE),
102
+ maxFigurePoints: Math.max(
103
+ 20,
104
+ Math.floor(config2.TRENDSHIFT_MAX_FIGURE_POINTS ?? 180)
105
+ )
106
+ });
107
+ var createTrendShiftEngine = ({
108
+ config: config2,
109
+ initialCandles = []
110
+ }) => {
111
+ const {
112
+ mult,
113
+ slope,
114
+ atrLength,
115
+ widthPct,
116
+ minFlipAtr,
117
+ confirmFlipWithClose,
118
+ maxFigurePoints
119
+ } = getConfigNumbers(config2);
120
+ const state = {
121
+ atrState: {
122
+ value: null,
123
+ count: 0
124
+ },
125
+ avg: null,
126
+ hold: null,
127
+ rawTrend: 1,
128
+ trendState: 1,
129
+ prevClose: null,
130
+ series: {
131
+ avg: [],
132
+ upper: [],
133
+ lower: []
134
+ },
135
+ snapshot: null
136
+ };
137
+ const apply = (candle) => {
138
+ const close = Number(candle.close);
139
+ if (!Number.isFinite(close)) {
140
+ return {
141
+ snapshot: state.snapshot,
142
+ series: state.series
143
+ };
144
+ }
145
+ const tr = calculateTrueRange(candle, state.prevClose);
146
+ state.atrState = updateAtrState({
147
+ atrState: state.atrState,
148
+ tr,
149
+ period: atrLength
150
+ });
151
+ const adaptiveAtr = Math.max((state.atrState.value ?? tr) * mult, 1e-9);
152
+ const prevAvg = state.avg ?? close;
153
+ const prevHold = state.hold ?? adaptiveAtr;
154
+ const prevRawTrend = state.rawTrend;
155
+ const prevTrendState = state.trendState;
156
+ const avg = Math.abs(close - prevAvg) > adaptiveAtr ? (close + prevAvg) / 2 : prevAvg + prevRawTrend * (prevHold / mult / Math.max(slope, 1));
157
+ const rawTrend = avg > prevAvg ? 1 : avg < prevAvg ? -1 : prevRawTrend;
158
+ const hold = rawTrend !== prevRawTrend ? adaptiveAtr : prevHold + (adaptiveAtr - prevHold) / Math.max(slope, 1);
159
+ const upper = avg + widthPct * hold;
160
+ const lower = avg - widthPct * hold;
161
+ const closeDistance = Math.abs(close - avg);
162
+ const flipDistanceOk = closeDistance >= adaptiveAtr * minFlipAtr;
163
+ const bullFlipRaw = rawTrend === 1 && prevTrendState !== 1;
164
+ const bearFlipRaw = rawTrend === -1 && prevTrendState !== -1;
165
+ const bullFlip = bullFlipRaw && flipDistanceOk && (!confirmFlipWithClose || close > avg);
166
+ const bearFlip = bearFlipRaw && flipDistanceOk && (!confirmFlipWithClose || close < avg);
167
+ const trendState = bullFlip ? 1 : bearFlip ? -1 : prevTrendState;
168
+ const closeVsAvgPct = avg !== 0 ? (close - avg) / avg * 100 : 0;
169
+ const bandWidthPct = avg !== 0 ? (upper - lower) / avg * 100 : 0;
170
+ const avgSlopePct = prevAvg !== 0 ? (avg - prevAvg) / prevAvg * 100 : 0;
171
+ const distanceAtrRatio = adaptiveAtr > 0 ? closeDistance / adaptiveAtr : 0;
172
+ const labelOffset = hold * 0.2;
173
+ state.avg = avg;
174
+ state.hold = hold;
175
+ state.rawTrend = rawTrend;
176
+ state.trendState = trendState;
177
+ state.prevClose = close;
178
+ state.series.avg = trimSeries(
179
+ [...state.series.avg, { timestamp: candle.timestamp, value: avg }],
180
+ maxFigurePoints
181
+ );
182
+ state.series.upper = trimSeries(
183
+ [...state.series.upper, { timestamp: candle.timestamp, value: upper }],
184
+ maxFigurePoints
185
+ );
186
+ state.series.lower = trimSeries(
187
+ [...state.series.lower, { timestamp: candle.timestamp, value: lower }],
188
+ maxFigurePoints
189
+ );
190
+ state.snapshot = {
191
+ avg,
192
+ upper,
193
+ lower,
194
+ hold,
195
+ adaptiveAtr,
196
+ rawTrend,
197
+ trendState,
198
+ bullFlipRaw,
199
+ bearFlipRaw,
200
+ bullFlip,
201
+ bearFlip,
202
+ flipDistanceOk,
203
+ closeVsAvgPct,
204
+ bandWidthPct,
205
+ avgSlopePct,
206
+ distanceAtrRatio,
207
+ labelOffset,
208
+ timestamp: candle.timestamp,
209
+ close
210
+ };
211
+ return {
212
+ snapshot: state.snapshot,
213
+ series: state.series
214
+ };
215
+ };
216
+ for (const candle of initialCandles) {
217
+ apply(candle);
218
+ }
219
+ return {
220
+ next: apply,
221
+ getState: () => ({
222
+ snapshot: state.snapshot,
223
+ series: state.series
224
+ })
225
+ };
226
+ };
227
+
228
+ // src/TrendShift/figures.ts
229
+ var buildTrendShiftFigures = ({
230
+ series,
231
+ direction,
232
+ entryTimestamp,
233
+ entryPrice
234
+ }) => {
235
+ const trendColor = direction === "LONG" ? "#00b894" : "#d63031";
236
+ const lines = [
237
+ {
238
+ id: "trendshift-upper",
239
+ kind: "trendshift_upper",
240
+ points: series.upper,
241
+ color: trendColor,
242
+ width: 1,
243
+ style: "dashed"
244
+ },
245
+ {
246
+ id: "trendshift-avg",
247
+ kind: "trendshift_avg",
248
+ points: series.avg,
249
+ color: trendColor,
250
+ width: 2,
251
+ style: "solid"
252
+ },
253
+ {
254
+ id: "trendshift-lower",
255
+ kind: "trendshift_lower",
256
+ points: series.lower,
257
+ color: trendColor,
258
+ width: 1,
259
+ style: "dashed"
260
+ }
261
+ ].filter((line) => Array.isArray(line.points) && line.points.length > 0);
262
+ const points = [
263
+ {
264
+ id: `trendshift-entry-${entryTimestamp}`,
265
+ kind: "trendshift_entry",
266
+ points: [{ timestamp: entryTimestamp, value: entryPrice }],
267
+ color: trendColor,
268
+ radius: 4
269
+ }
270
+ ];
271
+ return { lines, points };
272
+ };
273
+
274
+ // src/TrendShift/core.ts
275
+ var isOpenPosition = (position) => Boolean(
276
+ 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")
277
+ );
278
+ var createTrendShiftCore = async ({ config: config2, data: initialData, strategyApi, indicatorsState }) => {
279
+ const engine = createTrendShiftEngine({
280
+ config: config2,
281
+ initialCandles: initialData
282
+ });
283
+ const lastTradeController = strategyApi.createLastTradeController();
284
+ return async (candle) => {
285
+ indicatorsState.onBar();
286
+ const runtimeState = engine.next(candle);
287
+ const snapshot = runtimeState.snapshot;
288
+ if (!snapshot) {
289
+ return strategyApi.skip("WAIT_DATA");
290
+ }
291
+ const position = await strategyApi.getCurrentPosition();
292
+ if (isOpenPosition(position)) {
293
+ const oppositeBullExit = position.direction === "SHORT" && snapshot.bullFlip;
294
+ const oppositeBearExit = position.direction === "LONG" && snapshot.bearFlip;
295
+ if (Boolean(config2.TRENDSHIFT_EXIT_ON_OPPOSITE_FLIP) && (oppositeBullExit || oppositeBearExit)) {
296
+ return strategyApi.exit({
297
+ code: "TRENDSHIFT_OPPOSITE_FLIP_EXIT",
298
+ direction: position.direction
299
+ });
300
+ }
301
+ return strategyApi.skip("POSITION_EXISTS");
302
+ }
303
+ if (lastTradeController.isInCooldown(candle.timestamp)) {
304
+ return strategyApi.skip("DEV_TRADE_COOLDOWN");
305
+ }
306
+ const isBullEntry = snapshot.bullFlip;
307
+ const isBearEntry = snapshot.bearFlip;
308
+ if (!isBullEntry && !isBearEntry) {
309
+ return strategyApi.skip("NO_SIGNAL");
310
+ }
311
+ const modeConfig = isBullEntry ? config2.LONG : config2.SHORT;
312
+ if (!modeConfig.enable) {
313
+ return strategyApi.skip("STRATEGY_DISABLED");
314
+ }
315
+ const indicators = indicatorsState.snapshot();
316
+ const { timestamp, currentPrice } = await strategyApi.getMarketData();
317
+ const direction = modeConfig.direction;
318
+ const signalContext = buildTrendShiftSignalContext({
319
+ snapshot: {
320
+ ...snapshot,
321
+ close: currentPrice
322
+ },
323
+ indicators
324
+ });
325
+ const { stopLossPrice, takeProfitPrice, riskRatio, qty } = strategyApi.getDirectionalTpSlPrices({
326
+ price: currentPrice,
327
+ direction,
328
+ takeProfitDelta: modeConfig.TP,
329
+ stopLossDelta: modeConfig.SL,
330
+ unit: "percent",
331
+ maxLossValue: config2.MAX_LOSS_VALUE,
332
+ feePercent: Number(config2.FEE_PERCENT ?? 0)
333
+ });
334
+ if (!qty || !Number.isFinite(qty) || qty <= 0) {
335
+ return strategyApi.skip("INVALID_QTY");
336
+ }
337
+ if (riskRatio <= modeConfig.minRiskRatio) {
338
+ return strategyApi.skip(`RISK_RATIO:${round(riskRatio)}`);
339
+ }
340
+ lastTradeController.markTrade(timestamp);
341
+ return strategyApi.entry({
342
+ code: isBullEntry ? "TRENDSHIFT_BULLISH_FLIP" : "TRENDSHIFT_BEARISH_FLIP",
343
+ direction,
344
+ indicators,
345
+ additionalIndicators: {
346
+ trendShiftContext: signalContext
347
+ },
348
+ figures: buildTrendShiftFigures({
349
+ series: runtimeState.series,
350
+ direction,
351
+ entryTimestamp: timestamp,
352
+ entryPrice: currentPrice
353
+ }),
354
+ orderPlan: {
355
+ qty,
356
+ stopLossPrice,
357
+ takeProfits: [{ rate: 1, price: takeProfitPrice }]
358
+ }
359
+ });
360
+ };
361
+ };
362
+
363
+ // src/TrendShift/strategy.ts
364
+ var TrendShiftStrategyCreator = createStrategyRuntime({
365
+ strategyName: "TrendShift",
366
+ defaults: config,
367
+ createCore: createTrendShiftCore,
368
+ manifest: trendShiftManifest,
369
+ strategyDirectory: __dirname
370
+ });
371
+ export {
372
+ TrendShiftStrategyCreator
373
+ };
@@ -1,65 +1,14 @@
1
1
  import {
2
2
  buildVolumeDivergenceSetupFeatures,
3
+ config,
3
4
  getVolumeDivergenceEntryThresholds,
4
5
  volumeDivergenceManifest
5
- } from "./chunk-XMRB45ZO.mjs";
6
+ } from "./chunk-QVWMBYYM.mjs";
6
7
  import "./chunk-HEBXNMVQ.mjs";
7
8
 
8
9
  // src/VolumeDivergence/strategy.ts
9
10
  import { createStrategyRuntime } from "@tradejs/node/strategies";
10
11
 
11
- // src/VolumeDivergence/config.ts
12
- var config = {
13
- ENV: "BACKTEST",
14
- INTERVAL: "15",
15
- MAKE_ORDERS: true,
16
- CLOSE_OPPOSITE_POSITIONS: false,
17
- BACKTEST_PRICE_MODE: "mid",
18
- AI_ENABLED: false,
19
- ML_ENABLED: false,
20
- ML_THRESHOLD: 0.1,
21
- MIN_AI_QUALITY: 3,
22
- FEE_PERCENT: 5e-3,
23
- MAX_LOSS_VALUE: 10,
24
- MA_FAST: 14,
25
- MA_MEDIUM: 49,
26
- MA_SLOW: 50,
27
- OBV_SMA: 10,
28
- ATR: 14,
29
- ATR_PCT_SHORT: 7,
30
- ATR_PCT_LONG: 30,
31
- BB: 20,
32
- BB_STD: 2,
33
- MACD_FAST: 12,
34
- MACD_SLOW: 26,
35
- MACD_SIGNAL: 9,
36
- LEVEL_LOOKBACK: 20,
37
- LEVEL_DELAY: 2,
38
- NORMALIZATION_LENGTH: 100,
39
- PIVOT_LOOKBACK_LEFT: 8,
40
- PIVOT_LOOKBACK_RIGHT: 3,
41
- MIN_BARS_BETWEEN_PIVOTS: 4,
42
- MAX_BARS_BETWEEN_PIVOTS: 36,
43
- ALLOW_STRUCTURE_ADVANCE_ENTRY: false,
44
- MIN_DIVERGENCE_AMPLITUDE_ATR_RATIO: 0.35,
45
- MIN_RECLAIM_PCT: 105,
46
- MIN_CONFIRMATION_CANDLE_QUALITY: 0.58,
47
- BULLISH: {
48
- enable: true,
49
- direction: "LONG",
50
- TP: 4,
51
- SL: 1.3,
52
- minRiskRatio: 2
53
- },
54
- BEARISH: {
55
- enable: true,
56
- direction: "SHORT",
57
- TP: 4,
58
- SL: 1.3,
59
- minRiskRatio: 2
60
- }
61
- };
62
-
63
12
  // src/VolumeDivergence/core.ts
64
13
  import { round } from "@tradejs/core/math";
65
14
 
@@ -114,57 +63,11 @@ var buildVolumeDivergenceFigures = ({
114
63
  });
115
64
 
116
65
  // src/VolumeDivergence/core.ts
117
- var BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
118
66
  var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
119
67
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
120
68
  var isOpenPosition = (position) => Boolean(
121
69
  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")
122
70
  );
123
- var getFavorableMovePct = ({
124
- direction,
125
- entryPrice,
126
- currentPrice
127
- }) => {
128
- if (!Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || entryPrice <= 0) {
129
- return null;
130
- }
131
- return direction === "LONG" ? (currentPrice - entryPrice) / entryPrice * 100 : (entryPrice - currentPrice) / entryPrice * 100;
132
- };
133
- var getPositionStopLossPrice = (position) => {
134
- if (!position || typeof position !== "object") {
135
- return null;
136
- }
137
- const slPrice = Number(
138
- position.slPrice ?? Number.NaN
139
- );
140
- if (Number.isFinite(slPrice)) {
141
- return slPrice;
142
- }
143
- const signalStopLossPrice = Number(
144
- position.signal?.prices?.stopLossPrice ?? Number.NaN
145
- );
146
- return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
147
- };
148
- var getPositionRiskPct = ({
149
- direction,
150
- entryPrice,
151
- stopLossPrice
152
- }) => {
153
- if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice) || entryPrice <= 0) {
154
- return null;
155
- }
156
- return direction === "LONG" ? (entryPrice - stopLossPrice) / entryPrice * 100 : (stopLossPrice - entryPrice) / entryPrice * 100;
157
- };
158
- var isBreakEvenStopAlreadyApplied = ({
159
- direction,
160
- entryPrice,
161
- stopLossPrice
162
- }) => {
163
- if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice)) {
164
- return false;
165
- }
166
- return direction === "LONG" ? stopLossPrice >= entryPrice : stopLossPrice <= entryPrice;
167
- };
168
71
  var compactQueue = (queue) => {
169
72
  if (queue.start <= 1024 || queue.start * 2 <= queue.indices.length) {
170
73
  return;
@@ -565,32 +468,6 @@ var createVolumeDivergenceCore = async ({ config: config2, strategyApi, indicato
565
468
  const timestamp = Number(candle.timestamp);
566
469
  const currentPosition = await strategyApi.getCurrentPosition();
567
470
  if (isOpenPosition(currentPosition)) {
568
- const { currentPrice: currentPrice2 } = await strategyApi.getMarketData();
569
- const activeModeConfig = currentPosition.direction === "LONG" ? BULLISH : BEARISH;
570
- const currentStopLossPrice = getPositionStopLossPrice(currentPosition);
571
- const favorableMovePct = getFavorableMovePct({
572
- direction: currentPosition.direction,
573
- entryPrice: currentPosition.price,
574
- currentPrice: currentPrice2
575
- });
576
- const currentPositionRiskPct = getPositionRiskPct({
577
- direction: currentPosition.direction,
578
- entryPrice: currentPosition.price,
579
- stopLossPrice: currentStopLossPrice
580
- });
581
- if (!isBreakEvenStopAlreadyApplied({
582
- direction: currentPosition.direction,
583
- entryPrice: currentPosition.price,
584
- stopLossPrice: currentStopLossPrice
585
- }) && favorableMovePct != null && favorableMovePct >= (currentPositionRiskPct ?? activeModeConfig.SL) * BREAK_EVEN_TRIGGER_RISK_MULTIPLIER) {
586
- return strategyApi.protect({
587
- code: "VOLUME_DIVERGENCE_MOVE_STOP_TO_BREAK_EVEN",
588
- protectPlan: {
589
- direction: currentPosition.direction,
590
- stopLossPrice: currentPosition.price
591
- }
592
- });
593
- }
594
471
  return strategyApi.skip("POSITION_EXISTS");
595
472
  }
596
473
  if (candleWindow.length < PIVOT_LOOKBACK_LEFT + PIVOT_LOOKBACK_RIGHT + 2) {