@tradejs/strategies 1.0.5 → 1.0.8

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
+ };