@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,1631 @@
|
|
|
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/indicators.ts
|
|
31
|
+
var indicators_exports = {};
|
|
32
|
+
__export(indicators_exports, {
|
|
33
|
+
alignSortedCandlesByTimestamp: () => alignSortedCandlesByTimestamp,
|
|
34
|
+
alignSpreadRows: () => alignSpreadRows,
|
|
35
|
+
applyIndicatorsToHistory: () => applyIndicatorsToHistory,
|
|
36
|
+
buildMlCandleIndicators: () => buildMlCandleIndicators,
|
|
37
|
+
buildMlTimeframeIndicators: () => buildMlTimeframeIndicators,
|
|
38
|
+
buildReturnsFromCandles: () => buildReturnsFromCandles,
|
|
39
|
+
calculateCoinBtcCorrelation: () => calculateCoinBtcCorrelation,
|
|
40
|
+
calculatePearsonCorrelation: () => calculatePearsonCorrelation,
|
|
41
|
+
coinalyzePointsToRows: () => coinalyzePointsToRows,
|
|
42
|
+
coinbaseProductFromSymbol: () => coinbaseProductFromSymbol,
|
|
43
|
+
createIndicators: () => createIndicators,
|
|
44
|
+
createSpreadSmoother: () => createSpreadSmoother,
|
|
45
|
+
createTrendlineEngine: () => createTrendlineEngine,
|
|
46
|
+
detectRawSupportResistance: () => detectRawSupportResistance,
|
|
47
|
+
getPluginIndicatorCatalog: () => getPluginIndicatorCatalog,
|
|
48
|
+
getPluginIndicatorRenderers: () => getPluginIndicatorRenderers,
|
|
49
|
+
getRegisteredIndicatorEntries: () => getRegisteredIndicatorEntries,
|
|
50
|
+
getSupportResistanceLevels: () => getSupportResistanceLevels,
|
|
51
|
+
intervalToMs: () => intervalToMs,
|
|
52
|
+
mergeCoinalyzeMetrics: () => mergeCoinalyzeMetrics,
|
|
53
|
+
normalizeCoinalyzeSymbols: () => normalizeCoinalyzeSymbols,
|
|
54
|
+
normalizeDerivativesIntervals: () => normalizeDerivativesIntervals,
|
|
55
|
+
registerIndicatorEntries: () => registerIndicatorEntries,
|
|
56
|
+
resetIndicatorRegistryCache: () => resetIndicatorRegistryCache,
|
|
57
|
+
rollingMeanStd: () => rollingMeanStd,
|
|
58
|
+
smoothSpreadSeries: () => smoothSpreadSeries,
|
|
59
|
+
toArrayData: () => toArrayData,
|
|
60
|
+
toCoinalyzeTimestampMs: () => toCoinalyzeTimestampMs,
|
|
61
|
+
toFiniteNumber: () => toFiniteNumber2
|
|
62
|
+
});
|
|
63
|
+
module.exports = __toCommonJS(indicators_exports);
|
|
64
|
+
|
|
65
|
+
// src/utils/math.ts
|
|
66
|
+
var import_lodash = __toESM(require("lodash"));
|
|
67
|
+
var round = (value, precision = 2) => precision > 0 ? Math.round(value * 10 ** precision) / 10 ** precision : Math.round(value);
|
|
68
|
+
|
|
69
|
+
// src/utils/correlation.ts
|
|
70
|
+
var alignSortedCandlesByTimestamp = (coinCandles, btcCandles) => {
|
|
71
|
+
const alignedCoinCandles = [];
|
|
72
|
+
const alignedBtcCandles = [];
|
|
73
|
+
let coinIndex = 0;
|
|
74
|
+
let btcIndex = 0;
|
|
75
|
+
while (coinIndex < coinCandles.length && btcIndex < btcCandles.length) {
|
|
76
|
+
const coinTimestampMs = coinCandles[coinIndex].timestamp;
|
|
77
|
+
const btcTimestampMs = btcCandles[btcIndex].timestamp;
|
|
78
|
+
if (coinTimestampMs === btcTimestampMs) {
|
|
79
|
+
alignedCoinCandles.push(coinCandles[coinIndex]);
|
|
80
|
+
alignedBtcCandles.push(btcCandles[btcIndex]);
|
|
81
|
+
coinIndex += 1;
|
|
82
|
+
btcIndex += 1;
|
|
83
|
+
} else if (coinTimestampMs < btcTimestampMs) {
|
|
84
|
+
coinIndex += 1;
|
|
85
|
+
} else {
|
|
86
|
+
btcIndex += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
alignedCoinCandles,
|
|
91
|
+
alignedBtcCandles
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
var buildReturnsFromCandles = (candles) => {
|
|
95
|
+
const returns = [];
|
|
96
|
+
for (let candleIndex = 1; candleIndex < candles.length; candleIndex += 1) {
|
|
97
|
+
const previousClose = candles[candleIndex - 1].close;
|
|
98
|
+
const currentClose = candles[candleIndex].close;
|
|
99
|
+
if (!Number.isFinite(previousClose) || !Number.isFinite(currentClose)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const change = (currentClose - previousClose) / previousClose;
|
|
103
|
+
returns.push(change);
|
|
104
|
+
}
|
|
105
|
+
return returns;
|
|
106
|
+
};
|
|
107
|
+
var calculatePearsonCorrelation = (firstSeries, secondSeries) => {
|
|
108
|
+
if (firstSeries.length !== secondSeries.length) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"calculatePearsonCorrelation: series lengths are different"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const length = firstSeries.length;
|
|
114
|
+
if (length === 0) return null;
|
|
115
|
+
if (length === 1) return null;
|
|
116
|
+
const sumFirst = firstSeries.reduce((sum, value) => sum + value, 0);
|
|
117
|
+
const sumSecond = secondSeries.reduce((sum, value) => sum + value, 0);
|
|
118
|
+
const meanFirst = sumFirst / length;
|
|
119
|
+
const meanSecond = sumSecond / length;
|
|
120
|
+
let covariance = 0;
|
|
121
|
+
let varianceFirst = 0;
|
|
122
|
+
let varianceSecond = 0;
|
|
123
|
+
for (let index = 0; index < length; index += 1) {
|
|
124
|
+
const deltaFirst = firstSeries[index] - meanFirst;
|
|
125
|
+
const deltaSecond = secondSeries[index] - meanSecond;
|
|
126
|
+
covariance += deltaFirst * deltaSecond;
|
|
127
|
+
varianceFirst += deltaFirst * deltaFirst;
|
|
128
|
+
varianceSecond += deltaSecond * deltaSecond;
|
|
129
|
+
}
|
|
130
|
+
if (varianceFirst === 0 || varianceSecond === 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const correlation = covariance / Math.sqrt(varianceFirst * varianceSecond);
|
|
134
|
+
return correlation;
|
|
135
|
+
};
|
|
136
|
+
var calculateCoinBtcCorrelation = (coinCandles, btcCandles) => {
|
|
137
|
+
const { alignedCoinCandles, alignedBtcCandles } = alignSortedCandlesByTimestamp(coinCandles, btcCandles);
|
|
138
|
+
const MIN_LENGTH_FOR_CORRELATION = 10;
|
|
139
|
+
if (alignedCoinCandles.length <= MIN_LENGTH_FOR_CORRELATION) {
|
|
140
|
+
return {
|
|
141
|
+
correlation: null,
|
|
142
|
+
alignedCoinCandles,
|
|
143
|
+
alignedBtcCandles,
|
|
144
|
+
coinReturns: [],
|
|
145
|
+
btcReturns: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const coinReturns = buildReturnsFromCandles(alignedCoinCandles);
|
|
149
|
+
const btcReturns = buildReturnsFromCandles(alignedBtcCandles);
|
|
150
|
+
const minReturnsLength = Math.min(coinReturns.length, btcReturns.length);
|
|
151
|
+
const slicedCoinReturns = coinReturns.slice(-minReturnsLength);
|
|
152
|
+
const slicedBtcReturns = btcReturns.slice(-minReturnsLength);
|
|
153
|
+
const correlation = calculatePearsonCorrelation(
|
|
154
|
+
slicedCoinReturns,
|
|
155
|
+
slicedBtcReturns
|
|
156
|
+
);
|
|
157
|
+
return {
|
|
158
|
+
correlation: correlation ? round(correlation) : correlation,
|
|
159
|
+
alignedCoinCandles,
|
|
160
|
+
alignedBtcCandles,
|
|
161
|
+
coinReturns: slicedCoinReturns,
|
|
162
|
+
btcReturns: slicedBtcReturns
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/utils/derivativesFeatureUtils.ts
|
|
167
|
+
var SUPPORTED_DERIVATIVE_INTERVALS = [
|
|
168
|
+
"15m",
|
|
169
|
+
"1h"
|
|
170
|
+
];
|
|
171
|
+
var parseDerivativesIntervals = (value) => {
|
|
172
|
+
const supported = new Set(
|
|
173
|
+
SUPPORTED_DERIVATIVE_INTERVALS
|
|
174
|
+
);
|
|
175
|
+
const values = String(value ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
176
|
+
return values.filter(
|
|
177
|
+
(item) => supported.has(item)
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
var toFiniteNumber = (value, fallback = 0) => {
|
|
181
|
+
const num = Number(value);
|
|
182
|
+
return Number.isFinite(num) ? num : fallback;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/utils/derivativesCoinalyze.ts
|
|
186
|
+
var normalizeCoinalyzeSymbols = (input) => String(input ?? "").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
|
|
187
|
+
var normalizeDerivativesIntervals = (input) => parseDerivativesIntervals(input);
|
|
188
|
+
var toCoinalyzeTimestampMs = (value) => {
|
|
189
|
+
const num = Number(value);
|
|
190
|
+
if (!Number.isFinite(num)) return null;
|
|
191
|
+
if (num > 1e13) return Math.floor(num / 1e3);
|
|
192
|
+
if (num > 1e10) return Math.floor(num);
|
|
193
|
+
return Math.floor(num * 1e3);
|
|
194
|
+
};
|
|
195
|
+
var toFiniteNumber2 = (value) => Number.isFinite(Number(value)) ? toFiniteNumber(value) : null;
|
|
196
|
+
var toArrayData = (value) => {
|
|
197
|
+
if (Array.isArray(value)) return value;
|
|
198
|
+
if (value && typeof value === "object") {
|
|
199
|
+
const maybeData = value.data;
|
|
200
|
+
if (Array.isArray(maybeData)) return maybeData;
|
|
201
|
+
const maybeResult = value.result;
|
|
202
|
+
if (Array.isArray(maybeResult)) return maybeResult;
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
};
|
|
206
|
+
var mergeCoinalyzeMetrics = (params) => {
|
|
207
|
+
const { symbol, oiRaw, fundingRaw, liqRaw } = params;
|
|
208
|
+
const points = /* @__PURE__ */ new Map();
|
|
209
|
+
const upsertPoint = (ts) => {
|
|
210
|
+
let point = points.get(ts);
|
|
211
|
+
if (!point) {
|
|
212
|
+
point = { symbol, ts };
|
|
213
|
+
points.set(ts, point);
|
|
214
|
+
}
|
|
215
|
+
return point;
|
|
216
|
+
};
|
|
217
|
+
for (const item of toArrayData(oiRaw)) {
|
|
218
|
+
const ts = toCoinalyzeTimestampMs(
|
|
219
|
+
item.t ?? item.ts ?? item.time ?? item.timestamp
|
|
220
|
+
);
|
|
221
|
+
if (!ts) continue;
|
|
222
|
+
const point = upsertPoint(ts);
|
|
223
|
+
point.openInterest = toFiniteNumber2(
|
|
224
|
+
item.open_interest ?? item.openInterest ?? item.oi ?? item.value
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
for (const item of toArrayData(fundingRaw)) {
|
|
228
|
+
const ts = toCoinalyzeTimestampMs(
|
|
229
|
+
item.t ?? item.ts ?? item.time ?? item.timestamp
|
|
230
|
+
);
|
|
231
|
+
if (!ts) continue;
|
|
232
|
+
const point = upsertPoint(ts);
|
|
233
|
+
point.fundingRate = toFiniteNumber2(
|
|
234
|
+
item.funding_rate ?? item.fundingRate ?? item.rate ?? item.value
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
for (const item of toArrayData(liqRaw)) {
|
|
238
|
+
const ts = toCoinalyzeTimestampMs(
|
|
239
|
+
item.t ?? item.ts ?? item.time ?? item.timestamp
|
|
240
|
+
);
|
|
241
|
+
if (!ts) continue;
|
|
242
|
+
const point = upsertPoint(ts);
|
|
243
|
+
point.liqLong = toFiniteNumber2(
|
|
244
|
+
item.liq_long ?? item.long_liq ?? item.longLiquidations ?? item.long
|
|
245
|
+
);
|
|
246
|
+
point.liqShort = toFiniteNumber2(
|
|
247
|
+
item.liq_short ?? item.short_liq ?? item.shortLiquidations ?? item.short
|
|
248
|
+
);
|
|
249
|
+
point.liqTotal = toFiniteNumber2(
|
|
250
|
+
item.liq_total ?? item.total_liq ?? item.totalLiquidations
|
|
251
|
+
) ?? (point.liqLong ?? 0) + (point.liqShort ?? 0);
|
|
252
|
+
}
|
|
253
|
+
return [...points.values()].sort((a, b) => a.ts - b.ts);
|
|
254
|
+
};
|
|
255
|
+
var coinalyzePointsToRows = (points, interval, source) => points.map((point) => ({
|
|
256
|
+
symbol: point.symbol,
|
|
257
|
+
interval,
|
|
258
|
+
ts: new Date(point.ts),
|
|
259
|
+
openInterest: point.openInterest ?? null,
|
|
260
|
+
fundingRate: point.fundingRate ?? null,
|
|
261
|
+
liqLong: point.liqLong ?? null,
|
|
262
|
+
liqShort: point.liqShort ?? null,
|
|
263
|
+
liqTotal: point.liqTotal ?? null,
|
|
264
|
+
source
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
// src/utils/indicators.ts
|
|
268
|
+
var import_technicalindicators = require("technicalindicators");
|
|
269
|
+
|
|
270
|
+
// src/constants/index.ts
|
|
271
|
+
var CORRELATION_WINDOW = 50;
|
|
272
|
+
var ML_BASE_CANDLES_WINDOW = 50;
|
|
273
|
+
var TRENDLINE_DEFAULTS = {
|
|
274
|
+
maxLines: 20,
|
|
275
|
+
range: 15,
|
|
276
|
+
firstRange: 80,
|
|
277
|
+
epsilon: 3e-3,
|
|
278
|
+
epsilonOffset: 5e-3,
|
|
279
|
+
minTouches: 4,
|
|
280
|
+
minDistance: 50,
|
|
281
|
+
minTouchGap: 15,
|
|
282
|
+
maxTouchGap: 60,
|
|
283
|
+
offset: 1e3,
|
|
284
|
+
capture: false,
|
|
285
|
+
bestLines: 4,
|
|
286
|
+
maxDistance: 2e3
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/utils/array.ts
|
|
290
|
+
var import_lodash2 = __toESM(require("lodash"));
|
|
291
|
+
var cloneArrayValues = (record) => Object.fromEntries(
|
|
292
|
+
Object.entries(record).map(([key, value]) => [
|
|
293
|
+
key,
|
|
294
|
+
Array.isArray(value) ? value.slice() : value
|
|
295
|
+
])
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// src/utils/indicatorPlugins.ts
|
|
299
|
+
var warn = (message, ...args) => {
|
|
300
|
+
console.warn(`[core:indicators] ${message}`, ...args);
|
|
301
|
+
};
|
|
302
|
+
var pluginIndicatorEntries = /* @__PURE__ */ new Map();
|
|
303
|
+
var registerIndicatorEntries = (entries, source) => {
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
const indicatorId = entry.indicator?.id;
|
|
306
|
+
if (!indicatorId) {
|
|
307
|
+
warn("Skip indicator entry without id from %s", source);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (pluginIndicatorEntries.has(indicatorId)) {
|
|
311
|
+
warn(
|
|
312
|
+
'Skip duplicate indicator "%s" from %s: already registered',
|
|
313
|
+
indicatorId,
|
|
314
|
+
source
|
|
315
|
+
);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
pluginIndicatorEntries.set(indicatorId, entry);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
var getRegisteredIndicatorEntries = () => [
|
|
322
|
+
...pluginIndicatorEntries.values()
|
|
323
|
+
];
|
|
324
|
+
var getPluginIndicatorCatalog = () => getRegisteredIndicatorEntries().map((entry) => ({
|
|
325
|
+
id: entry.indicator.id,
|
|
326
|
+
label: entry.indicator.label,
|
|
327
|
+
enabled: entry.indicator.enabled,
|
|
328
|
+
periods: entry.indicator.periods
|
|
329
|
+
}));
|
|
330
|
+
var getPluginIndicatorRenderers = () => getRegisteredIndicatorEntries().filter(
|
|
331
|
+
(entry) => Boolean(entry.renderer)
|
|
332
|
+
).map((entry) => ({
|
|
333
|
+
indicatorId: entry.indicator.id,
|
|
334
|
+
renderer: entry.renderer
|
|
335
|
+
}));
|
|
336
|
+
var resetIndicatorRegistryCache = () => {
|
|
337
|
+
pluginIndicatorEntries.clear();
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/utils/spread.ts
|
|
341
|
+
var DEFAULT_SPREAD_WINDOW = 50;
|
|
342
|
+
var toFinitePrice = (value) => {
|
|
343
|
+
if (value == null) return null;
|
|
344
|
+
const num = Number(value);
|
|
345
|
+
return Number.isFinite(num) ? num : null;
|
|
346
|
+
};
|
|
347
|
+
var toFiniteSpread = (value) => {
|
|
348
|
+
if (value == null) return null;
|
|
349
|
+
const num = Number(value);
|
|
350
|
+
return Number.isFinite(num) ? num : null;
|
|
351
|
+
};
|
|
352
|
+
var createSpreadSmoother = (window = DEFAULT_SPREAD_WINDOW) => {
|
|
353
|
+
const binanceWindow = [];
|
|
354
|
+
const coinbaseWindow = [];
|
|
355
|
+
let binanceSum = 0;
|
|
356
|
+
let coinbaseSum = 0;
|
|
357
|
+
const next = (params) => {
|
|
358
|
+
const binance = toFinitePrice(params.binancePrice);
|
|
359
|
+
const coinbase = toFinitePrice(params.coinbasePrice);
|
|
360
|
+
if (binance != null && coinbase != null) {
|
|
361
|
+
binanceWindow.push(binance);
|
|
362
|
+
coinbaseWindow.push(coinbase);
|
|
363
|
+
binanceSum += binance;
|
|
364
|
+
coinbaseSum += coinbase;
|
|
365
|
+
if (binanceWindow.length > window) {
|
|
366
|
+
binanceSum -= binanceWindow.shift() ?? 0;
|
|
367
|
+
coinbaseSum -= coinbaseWindow.shift() ?? 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
let spread = toFiniteSpread(params.fallbackSpread);
|
|
371
|
+
if (binanceWindow.length > 0 && coinbaseWindow.length > 0) {
|
|
372
|
+
const avgBinance = binanceSum / binanceWindow.length;
|
|
373
|
+
const avgCoinbase = coinbaseSum / coinbaseWindow.length;
|
|
374
|
+
if (Number.isFinite(avgBinance) && avgBinance > 0) {
|
|
375
|
+
spread = (avgCoinbase - avgBinance) / avgBinance;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return toFiniteSpread(spread);
|
|
379
|
+
};
|
|
380
|
+
return { next };
|
|
381
|
+
};
|
|
382
|
+
var smoothSpreadSeries = (points, window = DEFAULT_SPREAD_WINDOW) => {
|
|
383
|
+
const smoother = createSpreadSmoother(window);
|
|
384
|
+
return points.map((point) => ({
|
|
385
|
+
timestamp: point.timestamp,
|
|
386
|
+
spread: smoother.next({
|
|
387
|
+
binancePrice: point.binancePrice,
|
|
388
|
+
coinbasePrice: point.coinbasePrice,
|
|
389
|
+
fallbackSpread: point.spread
|
|
390
|
+
})
|
|
391
|
+
}));
|
|
392
|
+
};
|
|
393
|
+
var intervalToMs = (interval) => interval === "1h" ? 60 * 60 * 1e3 : 15 * 60 * 1e3;
|
|
394
|
+
var coinbaseProductFromSymbol = (symbol) => {
|
|
395
|
+
const upper = symbol.trim().toUpperCase();
|
|
396
|
+
const quoteSuffixes = ["USDT", "USDC", "BUSD", "USD"];
|
|
397
|
+
for (const suffix of quoteSuffixes) {
|
|
398
|
+
if (upper.endsWith(suffix)) {
|
|
399
|
+
const base = upper.slice(0, -suffix.length);
|
|
400
|
+
if (!base) return null;
|
|
401
|
+
return `${base}-USD`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
};
|
|
406
|
+
var alignSpreadRows = (params) => {
|
|
407
|
+
const { symbol, interval, binance, coinbase, source } = params;
|
|
408
|
+
if (!binance.length || !coinbase.length) return [];
|
|
409
|
+
const coinbaseByTs = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const row of coinbase) {
|
|
411
|
+
if (!Number.isFinite(row.ts) || !Number.isFinite(row.close)) continue;
|
|
412
|
+
coinbaseByTs.set(row.ts, row.close);
|
|
413
|
+
}
|
|
414
|
+
const rows = [];
|
|
415
|
+
for (const row of binance) {
|
|
416
|
+
if (!Number.isFinite(row.ts) || !Number.isFinite(row.close) || row.close <= 0) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const cb = coinbaseByTs.get(row.ts);
|
|
420
|
+
if (cb == null || !Number.isFinite(cb)) continue;
|
|
421
|
+
const spread = (cb - row.close) / row.close;
|
|
422
|
+
rows.push({
|
|
423
|
+
symbol,
|
|
424
|
+
interval,
|
|
425
|
+
ts: new Date(row.ts),
|
|
426
|
+
binancePrice: row.close,
|
|
427
|
+
coinbasePrice: cb,
|
|
428
|
+
spread,
|
|
429
|
+
source
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return rows;
|
|
433
|
+
};
|
|
434
|
+
var rollingMeanStd = (values, endIndex, window) => {
|
|
435
|
+
const start = Math.max(0, endIndex - window + 1);
|
|
436
|
+
const slice = values.slice(start, endIndex + 1).map((x) => toFiniteNumber(x, Number.NaN)).filter((x) => Number.isFinite(x));
|
|
437
|
+
if (!slice.length) return { mean: 0, std: 0 };
|
|
438
|
+
const mean = slice.reduce((acc, x) => acc + x, 0) / slice.length;
|
|
439
|
+
if (slice.length < 2) return { mean, std: 0 };
|
|
440
|
+
const variance = slice.reduce((acc, x) => acc + (x - mean) * (x - mean), 0) / slice.length;
|
|
441
|
+
return { mean, std: Math.sqrt(Math.max(variance, 0)) };
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/utils/indicators.ts
|
|
445
|
+
var CANDLE_WINDOW = ML_BASE_CANDLES_WINDOW;
|
|
446
|
+
var BASE_INTERVAL_MINUTES = 15;
|
|
447
|
+
var INDICATOR_TIMEFRAMES = [
|
|
448
|
+
{ minutes: 60, suffix: "1h" },
|
|
449
|
+
{ minutes: 240, suffix: "4h" },
|
|
450
|
+
{ minutes: 1440, suffix: "1d" }
|
|
451
|
+
];
|
|
452
|
+
var DEFAULT_INDICATOR_PERIODS = {
|
|
453
|
+
maFast: 14,
|
|
454
|
+
maMedium: 49,
|
|
455
|
+
maSlow: 50,
|
|
456
|
+
obvSma: 10,
|
|
457
|
+
atr: 14,
|
|
458
|
+
atrPctShort: 7,
|
|
459
|
+
atrPctLong: 30,
|
|
460
|
+
bb: 20,
|
|
461
|
+
bbStd: 2,
|
|
462
|
+
macdFast: 12,
|
|
463
|
+
macdSlow: 26,
|
|
464
|
+
macdSignal: 9,
|
|
465
|
+
levelLookback: 20,
|
|
466
|
+
levelDelay: 2
|
|
467
|
+
};
|
|
468
|
+
var ONE_HOUR_MS = 36e5;
|
|
469
|
+
var ONE_DAY_MS = 864e5;
|
|
470
|
+
var toMlCandle = (candle) => ({
|
|
471
|
+
open: Number(candle.open) || 0,
|
|
472
|
+
high: Number(candle.high) || 0,
|
|
473
|
+
low: Number(candle.low) || 0,
|
|
474
|
+
close: Number(candle.close) || 0,
|
|
475
|
+
volume: Number(candle.volume) || 0,
|
|
476
|
+
turnover: Number(candle.turnover) || 0,
|
|
477
|
+
timestamp: Number(candle.timestamp) || 0
|
|
478
|
+
});
|
|
479
|
+
var resampleCandles = (candles, targetMinutes) => {
|
|
480
|
+
if (targetMinutes <= BASE_INTERVAL_MINUTES) return candles.map(toMlCandle);
|
|
481
|
+
const bucketMs = targetMinutes * 6e4;
|
|
482
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const raw of candles) {
|
|
484
|
+
const candle = toMlCandle(raw);
|
|
485
|
+
const ts = candle.timestamp;
|
|
486
|
+
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
487
|
+
const bucket = Math.floor(ts / bucketMs) * bucketMs;
|
|
488
|
+
const current = buckets.get(bucket);
|
|
489
|
+
if (!current) {
|
|
490
|
+
buckets.set(bucket, { ...candle, timestamp: bucket });
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
current.high = Math.max(current.high, candle.high);
|
|
494
|
+
current.low = Math.min(current.low, candle.low);
|
|
495
|
+
current.close = candle.close;
|
|
496
|
+
current.volume += candle.volume;
|
|
497
|
+
current.turnover += candle.turnover;
|
|
498
|
+
}
|
|
499
|
+
return [...buckets.entries()].sort((a, b) => a[0] - b[0]).map(([, candle]) => candle);
|
|
500
|
+
};
|
|
501
|
+
var buildMlCandleIndicators = (candles, btcCandles) => ({
|
|
502
|
+
candles15m: candles.slice(-CANDLE_WINDOW).map(toMlCandle),
|
|
503
|
+
candles1h: resampleCandles(candles, 60).slice(-CANDLE_WINDOW),
|
|
504
|
+
candles4h: resampleCandles(candles, 240).slice(-CANDLE_WINDOW),
|
|
505
|
+
candles1d: resampleCandles(candles, 1440).slice(-CANDLE_WINDOW),
|
|
506
|
+
btcCandles15m: btcCandles.slice(-CANDLE_WINDOW).map(toMlCandle),
|
|
507
|
+
btcCandles1h: resampleCandles(btcCandles, 60).slice(-CANDLE_WINDOW),
|
|
508
|
+
btcCandles4h: resampleCandles(btcCandles, 240).slice(-CANDLE_WINDOW),
|
|
509
|
+
btcCandles1d: resampleCandles(btcCandles, 1440).slice(-CANDLE_WINDOW)
|
|
510
|
+
});
|
|
511
|
+
var percentChange = (current, previous) => {
|
|
512
|
+
if (!Number.isFinite(current) || !Number.isFinite(previous) || previous === 0) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
return (current - previous) / previous * 100;
|
|
516
|
+
};
|
|
517
|
+
var applyIndicatorsToHistory = (indicators, pushIndicator) => {
|
|
518
|
+
pushIndicator("maFast", indicators.maFast);
|
|
519
|
+
pushIndicator("maMedium", indicators.maMedium);
|
|
520
|
+
pushIndicator("maSlow", indicators.maSlow);
|
|
521
|
+
pushIndicator("atr", indicators.atr);
|
|
522
|
+
pushIndicator("atrPct", indicators.atrPct);
|
|
523
|
+
pushIndicator("bbUpper", indicators.bbUpper);
|
|
524
|
+
pushIndicator("bbMiddle", indicators.bbMiddle);
|
|
525
|
+
pushIndicator("bbLower", indicators.bbLower);
|
|
526
|
+
pushIndicator("obv", indicators.obv);
|
|
527
|
+
pushIndicator("smaObv", indicators.smaObv);
|
|
528
|
+
pushIndicator("macd", indicators.macd);
|
|
529
|
+
pushIndicator("macdSignal", indicators.macdSignal);
|
|
530
|
+
pushIndicator("macdHistogram", indicators.macdHistogram);
|
|
531
|
+
pushIndicator("price24hPcnt", indicators.price24hPcnt ?? void 0);
|
|
532
|
+
pushIndicator("price1hPcnt", indicators.price1hPcnt ?? void 0);
|
|
533
|
+
pushIndicator("highPrice1h", indicators.highPrice1h ?? void 0);
|
|
534
|
+
pushIndicator("lowPrice1h", indicators.lowPrice1h ?? void 0);
|
|
535
|
+
pushIndicator("volume1h", indicators.volume1h ?? void 0);
|
|
536
|
+
pushIndicator("highPrice24h", indicators.highPrice24h ?? void 0);
|
|
537
|
+
pushIndicator("lowPrice24h", indicators.lowPrice24h ?? void 0);
|
|
538
|
+
pushIndicator("volume24h", indicators.volume24h ?? void 0);
|
|
539
|
+
pushIndicator("highLevel", indicators.highLevel ?? void 0);
|
|
540
|
+
pushIndicator("lowLevel", indicators.lowLevel ?? void 0);
|
|
541
|
+
pushIndicator("prevClose", indicators.prevClose ?? void 0);
|
|
542
|
+
pushIndicator("correlation", indicators.correlation ?? void 0);
|
|
543
|
+
pushIndicator("spread", indicators.spread ?? void 0);
|
|
544
|
+
};
|
|
545
|
+
var createIndicators = (data, btcData = [], options = {}) => {
|
|
546
|
+
const indicatorPluginEntries = getRegisteredIndicatorEntries();
|
|
547
|
+
const includeMlPayload = options.includeMlPayload !== false;
|
|
548
|
+
const indicatorPeriods = {
|
|
549
|
+
...DEFAULT_INDICATOR_PERIODS,
|
|
550
|
+
...options.periods || {}
|
|
551
|
+
};
|
|
552
|
+
const closes = [];
|
|
553
|
+
const highs = [];
|
|
554
|
+
const lows = [];
|
|
555
|
+
const volumes = [];
|
|
556
|
+
const timestamps = [];
|
|
557
|
+
const candlesHistory = [];
|
|
558
|
+
const btcCandlesHistory = [];
|
|
559
|
+
const btcBinanceCandles = (options.btcBinanceData ?? []).map(toMlCandle);
|
|
560
|
+
const btcCoinbaseCandles = (options.btcCoinbaseData ?? []).map(toMlCandle);
|
|
561
|
+
const spreadSmoother = createSpreadSmoother();
|
|
562
|
+
let btcBinanceCursor = 0;
|
|
563
|
+
let btcCoinbaseCursor = 0;
|
|
564
|
+
const obv = new import_technicalindicators.OBV({ close: [], volume: [] });
|
|
565
|
+
const smaObv = new import_technicalindicators.SMA({ period: indicatorPeriods.obvSma, values: [] });
|
|
566
|
+
const ma14 = new import_technicalindicators.SMA({ period: indicatorPeriods.maFast, values: [] });
|
|
567
|
+
const ma49 = new import_technicalindicators.SMA({ period: indicatorPeriods.maMedium, values: [] });
|
|
568
|
+
const ma50 = new import_technicalindicators.SMA({ period: indicatorPeriods.maSlow, values: [] });
|
|
569
|
+
const atr = new import_technicalindicators.ATR({
|
|
570
|
+
period: indicatorPeriods.atr,
|
|
571
|
+
high: [],
|
|
572
|
+
low: [],
|
|
573
|
+
close: []
|
|
574
|
+
});
|
|
575
|
+
const atrPctShort = new import_technicalindicators.SMA({
|
|
576
|
+
period: indicatorPeriods.atrPctShort,
|
|
577
|
+
values: []
|
|
578
|
+
});
|
|
579
|
+
const atrPctLong = new import_technicalindicators.SMA({
|
|
580
|
+
period: indicatorPeriods.atrPctLong,
|
|
581
|
+
values: []
|
|
582
|
+
});
|
|
583
|
+
const bb = new import_technicalindicators.BollingerBands({
|
|
584
|
+
period: indicatorPeriods.bb,
|
|
585
|
+
values: [],
|
|
586
|
+
stdDev: indicatorPeriods.bbStd
|
|
587
|
+
});
|
|
588
|
+
const macd = new import_technicalindicators.MACD({
|
|
589
|
+
fastPeriod: indicatorPeriods.macdFast,
|
|
590
|
+
slowPeriod: indicatorPeriods.macdSlow,
|
|
591
|
+
signalPeriod: indicatorPeriods.macdSignal,
|
|
592
|
+
values: [],
|
|
593
|
+
SimpleMAOscillator: false,
|
|
594
|
+
SimpleMASignal: false
|
|
595
|
+
});
|
|
596
|
+
const indicatorHistory = {};
|
|
597
|
+
const indicatorPluginErrorShown = /* @__PURE__ */ new Set();
|
|
598
|
+
const pushIndicator = (key, value) => {
|
|
599
|
+
if (value == null) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!indicatorHistory[key]) {
|
|
603
|
+
indicatorHistory[key] = [];
|
|
604
|
+
}
|
|
605
|
+
indicatorHistory[key].push(value);
|
|
606
|
+
if (indicatorHistory[key].length > ML_BASE_CANDLES_WINDOW) {
|
|
607
|
+
indicatorHistory[key].splice(
|
|
608
|
+
0,
|
|
609
|
+
indicatorHistory[key].length - ML_BASE_CANDLES_WINDOW
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
const resolveCloseAtOrBefore = (candles, cursor, targetTs) => {
|
|
614
|
+
let idx = cursor;
|
|
615
|
+
while (idx + 1 < candles.length && candles[idx + 1].timestamp <= targetTs) {
|
|
616
|
+
idx += 1;
|
|
617
|
+
}
|
|
618
|
+
const close = idx < candles.length && candles[idx].timestamp <= targetTs ? candles[idx].close : null;
|
|
619
|
+
return { close, cursor: idx };
|
|
620
|
+
};
|
|
621
|
+
let window1hStart = 0;
|
|
622
|
+
let window24hStart = 0;
|
|
623
|
+
const computeWindow = (currentTimestamp, windowMs, startIdx) => {
|
|
624
|
+
const windowStart = currentTimestamp - windowMs;
|
|
625
|
+
if (timestamps.length === 0 || timestamps[0] > windowStart) {
|
|
626
|
+
return {
|
|
627
|
+
startIdx,
|
|
628
|
+
high: null,
|
|
629
|
+
low: null,
|
|
630
|
+
volume: null,
|
|
631
|
+
startClose: null,
|
|
632
|
+
hasFullWindow: false
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
let idx = startIdx;
|
|
636
|
+
while (idx < timestamps.length && timestamps[idx] < windowStart) {
|
|
637
|
+
idx += 1;
|
|
638
|
+
}
|
|
639
|
+
let high = -Infinity;
|
|
640
|
+
let low = Infinity;
|
|
641
|
+
let volume = 0;
|
|
642
|
+
for (let i = idx; i < highs.length; i += 1) {
|
|
643
|
+
const highValue = highs[i];
|
|
644
|
+
const lowValue = lows[i];
|
|
645
|
+
const volumeValue = volumes[i];
|
|
646
|
+
if (highValue > high) high = highValue;
|
|
647
|
+
if (lowValue < low) low = lowValue;
|
|
648
|
+
volume += volumeValue;
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
startIdx: idx,
|
|
652
|
+
high,
|
|
653
|
+
low,
|
|
654
|
+
volume,
|
|
655
|
+
startClose: closes[idx],
|
|
656
|
+
hasFullWindow: true
|
|
657
|
+
};
|
|
658
|
+
};
|
|
659
|
+
const findNearestStartClose = (currentTimestamp, windowMs) => {
|
|
660
|
+
if (timestamps.length === 0) {
|
|
661
|
+
return { startClose: null, startIdx: 0 };
|
|
662
|
+
}
|
|
663
|
+
const windowStart = currentTimestamp - windowMs;
|
|
664
|
+
let idx = 0;
|
|
665
|
+
while (idx < timestamps.length && timestamps[idx] < windowStart) {
|
|
666
|
+
idx += 1;
|
|
667
|
+
}
|
|
668
|
+
if (idx <= 0) {
|
|
669
|
+
return { startClose: closes[0], startIdx: 0 };
|
|
670
|
+
}
|
|
671
|
+
if (idx >= timestamps.length) {
|
|
672
|
+
const lastIdx = timestamps.length - 1;
|
|
673
|
+
return { startClose: closes[lastIdx], startIdx: lastIdx };
|
|
674
|
+
}
|
|
675
|
+
const prevIdx = idx - 1;
|
|
676
|
+
const currentIdx = timestamps.length - 1;
|
|
677
|
+
if (idx === currentIdx && timestamps[idx] > windowStart) {
|
|
678
|
+
return { startClose: closes[prevIdx], startIdx: prevIdx };
|
|
679
|
+
}
|
|
680
|
+
const prevDiff = windowStart - timestamps[prevIdx];
|
|
681
|
+
const nextDiff = timestamps[idx] - windowStart;
|
|
682
|
+
const chosenIdx = prevDiff <= nextDiff ? prevIdx : idx;
|
|
683
|
+
return { startClose: closes[chosenIdx], startIdx: chosenIdx };
|
|
684
|
+
};
|
|
685
|
+
const next = (candle, btcCandle) => {
|
|
686
|
+
candlesHistory.push(candle);
|
|
687
|
+
if (btcCandle) {
|
|
688
|
+
btcCandlesHistory.push(btcCandle);
|
|
689
|
+
}
|
|
690
|
+
closes.push(candle.close);
|
|
691
|
+
highs.push(candle.high);
|
|
692
|
+
lows.push(candle.low);
|
|
693
|
+
volumes.push(candle.volume);
|
|
694
|
+
timestamps.push(candle.timestamp);
|
|
695
|
+
const ma14Value = ma14.nextValue(candle.close);
|
|
696
|
+
const ma49Value = ma49.nextValue(candle.close);
|
|
697
|
+
const ma50Value = ma50.nextValue(candle.close);
|
|
698
|
+
const atrValue = atr.nextValue(candle);
|
|
699
|
+
const atrPctValue = atrValue != null && Number.isFinite(atrValue) && candle.close ? atrValue / candle.close * 100 : null;
|
|
700
|
+
const atrPctShortValue = atrPctValue == null ? null : atrPctShort.nextValue(atrPctValue);
|
|
701
|
+
const atrPctLongValue = atrPctValue == null ? null : atrPctLong.nextValue(atrPctValue);
|
|
702
|
+
const atrPctRatio = typeof atrPctShortValue === "number" && Number.isFinite(atrPctShortValue) && typeof atrPctLongValue === "number" && Number.isFinite(atrPctLongValue) && atrPctLongValue !== 0 ? atrPctShortValue / atrPctLongValue : null;
|
|
703
|
+
const bbValue = bb.nextValue(candle.close);
|
|
704
|
+
const obvValue = obv.nextValue(candle);
|
|
705
|
+
const smaObvValue = obvValue == null ? null : smaObv.nextValue(obvValue);
|
|
706
|
+
const macdValue = macd.nextValue(candle.close);
|
|
707
|
+
const currentTimestamp = candle.timestamp;
|
|
708
|
+
const len = candlesHistory.length;
|
|
709
|
+
const prevCandle = len > 1 ? candlesHistory[len - 2] : null;
|
|
710
|
+
const correlation = btcCandlesHistory.length > 0 ? calculateCoinBtcCorrelation(
|
|
711
|
+
candlesHistory.slice(-CORRELATION_WINDOW),
|
|
712
|
+
btcCandlesHistory.slice(-CORRELATION_WINDOW)
|
|
713
|
+
).correlation ?? 0 : 0;
|
|
714
|
+
let spread = null;
|
|
715
|
+
if (btcBinanceCandles.length > 0 && btcCoinbaseCandles.length > 0) {
|
|
716
|
+
const binanceResolved = resolveCloseAtOrBefore(
|
|
717
|
+
btcBinanceCandles,
|
|
718
|
+
btcBinanceCursor,
|
|
719
|
+
currentTimestamp
|
|
720
|
+
);
|
|
721
|
+
const coinbaseResolved = resolveCloseAtOrBefore(
|
|
722
|
+
btcCoinbaseCandles,
|
|
723
|
+
btcCoinbaseCursor,
|
|
724
|
+
currentTimestamp
|
|
725
|
+
);
|
|
726
|
+
btcBinanceCursor = binanceResolved.cursor;
|
|
727
|
+
btcCoinbaseCursor = coinbaseResolved.cursor;
|
|
728
|
+
if (binanceResolved.close != null && coinbaseResolved.close != null && Number.isFinite(binanceResolved.close) && Number.isFinite(coinbaseResolved.close) && binanceResolved.close > 0) {
|
|
729
|
+
spread = spreadSmoother.next({
|
|
730
|
+
binancePrice: binanceResolved.close,
|
|
731
|
+
coinbasePrice: coinbaseResolved.close
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const computePluginSeries = (baseResult2) => {
|
|
736
|
+
const pluginSeries2 = {};
|
|
737
|
+
for (const pluginEntry of indicatorPluginEntries) {
|
|
738
|
+
if (!pluginEntry.compute) continue;
|
|
739
|
+
const historyKey = pluginEntry.historyKey || pluginEntry.indicator.id;
|
|
740
|
+
try {
|
|
741
|
+
const pluginValue = pluginEntry.compute({
|
|
742
|
+
candle,
|
|
743
|
+
btcCandle,
|
|
744
|
+
data: candlesHistory,
|
|
745
|
+
btcData: btcCandlesHistory,
|
|
746
|
+
baseResult: baseResult2
|
|
747
|
+
});
|
|
748
|
+
if (pluginValue == null || typeof pluginValue !== "number" || !Number.isFinite(pluginValue)) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
pluginSeries2[historyKey] = pluginValue;
|
|
752
|
+
pushIndicator(historyKey, pluginValue);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (indicatorPluginErrorShown.has(historyKey)) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
indicatorPluginErrorShown.add(historyKey);
|
|
758
|
+
console.warn(
|
|
759
|
+
`Indicator plugin "${historyKey}" compute failed: ${String(error)}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return pluginSeries2;
|
|
764
|
+
};
|
|
765
|
+
if (ma14Value == null || ma49Value == null || ma50Value == null || atrValue == null || !bbValue || obvValue == null || smaObvValue == null || !macdValue) {
|
|
766
|
+
computePluginSeries({
|
|
767
|
+
prevCandle,
|
|
768
|
+
correlation,
|
|
769
|
+
spread,
|
|
770
|
+
candle
|
|
771
|
+
});
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const window1h = computeWindow(
|
|
775
|
+
currentTimestamp,
|
|
776
|
+
ONE_HOUR_MS,
|
|
777
|
+
window1hStart
|
|
778
|
+
);
|
|
779
|
+
window1hStart = window1h.startIdx;
|
|
780
|
+
const window24h = computeWindow(
|
|
781
|
+
currentTimestamp,
|
|
782
|
+
ONE_DAY_MS,
|
|
783
|
+
window24hStart
|
|
784
|
+
);
|
|
785
|
+
window24hStart = window24h.startIdx;
|
|
786
|
+
const price1hStart = findNearestStartClose(currentTimestamp, ONE_HOUR_MS);
|
|
787
|
+
const price24hStart = findNearestStartClose(currentTimestamp, ONE_DAY_MS);
|
|
788
|
+
const price1hPcntRaw = price1hStart.startClose != null ? percentChange(candle.close, price1hStart.startClose) : null;
|
|
789
|
+
const price24hPcntRaw = price24hStart.startClose != null ? percentChange(candle.close, price24hStart.startClose) : null;
|
|
790
|
+
const price1hPcnt = price1hPcntRaw ?? 0;
|
|
791
|
+
const price24hPcnt = price24hPcntRaw ?? 0;
|
|
792
|
+
const highPrice1h = window1h.hasFullWindow ? window1h.high : null;
|
|
793
|
+
const lowPrice1h = window1h.hasFullWindow ? window1h.low : null;
|
|
794
|
+
const volume1h = window1h.hasFullWindow ? window1h.volume : null;
|
|
795
|
+
const highPrice24h = window24h.hasFullWindow ? window24h.high : null;
|
|
796
|
+
const lowPrice24h = window24h.hasFullWindow ? window24h.low : null;
|
|
797
|
+
const volume24h = window24h.hasFullWindow ? window24h.volume : null;
|
|
798
|
+
let highLevel = null;
|
|
799
|
+
let lowLevel = null;
|
|
800
|
+
if (len >= indicatorPeriods.levelLookback + indicatorPeriods.levelDelay) {
|
|
801
|
+
const window = candlesHistory.slice(
|
|
802
|
+
len - indicatorPeriods.levelLookback - indicatorPeriods.levelDelay,
|
|
803
|
+
len - indicatorPeriods.levelDelay
|
|
804
|
+
);
|
|
805
|
+
highLevel = Math.max(...window.map((item) => item.high));
|
|
806
|
+
lowLevel = Math.min(...window.map((item) => item.low));
|
|
807
|
+
}
|
|
808
|
+
const baseResult = {
|
|
809
|
+
maFast: ma14Value,
|
|
810
|
+
maMedium: ma49Value,
|
|
811
|
+
maSlow: ma50Value,
|
|
812
|
+
atr: atrValue,
|
|
813
|
+
atrPct: atrPctRatio,
|
|
814
|
+
bbUpper: bbValue.upper,
|
|
815
|
+
bbMiddle: bbValue.middle,
|
|
816
|
+
bbLower: bbValue.lower,
|
|
817
|
+
obv: obvValue,
|
|
818
|
+
smaObv: smaObvValue,
|
|
819
|
+
macd: macdValue.MACD,
|
|
820
|
+
macdSignal: macdValue.signal,
|
|
821
|
+
macdHistogram: macdValue.histogram,
|
|
822
|
+
price24hPcnt,
|
|
823
|
+
price1hPcnt,
|
|
824
|
+
highPrice1h,
|
|
825
|
+
lowPrice1h,
|
|
826
|
+
volume1h,
|
|
827
|
+
highPrice24h,
|
|
828
|
+
lowPrice24h,
|
|
829
|
+
volume24h,
|
|
830
|
+
highLevel,
|
|
831
|
+
lowLevel,
|
|
832
|
+
prevClose: prevCandle?.close ?? null,
|
|
833
|
+
correlation,
|
|
834
|
+
spread
|
|
835
|
+
};
|
|
836
|
+
applyIndicatorsToHistory(baseResult, pushIndicator);
|
|
837
|
+
const pluginSeries = computePluginSeries({
|
|
838
|
+
...baseResult,
|
|
839
|
+
candle,
|
|
840
|
+
prevCandle,
|
|
841
|
+
correlation
|
|
842
|
+
});
|
|
843
|
+
const result = {
|
|
844
|
+
...baseResult,
|
|
845
|
+
...pluginSeries,
|
|
846
|
+
candle,
|
|
847
|
+
prevCandle,
|
|
848
|
+
highLevel,
|
|
849
|
+
lowLevel,
|
|
850
|
+
correlation
|
|
851
|
+
};
|
|
852
|
+
return result;
|
|
853
|
+
};
|
|
854
|
+
data.forEach((candle, index) => {
|
|
855
|
+
next(candle, btcData[index]);
|
|
856
|
+
});
|
|
857
|
+
return {
|
|
858
|
+
next,
|
|
859
|
+
result: () => {
|
|
860
|
+
const baseHistory = cloneArrayValues(indicatorHistory);
|
|
861
|
+
if (!includeMlPayload) {
|
|
862
|
+
return baseHistory;
|
|
863
|
+
}
|
|
864
|
+
const fullHistory = {
|
|
865
|
+
...baseHistory,
|
|
866
|
+
...buildMlTimeframeIndicators(candlesHistory, indicatorPeriods),
|
|
867
|
+
...buildMlCandleIndicators(candlesHistory, btcCandlesHistory),
|
|
868
|
+
...buildIndicatorSeriesByTimeframes(
|
|
869
|
+
btcCandlesHistory,
|
|
870
|
+
indicatorPeriods,
|
|
871
|
+
"btc"
|
|
872
|
+
)
|
|
873
|
+
};
|
|
874
|
+
return fullHistory;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
};
|
|
878
|
+
var buildMlTimeframeIndicators = (candles, periods = {}) => {
|
|
879
|
+
const result = {};
|
|
880
|
+
const indicatorPeriods = {
|
|
881
|
+
...DEFAULT_INDICATOR_PERIODS,
|
|
882
|
+
...periods
|
|
883
|
+
};
|
|
884
|
+
for (const timeframe of INDICATOR_TIMEFRAMES) {
|
|
885
|
+
const tfCandles = resampleCandles(candles, timeframe.minutes);
|
|
886
|
+
if (tfCandles.length === 0) continue;
|
|
887
|
+
const history = createIndicators(tfCandles, [], {
|
|
888
|
+
includeMlPayload: false,
|
|
889
|
+
periods: indicatorPeriods
|
|
890
|
+
}).result();
|
|
891
|
+
for (const [key, values] of Object.entries(history)) {
|
|
892
|
+
result[`${key}${timeframe.suffix}`] = values;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return cloneArrayValues(result);
|
|
896
|
+
};
|
|
897
|
+
var withSourcePrefix = (key, sourcePrefix = "") => {
|
|
898
|
+
if (!sourcePrefix) return key;
|
|
899
|
+
return `${sourcePrefix}${key[0].toUpperCase()}${key.slice(1)}`;
|
|
900
|
+
};
|
|
901
|
+
var buildIndicatorSeriesByTimeframes = (candles, periods, sourcePrefix = "") => {
|
|
902
|
+
const result = {};
|
|
903
|
+
if (candles.length === 0) return result;
|
|
904
|
+
const baseHistory = createIndicators(candles, [], {
|
|
905
|
+
includeMlPayload: false,
|
|
906
|
+
periods
|
|
907
|
+
}).result();
|
|
908
|
+
for (const [key, values] of Object.entries(baseHistory)) {
|
|
909
|
+
result[withSourcePrefix(key, sourcePrefix)] = values;
|
|
910
|
+
}
|
|
911
|
+
const timeframeHistory = buildMlTimeframeIndicators(candles, periods);
|
|
912
|
+
for (const [key, values] of Object.entries(timeframeHistory)) {
|
|
913
|
+
result[withSourcePrefix(key, sourcePrefix)] = values;
|
|
914
|
+
}
|
|
915
|
+
return cloneArrayValues(result);
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
// src/utils/supportResistance.ts
|
|
919
|
+
var MERGE_THRESHOLD_PCT = 0.01;
|
|
920
|
+
var detectRawSupportResistance = (data, lookAround = 2) => {
|
|
921
|
+
const supports = [];
|
|
922
|
+
const resistances = [];
|
|
923
|
+
for (let i = lookAround; i < data.length - lookAround; i++) {
|
|
924
|
+
const cur = data[i];
|
|
925
|
+
const curLow = cur.low;
|
|
926
|
+
const curHigh = cur.high;
|
|
927
|
+
let isSupport = true;
|
|
928
|
+
let isResistance = true;
|
|
929
|
+
for (let j = i - lookAround; j <= i + lookAround; j++) {
|
|
930
|
+
if (j === i) continue;
|
|
931
|
+
if (data[j].low < curLow) isSupport = false;
|
|
932
|
+
if (data[j].high > curHigh) isResistance = false;
|
|
933
|
+
}
|
|
934
|
+
if (isSupport) supports.push(curLow);
|
|
935
|
+
if (isResistance) resistances.push(curHigh);
|
|
936
|
+
}
|
|
937
|
+
return { supports, resistances };
|
|
938
|
+
};
|
|
939
|
+
var mergeCloseLevels = (levels) => {
|
|
940
|
+
if (!levels.length) return [];
|
|
941
|
+
const sorted = [...levels].sort((a, b) => a - b);
|
|
942
|
+
const merged = [];
|
|
943
|
+
let bucket = [sorted[0]];
|
|
944
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
945
|
+
const prev = bucket[bucket.length - 1];
|
|
946
|
+
const cur = sorted[i];
|
|
947
|
+
const diffPct = Math.abs(cur - prev) / prev;
|
|
948
|
+
if (diffPct <= MERGE_THRESHOLD_PCT) {
|
|
949
|
+
bucket.push(cur);
|
|
950
|
+
} else {
|
|
951
|
+
merged.push(bucket);
|
|
952
|
+
bucket = [cur];
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
merged.push(bucket);
|
|
956
|
+
const averaged = merged.map((group) => {
|
|
957
|
+
const sum = group.reduce((acc, v) => acc + v, 0);
|
|
958
|
+
return sum / group.length;
|
|
959
|
+
});
|
|
960
|
+
return averaged;
|
|
961
|
+
};
|
|
962
|
+
var getSupportResistanceLevels = (data) => {
|
|
963
|
+
if (!data || data.length === 0) {
|
|
964
|
+
return { supportLevels: [], resistanceLevels: [] };
|
|
965
|
+
}
|
|
966
|
+
const { supports, resistances } = detectRawSupportResistance(data, 2);
|
|
967
|
+
const uniqSupports = mergeCloseLevels(supports);
|
|
968
|
+
const uniqResistances = mergeCloseLevels(resistances);
|
|
969
|
+
const supportLevels = uniqSupports.slice(0, 5).map((price, idx) => ({
|
|
970
|
+
id: `support-${idx}`,
|
|
971
|
+
price
|
|
972
|
+
}));
|
|
973
|
+
const resistanceLevels = uniqResistances.slice(0, 5).map((price, idx) => ({
|
|
974
|
+
id: `resistance-${idx}`,
|
|
975
|
+
price
|
|
976
|
+
}));
|
|
977
|
+
return { supportLevels, resistanceLevels };
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
// src/utils/timestamp.ts
|
|
981
|
+
var import_date_fns = require("date-fns");
|
|
982
|
+
var import_date_fns2 = require("date-fns");
|
|
983
|
+
var toMs = (ts) => ts < 1e12 ? ts * 1e3 : ts;
|
|
984
|
+
|
|
985
|
+
// src/utils/trendLine/engine.ts
|
|
986
|
+
var DEFAULTS = TRENDLINE_DEFAULTS;
|
|
987
|
+
var toleranceAt = (lineY, epsilonPct) => Math.max(0, Math.abs(lineY) * epsilonPct);
|
|
988
|
+
var buildLineEvaluator = (params) => {
|
|
989
|
+
const { t1, y1, t2, y2 } = params;
|
|
990
|
+
const deltaTime = t2 - t1;
|
|
991
|
+
if (deltaTime === 0) return (_timeMs) => y1;
|
|
992
|
+
const slope = (y2 - y1) / deltaTime;
|
|
993
|
+
return (timeMs) => y1 + slope * (timeMs - t1);
|
|
994
|
+
};
|
|
995
|
+
var buildAlphaSeries = (params) => {
|
|
996
|
+
const { timestampsMs, closeSeries, evaluateY, window } = params;
|
|
997
|
+
if (!timestampsMs.length || !closeSeries.length) return [];
|
|
998
|
+
const startIndex = Math.max(0, timestampsMs.length - window);
|
|
999
|
+
const result = [];
|
|
1000
|
+
for (let i = startIndex; i < timestampsMs.length; i++) {
|
|
1001
|
+
const close = closeSeries[i];
|
|
1002
|
+
if (!Number.isFinite(close) || close == 0) {
|
|
1003
|
+
result.push(0);
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const lineY = evaluateY(timestampsMs[i]);
|
|
1007
|
+
result.push(lineY / close);
|
|
1008
|
+
}
|
|
1009
|
+
return result;
|
|
1010
|
+
};
|
|
1011
|
+
var hasTooLargeTouchGaps = (touchIndices, maxTouchGap) => {
|
|
1012
|
+
if (!Number.isFinite(maxTouchGap) || maxTouchGap <= 0) return false;
|
|
1013
|
+
if (touchIndices.length < 2) return true;
|
|
1014
|
+
for (let index = 1; index < touchIndices.length; index++) {
|
|
1015
|
+
if (touchIndices[index] - touchIndices[index - 1] > maxTouchGap)
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
};
|
|
1020
|
+
var collectTouchIndices = (params) => {
|
|
1021
|
+
const {
|
|
1022
|
+
bodySeriesForTouches,
|
|
1023
|
+
timestampsMs,
|
|
1024
|
+
startIndex,
|
|
1025
|
+
endIndex,
|
|
1026
|
+
evaluateY,
|
|
1027
|
+
epsilon,
|
|
1028
|
+
minTouchGap
|
|
1029
|
+
} = params;
|
|
1030
|
+
const touchIndices = [];
|
|
1031
|
+
let lastTouchIndex = -Infinity;
|
|
1032
|
+
for (let barIndex = startIndex; barIndex <= endIndex; barIndex++) {
|
|
1033
|
+
const lineY = evaluateY(timestampsMs[barIndex]);
|
|
1034
|
+
const tolerance = toleranceAt(lineY, epsilon);
|
|
1035
|
+
const bodyValue = bodySeriesForTouches[barIndex];
|
|
1036
|
+
if (Math.abs(bodyValue - lineY) <= tolerance) {
|
|
1037
|
+
if (touchIndices.length === 0 || barIndex - lastTouchIndex >= minTouchGap) {
|
|
1038
|
+
touchIndices.push(barIndex);
|
|
1039
|
+
lastTouchIndex = barIndex;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return touchIndices;
|
|
1044
|
+
};
|
|
1045
|
+
var hasCloseBreachInRange = (params) => {
|
|
1046
|
+
const {
|
|
1047
|
+
mode,
|
|
1048
|
+
closeSeries,
|
|
1049
|
+
timestampsMs,
|
|
1050
|
+
startIndex,
|
|
1051
|
+
endIndex,
|
|
1052
|
+
evaluateY,
|
|
1053
|
+
epsilon
|
|
1054
|
+
} = params;
|
|
1055
|
+
if (startIndex > endIndex) return false;
|
|
1056
|
+
for (let barIndex = startIndex; barIndex <= endIndex; barIndex++) {
|
|
1057
|
+
const lineY = evaluateY(timestampsMs[barIndex]);
|
|
1058
|
+
const tolerance = toleranceAt(lineY, epsilon);
|
|
1059
|
+
const closePrice = closeSeries[barIndex];
|
|
1060
|
+
if (mode === "lows") {
|
|
1061
|
+
if (closePrice < lineY - tolerance) return true;
|
|
1062
|
+
} else {
|
|
1063
|
+
if (closePrice > lineY + tolerance) return true;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return false;
|
|
1067
|
+
};
|
|
1068
|
+
var BLOCK_SIZE = 64;
|
|
1069
|
+
var getBlockIndex = (barIndex) => Math.floor(barIndex / BLOCK_SIZE);
|
|
1070
|
+
var ensureBlockValue = (arr, blockIndex, initial) => {
|
|
1071
|
+
while (arr.length <= blockIndex) arr.push(initial);
|
|
1072
|
+
};
|
|
1073
|
+
var updateBlockStats = (stats, barIndex, lowValue, highValue) => {
|
|
1074
|
+
const blockIndex = getBlockIndex(barIndex);
|
|
1075
|
+
ensureBlockValue(stats.lowBlockMins, blockIndex, Number.POSITIVE_INFINITY);
|
|
1076
|
+
ensureBlockValue(stats.highBlockMaxs, blockIndex, Number.NEGATIVE_INFINITY);
|
|
1077
|
+
stats.lowBlockMins[blockIndex] = Math.min(
|
|
1078
|
+
stats.lowBlockMins[blockIndex],
|
|
1079
|
+
lowValue
|
|
1080
|
+
);
|
|
1081
|
+
stats.highBlockMaxs[blockIndex] = Math.max(
|
|
1082
|
+
stats.highBlockMaxs[blockIndex],
|
|
1083
|
+
highValue
|
|
1084
|
+
);
|
|
1085
|
+
};
|
|
1086
|
+
var hasWickBreachOnSegmentFast = (params) => {
|
|
1087
|
+
const {
|
|
1088
|
+
mode,
|
|
1089
|
+
lowSeries,
|
|
1090
|
+
highSeries,
|
|
1091
|
+
timestampsMs,
|
|
1092
|
+
startIndex,
|
|
1093
|
+
endIndex,
|
|
1094
|
+
evaluateY,
|
|
1095
|
+
epsilon,
|
|
1096
|
+
blockStats
|
|
1097
|
+
} = params;
|
|
1098
|
+
if (startIndex > endIndex) return false;
|
|
1099
|
+
const scanBarsPrecisely = (fromIndex, toIndex) => {
|
|
1100
|
+
for (let barIndex = fromIndex; barIndex <= toIndex; barIndex++) {
|
|
1101
|
+
const lineY = evaluateY(timestampsMs[barIndex]);
|
|
1102
|
+
const tolerance = toleranceAt(lineY, epsilon);
|
|
1103
|
+
if (mode === "lows") {
|
|
1104
|
+
if (lowSeries[barIndex] < lineY - tolerance) return true;
|
|
1105
|
+
} else {
|
|
1106
|
+
if (highSeries[barIndex] > lineY + tolerance) return true;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return false;
|
|
1110
|
+
};
|
|
1111
|
+
const startBlockIndex = getBlockIndex(startIndex);
|
|
1112
|
+
const endBlockIndex = getBlockIndex(endIndex);
|
|
1113
|
+
if (startBlockIndex === endBlockIndex)
|
|
1114
|
+
return scanBarsPrecisely(startIndex, endIndex);
|
|
1115
|
+
const firstBlockEndIndex = (startBlockIndex + 1) * BLOCK_SIZE - 1;
|
|
1116
|
+
if (scanBarsPrecisely(startIndex, Math.min(firstBlockEndIndex, endIndex)))
|
|
1117
|
+
return true;
|
|
1118
|
+
const lastBlockStartIndex = endBlockIndex * BLOCK_SIZE;
|
|
1119
|
+
if (scanBarsPrecisely(Math.max(lastBlockStartIndex, startIndex), endIndex))
|
|
1120
|
+
return true;
|
|
1121
|
+
for (let blockIndex = startBlockIndex + 1; blockIndex <= endBlockIndex - 1; blockIndex++) {
|
|
1122
|
+
const blockStartIndex = blockIndex * BLOCK_SIZE;
|
|
1123
|
+
const blockEndIndex = blockStartIndex + BLOCK_SIZE - 1;
|
|
1124
|
+
const startTimestamp = timestampsMs[blockStartIndex];
|
|
1125
|
+
const endTimestamp = timestampsMs[blockEndIndex];
|
|
1126
|
+
const startLineY = evaluateY(startTimestamp);
|
|
1127
|
+
const endLineY = evaluateY(endTimestamp);
|
|
1128
|
+
if (mode === "lows") {
|
|
1129
|
+
const startThreshold = startLineY - toleranceAt(startLineY, epsilon);
|
|
1130
|
+
const endThreshold = endLineY - toleranceAt(endLineY, epsilon);
|
|
1131
|
+
const maxThresholdInBlock = Math.max(startThreshold, endThreshold);
|
|
1132
|
+
const blockLowMin = blockStats.lowBlockMins[blockIndex];
|
|
1133
|
+
if (blockLowMin >= maxThresholdInBlock) continue;
|
|
1134
|
+
if (scanBarsPrecisely(blockStartIndex, blockEndIndex)) return true;
|
|
1135
|
+
} else {
|
|
1136
|
+
const startThreshold = startLineY + toleranceAt(startLineY, epsilon);
|
|
1137
|
+
const endThreshold = endLineY + toleranceAt(endLineY, epsilon);
|
|
1138
|
+
const minThresholdInBlock = Math.min(startThreshold, endThreshold);
|
|
1139
|
+
const blockHighMax = blockStats.highBlockMaxs[blockIndex];
|
|
1140
|
+
if (blockHighMax <= minThresholdInBlock) continue;
|
|
1141
|
+
if (scanBarsPrecisely(blockStartIndex, blockEndIndex)) return true;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
};
|
|
1146
|
+
var createTrendlineEngine = (initialCandles, options) => {
|
|
1147
|
+
const opts = {
|
|
1148
|
+
mode: options.mode,
|
|
1149
|
+
maxLines: options.maxLines ?? DEFAULTS.maxLines,
|
|
1150
|
+
range: options.range ?? DEFAULTS.range,
|
|
1151
|
+
firstRange: options.firstRange ?? DEFAULTS.firstRange,
|
|
1152
|
+
epsilon: options.epsilon ?? DEFAULTS.epsilon,
|
|
1153
|
+
epsilonOffset: options.epsilonOffset ?? DEFAULTS.epsilonOffset,
|
|
1154
|
+
minTouches: options.minTouches ?? DEFAULTS.minTouches,
|
|
1155
|
+
minDistance: options.minDistance ?? DEFAULTS.minDistance,
|
|
1156
|
+
minTouchGap: options.minTouchGap ?? DEFAULTS.minTouchGap,
|
|
1157
|
+
maxTouchGap: options.maxTouchGap ?? DEFAULTS.maxTouchGap,
|
|
1158
|
+
offset: options.offset ?? DEFAULTS.offset,
|
|
1159
|
+
capture: options.capture ?? DEFAULTS.capture,
|
|
1160
|
+
bestLines: options.bestLines ?? DEFAULTS.bestLines,
|
|
1161
|
+
maxDistance: options.maxDistance ?? DEFAULTS.maxDistance
|
|
1162
|
+
};
|
|
1163
|
+
let timestampsMs = [];
|
|
1164
|
+
let closeSeries = [];
|
|
1165
|
+
let lowSeries = [];
|
|
1166
|
+
let highSeries = [];
|
|
1167
|
+
let shadowSeries = [];
|
|
1168
|
+
const firstRangeWindowSize = 2 * opts.firstRange + 1;
|
|
1169
|
+
let lowFirstDeque = [];
|
|
1170
|
+
let highFirstDeque = [];
|
|
1171
|
+
let lowFirstCenter = [];
|
|
1172
|
+
let highFirstCenter = [];
|
|
1173
|
+
const blockStats = { lowBlockMins: [], highBlockMaxs: [] };
|
|
1174
|
+
let extremaDeque = [];
|
|
1175
|
+
let rawExtremaPoints = [];
|
|
1176
|
+
let clusteredAnchors = [];
|
|
1177
|
+
let currentClusterBest = null;
|
|
1178
|
+
let lastRawExtremum = null;
|
|
1179
|
+
let activeLines = [];
|
|
1180
|
+
let pairCache = /* @__PURE__ */ new Map();
|
|
1181
|
+
const MAX_PAIR_CACHE = 5e3;
|
|
1182
|
+
const resetState = () => {
|
|
1183
|
+
timestampsMs = [];
|
|
1184
|
+
closeSeries = [];
|
|
1185
|
+
lowSeries = [];
|
|
1186
|
+
highSeries = [];
|
|
1187
|
+
shadowSeries = [];
|
|
1188
|
+
blockStats.lowBlockMins = [];
|
|
1189
|
+
blockStats.highBlockMaxs = [];
|
|
1190
|
+
extremaDeque = [];
|
|
1191
|
+
rawExtremaPoints = [];
|
|
1192
|
+
lowFirstDeque = [];
|
|
1193
|
+
highFirstDeque = [];
|
|
1194
|
+
lowFirstCenter = [];
|
|
1195
|
+
highFirstCenter = [];
|
|
1196
|
+
clusteredAnchors = [];
|
|
1197
|
+
currentClusterBest = null;
|
|
1198
|
+
lastRawExtremum = null;
|
|
1199
|
+
activeLines = [];
|
|
1200
|
+
pairCache.clear();
|
|
1201
|
+
};
|
|
1202
|
+
const updateExtremaDeque = (barIndex) => {
|
|
1203
|
+
const windowSize = 2 * opts.range + 1;
|
|
1204
|
+
const findMin = opts.mode === "lows";
|
|
1205
|
+
const isBetter = findMin ? (leftValue, rightValue) => leftValue <= rightValue : (leftValue, rightValue) => leftValue >= rightValue;
|
|
1206
|
+
while (extremaDeque.length > 0 && !isBetter(
|
|
1207
|
+
shadowSeries[extremaDeque[extremaDeque.length - 1]],
|
|
1208
|
+
shadowSeries[barIndex]
|
|
1209
|
+
)) {
|
|
1210
|
+
extremaDeque.pop();
|
|
1211
|
+
}
|
|
1212
|
+
extremaDeque.push(barIndex);
|
|
1213
|
+
const startIndex = barIndex - windowSize + 1;
|
|
1214
|
+
while (extremaDeque.length > 0 && extremaDeque[0] < startIndex) {
|
|
1215
|
+
extremaDeque.shift();
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
const isStrongFirstAnchorFast = (anchorIndex) => {
|
|
1219
|
+
const lastBarIndex = lowSeries.length - 1;
|
|
1220
|
+
const startIndex = Math.max(0, anchorIndex - opts.firstRange);
|
|
1221
|
+
const endIndex = Math.min(lastBarIndex, anchorIndex + opts.firstRange);
|
|
1222
|
+
const canUseCenter = anchorIndex - opts.firstRange >= 0 && anchorIndex + opts.firstRange <= lastBarIndex;
|
|
1223
|
+
if (opts.mode === "lows") {
|
|
1224
|
+
if (canUseCenter && Number.isFinite(lowFirstCenter[anchorIndex])) {
|
|
1225
|
+
return lowSeries[anchorIndex] === lowFirstCenter[anchorIndex];
|
|
1226
|
+
}
|
|
1227
|
+
let windowMin = Number.POSITIVE_INFINITY;
|
|
1228
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
1229
|
+
if (lowSeries[i] < windowMin) windowMin = lowSeries[i];
|
|
1230
|
+
}
|
|
1231
|
+
return lowSeries[anchorIndex] === windowMin;
|
|
1232
|
+
}
|
|
1233
|
+
if (canUseCenter && Number.isFinite(highFirstCenter[anchorIndex])) {
|
|
1234
|
+
return highSeries[anchorIndex] === highFirstCenter[anchorIndex];
|
|
1235
|
+
}
|
|
1236
|
+
let windowMax = Number.NEGATIVE_INFINITY;
|
|
1237
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
1238
|
+
if (highSeries[i] > windowMax) windowMax = highSeries[i];
|
|
1239
|
+
}
|
|
1240
|
+
return highSeries[anchorIndex] === windowMax;
|
|
1241
|
+
};
|
|
1242
|
+
const updateFirstRangeExtrema = (barIndex) => {
|
|
1243
|
+
if (firstRangeWindowSize <= 1) return;
|
|
1244
|
+
while (lowFirstDeque.length > 0 && lowSeries[lowFirstDeque[lowFirstDeque.length - 1]] >= lowSeries[barIndex]) {
|
|
1245
|
+
lowFirstDeque.pop();
|
|
1246
|
+
}
|
|
1247
|
+
lowFirstDeque.push(barIndex);
|
|
1248
|
+
while (highFirstDeque.length > 0 && highSeries[highFirstDeque[highFirstDeque.length - 1]] <= highSeries[barIndex]) {
|
|
1249
|
+
highFirstDeque.pop();
|
|
1250
|
+
}
|
|
1251
|
+
highFirstDeque.push(barIndex);
|
|
1252
|
+
const startIndex = barIndex - firstRangeWindowSize + 1;
|
|
1253
|
+
while (lowFirstDeque.length > 0 && lowFirstDeque[0] < startIndex) {
|
|
1254
|
+
lowFirstDeque.shift();
|
|
1255
|
+
}
|
|
1256
|
+
while (highFirstDeque.length > 0 && highFirstDeque[0] < startIndex) {
|
|
1257
|
+
highFirstDeque.shift();
|
|
1258
|
+
}
|
|
1259
|
+
if (startIndex >= 0) {
|
|
1260
|
+
const centerIndex = barIndex - opts.firstRange;
|
|
1261
|
+
lowFirstCenter[centerIndex] = lowSeries[lowFirstDeque[0]];
|
|
1262
|
+
highFirstCenter[centerIndex] = highSeries[highFirstDeque[0]];
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
const getPairCache = (leftAnchor, rightAnchor) => {
|
|
1266
|
+
const key = `${leftAnchor.x}|${rightAnchor.x}`;
|
|
1267
|
+
const cached = pairCache.get(key);
|
|
1268
|
+
if (cached) return cached;
|
|
1269
|
+
const evaluateY = buildLineEvaluator({
|
|
1270
|
+
t1: leftAnchor.t,
|
|
1271
|
+
y1: leftAnchor.y,
|
|
1272
|
+
t2: rightAnchor.t,
|
|
1273
|
+
y2: rightAnchor.y
|
|
1274
|
+
});
|
|
1275
|
+
const touchIndices = collectTouchIndices({
|
|
1276
|
+
bodySeriesForTouches: shadowSeries,
|
|
1277
|
+
timestampsMs,
|
|
1278
|
+
startIndex: leftAnchor.x,
|
|
1279
|
+
endIndex: rightAnchor.x,
|
|
1280
|
+
evaluateY,
|
|
1281
|
+
epsilon: opts.epsilon,
|
|
1282
|
+
minTouchGap: opts.minTouchGap
|
|
1283
|
+
});
|
|
1284
|
+
const touchCount = touchIndices.length;
|
|
1285
|
+
const touchSpan = touchCount > 1 ? touchIndices[touchCount - 1] - touchIndices[0] : 0;
|
|
1286
|
+
const lastTouches = touchCount > 2 ? touchIndices.slice(-2) : touchIndices;
|
|
1287
|
+
const hasTouchGap = hasTooLargeTouchGaps(
|
|
1288
|
+
[...lastTouches, rightAnchor.x],
|
|
1289
|
+
opts.maxTouchGap
|
|
1290
|
+
);
|
|
1291
|
+
const wickBreached = hasWickBreachOnSegmentFast({
|
|
1292
|
+
mode: opts.mode,
|
|
1293
|
+
lowSeries,
|
|
1294
|
+
highSeries,
|
|
1295
|
+
timestampsMs,
|
|
1296
|
+
startIndex: leftAnchor.x,
|
|
1297
|
+
endIndex: rightAnchor.x,
|
|
1298
|
+
evaluateY,
|
|
1299
|
+
epsilon: opts.epsilon,
|
|
1300
|
+
blockStats
|
|
1301
|
+
});
|
|
1302
|
+
const entry = {
|
|
1303
|
+
distance: rightAnchor.x - leftAnchor.x,
|
|
1304
|
+
evaluateY,
|
|
1305
|
+
touchIndices,
|
|
1306
|
+
touchCount,
|
|
1307
|
+
touchSpan,
|
|
1308
|
+
hasTouchGap,
|
|
1309
|
+
wickBreached
|
|
1310
|
+
};
|
|
1311
|
+
if (pairCache.size > MAX_PAIR_CACHE) {
|
|
1312
|
+
pairCache.clear();
|
|
1313
|
+
}
|
|
1314
|
+
pairCache.set(key, entry);
|
|
1315
|
+
return entry;
|
|
1316
|
+
};
|
|
1317
|
+
const rebuildCandidatesLikeBatch = () => {
|
|
1318
|
+
const anchors = currentClusterBest != null ? [...clusteredAnchors, currentClusterBest] : clusteredAnchors;
|
|
1319
|
+
const anchorsLength = anchors.length;
|
|
1320
|
+
const lastBarIndex = timestampsMs.length - 1;
|
|
1321
|
+
if (lastBarIndex < 0) {
|
|
1322
|
+
activeLines = [];
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (rawExtremaPoints.length < opts.minTouches || anchorsLength < opts.minTouches) {
|
|
1326
|
+
activeLines = [];
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const candidates = [];
|
|
1330
|
+
const anchorXs = anchors.map((pt) => pt.x);
|
|
1331
|
+
const lowerBound = (arr, value) => {
|
|
1332
|
+
let left = 0;
|
|
1333
|
+
let right = arr.length;
|
|
1334
|
+
while (left < right) {
|
|
1335
|
+
const mid = left + right >> 1;
|
|
1336
|
+
if (arr[mid] < value) left = mid + 1;
|
|
1337
|
+
else right = mid;
|
|
1338
|
+
}
|
|
1339
|
+
return left;
|
|
1340
|
+
};
|
|
1341
|
+
const upperBound = (arr, value) => {
|
|
1342
|
+
let left = 0;
|
|
1343
|
+
let right = arr.length;
|
|
1344
|
+
while (left < right) {
|
|
1345
|
+
const mid = left + right >> 1;
|
|
1346
|
+
if (arr[mid] <= value) left = mid + 1;
|
|
1347
|
+
else right = mid;
|
|
1348
|
+
}
|
|
1349
|
+
return left - 1;
|
|
1350
|
+
};
|
|
1351
|
+
for (let rightAnchorIndex = anchorsLength - 1; rightAnchorIndex >= 0; rightAnchorIndex--) {
|
|
1352
|
+
const rightAnchor = anchors[rightAnchorIndex];
|
|
1353
|
+
const rightX = rightAnchor.x;
|
|
1354
|
+
const leftMinX = rightX - opts.maxDistance;
|
|
1355
|
+
const leftMaxX = rightX - opts.minDistance;
|
|
1356
|
+
let leftStart = lowerBound(anchorXs, leftMinX);
|
|
1357
|
+
let leftEnd = upperBound(anchorXs, leftMaxX);
|
|
1358
|
+
if (leftEnd >= rightAnchorIndex) leftEnd = rightAnchorIndex - 1;
|
|
1359
|
+
if (leftStart > leftEnd) continue;
|
|
1360
|
+
for (let leftAnchorIndex = leftEnd; leftAnchorIndex >= leftStart; leftAnchorIndex--) {
|
|
1361
|
+
if (candidates.length >= opts.maxLines) break;
|
|
1362
|
+
const leftAnchor = anchors[leftAnchorIndex];
|
|
1363
|
+
const distance = rightAnchor.x - leftAnchor.x;
|
|
1364
|
+
if (distance < opts.minDistance) continue;
|
|
1365
|
+
if (distance > opts.maxDistance) break;
|
|
1366
|
+
if (!isStrongFirstAnchorFast(leftAnchor.x)) continue;
|
|
1367
|
+
const slope = (rightAnchor.y - leftAnchor.y) / (rightAnchor.x - leftAnchor.x);
|
|
1368
|
+
if (opts.mode === "lows" && slope <= 0) continue;
|
|
1369
|
+
if (opts.mode === "highs" && slope >= 0) continue;
|
|
1370
|
+
const cached = getPairCache(leftAnchor, rightAnchor);
|
|
1371
|
+
if (cached.touchCount < opts.minTouches) continue;
|
|
1372
|
+
if (cached.hasTouchGap) continue;
|
|
1373
|
+
if (cached.touchSpan < opts.minDistance) continue;
|
|
1374
|
+
if (cached.wickBreached) continue;
|
|
1375
|
+
const closeBreachEndIndex = lastBarIndex - Math.max(0, opts.offset);
|
|
1376
|
+
const closeBreachStartIndex = rightAnchor.x + 1;
|
|
1377
|
+
if (closeBreachStartIndex <= closeBreachEndIndex) {
|
|
1378
|
+
const closeBreached = hasCloseBreachInRange({
|
|
1379
|
+
mode: opts.mode,
|
|
1380
|
+
closeSeries,
|
|
1381
|
+
timestampsMs,
|
|
1382
|
+
startIndex: closeBreachStartIndex,
|
|
1383
|
+
endIndex: closeBreachEndIndex,
|
|
1384
|
+
evaluateY: cached.evaluateY,
|
|
1385
|
+
epsilon: opts.epsilon
|
|
1386
|
+
});
|
|
1387
|
+
if (closeBreached) continue;
|
|
1388
|
+
}
|
|
1389
|
+
const captureHitIndices = [];
|
|
1390
|
+
if (opts.capture) {
|
|
1391
|
+
const captureStartIndex = Math.max(
|
|
1392
|
+
rightAnchor.x + 1,
|
|
1393
|
+
lastBarIndex - opts.offset + 1
|
|
1394
|
+
);
|
|
1395
|
+
const captureEndIndex = lastBarIndex;
|
|
1396
|
+
for (let barIndex = captureStartIndex; barIndex <= captureEndIndex; barIndex++) {
|
|
1397
|
+
const lineY = cached.evaluateY(timestampsMs[barIndex]);
|
|
1398
|
+
const offsetTolerance = toleranceAt(lineY, opts.epsilonOffset);
|
|
1399
|
+
const hit = opts.mode === "lows" ? lowSeries[barIndex] <= lineY - offsetTolerance : highSeries[barIndex] >= lineY + offsetTolerance;
|
|
1400
|
+
if (hit) captureHitIndices.push(barIndex);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const runtime = {
|
|
1404
|
+
leftAnchor,
|
|
1405
|
+
rightAnchor,
|
|
1406
|
+
distance: cached.distance,
|
|
1407
|
+
evaluateY: cached.evaluateY,
|
|
1408
|
+
touchIndices: cached.touchIndices,
|
|
1409
|
+
captureHitIndices,
|
|
1410
|
+
invalid: false
|
|
1411
|
+
};
|
|
1412
|
+
candidates.push(runtime);
|
|
1413
|
+
}
|
|
1414
|
+
if (candidates.length >= opts.maxLines) break;
|
|
1415
|
+
}
|
|
1416
|
+
activeLines = candidates;
|
|
1417
|
+
};
|
|
1418
|
+
const maybeFinalizeClusterAndRebuild = (rawPoint) => {
|
|
1419
|
+
if (!currentClusterBest) {
|
|
1420
|
+
currentClusterBest = rawPoint;
|
|
1421
|
+
lastRawExtremum = rawPoint;
|
|
1422
|
+
rebuildCandidatesLikeBatch();
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (rawPoint.x - lastRawExtremum.x < opts.minDistance) {
|
|
1426
|
+
const better = opts.mode === "lows" ? rawPoint.y < currentClusterBest.y : rawPoint.y > currentClusterBest.y;
|
|
1427
|
+
if (better) {
|
|
1428
|
+
currentClusterBest = rawPoint;
|
|
1429
|
+
rebuildCandidatesLikeBatch();
|
|
1430
|
+
}
|
|
1431
|
+
lastRawExtremum = rawPoint;
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
clusteredAnchors.push(currentClusterBest);
|
|
1435
|
+
currentClusterBest = rawPoint;
|
|
1436
|
+
lastRawExtremum = rawPoint;
|
|
1437
|
+
rebuildCandidatesLikeBatch();
|
|
1438
|
+
};
|
|
1439
|
+
const maybeAddRawExtremum = (endIndex) => {
|
|
1440
|
+
const range = opts.range;
|
|
1441
|
+
if (endIndex < 2 * range) return;
|
|
1442
|
+
const centerIndex = endIndex - range;
|
|
1443
|
+
const extremaValue = shadowSeries[extremaDeque[0]];
|
|
1444
|
+
if (shadowSeries[centerIndex] !== extremaValue) return;
|
|
1445
|
+
const rawPoint = {
|
|
1446
|
+
x: centerIndex,
|
|
1447
|
+
y: shadowSeries[centerIndex],
|
|
1448
|
+
t: timestampsMs[centerIndex]
|
|
1449
|
+
};
|
|
1450
|
+
rawExtremaPoints.push(rawPoint);
|
|
1451
|
+
maybeFinalizeClusterAndRebuild(rawPoint);
|
|
1452
|
+
};
|
|
1453
|
+
const updateCloseBreachDeferred = (line, lastBarIndex) => {
|
|
1454
|
+
if (line.invalid) return;
|
|
1455
|
+
if (opts.offset <= 0) return;
|
|
1456
|
+
const checkIndex = lastBarIndex - opts.offset;
|
|
1457
|
+
if (checkIndex <= line.rightAnchor.x) return;
|
|
1458
|
+
if (checkIndex < 0 || checkIndex >= timestampsMs.length) return;
|
|
1459
|
+
const timestamp = timestampsMs[checkIndex];
|
|
1460
|
+
const lineY = line.evaluateY(timestamp);
|
|
1461
|
+
const tolerance = toleranceAt(lineY, opts.epsilon);
|
|
1462
|
+
const closePrice = closeSeries[checkIndex];
|
|
1463
|
+
if (opts.mode === "lows") {
|
|
1464
|
+
if (closePrice < lineY - tolerance) line.invalid = true;
|
|
1465
|
+
} else {
|
|
1466
|
+
if (closePrice > lineY + tolerance) line.invalid = true;
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
const updateCaptureSlidingWindow = (line, lastBarIndex) => {
|
|
1470
|
+
if (line.invalid) return;
|
|
1471
|
+
if (!opts.capture) return;
|
|
1472
|
+
if (opts.offset <= 0) return;
|
|
1473
|
+
const windowStartIndex = Math.max(
|
|
1474
|
+
line.rightAnchor.x + 1,
|
|
1475
|
+
lastBarIndex - opts.offset + 1
|
|
1476
|
+
);
|
|
1477
|
+
if (lastBarIndex >= windowStartIndex) {
|
|
1478
|
+
const timestamp = timestampsMs[lastBarIndex];
|
|
1479
|
+
const lineY = line.evaluateY(timestamp);
|
|
1480
|
+
const offsetTolerance = toleranceAt(lineY, opts.epsilonOffset);
|
|
1481
|
+
const hit = opts.mode === "lows" ? lowSeries[lastBarIndex] <= lineY - offsetTolerance : highSeries[lastBarIndex] >= lineY + offsetTolerance;
|
|
1482
|
+
if (hit) line.captureHitIndices.push(lastBarIndex);
|
|
1483
|
+
}
|
|
1484
|
+
while (line.captureHitIndices.length > 0 && line.captureHitIndices[0] < windowStartIndex) {
|
|
1485
|
+
line.captureHitIndices.shift();
|
|
1486
|
+
}
|
|
1487
|
+
if (line.captureHitIndices.length === 0) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
const gcInvalidLines = () => {
|
|
1492
|
+
activeLines = activeLines.filter((line) => !line.invalid);
|
|
1493
|
+
};
|
|
1494
|
+
const buildResult = () => {
|
|
1495
|
+
if (!timestampsMs.length) return [];
|
|
1496
|
+
const lastBarIndex = timestampsMs.length - 1;
|
|
1497
|
+
const lastTimestamp = timestampsMs[lastBarIndex];
|
|
1498
|
+
const filtered = activeLines.filter((line) => {
|
|
1499
|
+
if (line.invalid) return false;
|
|
1500
|
+
if (line.touchIndices.length < opts.minTouches) return false;
|
|
1501
|
+
const touchSpan = line.touchIndices[line.touchIndices.length - 1] - line.touchIndices[0];
|
|
1502
|
+
if (touchSpan < opts.minDistance) return false;
|
|
1503
|
+
const lastTouches = line.touchIndices.length > 2 ? line.touchIndices.slice(-2) : line.touchIndices;
|
|
1504
|
+
if (hasTooLargeTouchGaps(
|
|
1505
|
+
[...lastTouches, line.rightAnchor.x],
|
|
1506
|
+
opts.maxTouchGap
|
|
1507
|
+
))
|
|
1508
|
+
return false;
|
|
1509
|
+
if (opts.capture) {
|
|
1510
|
+
const windowStartIndex = Math.max(
|
|
1511
|
+
line.rightAnchor.x + 1,
|
|
1512
|
+
lastBarIndex - opts.offset + 1
|
|
1513
|
+
);
|
|
1514
|
+
while (line.captureHitIndices.length > 0 && line.captureHitIndices[0] < windowStartIndex) {
|
|
1515
|
+
line.captureHitIndices.shift();
|
|
1516
|
+
}
|
|
1517
|
+
if (line.captureHitIndices.length === 0) return false;
|
|
1518
|
+
}
|
|
1519
|
+
return true;
|
|
1520
|
+
});
|
|
1521
|
+
filtered.sort((a, b) => b.leftAnchor.x - a.leftAnchor.x);
|
|
1522
|
+
const takeCount = Math.max(
|
|
1523
|
+
1,
|
|
1524
|
+
Math.min(opts.bestLines, opts.maxLines, filtered.length)
|
|
1525
|
+
);
|
|
1526
|
+
const best = filtered.slice(0, takeCount);
|
|
1527
|
+
return best.map((line, index) => ({
|
|
1528
|
+
id: `${opts.mode}TrendLine-${index + 1}`,
|
|
1529
|
+
mode: opts.mode,
|
|
1530
|
+
distance: line.distance,
|
|
1531
|
+
points: [
|
|
1532
|
+
{
|
|
1533
|
+
timestamp: line.leftAnchor.t,
|
|
1534
|
+
value: line.evaluateY(line.leftAnchor.t)
|
|
1535
|
+
},
|
|
1536
|
+
{ timestamp: lastTimestamp, value: line.evaluateY(lastTimestamp) }
|
|
1537
|
+
],
|
|
1538
|
+
touches: line.touchIndices.filter(
|
|
1539
|
+
(barIndex) => barIndex !== line.leftAnchor.x && barIndex !== line.rightAnchor.x
|
|
1540
|
+
).map((barIndex) => {
|
|
1541
|
+
const timestamp = timestampsMs[barIndex];
|
|
1542
|
+
return { timestamp, value: line.evaluateY(timestamp) };
|
|
1543
|
+
}),
|
|
1544
|
+
alpha: buildAlphaSeries({
|
|
1545
|
+
timestampsMs,
|
|
1546
|
+
closeSeries,
|
|
1547
|
+
evaluateY: line.evaluateY,
|
|
1548
|
+
window: 10
|
|
1549
|
+
})
|
|
1550
|
+
}));
|
|
1551
|
+
};
|
|
1552
|
+
const appendCandle = (candle) => {
|
|
1553
|
+
const barIndex = timestampsMs.length;
|
|
1554
|
+
const timestampMs = toMs(candle.timestamp);
|
|
1555
|
+
timestampsMs.push(timestampMs);
|
|
1556
|
+
closeSeries.push(candle.close);
|
|
1557
|
+
lowSeries.push(candle.low);
|
|
1558
|
+
highSeries.push(candle.high);
|
|
1559
|
+
const shadowValue = opts.mode === "lows" ? candle.low : candle.high;
|
|
1560
|
+
shadowSeries.push(shadowValue);
|
|
1561
|
+
updateBlockStats(blockStats, barIndex, candle.low, candle.high);
|
|
1562
|
+
updateFirstRangeExtrema(barIndex);
|
|
1563
|
+
updateExtremaDeque(barIndex);
|
|
1564
|
+
maybeAddRawExtremum(barIndex);
|
|
1565
|
+
let invalidatedByOffsetLogic = false;
|
|
1566
|
+
for (const line of activeLines) {
|
|
1567
|
+
const wasInvalid = line.invalid;
|
|
1568
|
+
updateCloseBreachDeferred(line, barIndex);
|
|
1569
|
+
updateCaptureSlidingWindow(line, barIndex);
|
|
1570
|
+
if (!wasInvalid && line.invalid) invalidatedByOffsetLogic = true;
|
|
1571
|
+
}
|
|
1572
|
+
gcInvalidLines();
|
|
1573
|
+
if (invalidatedByOffsetLogic) {
|
|
1574
|
+
rebuildCandidatesLikeBatch();
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
const next = (candle) => {
|
|
1578
|
+
appendCandle(candle);
|
|
1579
|
+
let result = buildResult();
|
|
1580
|
+
if (opts.capture && result.length === 0 && rawExtremaPoints.length) {
|
|
1581
|
+
rebuildCandidatesLikeBatch();
|
|
1582
|
+
result = buildResult();
|
|
1583
|
+
}
|
|
1584
|
+
return result;
|
|
1585
|
+
};
|
|
1586
|
+
const nextMany = (candles) => {
|
|
1587
|
+
let result = [];
|
|
1588
|
+
for (const candle of candles) result = next(candle);
|
|
1589
|
+
return result;
|
|
1590
|
+
};
|
|
1591
|
+
const getLines = () => buildResult();
|
|
1592
|
+
const reset = () => {
|
|
1593
|
+
resetState();
|
|
1594
|
+
if (initialCandles?.length) nextMany(initialCandles);
|
|
1595
|
+
};
|
|
1596
|
+
resetState();
|
|
1597
|
+
if (initialCandles?.length) nextMany(initialCandles);
|
|
1598
|
+
return { next, nextMany, reset, getLines };
|
|
1599
|
+
};
|
|
1600
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1601
|
+
0 && (module.exports = {
|
|
1602
|
+
alignSortedCandlesByTimestamp,
|
|
1603
|
+
alignSpreadRows,
|
|
1604
|
+
applyIndicatorsToHistory,
|
|
1605
|
+
buildMlCandleIndicators,
|
|
1606
|
+
buildMlTimeframeIndicators,
|
|
1607
|
+
buildReturnsFromCandles,
|
|
1608
|
+
calculateCoinBtcCorrelation,
|
|
1609
|
+
calculatePearsonCorrelation,
|
|
1610
|
+
coinalyzePointsToRows,
|
|
1611
|
+
coinbaseProductFromSymbol,
|
|
1612
|
+
createIndicators,
|
|
1613
|
+
createSpreadSmoother,
|
|
1614
|
+
createTrendlineEngine,
|
|
1615
|
+
detectRawSupportResistance,
|
|
1616
|
+
getPluginIndicatorCatalog,
|
|
1617
|
+
getPluginIndicatorRenderers,
|
|
1618
|
+
getRegisteredIndicatorEntries,
|
|
1619
|
+
getSupportResistanceLevels,
|
|
1620
|
+
intervalToMs,
|
|
1621
|
+
mergeCoinalyzeMetrics,
|
|
1622
|
+
normalizeCoinalyzeSymbols,
|
|
1623
|
+
normalizeDerivativesIntervals,
|
|
1624
|
+
registerIndicatorEntries,
|
|
1625
|
+
resetIndicatorRegistryCache,
|
|
1626
|
+
rollingMeanStd,
|
|
1627
|
+
smoothSpreadSeries,
|
|
1628
|
+
toArrayData,
|
|
1629
|
+
toCoinalyzeTimestampMs,
|
|
1630
|
+
toFiniteNumber
|
|
1631
|
+
});
|