@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,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-RYEPHOGL.mjs";
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
- asPineBoolean,
10957
- asFiniteNumber as asFiniteNumber2,
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
- asFiniteNumber,
11005
- getPinePlotSeries
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 = asFiniteNumber(item?.time);
11036
- const value = asFiniteNumber(item?.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
- entryLong: readBooleanPlot(pineContext, "entryLong"),
11128
- entryShort: readBooleanPlot(pineContext, "entryShort"),
11129
- invalidated: readBooleanPlot(pineContext, "invalidated"),
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, loadPineScript, strategyApi }) => {
11141
- const script = loadPineScript(AMR_PINE_FILE_NAME);
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 = getLookbackCandles(fullData, lookbackBars);
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.5",
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.5",
36
- "@tradejs/indicators": "^1.0.5",
37
- "@tradejs/node": "^1.0.5",
38
- "@tradejs/types": "^1.0.5"
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",