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