@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.
@@ -1,6 +1,7 @@
1
1
  import {
2
- adaptiveMomentumRibbonManifest
3
- } from "./chunk-RYEPHOGL.mjs";
2
+ adaptiveMomentumRibbonManifest,
3
+ config
4
+ } from "./chunk-IMLNXICX.mjs";
4
5
  import {
5
6
  __commonJS,
6
7
  __esm,
@@ -3233,9 +3234,9 @@ var require_buffer_list = __commonJS({
3233
3234
  }
3234
3235
  });
3235
3236
 
3236
- // ../../node_modules/safe-buffer/index.js
3237
+ // ../../node_modules/readable-stream/node_modules/safe-buffer/index.js
3237
3238
  var require_safe_buffer = __commonJS({
3238
- "../../node_modules/safe-buffer/index.js"(exports, module) {
3239
+ "../../node_modules/readable-stream/node_modules/safe-buffer/index.js"(exports, module) {
3239
3240
  "use strict";
3240
3241
  var buffer = __require("buffer");
3241
3242
  var Buffer2 = buffer.Buffer;
@@ -10914,51 +10915,8 @@ var require_winston = __commonJS({
10914
10915
  // src/AdaptiveMomentumRibbon/strategy.ts
10915
10916
  import { createStrategyRuntime } from "@tradejs/node/strategies";
10916
10917
 
10917
- // src/AdaptiveMomentumRibbon/config.ts
10918
- var config = {
10919
- ENV: "BACKTEST",
10920
- INTERVAL: "15",
10921
- MAKE_ORDERS: true,
10922
- CLOSE_OPPOSITE_POSITIONS: false,
10923
- BACKTEST_PRICE_MODE: "mid",
10924
- AI_ENABLED: false,
10925
- ML_ENABLED: false,
10926
- ML_THRESHOLD: 0.1,
10927
- MIN_AI_QUALITY: 3,
10928
- AMR_LOOKBACK_BARS: 400,
10929
- AMR_MOMENTUM_PERIOD: 20,
10930
- AMR_BUTTERWORTH_SMOOTHING: 3,
10931
- AMR_WAIT_CLOSE: true,
10932
- AMR_SHOW_INVALIDATION_LEVELS: true,
10933
- AMR_SHOW_KELTNER_CHANNEL: true,
10934
- AMR_KC_LENGTH: 20,
10935
- AMR_KC_MA_TYPE: "EMA",
10936
- AMR_ATR_LENGTH: 14,
10937
- AMR_ATR_MULTIPLIER: 2,
10938
- AMR_EXIT_ON_INVALIDATION: true,
10939
- AMR_LINE_PLOTS: ["kcMidline", "kcUpper", "kcLower", "invalidationLevel"],
10940
- LONG: {
10941
- enable: true,
10942
- direction: "LONG",
10943
- TP: 2,
10944
- SL: 1
10945
- },
10946
- SHORT: {
10947
- enable: true,
10948
- direction: "SHORT",
10949
- TP: 2,
10950
- SL: 1
10951
- }
10952
- };
10953
-
10954
10918
  // src/AdaptiveMomentumRibbon/core.ts
10955
- import {
10956
- asPineBoolean,
10957
- asFiniteNumber as asFiniteNumber2,
10958
- getLatestPinePlotValue,
10959
- runPineScript
10960
- } from "@tradejs/node/pine";
10961
- import { asPositiveInt, asPositiveNumber } from "@tradejs/core/math";
10919
+ import { asPositiveInt as asPositiveInt2, asPositiveNumber as asPositiveNumber2 } from "@tradejs/core/math";
10962
10920
 
10963
10921
  // ../infra/src/logger.ts
10964
10922
  var import_winston = __toESM(require_winston());
@@ -10999,11 +10957,437 @@ var logger = (0, import_winston.createLogger)({
10999
10957
  ]
11000
10958
  });
11001
10959
 
10960
+ // src/AdaptiveMomentumRibbon/engine.ts
10961
+ import { asPositiveInt, asPositiveNumber } from "@tradejs/core/math";
10962
+ var PI = 3.14159265359;
10963
+ var MAX_PLOT_POINTS = 240;
10964
+ var toFinite = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
10965
+ var percentileNearestRank = (values, percent) => {
10966
+ if (!values.length) {
10967
+ return null;
10968
+ }
10969
+ const sorted = [...values].sort((a, b) => a - b);
10970
+ const rank = Math.max(
10971
+ 0,
10972
+ Math.min(sorted.length - 1, Math.ceil(percent / 100 * sorted.length) - 1)
10973
+ );
10974
+ return sorted[rank] ?? null;
10975
+ };
10976
+ var stdev = (values) => {
10977
+ if (!values.length) {
10978
+ return null;
10979
+ }
10980
+ const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
10981
+ const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / values.length;
10982
+ return Math.sqrt(variance);
10983
+ };
10984
+ var pushAndTrim = (values, value, limit) => {
10985
+ values.push(value);
10986
+ if (values.length > limit) {
10987
+ values.splice(0, values.length - limit);
10988
+ }
10989
+ };
10990
+ var createMovingAverageState = (type, length) => {
10991
+ switch (type) {
10992
+ case "SMA":
10993
+ return {
10994
+ type,
10995
+ length,
10996
+ window: [],
10997
+ sum: 0
10998
+ };
10999
+ case "EMA":
11000
+ return {
11001
+ type,
11002
+ length,
11003
+ seedWindow: [],
11004
+ value: null
11005
+ };
11006
+ case "SMMA (RMA)":
11007
+ return {
11008
+ type,
11009
+ length,
11010
+ seedWindow: [],
11011
+ value: null
11012
+ };
11013
+ case "WMA":
11014
+ return {
11015
+ type,
11016
+ length,
11017
+ window: []
11018
+ };
11019
+ case "VWMA":
11020
+ return {
11021
+ type,
11022
+ length,
11023
+ priceWindow: [],
11024
+ volumeWindow: [],
11025
+ weightedSum: 0,
11026
+ volumeSum: 0
11027
+ };
11028
+ }
11029
+ };
11030
+ var updateMovingAverage = (state, price, volume) => {
11031
+ switch (state.type) {
11032
+ case "SMA": {
11033
+ state.window.push(price);
11034
+ state.sum += price;
11035
+ if (state.window.length > state.length) {
11036
+ state.sum -= state.window.shift() ?? 0;
11037
+ }
11038
+ if (state.window.length < state.length) {
11039
+ return null;
11040
+ }
11041
+ return state.sum / state.length;
11042
+ }
11043
+ case "EMA": {
11044
+ if (state.value == null) {
11045
+ state.seedWindow.push(price);
11046
+ if (state.seedWindow.length < state.length) {
11047
+ return null;
11048
+ }
11049
+ if (state.seedWindow.length === state.length) {
11050
+ state.value = state.seedWindow.reduce((sum, item) => sum + item, 0) / state.length;
11051
+ return state.value;
11052
+ }
11053
+ }
11054
+ const alpha = 2 / (state.length + 1);
11055
+ state.value = price * alpha + (state.value ?? price) * (1 - alpha);
11056
+ return state.value;
11057
+ }
11058
+ case "SMMA (RMA)": {
11059
+ if (state.value == null) {
11060
+ state.seedWindow.push(price);
11061
+ if (state.seedWindow.length < state.length) {
11062
+ return null;
11063
+ }
11064
+ if (state.seedWindow.length === state.length) {
11065
+ state.value = state.seedWindow.reduce((sum, item) => sum + item, 0) / state.length;
11066
+ return state.value;
11067
+ }
11068
+ }
11069
+ state.value = ((state.value ?? price) * (state.length - 1) + price) / state.length;
11070
+ return state.value;
11071
+ }
11072
+ case "WMA": {
11073
+ state.window.push(price);
11074
+ if (state.window.length > state.length) {
11075
+ state.window.shift();
11076
+ }
11077
+ if (state.window.length < state.length) {
11078
+ return null;
11079
+ }
11080
+ let weightedSum = 0;
11081
+ let weightSum = 0;
11082
+ for (let index = 0; index < state.window.length; index += 1) {
11083
+ const weight = index + 1;
11084
+ weightedSum += state.window[index] * weight;
11085
+ weightSum += weight;
11086
+ }
11087
+ return weightSum > 0 ? weightedSum / weightSum : null;
11088
+ }
11089
+ case "VWMA": {
11090
+ state.priceWindow.push(price);
11091
+ state.volumeWindow.push(volume);
11092
+ state.weightedSum += price * volume;
11093
+ state.volumeSum += volume;
11094
+ if (state.priceWindow.length > state.length) {
11095
+ const removedPrice = state.priceWindow.shift() ?? 0;
11096
+ const removedVolume = state.volumeWindow.shift() ?? 0;
11097
+ state.weightedSum -= removedPrice * removedVolume;
11098
+ state.volumeSum -= removedVolume;
11099
+ }
11100
+ if (state.priceWindow.length < state.length || state.volumeSum <= 0) {
11101
+ return null;
11102
+ }
11103
+ return state.weightedSum / state.volumeSum;
11104
+ }
11105
+ }
11106
+ };
11107
+ var createAtrState = (length) => ({
11108
+ length,
11109
+ previousClose: null,
11110
+ trSeedWindow: [],
11111
+ atrValue: null
11112
+ });
11113
+ var updateAtr = (state, candle) => {
11114
+ const previousClose = state.previousClose;
11115
+ const tr = previousClose == null ? candle.high - candle.low : Math.max(
11116
+ candle.high - candle.low,
11117
+ Math.abs(candle.high - previousClose),
11118
+ Math.abs(candle.low - previousClose)
11119
+ );
11120
+ state.previousClose = candle.close;
11121
+ if (state.atrValue == null) {
11122
+ state.trSeedWindow.push(tr);
11123
+ if (state.trSeedWindow.length < state.length) {
11124
+ return null;
11125
+ }
11126
+ if (state.trSeedWindow.length === state.length) {
11127
+ state.atrValue = state.trSeedWindow.reduce((sum, value) => sum + value, 0) / state.length;
11128
+ return state.atrValue;
11129
+ }
11130
+ }
11131
+ state.atrValue = ((state.atrValue ?? tr) * (state.length - 1) + tr) / state.length;
11132
+ return state.atrValue;
11133
+ };
11134
+ var createButterworthState = (length) => ({
11135
+ length,
11136
+ prev1: null,
11137
+ prev2: null
11138
+ });
11139
+ var updateButterworth = (state, source) => {
11140
+ if (source == null) {
11141
+ return null;
11142
+ }
11143
+ const safeLength = Math.max(state.length, 1);
11144
+ const a = Math.exp(-Math.sqrt(2) * PI / safeLength);
11145
+ const b = 2 * a * Math.cos(Math.sqrt(2) * PI / safeLength);
11146
+ const c2 = b;
11147
+ const c3 = -(a * a);
11148
+ const c1 = 1 - c2 - c3;
11149
+ if (state.prev1 == null || state.prev2 == null) {
11150
+ state.prev1 = source;
11151
+ state.prev2 = source;
11152
+ return source;
11153
+ }
11154
+ const result = c1 * source + c2 * state.prev1 + c3 * state.prev2;
11155
+ state.prev2 = state.prev1;
11156
+ state.prev1 = result;
11157
+ return result;
11158
+ };
11159
+ var pushPlotPoint = (plotSeries, plotName, candle, value) => {
11160
+ if (value == null) {
11161
+ return;
11162
+ }
11163
+ const points = plotSeries[plotName] ?? [];
11164
+ points.push({
11165
+ time: candle.timestamp,
11166
+ value
11167
+ });
11168
+ if (points.length > MAX_PLOT_POINTS) {
11169
+ points.splice(0, points.length - MAX_PLOT_POINTS);
11170
+ }
11171
+ plotSeries[plotName] = points;
11172
+ };
11173
+ var evaluateAdaptiveMomentumRibbon = ({
11174
+ candles,
11175
+ config: config2,
11176
+ linePlots
11177
+ }) => {
11178
+ const momentumPeriod = asPositiveInt(config2.AMR_MOMENTUM_PERIOD, 20);
11179
+ const smoothingLength = asPositiveInt(config2.AMR_BUTTERWORTH_SMOOTHING, 3);
11180
+ const waitClose = Boolean(config2.AMR_WAIT_CLOSE);
11181
+ const confirmOnNextBar = Boolean(config2.AMR_CONFIRM_ON_NEXT_BAR);
11182
+ const minSignalOscAbs = asPositiveNumber(config2.AMR_MIN_SIGNAL_OSC_ABS, 0.55);
11183
+ const requireKcBias = Boolean(config2.AMR_REQUIRE_KC_BIAS);
11184
+ const minBarsBetweenSignals = asPositiveInt(
11185
+ config2.AMR_MIN_BARS_BETWEEN_SIGNALS,
11186
+ 12
11187
+ );
11188
+ const showInvalidationLevels = Boolean(config2.AMR_SHOW_INVALIDATION_LEVELS);
11189
+ const showKeltnerChannel = Boolean(config2.AMR_SHOW_KELTNER_CHANNEL);
11190
+ const kcLength = asPositiveInt(config2.AMR_KC_LENGTH, 20);
11191
+ const kcMaType = config2.AMR_KC_MA_TYPE === "SMA" || config2.AMR_KC_MA_TYPE === "EMA" || config2.AMR_KC_MA_TYPE === "SMMA (RMA)" || config2.AMR_KC_MA_TYPE === "WMA" || config2.AMR_KC_MA_TYPE === "VWMA" ? config2.AMR_KC_MA_TYPE : "EMA";
11192
+ const atrLength = asPositiveInt(config2.AMR_ATR_LENGTH, 14);
11193
+ const atrMultiplier = asPositiveNumber(config2.AMR_ATR_MULTIPLIER, 2);
11194
+ const sourceWindow = [];
11195
+ const deviationWindow = [];
11196
+ const maState = createMovingAverageState(kcMaType, kcLength);
11197
+ const atrState = createAtrState(atrLength);
11198
+ const butterworthState = createButterworthState(smoothingLength);
11199
+ const plotSeries = {};
11200
+ let previousSignalOsc = null;
11201
+ let lastAcceptedSignalIndex = null;
11202
+ let pendingSignal = null;
11203
+ let invalidationLevel = null;
11204
+ let activeBuy = false;
11205
+ let activeSell = false;
11206
+ let lastSnapshot = {
11207
+ entryLong: false,
11208
+ entryShort: false,
11209
+ invalidated: false,
11210
+ activeBuy: false,
11211
+ activeSell: false,
11212
+ signalOsc: null,
11213
+ kcMidline: null,
11214
+ kcUpper: null,
11215
+ kcLower: null,
11216
+ invalidationLevel: null,
11217
+ lineValues: Object.fromEntries(
11218
+ linePlots.map((plotName) => [plotName, null])
11219
+ )
11220
+ };
11221
+ for (let index = 0; index < candles.length; index += 1) {
11222
+ const candle = candles[index];
11223
+ const previousCandle = index > 0 ? candles[index - 1] : null;
11224
+ const kcMidline = updateMovingAverage(
11225
+ maState,
11226
+ candle.close,
11227
+ Number(candle.volume ?? 0)
11228
+ );
11229
+ const atrValue = updateAtr(atrState, candle);
11230
+ const kcUpper = kcMidline != null && atrValue != null ? kcMidline + atrMultiplier * atrValue : null;
11231
+ const kcLower = kcMidline != null && atrValue != null ? kcMidline - atrMultiplier * atrValue : null;
11232
+ const sourceCandle = waitClose ? previousCandle : candle;
11233
+ const sourceClose = sourceCandle?.close ?? null;
11234
+ let signalOsc = null;
11235
+ let entryLong = false;
11236
+ let entryShort = false;
11237
+ if (sourceClose != null) {
11238
+ pushAndTrim(sourceWindow, sourceClose, momentumPeriod);
11239
+ if (sourceWindow.length >= momentumPeriod) {
11240
+ const medianValue = percentileNearestRank(sourceWindow, 50);
11241
+ const deviation = medianValue != null ? sourceClose - medianValue : null;
11242
+ if (deviation != null) {
11243
+ pushAndTrim(deviationWindow, deviation, momentumPeriod);
11244
+ }
11245
+ if (deviation != null && deviationWindow.length >= momentumPeriod) {
11246
+ const absoluteDeviationWindow = deviationWindow.map(
11247
+ (value) => Math.abs(value)
11248
+ );
11249
+ const medDeviation = percentileNearestRank(
11250
+ absoluteDeviationWindow,
11251
+ 50
11252
+ );
11253
+ const scale = medDeviation === 0 ? stdev(sourceWindow) : medDeviation != null ? medDeviation * 1.4826 : null;
11254
+ const rawOsc = scale != null && scale !== 0 ? deviation / scale : 0;
11255
+ signalOsc = updateButterworth(butterworthState, rawOsc);
11256
+ }
11257
+ }
11258
+ }
11259
+ if (signalOsc != null && previousSignalOsc != null) {
11260
+ const rawEntryLong = previousSignalOsc <= 0 && signalOsc > 0;
11261
+ const rawEntryShort = previousSignalOsc >= 0 && signalOsc < 0;
11262
+ const strongEnough = Math.abs(signalOsc) >= minSignalOscAbs;
11263
+ const spacingOk = lastAcceptedSignalIndex == null || index - lastAcceptedSignalIndex >= minBarsBetweenSignals;
11264
+ const longKcBiasOk = !requireKcBias || kcMidline != null && candle.close > kcMidline;
11265
+ const shortKcBiasOk = !requireKcBias || kcMidline != null && candle.close < kcMidline;
11266
+ if (confirmOnNextBar) {
11267
+ if (pendingSignal?.direction === "LONG") {
11268
+ const pendingStillValid = pendingSignal.invalidationLevel == null || candle.low >= pendingSignal.invalidationLevel;
11269
+ const confirmed = pendingStillValid && signalOsc > 0 && strongEnough && longKcBiasOk;
11270
+ if (confirmed) {
11271
+ entryLong = true;
11272
+ invalidationLevel = pendingSignal.invalidationLevel;
11273
+ lastAcceptedSignalIndex = index;
11274
+ }
11275
+ pendingSignal = null;
11276
+ } else if (pendingSignal?.direction === "SHORT") {
11277
+ const pendingStillValid = pendingSignal.invalidationLevel == null || candle.high <= pendingSignal.invalidationLevel;
11278
+ const confirmed = pendingStillValid && signalOsc < 0 && strongEnough && shortKcBiasOk;
11279
+ if (confirmed) {
11280
+ entryShort = true;
11281
+ invalidationLevel = pendingSignal.invalidationLevel;
11282
+ lastAcceptedSignalIndex = index;
11283
+ }
11284
+ pendingSignal = null;
11285
+ }
11286
+ if (!entryLong && !entryShort && spacingOk) {
11287
+ if (rawEntryLong && strongEnough && longKcBiasOk && sourceCandle) {
11288
+ pendingSignal = {
11289
+ direction: "LONG",
11290
+ invalidationLevel: sourceCandle.low
11291
+ };
11292
+ } else if (rawEntryShort && strongEnough && shortKcBiasOk && sourceCandle) {
11293
+ pendingSignal = {
11294
+ direction: "SHORT",
11295
+ invalidationLevel: sourceCandle.high
11296
+ };
11297
+ }
11298
+ }
11299
+ } else {
11300
+ entryLong = rawEntryLong && strongEnough && spacingOk && longKcBiasOk;
11301
+ entryShort = rawEntryShort && strongEnough && spacingOk && shortKcBiasOk;
11302
+ }
11303
+ }
11304
+ if (signalOsc != null) {
11305
+ previousSignalOsc = signalOsc;
11306
+ }
11307
+ if (entryLong && sourceCandle) {
11308
+ if (invalidationLevel == null) {
11309
+ invalidationLevel = sourceCandle.low;
11310
+ }
11311
+ activeBuy = true;
11312
+ activeSell = false;
11313
+ }
11314
+ if (entryShort && sourceCandle) {
11315
+ if (invalidationLevel == null) {
11316
+ invalidationLevel = sourceCandle.high;
11317
+ }
11318
+ activeSell = true;
11319
+ activeBuy = false;
11320
+ }
11321
+ const checkCandle = waitClose ? previousCandle : candle;
11322
+ let invalidated = false;
11323
+ if (activeBuy && checkCandle && invalidationLevel != null && checkCandle.low < invalidationLevel) {
11324
+ invalidated = true;
11325
+ }
11326
+ if (activeSell && checkCandle && invalidationLevel != null && checkCandle.high > invalidationLevel) {
11327
+ invalidated = true;
11328
+ }
11329
+ if (invalidated) {
11330
+ activeBuy = false;
11331
+ activeSell = false;
11332
+ }
11333
+ const displayedKcMidline = showKeltnerChannel ? kcMidline : null;
11334
+ const displayedKcUpper = showKeltnerChannel ? kcUpper : null;
11335
+ const displayedKcLower = showKeltnerChannel ? kcLower : null;
11336
+ const displayedInvalidationLevel = showInvalidationLevels ? invalidationLevel : null;
11337
+ pushPlotPoint(plotSeries, "signalOsc", candle, signalOsc);
11338
+ pushPlotPoint(plotSeries, "kcMidline", candle, displayedKcMidline);
11339
+ pushPlotPoint(plotSeries, "kcUpper", candle, displayedKcUpper);
11340
+ pushPlotPoint(plotSeries, "kcLower", candle, displayedKcLower);
11341
+ pushPlotPoint(
11342
+ plotSeries,
11343
+ "invalidationLevel",
11344
+ candle,
11345
+ displayedInvalidationLevel
11346
+ );
11347
+ const currentLineValues = {};
11348
+ for (const plotName of linePlots) {
11349
+ switch (plotName) {
11350
+ case "signalOsc":
11351
+ currentLineValues[plotName] = signalOsc;
11352
+ break;
11353
+ case "kcMidline":
11354
+ currentLineValues[plotName] = displayedKcMidline;
11355
+ break;
11356
+ case "kcUpper":
11357
+ currentLineValues[plotName] = displayedKcUpper;
11358
+ break;
11359
+ case "kcLower":
11360
+ currentLineValues[plotName] = displayedKcLower;
11361
+ break;
11362
+ case "invalidationLevel":
11363
+ currentLineValues[plotName] = displayedInvalidationLevel;
11364
+ break;
11365
+ default:
11366
+ currentLineValues[plotName] = null;
11367
+ break;
11368
+ }
11369
+ }
11370
+ lastSnapshot = {
11371
+ entryLong,
11372
+ entryShort,
11373
+ invalidated,
11374
+ activeBuy,
11375
+ activeSell,
11376
+ signalOsc: toFinite(signalOsc),
11377
+ kcMidline: toFinite(displayedKcMidline),
11378
+ kcUpper: toFinite(displayedKcUpper),
11379
+ kcLower: toFinite(displayedKcLower),
11380
+ invalidationLevel: toFinite(displayedInvalidationLevel),
11381
+ lineValues: currentLineValues
11382
+ };
11383
+ }
11384
+ return {
11385
+ snapshot: lastSnapshot,
11386
+ plotSeries
11387
+ };
11388
+ };
11389
+
11002
11390
  // src/AdaptiveMomentumRibbon/figures.ts
11003
- import {
11004
- asFiniteNumber,
11005
- getPinePlotSeries
11006
- } from "@tradejs/node/pine";
11007
11391
  var DEFAULT_COLORS = ["#2962ff", "#f23645", "#089981", "#f59e0b"];
11008
11392
  var LINE_STYLE_BY_PLOT = {
11009
11393
  kcMidline: {
@@ -11032,18 +11416,18 @@ var toFigurePoints = (series, maxPoints) => {
11032
11416
  const points = [];
11033
11417
  for (let i = start; i < series.length; i += 1) {
11034
11418
  const item = series[i];
11035
- const timestamp = asFiniteNumber(item?.time);
11036
- const value = asFiniteNumber(item?.value);
11037
- if (timestamp == null || value == null) continue;
11419
+ if (!Number.isFinite(item?.time) || !Number.isFinite(item?.value)) {
11420
+ continue;
11421
+ }
11038
11422
  points.push({
11039
- timestamp,
11040
- value
11423
+ timestamp: item.time,
11424
+ value: item.value
11041
11425
  });
11042
11426
  }
11043
11427
  return points;
11044
11428
  };
11045
11429
  var buildAdaptiveMomentumRibbonFigures = ({
11046
- pineContext,
11430
+ plotSeries,
11047
11431
  linePlots,
11048
11432
  direction,
11049
11433
  entryTimestamp,
@@ -11051,7 +11435,7 @@ var buildAdaptiveMomentumRibbonFigures = ({
11051
11435
  maxPoints = 180
11052
11436
  }) => {
11053
11437
  const lines = linePlots.map((plotName, index) => {
11054
- const series = getPinePlotSeries(pineContext, plotName);
11438
+ const series = plotSeries[plotName] ?? [];
11055
11439
  const points = toFigurePoints(series, maxPoints);
11056
11440
  if (!points.length) {
11057
11441
  return null;
@@ -11084,70 +11468,17 @@ var buildAdaptiveMomentumRibbonFigures = ({
11084
11468
  };
11085
11469
 
11086
11470
  // src/AdaptiveMomentumRibbon/core.ts
11087
- var AMR_PINE_FILE_NAME = "adaptiveMomentumRibbon.pine";
11088
- var asKcMaType = (value) => {
11089
- if (value === "SMA" || value === "EMA" || value === "SMMA (RMA)" || value === "WMA" || value === "VWMA") {
11090
- return value;
11091
- }
11092
- return "EMA";
11093
- };
11094
- var resolveAmrInputs = (config2) => ({
11095
- "Momentum Period": asPositiveInt(config2.AMR_MOMENTUM_PERIOD, 20),
11096
- "Butterworth Smoothing": asPositiveInt(config2.AMR_BUTTERWORTH_SMOOTHING, 3),
11097
- "Confirm Signals on Bar Close": Boolean(config2.AMR_WAIT_CLOSE),
11098
- "Show Invalidation Levels": Boolean(config2.AMR_SHOW_INVALIDATION_LEVELS),
11099
- "Show Keltner Channel": Boolean(config2.AMR_SHOW_KELTNER_CHANNEL),
11100
- "KC Length": asPositiveInt(config2.AMR_KC_LENGTH, 20),
11101
- "KC MA Type": asKcMaType(config2.AMR_KC_MA_TYPE),
11102
- "ATR Length": asPositiveInt(config2.AMR_ATR_LENGTH, 14),
11103
- "ATR Multiplier": asPositiveNumber(config2.AMR_ATR_MULTIPLIER, 2)
11104
- });
11105
11471
  var resolveLinePlots = (value) => {
11106
11472
  if (!Array.isArray(value)) {
11107
11473
  return [];
11108
11474
  }
11109
11475
  return value.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0);
11110
11476
  };
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
- var readAmrSnapshot = (pineContext, linePlots) => {
11120
- const lineValues = Object.fromEntries(
11121
- linePlots.map((plotName) => [
11122
- plotName,
11123
- readNumericPlot(pineContext, plotName)
11124
- ])
11125
- );
11126
- 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
11138
- };
11139
- };
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;
11477
+ var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, strategyApi }) => {
11478
+ const { LONG, SHORT, AMR_EXIT_ON_INVALIDATION, MAX_LOSS_VALUE, FEE_PERCENT } = config2;
11143
11479
  const linePlots = resolveLinePlots(config2.AMR_LINE_PLOTS);
11144
- const lookbackBars = asPositiveInt(config2.AMR_LOOKBACK_BARS, 0);
11145
- const pineInputs = resolveAmrInputs(config2);
11146
- const timeframe = String(config2.INTERVAL ?? "15");
11480
+ const lookbackBars = asPositiveInt2(config2.AMR_LOOKBACK_BARS, 0);
11147
11481
  return async () => {
11148
- if (!script) {
11149
- return strategyApi.skip("AMR_SCRIPT_EMPTY");
11150
- }
11151
11482
  const { fullData, currentPrice, timestamp } = await strategyApi.getMarketData();
11152
11483
  if (fullData.length < 2) {
11153
11484
  return strategyApi.skip("WAIT_DATA");
@@ -11156,27 +11487,25 @@ var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, loadPin
11156
11487
  const positionExists = Boolean(
11157
11488
  position && typeof position.qty === "number" && position.qty > 0
11158
11489
  );
11159
- const candles = getLookbackCandles(fullData, lookbackBars);
11160
- let pineContext;
11490
+ const candles = lookbackBars > 0 ? fullData.slice(-lookbackBars) : fullData;
11491
+ let evaluation;
11161
11492
  try {
11162
- pineContext = await runPineScript({
11493
+ evaluation = evaluateAdaptiveMomentumRibbon({
11163
11494
  candles,
11164
- script,
11165
- symbol,
11166
- timeframe,
11167
- inputs: pineInputs
11495
+ config: config2,
11496
+ linePlots
11168
11497
  });
11169
11498
  } catch (error) {
11170
11499
  if (typeof globalThis.setImmediate === "function") {
11171
11500
  logger.warn(
11172
- "AdaptiveMomentumRibbon pine run failed for %s: %s",
11501
+ "AdaptiveMomentumRibbon evaluation failed for %s: %s",
11173
11502
  symbol,
11174
11503
  String(error)
11175
11504
  );
11176
11505
  }
11177
- return strategyApi.skip("AMR_SCRIPT_FAILED");
11506
+ return strategyApi.skip("AMR_EVALUATION_FAILED");
11178
11507
  }
11179
- const amr = readAmrSnapshot(pineContext, linePlots);
11508
+ const { snapshot: amr, plotSeries } = evaluation;
11180
11509
  if (amr.entryLong && amr.entryShort) {
11181
11510
  return strategyApi.skip("AMR_SIGNAL_CONFLICT");
11182
11511
  }
@@ -11217,7 +11546,9 @@ var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, loadPin
11217
11546
  direction: modeConfig.direction,
11218
11547
  takeProfitDelta: modeConfig.TP,
11219
11548
  stopLossDelta: modeConfig.SL,
11220
- unit: "percent"
11549
+ unit: "percent",
11550
+ maxLossValue: MAX_LOSS_VALUE,
11551
+ feePercent: Number(FEE_PERCENT ?? 0)
11221
11552
  });
11222
11553
  if (!qty || !Number.isFinite(qty) || qty <= 0) {
11223
11554
  return strategyApi.skip("INVALID_QTY");
@@ -11226,14 +11557,39 @@ var createAdaptiveMomentumRibbonCore = async ({ config: config2, symbol, loadPin
11226
11557
  code: amr.entryLong ? "AMR_ENTRY_LONG" : "AMR_ENTRY_SHORT",
11227
11558
  direction: modeConfig.direction,
11228
11559
  figures: buildAdaptiveMomentumRibbonFigures({
11229
- pineContext,
11560
+ plotSeries,
11230
11561
  linePlots,
11231
11562
  direction: modeConfig.direction,
11232
11563
  entryTimestamp: timestamp,
11233
11564
  entryPrice: currentPrice
11234
11565
  }),
11235
11566
  additionalIndicators: {
11236
- amr
11567
+ amr,
11568
+ amrSignalTiming: {
11569
+ entryTiming: "zero_cross",
11570
+ waitClose: Boolean(config2.AMR_WAIT_CLOSE),
11571
+ confirmOnNextBar: Boolean(config2.AMR_CONFIRM_ON_NEXT_BAR),
11572
+ lookbackBars
11573
+ },
11574
+ amrConfigSnapshot: {
11575
+ momentumPeriod: asPositiveInt2(config2.AMR_MOMENTUM_PERIOD, 20),
11576
+ butterworthSmoothing: asPositiveInt2(
11577
+ config2.AMR_BUTTERWORTH_SMOOTHING,
11578
+ 3
11579
+ ),
11580
+ minSignalOscAbs: asPositiveNumber2(
11581
+ config2.AMR_MIN_SIGNAL_OSC_ABS,
11582
+ 0.55
11583
+ ),
11584
+ requireKcBias: Boolean(config2.AMR_REQUIRE_KC_BIAS),
11585
+ minBarsBetweenSignals: asPositiveInt2(
11586
+ config2.AMR_MIN_BARS_BETWEEN_SIGNALS,
11587
+ 12
11588
+ ),
11589
+ kcLength: asPositiveInt2(config2.AMR_KC_LENGTH, 20),
11590
+ atrLength: asPositiveInt2(config2.AMR_ATR_LENGTH, 14),
11591
+ atrMultiplier: asPositiveNumber2(config2.AMR_ATR_MULTIPLIER, 2)
11592
+ }
11237
11593
  },
11238
11594
  orderPlan: {
11239
11595
  qty,