@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.
Files changed (68) hide show
  1. package/README.md +60 -0
  2. package/dist/api.d.mts +7 -0
  3. package/dist/api.d.ts +7 -0
  4. package/dist/api.js +64 -0
  5. package/dist/api.mjs +39 -0
  6. package/dist/async.d.mts +4 -0
  7. package/dist/async.d.ts +4 -0
  8. package/dist/async.js +48 -0
  9. package/dist/async.mjs +20 -0
  10. package/dist/backtest.d.mts +45 -0
  11. package/dist/backtest.d.ts +45 -0
  12. package/dist/backtest.js +574 -0
  13. package/dist/backtest.mjs +355 -0
  14. package/dist/chunk-AYC2QVKI.mjs +35 -0
  15. package/dist/chunk-JG2QPVAV.mjs +190 -0
  16. package/dist/chunk-LIGD3WWX.mjs +1545 -0
  17. package/dist/chunk-M7QGVZ3J.mjs +61 -0
  18. package/dist/chunk-NQ7D3T4E.mjs +10 -0
  19. package/dist/chunk-PXLXXXLA.mjs +67 -0
  20. package/dist/config.d.mts +14 -0
  21. package/dist/config.d.ts +14 -0
  22. package/dist/config.js +49 -0
  23. package/dist/config.mjs +21 -0
  24. package/dist/constants.d.mts +41 -0
  25. package/dist/constants.d.ts +41 -0
  26. package/dist/constants.js +238 -0
  27. package/dist/constants.mjs +50 -0
  28. package/dist/data.d.mts +9 -0
  29. package/dist/data.d.ts +9 -0
  30. package/dist/data.js +100 -0
  31. package/dist/data.mjs +12 -0
  32. package/dist/figures.d.mts +103 -0
  33. package/dist/figures.d.ts +103 -0
  34. package/dist/figures.js +274 -0
  35. package/dist/figures.mjs +239 -0
  36. package/dist/indicators-x3xKl3_W.d.mts +90 -0
  37. package/dist/indicators-x3xKl3_W.d.ts +90 -0
  38. package/dist/indicators.d.mts +124 -0
  39. package/dist/indicators.d.ts +124 -0
  40. package/dist/indicators.js +1631 -0
  41. package/dist/indicators.mjs +66 -0
  42. package/dist/json.d.mts +3 -0
  43. package/dist/json.d.ts +3 -0
  44. package/dist/json.js +34 -0
  45. package/dist/json.mjs +7 -0
  46. package/dist/math.d.mts +35 -0
  47. package/dist/math.d.ts +35 -0
  48. package/dist/math.js +98 -0
  49. package/dist/math.mjs +38 -0
  50. package/dist/pine.d.mts +29 -0
  51. package/dist/pine.d.ts +29 -0
  52. package/dist/pine.js +59 -0
  53. package/dist/pine.mjs +29 -0
  54. package/dist/strategies.d.mts +104 -0
  55. package/dist/strategies.d.ts +104 -0
  56. package/dist/strategies.js +1080 -0
  57. package/dist/strategies.mjs +390 -0
  58. package/dist/tickers.d.mts +7 -0
  59. package/dist/tickers.d.ts +7 -0
  60. package/dist/tickers.js +166 -0
  61. package/dist/tickers.mjs +125 -0
  62. package/dist/time-DEyFa2vI.d.mts +11 -0
  63. package/dist/time-DEyFa2vI.d.ts +11 -0
  64. package/dist/time.d.mts +2 -0
  65. package/dist/time.d.ts +2 -0
  66. package/dist/time.js +58 -0
  67. package/dist/time.mjs +15 -0
  68. 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
+ });