@tradejs/core 1.0.0
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/README.md +60 -0
- package/dist/api.d.mts +7 -0
- package/dist/api.d.ts +7 -0
- package/dist/api.js +64 -0
- package/dist/api.mjs +39 -0
- package/dist/async.d.mts +4 -0
- package/dist/async.d.ts +4 -0
- package/dist/async.js +48 -0
- package/dist/async.mjs +20 -0
- package/dist/backtest.d.mts +45 -0
- package/dist/backtest.d.ts +45 -0
- package/dist/backtest.js +574 -0
- package/dist/backtest.mjs +355 -0
- package/dist/chunk-AYC2QVKI.mjs +35 -0
- package/dist/chunk-JG2QPVAV.mjs +190 -0
- package/dist/chunk-LIGD3WWX.mjs +1545 -0
- package/dist/chunk-M7QGVZ3J.mjs +61 -0
- package/dist/chunk-NQ7D3T4E.mjs +10 -0
- package/dist/chunk-PXLXXXLA.mjs +67 -0
- package/dist/config.d.mts +14 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +49 -0
- package/dist/config.mjs +21 -0
- package/dist/constants.d.mts +41 -0
- package/dist/constants.d.ts +41 -0
- package/dist/constants.js +238 -0
- package/dist/constants.mjs +50 -0
- package/dist/data.d.mts +9 -0
- package/dist/data.d.ts +9 -0
- package/dist/data.js +100 -0
- package/dist/data.mjs +12 -0
- package/dist/figures.d.mts +103 -0
- package/dist/figures.d.ts +103 -0
- package/dist/figures.js +274 -0
- package/dist/figures.mjs +239 -0
- package/dist/indicators-x3xKl3_W.d.mts +90 -0
- package/dist/indicators-x3xKl3_W.d.ts +90 -0
- package/dist/indicators.d.mts +124 -0
- package/dist/indicators.d.ts +124 -0
- package/dist/indicators.js +1631 -0
- package/dist/indicators.mjs +66 -0
- package/dist/json.d.mts +3 -0
- package/dist/json.d.ts +3 -0
- package/dist/json.js +34 -0
- package/dist/json.mjs +7 -0
- package/dist/math.d.mts +35 -0
- package/dist/math.d.ts +35 -0
- package/dist/math.js +98 -0
- package/dist/math.mjs +38 -0
- package/dist/pine.d.mts +29 -0
- package/dist/pine.d.ts +29 -0
- package/dist/pine.js +59 -0
- package/dist/pine.mjs +29 -0
- package/dist/strategies.d.mts +104 -0
- package/dist/strategies.d.ts +104 -0
- package/dist/strategies.js +1080 -0
- package/dist/strategies.mjs +390 -0
- package/dist/tickers.d.mts +7 -0
- package/dist/tickers.d.ts +7 -0
- package/dist/tickers.js +166 -0
- package/dist/tickers.mjs +125 -0
- package/dist/time-DEyFa2vI.d.mts +11 -0
- package/dist/time-DEyFa2vI.d.ts +11 -0
- package/dist/time.d.mts +2 -0
- package/dist/time.d.ts +2 -0
- package/dist/time.js +58 -0
- package/dist/time.mjs +15 -0
- package/package.json +99 -0
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/strategies.ts
|
|
31
|
+
var strategies_exports = {};
|
|
32
|
+
__export(strategies_exports, {
|
|
33
|
+
buildDefaultIndicatorPeriods: () => buildDefaultIndicatorPeriods,
|
|
34
|
+
buildEntrySignalDecision: () => buildEntrySignalDecision,
|
|
35
|
+
buildStrategySignal: () => buildStrategySignal,
|
|
36
|
+
calculateRiskRatio: () => calculateRiskRatio,
|
|
37
|
+
createLastTradeController: () => createLastTradeController,
|
|
38
|
+
createStrategyAPI: () => createStrategyAPI,
|
|
39
|
+
createStrategyIndicatorsState: () => createStrategyIndicatorsState,
|
|
40
|
+
getDirectionalTpSlPrices: () => getDirectionalTpSlPrices,
|
|
41
|
+
getStrategyMarketSnapshot: () => getStrategyMarketSnapshot,
|
|
42
|
+
mapAiRuntimeFromConfig: () => mapAiRuntimeFromConfig,
|
|
43
|
+
mapMlRuntimeFromConfig: () => mapMlRuntimeFromConfig
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(strategies_exports);
|
|
46
|
+
|
|
47
|
+
// src/utils/math.ts
|
|
48
|
+
var import_lodash = __toESM(require("lodash"));
|
|
49
|
+
var round = (value, precision = 2) => precision > 0 ? Math.round(value * 10 ** precision) / 10 ** precision : Math.round(value);
|
|
50
|
+
|
|
51
|
+
// src/utils/correlation.ts
|
|
52
|
+
var alignSortedCandlesByTimestamp = (coinCandles, btcCandles) => {
|
|
53
|
+
const alignedCoinCandles = [];
|
|
54
|
+
const alignedBtcCandles = [];
|
|
55
|
+
let coinIndex = 0;
|
|
56
|
+
let btcIndex = 0;
|
|
57
|
+
while (coinIndex < coinCandles.length && btcIndex < btcCandles.length) {
|
|
58
|
+
const coinTimestampMs = coinCandles[coinIndex].timestamp;
|
|
59
|
+
const btcTimestampMs = btcCandles[btcIndex].timestamp;
|
|
60
|
+
if (coinTimestampMs === btcTimestampMs) {
|
|
61
|
+
alignedCoinCandles.push(coinCandles[coinIndex]);
|
|
62
|
+
alignedBtcCandles.push(btcCandles[btcIndex]);
|
|
63
|
+
coinIndex += 1;
|
|
64
|
+
btcIndex += 1;
|
|
65
|
+
} else if (coinTimestampMs < btcTimestampMs) {
|
|
66
|
+
coinIndex += 1;
|
|
67
|
+
} else {
|
|
68
|
+
btcIndex += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
alignedCoinCandles,
|
|
73
|
+
alignedBtcCandles
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
var buildReturnsFromCandles = (candles) => {
|
|
77
|
+
const returns = [];
|
|
78
|
+
for (let candleIndex = 1; candleIndex < candles.length; candleIndex += 1) {
|
|
79
|
+
const previousClose = candles[candleIndex - 1].close;
|
|
80
|
+
const currentClose = candles[candleIndex].close;
|
|
81
|
+
if (!Number.isFinite(previousClose) || !Number.isFinite(currentClose)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const change = (currentClose - previousClose) / previousClose;
|
|
85
|
+
returns.push(change);
|
|
86
|
+
}
|
|
87
|
+
return returns;
|
|
88
|
+
};
|
|
89
|
+
var calculatePearsonCorrelation = (firstSeries, secondSeries) => {
|
|
90
|
+
if (firstSeries.length !== secondSeries.length) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"calculatePearsonCorrelation: series lengths are different"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const length = firstSeries.length;
|
|
96
|
+
if (length === 0) return null;
|
|
97
|
+
if (length === 1) return null;
|
|
98
|
+
const sumFirst = firstSeries.reduce((sum, value) => sum + value, 0);
|
|
99
|
+
const sumSecond = secondSeries.reduce((sum, value) => sum + value, 0);
|
|
100
|
+
const meanFirst = sumFirst / length;
|
|
101
|
+
const meanSecond = sumSecond / length;
|
|
102
|
+
let covariance = 0;
|
|
103
|
+
let varianceFirst = 0;
|
|
104
|
+
let varianceSecond = 0;
|
|
105
|
+
for (let index = 0; index < length; index += 1) {
|
|
106
|
+
const deltaFirst = firstSeries[index] - meanFirst;
|
|
107
|
+
const deltaSecond = secondSeries[index] - meanSecond;
|
|
108
|
+
covariance += deltaFirst * deltaSecond;
|
|
109
|
+
varianceFirst += deltaFirst * deltaFirst;
|
|
110
|
+
varianceSecond += deltaSecond * deltaSecond;
|
|
111
|
+
}
|
|
112
|
+
if (varianceFirst === 0 || varianceSecond === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const correlation = covariance / Math.sqrt(varianceFirst * varianceSecond);
|
|
116
|
+
return correlation;
|
|
117
|
+
};
|
|
118
|
+
var calculateCoinBtcCorrelation = (coinCandles, btcCandles) => {
|
|
119
|
+
const { alignedCoinCandles, alignedBtcCandles } = alignSortedCandlesByTimestamp(coinCandles, btcCandles);
|
|
120
|
+
const MIN_LENGTH_FOR_CORRELATION = 10;
|
|
121
|
+
if (alignedCoinCandles.length <= MIN_LENGTH_FOR_CORRELATION) {
|
|
122
|
+
return {
|
|
123
|
+
correlation: null,
|
|
124
|
+
alignedCoinCandles,
|
|
125
|
+
alignedBtcCandles,
|
|
126
|
+
coinReturns: [],
|
|
127
|
+
btcReturns: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const coinReturns = buildReturnsFromCandles(alignedCoinCandles);
|
|
131
|
+
const btcReturns = buildReturnsFromCandles(alignedBtcCandles);
|
|
132
|
+
const minReturnsLength = Math.min(coinReturns.length, btcReturns.length);
|
|
133
|
+
const slicedCoinReturns = coinReturns.slice(-minReturnsLength);
|
|
134
|
+
const slicedBtcReturns = btcReturns.slice(-minReturnsLength);
|
|
135
|
+
const correlation = calculatePearsonCorrelation(
|
|
136
|
+
slicedCoinReturns,
|
|
137
|
+
slicedBtcReturns
|
|
138
|
+
);
|
|
139
|
+
return {
|
|
140
|
+
correlation: correlation ? round(correlation) : correlation,
|
|
141
|
+
alignedCoinCandles,
|
|
142
|
+
alignedBtcCandles,
|
|
143
|
+
coinReturns: slicedCoinReturns,
|
|
144
|
+
btcReturns: slicedBtcReturns
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/utils/indicators.ts
|
|
149
|
+
var import_technicalindicators = require("technicalindicators");
|
|
150
|
+
|
|
151
|
+
// src/constants/index.ts
|
|
152
|
+
var FEE_PERCENT = 5e-3;
|
|
153
|
+
var CORRELATION_WINDOW = 50;
|
|
154
|
+
var ML_BASE_CANDLES_WINDOW = 50;
|
|
155
|
+
|
|
156
|
+
// src/utils/array.ts
|
|
157
|
+
var import_lodash2 = __toESM(require("lodash"));
|
|
158
|
+
var cloneArrayValues = (record) => Object.fromEntries(
|
|
159
|
+
Object.entries(record).map(([key, value]) => [
|
|
160
|
+
key,
|
|
161
|
+
Array.isArray(value) ? value.slice() : value
|
|
162
|
+
])
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// src/utils/indicatorPlugins.ts
|
|
166
|
+
var pluginIndicatorEntries = /* @__PURE__ */ new Map();
|
|
167
|
+
var getRegisteredIndicatorEntries = () => [
|
|
168
|
+
...pluginIndicatorEntries.values()
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// src/utils/spread.ts
|
|
172
|
+
var DEFAULT_SPREAD_WINDOW = 50;
|
|
173
|
+
var toFinitePrice = (value) => {
|
|
174
|
+
if (value == null) return null;
|
|
175
|
+
const num = Number(value);
|
|
176
|
+
return Number.isFinite(num) ? num : null;
|
|
177
|
+
};
|
|
178
|
+
var toFiniteSpread = (value) => {
|
|
179
|
+
if (value == null) return null;
|
|
180
|
+
const num = Number(value);
|
|
181
|
+
return Number.isFinite(num) ? num : null;
|
|
182
|
+
};
|
|
183
|
+
var createSpreadSmoother = (window = DEFAULT_SPREAD_WINDOW) => {
|
|
184
|
+
const binanceWindow = [];
|
|
185
|
+
const coinbaseWindow = [];
|
|
186
|
+
let binanceSum = 0;
|
|
187
|
+
let coinbaseSum = 0;
|
|
188
|
+
const next = (params) => {
|
|
189
|
+
const binance = toFinitePrice(params.binancePrice);
|
|
190
|
+
const coinbase = toFinitePrice(params.coinbasePrice);
|
|
191
|
+
if (binance != null && coinbase != null) {
|
|
192
|
+
binanceWindow.push(binance);
|
|
193
|
+
coinbaseWindow.push(coinbase);
|
|
194
|
+
binanceSum += binance;
|
|
195
|
+
coinbaseSum += coinbase;
|
|
196
|
+
if (binanceWindow.length > window) {
|
|
197
|
+
binanceSum -= binanceWindow.shift() ?? 0;
|
|
198
|
+
coinbaseSum -= coinbaseWindow.shift() ?? 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let spread = toFiniteSpread(params.fallbackSpread);
|
|
202
|
+
if (binanceWindow.length > 0 && coinbaseWindow.length > 0) {
|
|
203
|
+
const avgBinance = binanceSum / binanceWindow.length;
|
|
204
|
+
const avgCoinbase = coinbaseSum / coinbaseWindow.length;
|
|
205
|
+
if (Number.isFinite(avgBinance) && avgBinance > 0) {
|
|
206
|
+
spread = (avgCoinbase - avgBinance) / avgBinance;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return toFiniteSpread(spread);
|
|
210
|
+
};
|
|
211
|
+
return { next };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/utils/indicators.ts
|
|
215
|
+
var CANDLE_WINDOW = ML_BASE_CANDLES_WINDOW;
|
|
216
|
+
var BASE_INTERVAL_MINUTES = 15;
|
|
217
|
+
var INDICATOR_TIMEFRAMES = [
|
|
218
|
+
{ minutes: 60, suffix: "1h" },
|
|
219
|
+
{ minutes: 240, suffix: "4h" },
|
|
220
|
+
{ minutes: 1440, suffix: "1d" }
|
|
221
|
+
];
|
|
222
|
+
var DEFAULT_INDICATOR_PERIODS = {
|
|
223
|
+
maFast: 14,
|
|
224
|
+
maMedium: 49,
|
|
225
|
+
maSlow: 50,
|
|
226
|
+
obvSma: 10,
|
|
227
|
+
atr: 14,
|
|
228
|
+
atrPctShort: 7,
|
|
229
|
+
atrPctLong: 30,
|
|
230
|
+
bb: 20,
|
|
231
|
+
bbStd: 2,
|
|
232
|
+
macdFast: 12,
|
|
233
|
+
macdSlow: 26,
|
|
234
|
+
macdSignal: 9,
|
|
235
|
+
levelLookback: 20,
|
|
236
|
+
levelDelay: 2
|
|
237
|
+
};
|
|
238
|
+
var ONE_HOUR_MS = 36e5;
|
|
239
|
+
var ONE_DAY_MS = 864e5;
|
|
240
|
+
var toMlCandle = (candle) => ({
|
|
241
|
+
open: Number(candle.open) || 0,
|
|
242
|
+
high: Number(candle.high) || 0,
|
|
243
|
+
low: Number(candle.low) || 0,
|
|
244
|
+
close: Number(candle.close) || 0,
|
|
245
|
+
volume: Number(candle.volume) || 0,
|
|
246
|
+
turnover: Number(candle.turnover) || 0,
|
|
247
|
+
timestamp: Number(candle.timestamp) || 0
|
|
248
|
+
});
|
|
249
|
+
var resampleCandles = (candles, targetMinutes) => {
|
|
250
|
+
if (targetMinutes <= BASE_INTERVAL_MINUTES) return candles.map(toMlCandle);
|
|
251
|
+
const bucketMs = targetMinutes * 6e4;
|
|
252
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
253
|
+
for (const raw of candles) {
|
|
254
|
+
const candle = toMlCandle(raw);
|
|
255
|
+
const ts = candle.timestamp;
|
|
256
|
+
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
257
|
+
const bucket = Math.floor(ts / bucketMs) * bucketMs;
|
|
258
|
+
const current = buckets.get(bucket);
|
|
259
|
+
if (!current) {
|
|
260
|
+
buckets.set(bucket, { ...candle, timestamp: bucket });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
current.high = Math.max(current.high, candle.high);
|
|
264
|
+
current.low = Math.min(current.low, candle.low);
|
|
265
|
+
current.close = candle.close;
|
|
266
|
+
current.volume += candle.volume;
|
|
267
|
+
current.turnover += candle.turnover;
|
|
268
|
+
}
|
|
269
|
+
return [...buckets.entries()].sort((a, b) => a[0] - b[0]).map(([, candle]) => candle);
|
|
270
|
+
};
|
|
271
|
+
var buildMlCandleIndicators = (candles, btcCandles) => ({
|
|
272
|
+
candles15m: candles.slice(-CANDLE_WINDOW).map(toMlCandle),
|
|
273
|
+
candles1h: resampleCandles(candles, 60).slice(-CANDLE_WINDOW),
|
|
274
|
+
candles4h: resampleCandles(candles, 240).slice(-CANDLE_WINDOW),
|
|
275
|
+
candles1d: resampleCandles(candles, 1440).slice(-CANDLE_WINDOW),
|
|
276
|
+
btcCandles15m: btcCandles.slice(-CANDLE_WINDOW).map(toMlCandle),
|
|
277
|
+
btcCandles1h: resampleCandles(btcCandles, 60).slice(-CANDLE_WINDOW),
|
|
278
|
+
btcCandles4h: resampleCandles(btcCandles, 240).slice(-CANDLE_WINDOW),
|
|
279
|
+
btcCandles1d: resampleCandles(btcCandles, 1440).slice(-CANDLE_WINDOW)
|
|
280
|
+
});
|
|
281
|
+
var percentChange = (current, previous) => {
|
|
282
|
+
if (!Number.isFinite(current) || !Number.isFinite(previous) || previous === 0) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return (current - previous) / previous * 100;
|
|
286
|
+
};
|
|
287
|
+
var applyIndicatorsToHistory = (indicators, pushIndicator) => {
|
|
288
|
+
pushIndicator("maFast", indicators.maFast);
|
|
289
|
+
pushIndicator("maMedium", indicators.maMedium);
|
|
290
|
+
pushIndicator("maSlow", indicators.maSlow);
|
|
291
|
+
pushIndicator("atr", indicators.atr);
|
|
292
|
+
pushIndicator("atrPct", indicators.atrPct);
|
|
293
|
+
pushIndicator("bbUpper", indicators.bbUpper);
|
|
294
|
+
pushIndicator("bbMiddle", indicators.bbMiddle);
|
|
295
|
+
pushIndicator("bbLower", indicators.bbLower);
|
|
296
|
+
pushIndicator("obv", indicators.obv);
|
|
297
|
+
pushIndicator("smaObv", indicators.smaObv);
|
|
298
|
+
pushIndicator("macd", indicators.macd);
|
|
299
|
+
pushIndicator("macdSignal", indicators.macdSignal);
|
|
300
|
+
pushIndicator("macdHistogram", indicators.macdHistogram);
|
|
301
|
+
pushIndicator("price24hPcnt", indicators.price24hPcnt ?? void 0);
|
|
302
|
+
pushIndicator("price1hPcnt", indicators.price1hPcnt ?? void 0);
|
|
303
|
+
pushIndicator("highPrice1h", indicators.highPrice1h ?? void 0);
|
|
304
|
+
pushIndicator("lowPrice1h", indicators.lowPrice1h ?? void 0);
|
|
305
|
+
pushIndicator("volume1h", indicators.volume1h ?? void 0);
|
|
306
|
+
pushIndicator("highPrice24h", indicators.highPrice24h ?? void 0);
|
|
307
|
+
pushIndicator("lowPrice24h", indicators.lowPrice24h ?? void 0);
|
|
308
|
+
pushIndicator("volume24h", indicators.volume24h ?? void 0);
|
|
309
|
+
pushIndicator("highLevel", indicators.highLevel ?? void 0);
|
|
310
|
+
pushIndicator("lowLevel", indicators.lowLevel ?? void 0);
|
|
311
|
+
pushIndicator("prevClose", indicators.prevClose ?? void 0);
|
|
312
|
+
pushIndicator("correlation", indicators.correlation ?? void 0);
|
|
313
|
+
pushIndicator("spread", indicators.spread ?? void 0);
|
|
314
|
+
};
|
|
315
|
+
var createIndicators = (data, btcData = [], options = {}) => {
|
|
316
|
+
const indicatorPluginEntries = getRegisteredIndicatorEntries();
|
|
317
|
+
const includeMlPayload = options.includeMlPayload !== false;
|
|
318
|
+
const indicatorPeriods = {
|
|
319
|
+
...DEFAULT_INDICATOR_PERIODS,
|
|
320
|
+
...options.periods || {}
|
|
321
|
+
};
|
|
322
|
+
const closes = [];
|
|
323
|
+
const highs = [];
|
|
324
|
+
const lows = [];
|
|
325
|
+
const volumes = [];
|
|
326
|
+
const timestamps = [];
|
|
327
|
+
const candlesHistory = [];
|
|
328
|
+
const btcCandlesHistory = [];
|
|
329
|
+
const btcBinanceCandles = (options.btcBinanceData ?? []).map(toMlCandle);
|
|
330
|
+
const btcCoinbaseCandles = (options.btcCoinbaseData ?? []).map(toMlCandle);
|
|
331
|
+
const spreadSmoother = createSpreadSmoother();
|
|
332
|
+
let btcBinanceCursor = 0;
|
|
333
|
+
let btcCoinbaseCursor = 0;
|
|
334
|
+
const obv = new import_technicalindicators.OBV({ close: [], volume: [] });
|
|
335
|
+
const smaObv = new import_technicalindicators.SMA({ period: indicatorPeriods.obvSma, values: [] });
|
|
336
|
+
const ma14 = new import_technicalindicators.SMA({ period: indicatorPeriods.maFast, values: [] });
|
|
337
|
+
const ma49 = new import_technicalindicators.SMA({ period: indicatorPeriods.maMedium, values: [] });
|
|
338
|
+
const ma50 = new import_technicalindicators.SMA({ period: indicatorPeriods.maSlow, values: [] });
|
|
339
|
+
const atr = new import_technicalindicators.ATR({
|
|
340
|
+
period: indicatorPeriods.atr,
|
|
341
|
+
high: [],
|
|
342
|
+
low: [],
|
|
343
|
+
close: []
|
|
344
|
+
});
|
|
345
|
+
const atrPctShort = new import_technicalindicators.SMA({
|
|
346
|
+
period: indicatorPeriods.atrPctShort,
|
|
347
|
+
values: []
|
|
348
|
+
});
|
|
349
|
+
const atrPctLong = new import_technicalindicators.SMA({
|
|
350
|
+
period: indicatorPeriods.atrPctLong,
|
|
351
|
+
values: []
|
|
352
|
+
});
|
|
353
|
+
const bb = new import_technicalindicators.BollingerBands({
|
|
354
|
+
period: indicatorPeriods.bb,
|
|
355
|
+
values: [],
|
|
356
|
+
stdDev: indicatorPeriods.bbStd
|
|
357
|
+
});
|
|
358
|
+
const macd = new import_technicalindicators.MACD({
|
|
359
|
+
fastPeriod: indicatorPeriods.macdFast,
|
|
360
|
+
slowPeriod: indicatorPeriods.macdSlow,
|
|
361
|
+
signalPeriod: indicatorPeriods.macdSignal,
|
|
362
|
+
values: [],
|
|
363
|
+
SimpleMAOscillator: false,
|
|
364
|
+
SimpleMASignal: false
|
|
365
|
+
});
|
|
366
|
+
const indicatorHistory = {};
|
|
367
|
+
const indicatorPluginErrorShown = /* @__PURE__ */ new Set();
|
|
368
|
+
const pushIndicator = (key, value) => {
|
|
369
|
+
if (value == null) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!indicatorHistory[key]) {
|
|
373
|
+
indicatorHistory[key] = [];
|
|
374
|
+
}
|
|
375
|
+
indicatorHistory[key].push(value);
|
|
376
|
+
if (indicatorHistory[key].length > ML_BASE_CANDLES_WINDOW) {
|
|
377
|
+
indicatorHistory[key].splice(
|
|
378
|
+
0,
|
|
379
|
+
indicatorHistory[key].length - ML_BASE_CANDLES_WINDOW
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const resolveCloseAtOrBefore = (candles, cursor, targetTs) => {
|
|
384
|
+
let idx = cursor;
|
|
385
|
+
while (idx + 1 < candles.length && candles[idx + 1].timestamp <= targetTs) {
|
|
386
|
+
idx += 1;
|
|
387
|
+
}
|
|
388
|
+
const close = idx < candles.length && candles[idx].timestamp <= targetTs ? candles[idx].close : null;
|
|
389
|
+
return { close, cursor: idx };
|
|
390
|
+
};
|
|
391
|
+
let window1hStart = 0;
|
|
392
|
+
let window24hStart = 0;
|
|
393
|
+
const computeWindow = (currentTimestamp, windowMs, startIdx) => {
|
|
394
|
+
const windowStart = currentTimestamp - windowMs;
|
|
395
|
+
if (timestamps.length === 0 || timestamps[0] > windowStart) {
|
|
396
|
+
return {
|
|
397
|
+
startIdx,
|
|
398
|
+
high: null,
|
|
399
|
+
low: null,
|
|
400
|
+
volume: null,
|
|
401
|
+
startClose: null,
|
|
402
|
+
hasFullWindow: false
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
let idx = startIdx;
|
|
406
|
+
while (idx < timestamps.length && timestamps[idx] < windowStart) {
|
|
407
|
+
idx += 1;
|
|
408
|
+
}
|
|
409
|
+
let high = -Infinity;
|
|
410
|
+
let low = Infinity;
|
|
411
|
+
let volume = 0;
|
|
412
|
+
for (let i = idx; i < highs.length; i += 1) {
|
|
413
|
+
const highValue = highs[i];
|
|
414
|
+
const lowValue = lows[i];
|
|
415
|
+
const volumeValue = volumes[i];
|
|
416
|
+
if (highValue > high) high = highValue;
|
|
417
|
+
if (lowValue < low) low = lowValue;
|
|
418
|
+
volume += volumeValue;
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
startIdx: idx,
|
|
422
|
+
high,
|
|
423
|
+
low,
|
|
424
|
+
volume,
|
|
425
|
+
startClose: closes[idx],
|
|
426
|
+
hasFullWindow: true
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
const findNearestStartClose = (currentTimestamp, windowMs) => {
|
|
430
|
+
if (timestamps.length === 0) {
|
|
431
|
+
return { startClose: null, startIdx: 0 };
|
|
432
|
+
}
|
|
433
|
+
const windowStart = currentTimestamp - windowMs;
|
|
434
|
+
let idx = 0;
|
|
435
|
+
while (idx < timestamps.length && timestamps[idx] < windowStart) {
|
|
436
|
+
idx += 1;
|
|
437
|
+
}
|
|
438
|
+
if (idx <= 0) {
|
|
439
|
+
return { startClose: closes[0], startIdx: 0 };
|
|
440
|
+
}
|
|
441
|
+
if (idx >= timestamps.length) {
|
|
442
|
+
const lastIdx = timestamps.length - 1;
|
|
443
|
+
return { startClose: closes[lastIdx], startIdx: lastIdx };
|
|
444
|
+
}
|
|
445
|
+
const prevIdx = idx - 1;
|
|
446
|
+
const currentIdx = timestamps.length - 1;
|
|
447
|
+
if (idx === currentIdx && timestamps[idx] > windowStart) {
|
|
448
|
+
return { startClose: closes[prevIdx], startIdx: prevIdx };
|
|
449
|
+
}
|
|
450
|
+
const prevDiff = windowStart - timestamps[prevIdx];
|
|
451
|
+
const nextDiff = timestamps[idx] - windowStart;
|
|
452
|
+
const chosenIdx = prevDiff <= nextDiff ? prevIdx : idx;
|
|
453
|
+
return { startClose: closes[chosenIdx], startIdx: chosenIdx };
|
|
454
|
+
};
|
|
455
|
+
const next = (candle, btcCandle) => {
|
|
456
|
+
candlesHistory.push(candle);
|
|
457
|
+
if (btcCandle) {
|
|
458
|
+
btcCandlesHistory.push(btcCandle);
|
|
459
|
+
}
|
|
460
|
+
closes.push(candle.close);
|
|
461
|
+
highs.push(candle.high);
|
|
462
|
+
lows.push(candle.low);
|
|
463
|
+
volumes.push(candle.volume);
|
|
464
|
+
timestamps.push(candle.timestamp);
|
|
465
|
+
const ma14Value = ma14.nextValue(candle.close);
|
|
466
|
+
const ma49Value = ma49.nextValue(candle.close);
|
|
467
|
+
const ma50Value = ma50.nextValue(candle.close);
|
|
468
|
+
const atrValue = atr.nextValue(candle);
|
|
469
|
+
const atrPctValue = atrValue != null && Number.isFinite(atrValue) && candle.close ? atrValue / candle.close * 100 : null;
|
|
470
|
+
const atrPctShortValue = atrPctValue == null ? null : atrPctShort.nextValue(atrPctValue);
|
|
471
|
+
const atrPctLongValue = atrPctValue == null ? null : atrPctLong.nextValue(atrPctValue);
|
|
472
|
+
const atrPctRatio = typeof atrPctShortValue === "number" && Number.isFinite(atrPctShortValue) && typeof atrPctLongValue === "number" && Number.isFinite(atrPctLongValue) && atrPctLongValue !== 0 ? atrPctShortValue / atrPctLongValue : null;
|
|
473
|
+
const bbValue = bb.nextValue(candle.close);
|
|
474
|
+
const obvValue = obv.nextValue(candle);
|
|
475
|
+
const smaObvValue = obvValue == null ? null : smaObv.nextValue(obvValue);
|
|
476
|
+
const macdValue = macd.nextValue(candle.close);
|
|
477
|
+
const currentTimestamp = candle.timestamp;
|
|
478
|
+
const len = candlesHistory.length;
|
|
479
|
+
const prevCandle = len > 1 ? candlesHistory[len - 2] : null;
|
|
480
|
+
const correlation = btcCandlesHistory.length > 0 ? calculateCoinBtcCorrelation(
|
|
481
|
+
candlesHistory.slice(-CORRELATION_WINDOW),
|
|
482
|
+
btcCandlesHistory.slice(-CORRELATION_WINDOW)
|
|
483
|
+
).correlation ?? 0 : 0;
|
|
484
|
+
let spread = null;
|
|
485
|
+
if (btcBinanceCandles.length > 0 && btcCoinbaseCandles.length > 0) {
|
|
486
|
+
const binanceResolved = resolveCloseAtOrBefore(
|
|
487
|
+
btcBinanceCandles,
|
|
488
|
+
btcBinanceCursor,
|
|
489
|
+
currentTimestamp
|
|
490
|
+
);
|
|
491
|
+
const coinbaseResolved = resolveCloseAtOrBefore(
|
|
492
|
+
btcCoinbaseCandles,
|
|
493
|
+
btcCoinbaseCursor,
|
|
494
|
+
currentTimestamp
|
|
495
|
+
);
|
|
496
|
+
btcBinanceCursor = binanceResolved.cursor;
|
|
497
|
+
btcCoinbaseCursor = coinbaseResolved.cursor;
|
|
498
|
+
if (binanceResolved.close != null && coinbaseResolved.close != null && Number.isFinite(binanceResolved.close) && Number.isFinite(coinbaseResolved.close) && binanceResolved.close > 0) {
|
|
499
|
+
spread = spreadSmoother.next({
|
|
500
|
+
binancePrice: binanceResolved.close,
|
|
501
|
+
coinbasePrice: coinbaseResolved.close
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const computePluginSeries = (baseResult2) => {
|
|
506
|
+
const pluginSeries2 = {};
|
|
507
|
+
for (const pluginEntry of indicatorPluginEntries) {
|
|
508
|
+
if (!pluginEntry.compute) continue;
|
|
509
|
+
const historyKey = pluginEntry.historyKey || pluginEntry.indicator.id;
|
|
510
|
+
try {
|
|
511
|
+
const pluginValue = pluginEntry.compute({
|
|
512
|
+
candle,
|
|
513
|
+
btcCandle,
|
|
514
|
+
data: candlesHistory,
|
|
515
|
+
btcData: btcCandlesHistory,
|
|
516
|
+
baseResult: baseResult2
|
|
517
|
+
});
|
|
518
|
+
if (pluginValue == null || typeof pluginValue !== "number" || !Number.isFinite(pluginValue)) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
pluginSeries2[historyKey] = pluginValue;
|
|
522
|
+
pushIndicator(historyKey, pluginValue);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (indicatorPluginErrorShown.has(historyKey)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
indicatorPluginErrorShown.add(historyKey);
|
|
528
|
+
console.warn(
|
|
529
|
+
`Indicator plugin "${historyKey}" compute failed: ${String(error)}`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return pluginSeries2;
|
|
534
|
+
};
|
|
535
|
+
if (ma14Value == null || ma49Value == null || ma50Value == null || atrValue == null || !bbValue || obvValue == null || smaObvValue == null || !macdValue) {
|
|
536
|
+
computePluginSeries({
|
|
537
|
+
prevCandle,
|
|
538
|
+
correlation,
|
|
539
|
+
spread,
|
|
540
|
+
candle
|
|
541
|
+
});
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
const window1h = computeWindow(
|
|
545
|
+
currentTimestamp,
|
|
546
|
+
ONE_HOUR_MS,
|
|
547
|
+
window1hStart
|
|
548
|
+
);
|
|
549
|
+
window1hStart = window1h.startIdx;
|
|
550
|
+
const window24h = computeWindow(
|
|
551
|
+
currentTimestamp,
|
|
552
|
+
ONE_DAY_MS,
|
|
553
|
+
window24hStart
|
|
554
|
+
);
|
|
555
|
+
window24hStart = window24h.startIdx;
|
|
556
|
+
const price1hStart = findNearestStartClose(currentTimestamp, ONE_HOUR_MS);
|
|
557
|
+
const price24hStart = findNearestStartClose(currentTimestamp, ONE_DAY_MS);
|
|
558
|
+
const price1hPcntRaw = price1hStart.startClose != null ? percentChange(candle.close, price1hStart.startClose) : null;
|
|
559
|
+
const price24hPcntRaw = price24hStart.startClose != null ? percentChange(candle.close, price24hStart.startClose) : null;
|
|
560
|
+
const price1hPcnt = price1hPcntRaw ?? 0;
|
|
561
|
+
const price24hPcnt = price24hPcntRaw ?? 0;
|
|
562
|
+
const highPrice1h = window1h.hasFullWindow ? window1h.high : null;
|
|
563
|
+
const lowPrice1h = window1h.hasFullWindow ? window1h.low : null;
|
|
564
|
+
const volume1h = window1h.hasFullWindow ? window1h.volume : null;
|
|
565
|
+
const highPrice24h = window24h.hasFullWindow ? window24h.high : null;
|
|
566
|
+
const lowPrice24h = window24h.hasFullWindow ? window24h.low : null;
|
|
567
|
+
const volume24h = window24h.hasFullWindow ? window24h.volume : null;
|
|
568
|
+
let highLevel = null;
|
|
569
|
+
let lowLevel = null;
|
|
570
|
+
if (len >= indicatorPeriods.levelLookback + indicatorPeriods.levelDelay) {
|
|
571
|
+
const window = candlesHistory.slice(
|
|
572
|
+
len - indicatorPeriods.levelLookback - indicatorPeriods.levelDelay,
|
|
573
|
+
len - indicatorPeriods.levelDelay
|
|
574
|
+
);
|
|
575
|
+
highLevel = Math.max(...window.map((item) => item.high));
|
|
576
|
+
lowLevel = Math.min(...window.map((item) => item.low));
|
|
577
|
+
}
|
|
578
|
+
const baseResult = {
|
|
579
|
+
maFast: ma14Value,
|
|
580
|
+
maMedium: ma49Value,
|
|
581
|
+
maSlow: ma50Value,
|
|
582
|
+
atr: atrValue,
|
|
583
|
+
atrPct: atrPctRatio,
|
|
584
|
+
bbUpper: bbValue.upper,
|
|
585
|
+
bbMiddle: bbValue.middle,
|
|
586
|
+
bbLower: bbValue.lower,
|
|
587
|
+
obv: obvValue,
|
|
588
|
+
smaObv: smaObvValue,
|
|
589
|
+
macd: macdValue.MACD,
|
|
590
|
+
macdSignal: macdValue.signal,
|
|
591
|
+
macdHistogram: macdValue.histogram,
|
|
592
|
+
price24hPcnt,
|
|
593
|
+
price1hPcnt,
|
|
594
|
+
highPrice1h,
|
|
595
|
+
lowPrice1h,
|
|
596
|
+
volume1h,
|
|
597
|
+
highPrice24h,
|
|
598
|
+
lowPrice24h,
|
|
599
|
+
volume24h,
|
|
600
|
+
highLevel,
|
|
601
|
+
lowLevel,
|
|
602
|
+
prevClose: prevCandle?.close ?? null,
|
|
603
|
+
correlation,
|
|
604
|
+
spread
|
|
605
|
+
};
|
|
606
|
+
applyIndicatorsToHistory(baseResult, pushIndicator);
|
|
607
|
+
const pluginSeries = computePluginSeries({
|
|
608
|
+
...baseResult,
|
|
609
|
+
candle,
|
|
610
|
+
prevCandle,
|
|
611
|
+
correlation
|
|
612
|
+
});
|
|
613
|
+
const result = {
|
|
614
|
+
...baseResult,
|
|
615
|
+
...pluginSeries,
|
|
616
|
+
candle,
|
|
617
|
+
prevCandle,
|
|
618
|
+
highLevel,
|
|
619
|
+
lowLevel,
|
|
620
|
+
correlation
|
|
621
|
+
};
|
|
622
|
+
return result;
|
|
623
|
+
};
|
|
624
|
+
data.forEach((candle, index) => {
|
|
625
|
+
next(candle, btcData[index]);
|
|
626
|
+
});
|
|
627
|
+
return {
|
|
628
|
+
next,
|
|
629
|
+
result: () => {
|
|
630
|
+
const baseHistory = cloneArrayValues(indicatorHistory);
|
|
631
|
+
if (!includeMlPayload) {
|
|
632
|
+
return baseHistory;
|
|
633
|
+
}
|
|
634
|
+
const fullHistory = {
|
|
635
|
+
...baseHistory,
|
|
636
|
+
...buildMlTimeframeIndicators(candlesHistory, indicatorPeriods),
|
|
637
|
+
...buildMlCandleIndicators(candlesHistory, btcCandlesHistory),
|
|
638
|
+
...buildIndicatorSeriesByTimeframes(
|
|
639
|
+
btcCandlesHistory,
|
|
640
|
+
indicatorPeriods,
|
|
641
|
+
"btc"
|
|
642
|
+
)
|
|
643
|
+
};
|
|
644
|
+
return fullHistory;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
};
|
|
648
|
+
var buildMlTimeframeIndicators = (candles, periods = {}) => {
|
|
649
|
+
const result = {};
|
|
650
|
+
const indicatorPeriods = {
|
|
651
|
+
...DEFAULT_INDICATOR_PERIODS,
|
|
652
|
+
...periods
|
|
653
|
+
};
|
|
654
|
+
for (const timeframe of INDICATOR_TIMEFRAMES) {
|
|
655
|
+
const tfCandles = resampleCandles(candles, timeframe.minutes);
|
|
656
|
+
if (tfCandles.length === 0) continue;
|
|
657
|
+
const history = createIndicators(tfCandles, [], {
|
|
658
|
+
includeMlPayload: false,
|
|
659
|
+
periods: indicatorPeriods
|
|
660
|
+
}).result();
|
|
661
|
+
for (const [key, values] of Object.entries(history)) {
|
|
662
|
+
result[`${key}${timeframe.suffix}`] = values;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return cloneArrayValues(result);
|
|
666
|
+
};
|
|
667
|
+
var withSourcePrefix = (key, sourcePrefix = "") => {
|
|
668
|
+
if (!sourcePrefix) return key;
|
|
669
|
+
return `${sourcePrefix}${key[0].toUpperCase()}${key.slice(1)}`;
|
|
670
|
+
};
|
|
671
|
+
var buildIndicatorSeriesByTimeframes = (candles, periods, sourcePrefix = "") => {
|
|
672
|
+
const result = {};
|
|
673
|
+
if (candles.length === 0) return result;
|
|
674
|
+
const baseHistory = createIndicators(candles, [], {
|
|
675
|
+
includeMlPayload: false,
|
|
676
|
+
periods
|
|
677
|
+
}).result();
|
|
678
|
+
for (const [key, values] of Object.entries(baseHistory)) {
|
|
679
|
+
result[withSourcePrefix(key, sourcePrefix)] = values;
|
|
680
|
+
}
|
|
681
|
+
const timeframeHistory = buildMlTimeframeIndicators(candles, periods);
|
|
682
|
+
for (const [key, values] of Object.entries(timeframeHistory)) {
|
|
683
|
+
result[withSourcePrefix(key, sourcePrefix)] = values;
|
|
684
|
+
}
|
|
685
|
+
return cloneArrayValues(result);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/utils/timestamp.ts
|
|
689
|
+
var import_date_fns = require("date-fns");
|
|
690
|
+
var import_date_fns2 = require("date-fns");
|
|
691
|
+
var getTimestamp = (days = 0) => {
|
|
692
|
+
if (days > 0) {
|
|
693
|
+
return (0, import_date_fns2.getUnixTime)((0, import_date_fns2.subDays)(/* @__PURE__ */ new Date(), days)) * 1e3;
|
|
694
|
+
}
|
|
695
|
+
return (0, import_date_fns2.getUnixTime)(/* @__PURE__ */ new Date()) * 1e3;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/utils/strategyHelpers/indicators.ts
|
|
699
|
+
var buildDefaultIndicatorPeriods = (config) => ({
|
|
700
|
+
maFast: config.MA_FAST,
|
|
701
|
+
maMedium: config.MA_MEDIUM,
|
|
702
|
+
maSlow: config.MA_SLOW,
|
|
703
|
+
obvSma: config.OBV_SMA,
|
|
704
|
+
atr: config.ATR,
|
|
705
|
+
atrPctShort: config.ATR_PCT_SHORT,
|
|
706
|
+
atrPctLong: config.ATR_PCT_LONG,
|
|
707
|
+
bb: config.BB,
|
|
708
|
+
bbStd: config.BB_STD,
|
|
709
|
+
macdFast: config.MACD_FAST,
|
|
710
|
+
macdSlow: config.MACD_SLOW,
|
|
711
|
+
macdSignal: config.MACD_SIGNAL,
|
|
712
|
+
levelLookback: config.LEVEL_LOOKBACK,
|
|
713
|
+
levelDelay: config.LEVEL_DELAY
|
|
714
|
+
});
|
|
715
|
+
var createStrategyIndicatorsState = ({
|
|
716
|
+
env,
|
|
717
|
+
data,
|
|
718
|
+
btcData,
|
|
719
|
+
btcBinanceData,
|
|
720
|
+
btcCoinbaseData,
|
|
721
|
+
periods
|
|
722
|
+
}) => {
|
|
723
|
+
let controller = env === "BACKTEST" ? createIndicators(data, btcData, {
|
|
724
|
+
periods,
|
|
725
|
+
btcBinanceData,
|
|
726
|
+
btcCoinbaseData
|
|
727
|
+
}) : null;
|
|
728
|
+
let currentBarPair;
|
|
729
|
+
const withSnapshot = (value) => Object.assign(value, {
|
|
730
|
+
snapshot: () => value.result()
|
|
731
|
+
});
|
|
732
|
+
const applyBar = (candle, btcCandle) => {
|
|
733
|
+
if (!controller) return;
|
|
734
|
+
controller.next(candle, btcCandle);
|
|
735
|
+
};
|
|
736
|
+
const ensureControllerInitialized = () => {
|
|
737
|
+
if (controller) return withSnapshot(controller);
|
|
738
|
+
controller = createIndicators(data.slice(0, -1), btcData.slice(0, -1), {
|
|
739
|
+
periods,
|
|
740
|
+
btcBinanceData,
|
|
741
|
+
btcCoinbaseData
|
|
742
|
+
});
|
|
743
|
+
const lastCandle = data[data.length - 1];
|
|
744
|
+
const lastBtcCandle = btcData[btcData.length - 1];
|
|
745
|
+
if (lastCandle && lastBtcCandle) {
|
|
746
|
+
controller.next(lastCandle, lastBtcCandle);
|
|
747
|
+
}
|
|
748
|
+
return withSnapshot(controller);
|
|
749
|
+
};
|
|
750
|
+
return {
|
|
751
|
+
isInitialized: () => controller != null,
|
|
752
|
+
setCurrentBar: (candle, btcCandle) => {
|
|
753
|
+
currentBarPair = { candle, btcCandle };
|
|
754
|
+
},
|
|
755
|
+
onBar: (candle, btcCandle) => {
|
|
756
|
+
const resolvedCandle = candle ?? currentBarPair?.candle;
|
|
757
|
+
const resolvedBtcCandle = btcCandle ?? currentBarPair?.btcCandle;
|
|
758
|
+
if (!resolvedCandle || !resolvedBtcCandle) return;
|
|
759
|
+
applyBar(resolvedCandle, resolvedBtcCandle);
|
|
760
|
+
},
|
|
761
|
+
next: (candle, btcCandle) => {
|
|
762
|
+
if (!controller) return void 0;
|
|
763
|
+
return controller.next(candle, btcCandle);
|
|
764
|
+
},
|
|
765
|
+
// Lazy bootstrap for live mode: initialize on history before current bar and then apply current bar once.
|
|
766
|
+
ensureInitializedWithCurrentBar: ensureControllerInitialized,
|
|
767
|
+
snapshot: () => ensureControllerInitialized().snapshot(),
|
|
768
|
+
latestNumber: (key) => {
|
|
769
|
+
const snapshot = ensureControllerInitialized().snapshot();
|
|
770
|
+
const value = snapshot?.[key];
|
|
771
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
772
|
+
return void 0;
|
|
773
|
+
}
|
|
774
|
+
const last = value[value.length - 1];
|
|
775
|
+
return typeof last === "number" ? last : void 0;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// src/utils/strategyHelpers/market.ts
|
|
781
|
+
var getStrategyMarketSnapshot = async ({
|
|
782
|
+
env,
|
|
783
|
+
connector,
|
|
784
|
+
symbol,
|
|
785
|
+
interval,
|
|
786
|
+
cachedData,
|
|
787
|
+
preloadStart,
|
|
788
|
+
backtestPriceMode = "mid"
|
|
789
|
+
}) => {
|
|
790
|
+
const fullData = env === "BACKTEST" ? cachedData : await connector.kline({
|
|
791
|
+
symbol,
|
|
792
|
+
start: preloadStart,
|
|
793
|
+
end: getTimestamp(),
|
|
794
|
+
cacheOnly: false,
|
|
795
|
+
interval
|
|
796
|
+
});
|
|
797
|
+
const lastCandle = fullData[fullData.length - 1];
|
|
798
|
+
let currentPrice = lastCandle.close;
|
|
799
|
+
if (env === "BACKTEST") {
|
|
800
|
+
if (backtestPriceMode === "mid") {
|
|
801
|
+
currentPrice = (lastCandle.open + lastCandle.close) / 2;
|
|
802
|
+
} else if (backtestPriceMode === "open") {
|
|
803
|
+
currentPrice = lastCandle.open;
|
|
804
|
+
} else if (backtestPriceMode === "rand") {
|
|
805
|
+
const min = Math.min(lastCandle.low, lastCandle.high);
|
|
806
|
+
const max = Math.max(lastCandle.low, lastCandle.high);
|
|
807
|
+
currentPrice = min + Math.random() * (max - min);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
fullData,
|
|
812
|
+
lastCandle,
|
|
813
|
+
timestamp: lastCandle.timestamp,
|
|
814
|
+
currentPrice
|
|
815
|
+
};
|
|
816
|
+
};
|
|
817
|
+
var calculateRiskRatio = ({
|
|
818
|
+
direction,
|
|
819
|
+
currentPrice,
|
|
820
|
+
takeProfitPrice,
|
|
821
|
+
stopLossPrice
|
|
822
|
+
}) => {
|
|
823
|
+
const isLong = direction === "LONG";
|
|
824
|
+
const reward = isLong ? takeProfitPrice - currentPrice : currentPrice - takeProfitPrice;
|
|
825
|
+
const risk = isLong ? currentPrice - stopLossPrice : stopLossPrice - currentPrice;
|
|
826
|
+
return risk > 0 ? reward / risk : 0;
|
|
827
|
+
};
|
|
828
|
+
var getDirectionalTpSlPrices = ({
|
|
829
|
+
price,
|
|
830
|
+
direction,
|
|
831
|
+
takeProfitDelta,
|
|
832
|
+
stopLossDelta,
|
|
833
|
+
unit = "percent",
|
|
834
|
+
maxLossValue,
|
|
835
|
+
feePercent = FEE_PERCENT
|
|
836
|
+
}) => {
|
|
837
|
+
const deltaFactor = unit === "percent" ? 100 : 1;
|
|
838
|
+
const tp = takeProfitDelta / deltaFactor;
|
|
839
|
+
const sl = stopLossDelta / deltaFactor;
|
|
840
|
+
const isLong = direction === "LONG";
|
|
841
|
+
const stopLossPrice = isLong ? price * (1 - sl) : price * (1 + sl);
|
|
842
|
+
const takeProfitPrice = isLong ? price * (1 + tp) : price * (1 - tp);
|
|
843
|
+
const riskRatio = calculateRiskRatio({
|
|
844
|
+
direction,
|
|
845
|
+
currentPrice: price,
|
|
846
|
+
takeProfitPrice,
|
|
847
|
+
stopLossPrice
|
|
848
|
+
});
|
|
849
|
+
const slPercent = unit === "percent" ? stopLossDelta : stopLossDelta * 100;
|
|
850
|
+
const qty = typeof maxLossValue === "number" && Number.isFinite(maxLossValue) && maxLossValue > 0 ? maxLossValue / (price * (slPercent + feePercent) / 100) : void 0;
|
|
851
|
+
return {
|
|
852
|
+
stopLossPrice,
|
|
853
|
+
takeProfitPrice,
|
|
854
|
+
riskRatio,
|
|
855
|
+
qty
|
|
856
|
+
};
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// src/utils/strategyHelpers/state.ts
|
|
860
|
+
var createLastTradeController = ({
|
|
861
|
+
env,
|
|
862
|
+
enabled = env ? env === "BACKTEST" : true,
|
|
863
|
+
cooldownMs = 864e5
|
|
864
|
+
}) => {
|
|
865
|
+
let lastTradeTimestamp = null;
|
|
866
|
+
return {
|
|
867
|
+
isInCooldown: (timestamp) => Boolean(
|
|
868
|
+
enabled && lastTradeTimestamp != null && timestamp <= lastTradeTimestamp + cooldownMs
|
|
869
|
+
),
|
|
870
|
+
markTrade: (timestamp) => {
|
|
871
|
+
if (!enabled) return;
|
|
872
|
+
lastTradeTimestamp = timestamp;
|
|
873
|
+
},
|
|
874
|
+
getLastTradeTimestamp: () => lastTradeTimestamp
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// src/utils/uuid.ts
|
|
879
|
+
var import_uuid = require("uuid");
|
|
880
|
+
var uuid = (len = 12) => {
|
|
881
|
+
const uuid2 = (0, import_uuid.v4)();
|
|
882
|
+
return uuid2.slice(-len);
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// src/utils/strategyHelpers/signalBuilders.ts
|
|
886
|
+
var mapAiRuntimeFromConfig = (config, overrides = {}) => ({
|
|
887
|
+
enabled: Boolean(config.AI_ENABLED ?? true),
|
|
888
|
+
minQuality: Number(config.MIN_AI_QUALITY ?? 4),
|
|
889
|
+
...overrides
|
|
890
|
+
});
|
|
891
|
+
var mapMlRuntimeFromConfig = (config, overrides = {}) => ({
|
|
892
|
+
enabled: Boolean(config.ML_ENABLED ?? true),
|
|
893
|
+
mlThreshold: Number(config.ML_THRESHOLD ?? 0),
|
|
894
|
+
...overrides
|
|
895
|
+
});
|
|
896
|
+
var buildStrategySignal = ({
|
|
897
|
+
signalId,
|
|
898
|
+
strategy,
|
|
899
|
+
symbol,
|
|
900
|
+
interval,
|
|
901
|
+
direction,
|
|
902
|
+
timestamp,
|
|
903
|
+
prices,
|
|
904
|
+
figures = {},
|
|
905
|
+
indicators = {},
|
|
906
|
+
additionalIndicators,
|
|
907
|
+
isConfigFromBacktest
|
|
908
|
+
}) => ({
|
|
909
|
+
signalId,
|
|
910
|
+
strategy,
|
|
911
|
+
symbol,
|
|
912
|
+
interval,
|
|
913
|
+
direction,
|
|
914
|
+
timestamp,
|
|
915
|
+
figures,
|
|
916
|
+
prices,
|
|
917
|
+
indicators,
|
|
918
|
+
additionalIndicators,
|
|
919
|
+
isConfigFromBacktest
|
|
920
|
+
});
|
|
921
|
+
var buildEntrySignalDecision = ({
|
|
922
|
+
code,
|
|
923
|
+
entryContext,
|
|
924
|
+
figures,
|
|
925
|
+
indicators,
|
|
926
|
+
additionalIndicators,
|
|
927
|
+
signalId,
|
|
928
|
+
orderPlan,
|
|
929
|
+
runtime
|
|
930
|
+
}) => ({
|
|
931
|
+
kind: "entry",
|
|
932
|
+
code,
|
|
933
|
+
entryContext,
|
|
934
|
+
signal: buildStrategySignal({
|
|
935
|
+
signalId: signalId ?? uuid(),
|
|
936
|
+
strategy: entryContext.strategy,
|
|
937
|
+
symbol: entryContext.symbol,
|
|
938
|
+
interval: entryContext.interval,
|
|
939
|
+
direction: entryContext.direction,
|
|
940
|
+
timestamp: entryContext.timestamp,
|
|
941
|
+
prices: entryContext.prices,
|
|
942
|
+
figures,
|
|
943
|
+
indicators,
|
|
944
|
+
additionalIndicators,
|
|
945
|
+
isConfigFromBacktest: entryContext.isConfigFromBacktest
|
|
946
|
+
}),
|
|
947
|
+
orderPlan,
|
|
948
|
+
runtime
|
|
949
|
+
});
|
|
950
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
951
|
+
var toDefaultEntryCode = (strategy, direction) => `${strategy.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_${direction}_ENTRY`;
|
|
952
|
+
var resolveTakeProfitPrice = ({
|
|
953
|
+
direction,
|
|
954
|
+
takeProfits
|
|
955
|
+
}) => {
|
|
956
|
+
if (!Array.isArray(takeProfits) || takeProfits.length === 0) {
|
|
957
|
+
throw new Error("strategyApi.entry requires at least one takeProfit");
|
|
958
|
+
}
|
|
959
|
+
const prices = takeProfits.map((tp) => tp?.price).filter((price) => isFiniteNumber(price));
|
|
960
|
+
if (prices.length === 0) {
|
|
961
|
+
throw new Error("strategyApi.entry requires finite takeProfit prices");
|
|
962
|
+
}
|
|
963
|
+
return direction === "LONG" ? Math.max(...prices) : Math.min(...prices);
|
|
964
|
+
};
|
|
965
|
+
var createStrategyAPI = ({
|
|
966
|
+
strategy,
|
|
967
|
+
symbol,
|
|
968
|
+
interval,
|
|
969
|
+
env,
|
|
970
|
+
connector,
|
|
971
|
+
cachedData,
|
|
972
|
+
indicatorsState,
|
|
973
|
+
preloadStart,
|
|
974
|
+
backtestPriceMode,
|
|
975
|
+
isConfigFromBacktest
|
|
976
|
+
}) => {
|
|
977
|
+
const getCurrentPosition = () => connector.getPosition(symbol);
|
|
978
|
+
const isPositionExists = async () => {
|
|
979
|
+
const position = await getCurrentPosition();
|
|
980
|
+
return Boolean(
|
|
981
|
+
position && typeof position.qty === "number" && position.qty > 0
|
|
982
|
+
);
|
|
983
|
+
};
|
|
984
|
+
const getMarketData = async (params = {}) => {
|
|
985
|
+
const resolvedPreloadStart = params.preloadStart ?? preloadStart;
|
|
986
|
+
if (typeof resolvedPreloadStart !== "number") {
|
|
987
|
+
throw new Error("strategyApi.getMarketData requires preloadStart");
|
|
988
|
+
}
|
|
989
|
+
const snapshot = await getStrategyMarketSnapshot({
|
|
990
|
+
env,
|
|
991
|
+
connector,
|
|
992
|
+
symbol,
|
|
993
|
+
interval,
|
|
994
|
+
cachedData,
|
|
995
|
+
preloadStart: resolvedPreloadStart,
|
|
996
|
+
backtestPriceMode: params.backtestPriceMode ?? backtestPriceMode
|
|
997
|
+
});
|
|
998
|
+
return snapshot;
|
|
999
|
+
};
|
|
1000
|
+
return {
|
|
1001
|
+
skip: (code) => ({ kind: "skip", code }),
|
|
1002
|
+
entry: async ({
|
|
1003
|
+
code,
|
|
1004
|
+
direction,
|
|
1005
|
+
figures,
|
|
1006
|
+
indicators,
|
|
1007
|
+
additionalIndicators,
|
|
1008
|
+
signalId,
|
|
1009
|
+
orderPlan,
|
|
1010
|
+
runtime
|
|
1011
|
+
}) => {
|
|
1012
|
+
const marketData = await getMarketData();
|
|
1013
|
+
const currentPrice = marketData.currentPrice;
|
|
1014
|
+
const timestamp = marketData.timestamp;
|
|
1015
|
+
const stopLossPrice = orderPlan.stopLossPrice;
|
|
1016
|
+
const takeProfitPrice = resolveTakeProfitPrice({
|
|
1017
|
+
direction,
|
|
1018
|
+
takeProfits: orderPlan.takeProfits
|
|
1019
|
+
});
|
|
1020
|
+
if (!isFiniteNumber(stopLossPrice)) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
"strategyApi.entry requires finite orderPlan.stopLossPrice"
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const resolvedCode = code ?? toDefaultEntryCode(String(strategy), direction);
|
|
1026
|
+
const riskRatio = calculateRiskRatio({
|
|
1027
|
+
direction,
|
|
1028
|
+
currentPrice,
|
|
1029
|
+
takeProfitPrice,
|
|
1030
|
+
stopLossPrice
|
|
1031
|
+
});
|
|
1032
|
+
return buildEntrySignalDecision({
|
|
1033
|
+
code: resolvedCode,
|
|
1034
|
+
entryContext: {
|
|
1035
|
+
strategy,
|
|
1036
|
+
symbol,
|
|
1037
|
+
interval,
|
|
1038
|
+
direction,
|
|
1039
|
+
timestamp,
|
|
1040
|
+
prices: {
|
|
1041
|
+
currentPrice,
|
|
1042
|
+
takeProfitPrice,
|
|
1043
|
+
stopLossPrice,
|
|
1044
|
+
riskRatio
|
|
1045
|
+
},
|
|
1046
|
+
isConfigFromBacktest
|
|
1047
|
+
},
|
|
1048
|
+
figures,
|
|
1049
|
+
indicators,
|
|
1050
|
+
additionalIndicators,
|
|
1051
|
+
signalId,
|
|
1052
|
+
orderPlan,
|
|
1053
|
+
runtime
|
|
1054
|
+
});
|
|
1055
|
+
},
|
|
1056
|
+
getMarketData,
|
|
1057
|
+
nextIndicators: (candle, btcCandle) => indicatorsState?.next(candle, btcCandle),
|
|
1058
|
+
getCurrentPosition,
|
|
1059
|
+
isCurrentPositionExists: isPositionExists,
|
|
1060
|
+
getDirectionalTpSlPrices: (params) => getDirectionalTpSlPrices(params),
|
|
1061
|
+
createLastTradeController: (params) => createLastTradeController({
|
|
1062
|
+
env,
|
|
1063
|
+
...params
|
|
1064
|
+
})
|
|
1065
|
+
};
|
|
1066
|
+
};
|
|
1067
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1068
|
+
0 && (module.exports = {
|
|
1069
|
+
buildDefaultIndicatorPeriods,
|
|
1070
|
+
buildEntrySignalDecision,
|
|
1071
|
+
buildStrategySignal,
|
|
1072
|
+
calculateRiskRatio,
|
|
1073
|
+
createLastTradeController,
|
|
1074
|
+
createStrategyAPI,
|
|
1075
|
+
createStrategyIndicatorsState,
|
|
1076
|
+
getDirectionalTpSlPrices,
|
|
1077
|
+
getStrategyMarketSnapshot,
|
|
1078
|
+
mapAiRuntimeFromConfig,
|
|
1079
|
+
mapMlRuntimeFromConfig
|
|
1080
|
+
});
|