@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.
- package/dist/chunk-37ZWRG3W.mjs +128 -0
- package/dist/chunk-H4MHFD4B.mjs +62 -0
- package/dist/chunk-IMLNXICX.mjs +547 -0
- package/dist/chunk-QVWMBYYM.mjs +836 -0
- package/dist/chunk-SOVTOGY4.mjs +163 -0
- package/dist/chunk-TDUTYEGH.mjs +797 -0
- package/dist/chunk-UK4YHD2E.mjs +683 -0
- package/dist/index.d.mts +325 -4
- package/dist/index.d.ts +325 -4
- package/dist/index.js +5689 -1195
- package/dist/index.mjs +61 -14
- package/dist/{strategy-ZVNTA6UR.mjs → strategy-ABIO65CR.mjs} +3 -46
- package/dist/strategy-D7H5J3C4.mjs +348 -0
- package/dist/strategy-HQIPCUOY.mjs +399 -0
- package/dist/{strategy-Y4SOK6FF.mjs → strategy-JQIJILHQ.mjs} +28 -109
- package/dist/strategy-QEIPAPY4.mjs +373 -0
- package/dist/strategy-WYN4FZ5S.mjs +613 -0
- package/dist/{strategy-UHRWSGZC.mjs → strategy-XTUKPYPM.mjs} +484 -128
- package/package.json +5 -5
- package/dist/chunk-3PN7ZSJJ.mjs +0 -27
- package/dist/chunk-MVIV5ZII.mjs +0 -137
- package/dist/chunk-RDK2JK3K.mjs +0 -28
- package/dist/chunk-RYEPHOGL.mjs +0 -28
- package/dist/chunk-ULLCAH5C.mjs +0 -67
- package/dist/strategy-M3BRWDRR.mjs +0 -377
- package/dist/strategy-UZBWST3G.mjs +0 -156
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
adaptiveMomentumRibbonManifest
|
|
3
|
-
|
|
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
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
11112
|
-
|
|
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 =
|
|
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 =
|
|
11160
|
-
let
|
|
11490
|
+
const candles = lookbackBars > 0 ? fullData.slice(-lookbackBars) : fullData;
|
|
11491
|
+
let evaluation;
|
|
11161
11492
|
try {
|
|
11162
|
-
|
|
11493
|
+
evaluation = evaluateAdaptiveMomentumRibbon({
|
|
11163
11494
|
candles,
|
|
11164
|
-
|
|
11165
|
-
|
|
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
|
|
11501
|
+
"AdaptiveMomentumRibbon evaluation failed for %s: %s",
|
|
11173
11502
|
symbol,
|
|
11174
11503
|
String(error)
|
|
11175
11504
|
);
|
|
11176
11505
|
}
|
|
11177
|
-
return strategyApi.skip("
|
|
11506
|
+
return strategyApi.skip("AMR_EVALUATION_FAILED");
|
|
11178
11507
|
}
|
|
11179
|
-
const amr =
|
|
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
|
-
|
|
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,
|