@hoox-sh/pynets 0.1.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 (48) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +46 -0
  4. package/README.md +327 -0
  5. package/dist/browser.js +44 -0
  6. package/dist/index.d.ts +348 -0
  7. package/dist/index.js +41741 -0
  8. package/package.json +77 -0
  9. package/src/ast/builder.ts +1002 -0
  10. package/src/ast/dump.ts +43 -0
  11. package/src/ast/helper.ts +77 -0
  12. package/src/ast/nodes.ts +570 -0
  13. package/src/ast/unparse.ts +360 -0
  14. package/src/cli/rich.ts +378 -0
  15. package/src/cli.ts +627 -0
  16. package/src/generated/PinescriptLexer.interp +267 -0
  17. package/src/generated/PinescriptLexer.tokens +131 -0
  18. package/src/generated/PinescriptLexer.ts +673 -0
  19. package/src/generated/PinescriptLexerBase.ts +395 -0
  20. package/src/generated/PinescriptParser.interp +267 -0
  21. package/src/generated/PinescriptParser.tokens +131 -0
  22. package/src/generated/PinescriptParser.ts +8863 -0
  23. package/src/generated/PinescriptParserBase.ts +21 -0
  24. package/src/generated/PinescriptParserVisitor.ts +840 -0
  25. package/src/index.ts +55 -0
  26. package/src/parser/LexerBase.ts +395 -0
  27. package/src/parser/ParserBase.ts +21 -0
  28. package/src/parser/error.ts +33 -0
  29. package/src/runtime/array.ts +486 -0
  30. package/src/runtime/color.ts +164 -0
  31. package/src/runtime/drawings.ts +510 -0
  32. package/src/runtime/input.ts +102 -0
  33. package/src/runtime/interpret.ts +3897 -0
  34. package/src/runtime/library.ts +282 -0
  35. package/src/runtime/log.ts +69 -0
  36. package/src/runtime/map.ts +85 -0
  37. package/src/runtime/math.ts +231 -0
  38. package/src/runtime/matrix.ts +1154 -0
  39. package/src/runtime/provider.ts +232 -0
  40. package/src/runtime/request.ts +372 -0
  41. package/src/runtime/series.ts +44 -0
  42. package/src/runtime/str.ts +433 -0
  43. package/src/runtime/strategy.ts +1105 -0
  44. package/src/runtime/ta.ts +2093 -0
  45. package/src/runtime/ticker.ts +81 -0
  46. package/src/runtime/time.ts +439 -0
  47. package/src/runtime/timeframe.ts +85 -0
  48. package/src/runtime/udt.ts +119 -0
@@ -0,0 +1,2093 @@
1
+ /**
2
+ * Copyright (C) 2024-2026 jango_blockchained
3
+ * SPDX-License-Identifier: AGPL-3.0-or-later
4
+ *
5
+ * Incremental TA (Python `TechnicalHelpers` SoT).
6
+ * Stateful per call-site; one sample per bar.
7
+ */
8
+
9
+ export type Cell = number | null;
10
+
11
+ interface SmaState {
12
+ period: number;
13
+ window: Cell[];
14
+ sum: number;
15
+ count: number;
16
+ }
17
+
18
+ interface EmaState {
19
+ period: number;
20
+ ema: number | null;
21
+ seeded: boolean;
22
+ seedBuf: number[];
23
+ }
24
+
25
+ interface RmaState {
26
+ period: number;
27
+ rma: number | null;
28
+ seeded: boolean;
29
+ started: boolean;
30
+ seedBuf: number[];
31
+ }
32
+
33
+ interface AtrState {
34
+ period: number;
35
+ prevClose: Cell;
36
+ rma: RmaState;
37
+ }
38
+
39
+ interface RsiState {
40
+ period: number;
41
+ prev: number | null;
42
+ gainSeed: number[];
43
+ lossSeed: number[];
44
+ avgGain: number | null;
45
+ avgLoss: number | null;
46
+ seeded: boolean;
47
+ }
48
+
49
+ interface TrState {
50
+ prevClose: Cell;
51
+ }
52
+
53
+ interface HighestLowestState {
54
+ period: number;
55
+ window: Cell[];
56
+ }
57
+
58
+ interface StdevState {
59
+ period: number;
60
+ window: Cell[];
61
+ sum: number;
62
+ sumsq: number;
63
+ count: number;
64
+ }
65
+
66
+ interface ChangeState {
67
+ length: number;
68
+ window: Cell[];
69
+ }
70
+
71
+ interface WmaState {
72
+ period: number;
73
+ window: Cell[];
74
+ }
75
+
76
+ interface VwmaState {
77
+ period: number;
78
+ srcWindow: Cell[];
79
+ volWindow: Cell[];
80
+ sumPv: number;
81
+ sumV: number;
82
+ naCount: number;
83
+ }
84
+
85
+ interface CrossState {
86
+ prevA: Cell;
87
+ prevB: Cell;
88
+ started: boolean;
89
+ }
90
+
91
+ interface LinregState {
92
+ period: number;
93
+ offset: number;
94
+ window: Cell[];
95
+ }
96
+
97
+ interface VwapState {
98
+ cumPv: number;
99
+ cumV: number;
100
+ value: Cell;
101
+ }
102
+
103
+ interface RiseFallState {
104
+ length: number;
105
+ prev: Cell;
106
+ streak: number;
107
+ started: boolean;
108
+ }
109
+
110
+ interface AlmaState {
111
+ period: number;
112
+ offset: number;
113
+ sigma: number;
114
+ window: Cell[];
115
+ weights: number[];
116
+ wsum: number;
117
+ }
118
+
119
+ interface CmoState {
120
+ period: number;
121
+ window: Cell[];
122
+ up: number;
123
+ down: number;
124
+ }
125
+
126
+ interface KamaState {
127
+ period: number;
128
+ fast: number;
129
+ slow: number;
130
+ prices: number[];
131
+ diffs: number[];
132
+ vol: number;
133
+ kama: number | null;
134
+ seeded: boolean;
135
+ bars: number;
136
+ }
137
+
138
+ interface ObvState {
139
+ prev: Cell;
140
+ obv: number;
141
+ bars: number;
142
+ }
143
+
144
+ interface AccdistState {
145
+ ad: number;
146
+ }
147
+
148
+ interface PivotState {
149
+ left: number;
150
+ right: number;
151
+ window: Cell[];
152
+ n: number;
153
+ }
154
+
155
+ interface MfiState {
156
+ period: number;
157
+ tps: Cell[];
158
+ mfs: Cell[];
159
+ }
160
+
161
+ interface SarState {
162
+ start: number;
163
+ increment: number;
164
+ maximum: number;
165
+ started: boolean;
166
+ sar: Cell;
167
+ ep: Cell;
168
+ trend: number;
169
+ af: number;
170
+ }
171
+
172
+ interface AdxState {
173
+ period: number;
174
+ prevH: Cell;
175
+ prevL: Cell;
176
+ prevC: Cell;
177
+ n: number;
178
+ }
179
+
180
+ interface DmiState {
181
+ diLen: number;
182
+ prevH: Cell;
183
+ prevL: Cell;
184
+ prevC: Cell;
185
+ }
186
+
187
+ interface CorrState {
188
+ length: number;
189
+ a: Cell[];
190
+ b: Cell[];
191
+ }
192
+
193
+ interface SwmaState {
194
+ period: number;
195
+ window: Cell[];
196
+ weights: number[];
197
+ wsum: number;
198
+ }
199
+
200
+ interface WindowState {
201
+ period: number;
202
+ window: Cell[];
203
+ }
204
+
205
+ interface TsiState {
206
+ longPeriod: number;
207
+ shortPeriod: number;
208
+ prev: Cell;
209
+ nBars: number;
210
+ value: Cell;
211
+ }
212
+
213
+ interface CumState {
214
+ total: number;
215
+ }
216
+
217
+ interface BarsSinceState {
218
+ everTrue: boolean;
219
+ barsSince: number;
220
+ }
221
+
222
+ interface ValueWhenState {
223
+ occurrence: number;
224
+ hits: Cell[];
225
+ }
226
+
227
+ interface PercentileState {
228
+ period: number;
229
+ percentage: number;
230
+ window: Cell[];
231
+ }
232
+
233
+ interface PvtState {
234
+ pvt: number;
235
+ prev: Cell;
236
+ }
237
+
238
+ interface WadState {
239
+ wad: number;
240
+ prev: Cell;
241
+ started: boolean;
242
+ }
243
+
244
+ interface VolIndexState {
245
+ value: number;
246
+ prevClose: Cell;
247
+ prevVol: Cell;
248
+ started: boolean;
249
+ }
250
+
251
+ interface WvadState {
252
+ value: number;
253
+ }
254
+
255
+ export type PivotPoints = {
256
+ pp: Cell;
257
+ r1: Cell;
258
+ s1: Cell;
259
+ r2: Cell;
260
+ s2: Cell;
261
+ };
262
+
263
+ function finiteCell(value: Cell): Cell {
264
+ return value !== null && Number.isFinite(value) ? value : null;
265
+ }
266
+
267
+ /** Near-integer floats round (14.0000000001 → 14); else floor. Python `_float_to_period_int`. */
268
+ const PERIOD_INT_EPS = 1e-9;
269
+
270
+ /** Pine length: finite, >= 1 after near-int round / floor. */
271
+ function pinePeriod(period: number): number | null {
272
+ if (!Number.isFinite(period) || period <= 0) return null;
273
+ const nearest = Math.round(period);
274
+ const n = Math.abs(period - nearest) <= PERIOD_INT_EPS ? nearest : Math.floor(period);
275
+ return n > 0 ? n : null;
276
+ }
277
+
278
+ function rmaStep(st: RmaState, x: Cell, n: number): Cell {
279
+ const alpha = 1 / n;
280
+ if (!st.started) {
281
+ if (x === null) return null;
282
+ st.started = true;
283
+ }
284
+ if (!st.seeded) {
285
+ if (x !== null) st.seedBuf.push(x);
286
+ if (st.seedBuf.length < n) return null;
287
+ const seed = st.seedBuf.reduce((a, b) => a + b, 0) / n;
288
+ st.rma = seed;
289
+ st.seeded = true;
290
+ st.seedBuf = [];
291
+ return seed;
292
+ }
293
+ if (x === null) return st.rma;
294
+ st.rma = alpha * x + (1 - alpha) * (st.rma ?? x);
295
+ return st.rma;
296
+ }
297
+
298
+ export class TaEngine {
299
+ private readonly smaSites = new Map<string, SmaState>();
300
+ private readonly emaSites = new Map<string, EmaState>();
301
+ private readonly rmaSites = new Map<string, RmaState>();
302
+ private readonly atrSites = new Map<string, AtrState>();
303
+ private readonly rsiSites = new Map<string, RsiState>();
304
+ private readonly trSites = new Map<string, TrState>();
305
+ private readonly highestSites = new Map<string, HighestLowestState>();
306
+ private readonly lowestSites = new Map<string, HighestLowestState>();
307
+ private readonly stdevSites = new Map<string, StdevState>();
308
+ private readonly changeSites = new Map<string, ChangeState>();
309
+ private readonly wmaSites = new Map<string, WmaState>();
310
+ private readonly sumSites = new Map<string, SmaState>();
311
+ private readonly rocSites = new Map<string, ChangeState>();
312
+ private readonly vwmaSites = new Map<string, VwmaState>();
313
+ private readonly cciSites = new Map<string, WmaState>();
314
+ private readonly crossoverSites = new Map<string, CrossState>();
315
+ private readonly crossunderSites = new Map<string, CrossState>();
316
+ private readonly linregSites = new Map<string, LinregState>();
317
+ private readonly vwapSites = new Map<string, VwapState>();
318
+ private readonly risingSites = new Map<string, RiseFallState>();
319
+ private readonly fallingSites = new Map<string, RiseFallState>();
320
+ private readonly almaSites = new Map<string, AlmaState>();
321
+ private readonly cmoSites = new Map<string, CmoState>();
322
+ private readonly kamaSites = new Map<string, KamaState>();
323
+ private readonly obvSites = new Map<string, ObvState>();
324
+ private readonly pivotHighSites = new Map<string, PivotState>();
325
+ private readonly pivotLowSites = new Map<string, PivotState>();
326
+ private readonly mfiSites = new Map<string, MfiState>();
327
+ private readonly sarSites = new Map<string, SarState>();
328
+ private readonly adxSites = new Map<string, AdxState>();
329
+ private readonly dmiSites = new Map<string, DmiState>();
330
+ private readonly correlationSites = new Map<string, CorrState>();
331
+ private readonly highestBarsSites = new Map<string, HighestLowestState>();
332
+ private readonly lowestBarsSites = new Map<string, HighestLowestState>();
333
+ private readonly swmaSites = new Map<string, SwmaState>();
334
+ private readonly cogSites = new Map<string, WindowState>();
335
+ private readonly tsiSites = new Map<string, TsiState>();
336
+ private readonly devSites = new Map<string, SmaState>();
337
+ private readonly varianceSites = new Map<string, StdevState>();
338
+ private readonly medianSites = new Map<string, WindowState>();
339
+ private readonly modeSites = new Map<string, WindowState>();
340
+ private readonly percentrankSites = new Map<string, WindowState>();
341
+ private readonly percentileNearestSites = new Map<string, PercentileState>();
342
+ private readonly percentileLinearSites = new Map<string, PercentileState>();
343
+ private readonly cumSites = new Map<string, CumState>();
344
+ private readonly barssinceSites = new Map<string, BarsSinceState>();
345
+ private readonly valuewhenSites = new Map<string, ValueWhenState>();
346
+ private readonly accdistSites = new Map<string, AccdistState>();
347
+ private readonly pvtSites = new Map<string, PvtState>();
348
+ private readonly wadSites = new Map<string, WadState>();
349
+ private readonly nviSites = new Map<string, VolIndexState>();
350
+ private readonly pviSites = new Map<string, VolIndexState>();
351
+ private readonly wvadSites = new Map<string, WvadState>();
352
+
353
+ sma(site: string, source: Cell, period: number): Cell {
354
+ const n = pinePeriod(period);
355
+ if (n === null) return null;
356
+
357
+ let st = this.smaSites.get(site);
358
+ if (st === undefined || st.period !== n) {
359
+ st = { period: n, window: [], sum: 0, count: 0 };
360
+ this.smaSites.set(site, st);
361
+ }
362
+
363
+ const x = finiteCell(source);
364
+ if (st.window.length === n) {
365
+ const old = st.window.shift()!;
366
+ if (old !== null) {
367
+ st.sum -= old;
368
+ st.count -= 1;
369
+ }
370
+ }
371
+ st.window.push(x);
372
+ if (x !== null) {
373
+ st.sum += x;
374
+ st.count += 1;
375
+ }
376
+
377
+ // Full window required; any na in the window → na (`_sma_inc_update`).
378
+ if (st.window.length < n || st.count !== n) return null;
379
+ return st.sum / n;
380
+ }
381
+
382
+ /** Pine EMA: SMA seed of first `period` finite samples, then α=2/(n+1). */
383
+ ema(site: string, source: Cell, period: number): Cell {
384
+ const n = pinePeriod(period);
385
+ if (n === null) return null;
386
+ let st = this.emaSites.get(site);
387
+ if (st === undefined || st.period !== n) {
388
+ st = { period: n, ema: null, seeded: false, seedBuf: [] };
389
+ this.emaSites.set(site, st);
390
+ }
391
+ const x = finiteCell(source);
392
+ const alpha = 2 / (n + 1);
393
+ if (!st.seeded) {
394
+ if (x === null) return null;
395
+ st.seedBuf.push(x);
396
+ if (st.seedBuf.length < n) return null;
397
+ const seed = st.seedBuf.reduce((a, b) => a + b, 0) / n;
398
+ st.ema = seed;
399
+ st.seeded = true;
400
+ st.seedBuf = [];
401
+ return seed;
402
+ }
403
+ if (x === null) return st.ema;
404
+ st.ema = st.ema == null ? x : alpha * x + (1 - alpha) * st.ema;
405
+ return st.ema;
406
+ }
407
+
408
+ /** Wilder RMA: mean seed, then α=1/n. na after seed keeps previous. */
409
+ rma(site: string, source: Cell, period: number): Cell {
410
+ const n = pinePeriod(period);
411
+ if (n === null) return null;
412
+ let st = this.rmaSites.get(site);
413
+ if (st === undefined || st.period !== n) {
414
+ st = { period: n, rma: null, seeded: false, started: false, seedBuf: [] };
415
+ this.rmaSites.set(site, st);
416
+ }
417
+ return rmaStep(st, finiteCell(source), n);
418
+ }
419
+
420
+ /** `ta.atr(length)` ≡ `ta.rma(ta.tr, length)`. First bar is na (no prev close). */
421
+ atr(site: string, high: Cell, low: Cell, close: Cell, period: number): Cell {
422
+ const n = pinePeriod(period);
423
+ if (n === null) return null;
424
+ let st = this.atrSites.get(site);
425
+ if (st === undefined || st.period !== n) {
426
+ st = {
427
+ period: n,
428
+ prevClose: null,
429
+ rma: { period: n, rma: null, seeded: false, started: false, seedBuf: [] },
430
+ };
431
+ this.atrSites.set(site, st);
432
+ }
433
+ const prev = st.prevClose;
434
+ st.prevClose = finiteCell(close);
435
+ if (prev === null) return null;
436
+ const h = finiteCell(high);
437
+ const l = finiteCell(low);
438
+ const c = finiteCell(close);
439
+ if (h === null || l === null || c === null) return null;
440
+ const tr = Math.max(h - l, Math.abs(h - prev), Math.abs(l - prev));
441
+ return rmaStep(st.rma, tr, n);
442
+ }
443
+
444
+ rsi(site: string, source: Cell, period: number): Cell {
445
+ const n = pinePeriod(period);
446
+ if (n === null) return null;
447
+
448
+ let st = this.rsiSites.get(site);
449
+ if (st === undefined || st.period !== n) {
450
+ st = {
451
+ period: n,
452
+ prev: null,
453
+ gainSeed: [],
454
+ lossSeed: [],
455
+ avgGain: null,
456
+ avgLoss: null,
457
+ seeded: false,
458
+ };
459
+ this.rsiSites.set(site, st);
460
+ }
461
+
462
+ const x = finiteCell(source);
463
+ // NA source: emit na and leave Wilder state untouched.
464
+ if (x === null) return null; // leave Wilder state untouched (no NaN-poison)
465
+
466
+ const prev = st.prev;
467
+ st.prev = x;
468
+ if (prev === null) return null;
469
+
470
+ const change = x - prev;
471
+ const gain = change > 0 ? change : 0;
472
+ const loss = change < 0 ? -change : 0;
473
+ const alpha = 1 / n;
474
+
475
+ if (!st.seeded) {
476
+ st.gainSeed.push(gain);
477
+ st.lossSeed.push(loss);
478
+ if (st.gainSeed.length < n) return null;
479
+ let gSum = 0;
480
+ let lSum = 0;
481
+ for (let i = 0; i < n; i++) {
482
+ gSum += st.gainSeed[i]!;
483
+ lSum += st.lossSeed[i]!;
484
+ }
485
+ st.avgGain = gSum / n;
486
+ st.avgLoss = lSum / n;
487
+ st.seeded = true;
488
+ st.gainSeed = [];
489
+ st.lossSeed = [];
490
+ } else {
491
+ st.avgGain = alpha * gain + (1 - alpha) * st.avgGain!;
492
+ st.avgLoss = alpha * loss + (1 - alpha) * st.avgLoss!;
493
+ }
494
+
495
+ // Python `_rsi_inc_update`: avg_loss == 0 → 100 (including both-zero).
496
+ if (st.avgLoss === 0) return 100;
497
+ const rs = st.avgGain! / st.avgLoss;
498
+ return 100 - 100 / (1 + rs);
499
+ }
500
+
501
+ /**
502
+ * Keltner: mid=EMA(close,length); bands=mid ± mult*ATR(length).
503
+ * Mid na → all na. Nested EMA/ATR keep their own per-site state.
504
+ */
505
+ kc(
506
+ site: string,
507
+ high: Cell,
508
+ low: Cell,
509
+ close: Cell,
510
+ length: number,
511
+ mult: number,
512
+ ): { mid: Cell; up: Cell; lo: Cell } {
513
+ const n = pinePeriod(length);
514
+ if (n === null) return { mid: null, up: null, lo: null };
515
+ const mid = this.ema(`${site}:ema`, close, n);
516
+ const atrVal = this.atr(`${site}:atr`, high, low, close, n);
517
+ if (mid === null) return { mid: null, up: null, lo: null };
518
+ // `_kc_inc_update`: None ATR → 0 width (nan ATR is not produced here).
519
+ const width = (atrVal ?? 0) * mult;
520
+ return { mid, up: mid + width, lo: mid - width };
521
+ }
522
+
523
+ /**
524
+ * Supertrend: ATR; mid=(h+l)/2; dir=-1 if close>=mid else 1;
525
+ * st=lower if dir<0 else upper. None/nan ATR → 0 (`_supertrend_inc_update`).
526
+ */
527
+ supertrend(
528
+ site: string,
529
+ high: Cell,
530
+ low: Cell,
531
+ close: Cell,
532
+ factor: number,
533
+ atrPeriod: number,
534
+ ): { st: Cell; dir: Cell } {
535
+ const n = pinePeriod(atrPeriod);
536
+ const atrVal = n === null ? null : this.atr(`${site}:atr`, high, low, close, n);
537
+ const atrF = atrVal === null || !Number.isFinite(atrVal) ? 0 : atrVal;
538
+ const h = finiteCell(high) ?? 0;
539
+ const l = finiteCell(low) ?? 0;
540
+ const c = finiteCell(close) ?? h;
541
+ const mid = (h + l) / 2;
542
+ const upper = mid + factor * atrF;
543
+ const lower = mid - factor * atrF;
544
+ const dir = c >= mid ? -1 : 1;
545
+ return { st: dir < 0 ? lower : upper, dir };
546
+ }
547
+
548
+ /** True range. First bar na (no prev close); else max(h-l, |h-prevC|, |l-prevC|). */
549
+ tr(site: string, high: Cell, low: Cell, close: Cell): Cell {
550
+ let st = this.trSites.get(site);
551
+ if (st === undefined) {
552
+ st = { prevClose: null };
553
+ this.trSites.set(site, st);
554
+ }
555
+ const prev = st.prevClose;
556
+ st.prevClose = finiteCell(close);
557
+ if (prev === null) return null;
558
+ const h = finiteCell(high);
559
+ const l = finiteCell(low);
560
+ if (h === null || l === null) return null;
561
+ return Math.max(h - l, Math.abs(h - prev), Math.abs(l - prev));
562
+ }
563
+
564
+ /** Highest in last `period` samples (na skipped). */
565
+ highest(site: string, source: Cell, period: number): Cell {
566
+ const n = pinePeriod(period);
567
+ if (n === null) return null;
568
+ let st = this.highestSites.get(site);
569
+ if (st === undefined || st.period !== n) {
570
+ st = { period: n, window: [] };
571
+ this.highestSites.set(site, st);
572
+ }
573
+ if (st.window.length === n) st.window.shift();
574
+ st.window.push(finiteCell(source));
575
+ if (st.window.length < n) return null;
576
+ let best: number | null = null;
577
+ for (const v of st.window) {
578
+ if (v !== null && (best === null || v > best)) best = v;
579
+ }
580
+ return best;
581
+ }
582
+
583
+ /** Lowest in last `period` samples (na skipped). */
584
+ lowest(site: string, source: Cell, period: number): Cell {
585
+ const n = pinePeriod(period);
586
+ if (n === null) return null;
587
+ let st = this.lowestSites.get(site);
588
+ if (st === undefined || st.period !== n) {
589
+ st = { period: n, window: [] };
590
+ this.lowestSites.set(site, st);
591
+ }
592
+ if (st.window.length === n) st.window.shift();
593
+ st.window.push(finiteCell(source));
594
+ if (st.window.length < n) return null;
595
+ let best: number | null = null;
596
+ for (const v of st.window) {
597
+ if (v !== null && (best === null || v < best)) best = v;
598
+ }
599
+ return best;
600
+ }
601
+
602
+ /** Sample stdev (ddof=1). Any na in window → na; period<=1 → na. */
603
+ stdev(site: string, source: Cell, period: number): Cell {
604
+ const n = pinePeriod(period);
605
+ if (n === null || n <= 1) return null;
606
+ let st = this.stdevSites.get(site);
607
+ if (st === undefined || st.period !== n) {
608
+ st = { period: n, window: [], sum: 0, sumsq: 0, count: 0 };
609
+ this.stdevSites.set(site, st);
610
+ }
611
+ const x = finiteCell(source);
612
+ if (st.window.length === n) {
613
+ const old = st.window.shift()!;
614
+ if (old !== null) {
615
+ st.sum -= old;
616
+ st.sumsq -= old * old;
617
+ st.count -= 1;
618
+ }
619
+ }
620
+ st.window.push(x);
621
+ if (x !== null) {
622
+ st.sum += x;
623
+ st.sumsq += x * x;
624
+ st.count += 1;
625
+ }
626
+ if (st.window.length < n || st.count !== n) return null;
627
+ let v = (st.sumsq - (st.sum * st.sum) / n) / (n - 1);
628
+ if (v < 0) v = 0;
629
+ return Math.sqrt(v);
630
+ }
631
+
632
+ /** `source - source[length]`. na if lookback missing. */
633
+ change(site: string, source: Cell, length = 1): Cell {
634
+ if (!Number.isFinite(length) || length < 0) return null;
635
+ const n = Math.trunc(length);
636
+ const x = finiteCell(source);
637
+ if (n === 0) return x === null ? null : 0;
638
+ let st = this.changeSites.get(site);
639
+ if (st === undefined || st.length !== n) {
640
+ st = { length: n, window: [] };
641
+ this.changeSites.set(site, st);
642
+ }
643
+ if (st.window.length === n + 1) st.window.shift();
644
+ st.window.push(x);
645
+ if (st.window.length <= n) return null;
646
+ const curr = st.window[st.window.length - 1]!;
647
+ const prev = st.window[0]!;
648
+ if (curr === null || prev === null) return null;
649
+ return curr - prev;
650
+ }
651
+
652
+ /** Weighted MA, weights 1..n (oldest=1). Any na in window → na. */
653
+ wma(site: string, source: Cell, period: number): Cell {
654
+ const n = pinePeriod(period);
655
+ if (n === null) return null;
656
+ let st = this.wmaSites.get(site);
657
+ if (st === undefined || st.period !== n) {
658
+ st = { period: n, window: [] };
659
+ this.wmaSites.set(site, st);
660
+ }
661
+ if (st.window.length === n) st.window.shift();
662
+ st.window.push(finiteCell(source));
663
+ if (st.window.length < n) return null;
664
+ let acc = 0;
665
+ for (let i = 0; i < n; i++) {
666
+ const v = st.window[i];
667
+ if (v == null) return null;
668
+ acc += v * (i + 1);
669
+ }
670
+ return acc / (n * (n + 1) / 2);
671
+ }
672
+
673
+ /** 1 when prev a<=b and now a>b; first bar 0. */
674
+ crossover(site: string, a: Cell, b: Cell): Cell {
675
+ return this.crossStep(this.crossoverSites, site, a, b, false);
676
+ }
677
+
678
+ /** 1 when prev a>=b and now a<b; first bar 0. */
679
+ crossunder(site: string, a: Cell, b: Cell): Cell {
680
+ return this.crossStep(this.crossunderSites, site, a, b, true);
681
+ }
682
+
683
+ /**
684
+ * MACD via nested EMA sites (`${site}:fast` / `:slow` / `:signal`).
685
+ * Either EMA na → macd null; hist only when macd and signal both finite.
686
+ */
687
+ macd(
688
+ site: string,
689
+ source: Cell,
690
+ fast = 12,
691
+ slow = 26,
692
+ signal = 9,
693
+ ): { macd: Cell; signal: Cell; hist: Cell } {
694
+ const ef = this.ema(`${site}:fast`, source, fast);
695
+ const es = this.ema(`${site}:slow`, source, slow);
696
+ const macdVal = ef === null || es === null ? null : ef - es;
697
+ const sigVal = this.ema(`${site}:signal`, macdVal, signal);
698
+ const hist =
699
+ macdVal !== null && sigVal !== null && Number.isFinite(macdVal) && Number.isFinite(sigVal)
700
+ ? macdVal - sigVal
701
+ : null;
702
+ return { macd: macdVal, signal: sigVal, hist };
703
+ }
704
+
705
+ /**
706
+ * Bollinger: mid=SMA, dev=sample stdev, bands=mid ± mult*dev.
707
+ * All null if mid or stdev null.
708
+ */
709
+ bb(
710
+ site: string,
711
+ source: Cell,
712
+ length: number,
713
+ mult = 2,
714
+ ): { mid: Cell; up: Cell; lo: Cell } {
715
+ const mid = this.sma(`${site}:sma`, source, length);
716
+ const dev = this.stdev(`${site}:stdev`, source, length);
717
+ if (mid === null || dev === null) return { mid: null, up: null, lo: null };
718
+ return { mid, up: mid + mult * dev, lo: mid - mult * dev };
719
+ }
720
+
721
+ /** Rolling sum. Full window required; any na in the window → na (like sma). */
722
+ sum(site: string, source: Cell, period: number): Cell {
723
+ const n = pinePeriod(period);
724
+ if (n === null) return null;
725
+
726
+ let st = this.sumSites.get(site);
727
+ if (st === undefined || st.period !== n) {
728
+ st = { period: n, window: [], sum: 0, count: 0 };
729
+ this.sumSites.set(site, st);
730
+ }
731
+
732
+ const x = finiteCell(source);
733
+ if (st.window.length === n) {
734
+ const old = st.window.shift()!;
735
+ if (old !== null) {
736
+ st.sum -= old;
737
+ st.count -= 1;
738
+ }
739
+ }
740
+ st.window.push(x);
741
+ if (x !== null) {
742
+ st.sum += x;
743
+ st.count += 1;
744
+ }
745
+
746
+ if (st.window.length < n || st.count !== n) return null;
747
+ return st.sum;
748
+ }
749
+
750
+ /** `100 * (source - source[length]) / source[length]`. na if lookback missing, denom 0, or length<=0. */
751
+ roc(site: string, source: Cell, length: number): Cell {
752
+ if (!Number.isFinite(length) || length <= 0) return null;
753
+ const n = Math.trunc(length);
754
+ const x = finiteCell(source);
755
+ if (n <= 0) return null;
756
+ let st = this.rocSites.get(site);
757
+ if (st === undefined || st.length !== n) {
758
+ st = { length: n, window: [] };
759
+ this.rocSites.set(site, st);
760
+ }
761
+ if (st.window.length === n + 1) st.window.shift();
762
+ st.window.push(x);
763
+ if (st.window.length <= n) return null;
764
+ const curr = st.window[st.window.length - 1]!;
765
+ const prev = st.window[0]!;
766
+ if (curr === null || prev === null || prev === 0) return null;
767
+ return 100 * (curr - prev) / prev;
768
+ }
769
+
770
+ /** `source - source[length]`. Same lag math as `change`. */
771
+ mom(site: string, source: Cell, length: number): Cell {
772
+ return this.change(site, source, length);
773
+ }
774
+
775
+ /** Volume-weighted MA: sum(src*vol)/sum(vol). Any na in window → na. */
776
+ vwma(site: string, source: Cell, volume: Cell, period: number): Cell {
777
+ const n = pinePeriod(period);
778
+ if (n === null) return null;
779
+ let st = this.vwmaSites.get(site);
780
+ if (st === undefined || st.period !== n) {
781
+ st = { period: n, srcWindow: [], volWindow: [], sumPv: 0, sumV: 0, naCount: 0 };
782
+ this.vwmaSites.set(site, st);
783
+ }
784
+ const s = finiteCell(source);
785
+ const v = finiteCell(volume);
786
+ if (st.srcWindow.length === n) {
787
+ const oldS = st.srcWindow.shift()!;
788
+ const oldV = st.volWindow.shift()!;
789
+ if (oldS === null || oldV === null) st.naCount -= 1;
790
+ else {
791
+ st.sumPv -= oldS * oldV;
792
+ st.sumV -= oldV;
793
+ }
794
+ }
795
+ st.srcWindow.push(s);
796
+ st.volWindow.push(v);
797
+ if (s === null || v === null) st.naCount += 1;
798
+ else {
799
+ st.sumPv += s * v;
800
+ st.sumV += v;
801
+ }
802
+ if (st.srcWindow.length < n || st.naCount > 0 || st.sumV === 0) return null;
803
+ return st.sumPv / st.sumV;
804
+ }
805
+
806
+ /** CCI: (tp - sma(tp)) / (0.015 * meanDev). Any na in window → na. */
807
+ cci(site: string, typicalPrice: Cell, period: number): Cell {
808
+ const n = pinePeriod(period);
809
+ if (n === null) return null;
810
+ let st = this.cciSites.get(site);
811
+ if (st === undefined || st.period !== n) {
812
+ st = { period: n, window: [] };
813
+ this.cciSites.set(site, st);
814
+ }
815
+ if (st.window.length === n) st.window.shift();
816
+ st.window.push(finiteCell(typicalPrice));
817
+ if (st.window.length < n) return null;
818
+ let acc = 0;
819
+ for (const v of st.window) {
820
+ if (v == null) return null;
821
+ acc += v;
822
+ }
823
+ const sma = acc / n;
824
+ let mad = 0;
825
+ for (const v of st.window) mad += Math.abs(v! - sma);
826
+ const meanDev = mad / n;
827
+ if (meanDev === 0) return null;
828
+ return (st.window[n - 1]! - sma) / (0.015 * meanDev);
829
+ }
830
+
831
+ /** Williams %R: -100*(hh-c)/(hh-ll). na if incomplete window or hh==ll. */
832
+ willr(site: string, high: Cell, low: Cell, close: Cell, period: number): Cell {
833
+ const n = pinePeriod(period);
834
+ if (n === null) return null;
835
+ const hh = this.highest(`${site}:hh`, high, n);
836
+ const ll = this.lowest(`${site}:ll`, low, n);
837
+ const c = finiteCell(close);
838
+ if (hh === null || ll === null || c === null || hh === ll) return null;
839
+ return -100 * (hh - c) / (hh - ll);
840
+ }
841
+
842
+ /** Stochastic %K: 100*(c-ll)/(hh-ll). na if incomplete window or hh==ll. */
843
+ stoch(site: string, source: Cell, high: Cell, low: Cell, length: number): Cell {
844
+ const n = pinePeriod(length);
845
+ if (n === null) return null;
846
+ const hh = this.highest(`${site}:hh`, high, n);
847
+ const ll = this.lowest(`${site}:ll`, low, n);
848
+ const c = finiteCell(source);
849
+ if (hh === null || ll === null || c === null || hh === ll) return null;
850
+ return 100 * (c - ll) / (hh - ll);
851
+ }
852
+
853
+ /** Least-squares line value at `length-1-offset`. Any na in window → na. */
854
+ linreg(site: string, source: Cell, length: number, offset = 0): Cell {
855
+ const n = pinePeriod(length);
856
+ if (n === null || n < 2) return null;
857
+ const off = Number.isFinite(offset) ? Math.trunc(offset) : 0;
858
+ let st = this.linregSites.get(site);
859
+ if (st === undefined || st.period !== n || st.offset !== off) {
860
+ st = { period: n, offset: off, window: [] };
861
+ this.linregSites.set(site, st);
862
+ }
863
+ if (st.window.length === n) st.window.shift();
864
+ st.window.push(finiteCell(source));
865
+ if (st.window.length < n) return null;
866
+ let sumY = 0;
867
+ for (const v of st.window) {
868
+ if (v == null) return null;
869
+ sumY += v;
870
+ }
871
+ const meanX = (n - 1) / 2;
872
+ const meanY = sumY / n;
873
+ let num = 0;
874
+ let den = 0;
875
+ for (let i = 0; i < n; i++) {
876
+ const dx = i - meanX;
877
+ num += dx * (st.window[i]! - meanY);
878
+ den += dx * dx;
879
+ }
880
+ if (den === 0) return meanY;
881
+ const slope = num / den;
882
+ return meanY + slope * ((n - 1 - off) - meanX);
883
+ }
884
+
885
+ /** Cumulative VWAP: sum(src*vol)/sum(vol). Skips na source; no session reset. */
886
+ vwap(site: string, source: Cell, volume: Cell): Cell {
887
+ let st = this.vwapSites.get(site);
888
+ if (st === undefined) {
889
+ st = { cumPv: 0, cumV: 0, value: null };
890
+ this.vwapSites.set(site, st);
891
+ }
892
+ const p = finiteCell(source);
893
+ if (p === null) return st.value;
894
+ const v = finiteCell(volume) ?? 0;
895
+ st.cumPv += p * v;
896
+ st.cumV += v;
897
+ st.value = st.cumV !== 0 ? st.cumPv / st.cumV : p;
898
+ return st.value;
899
+ }
900
+
901
+ /** 1 if `source > source[1]` for `length` bars; else 0. Warmup 0. */
902
+ rising(site: string, source: Cell, length: number): Cell {
903
+ return this.riseFallStep(this.risingSites, site, source, length, false);
904
+ }
905
+
906
+ /** 1 if `source < source[1]` for `length` bars; else 0. Warmup 0. */
907
+ falling(site: string, source: Cell, length: number): Cell {
908
+ return this.riseFallStep(this.fallingSites, site, source, length, true);
909
+ }
910
+
911
+ private riseFallStep(
912
+ sites: Map<string, RiseFallState>,
913
+ site: string,
914
+ source: Cell,
915
+ length: number,
916
+ falling: boolean,
917
+ ): Cell {
918
+ const n = pinePeriod(length);
919
+ if (n === null) return 0;
920
+ let st = sites.get(site);
921
+ if (st === undefined || st.length !== n) {
922
+ st = { length: n, prev: null, streak: 0, started: false };
923
+ sites.set(site, st);
924
+ }
925
+ const x = finiteCell(source);
926
+ if (!st.started) {
927
+ st.prev = x;
928
+ st.started = true;
929
+ st.streak = 0;
930
+ return 0;
931
+ }
932
+ const prev = st.prev;
933
+ const step = x !== null && prev !== null && (falling ? x < prev : x > prev);
934
+ st.streak = step ? st.streak + 1 : 0;
935
+ st.prev = x;
936
+ return st.streak >= n ? 1 : 0;
937
+ }
938
+
939
+ private crossStep(
940
+ sites: Map<string, CrossState>,
941
+ site: string,
942
+ a: Cell,
943
+ b: Cell,
944
+ under: boolean,
945
+ ): Cell {
946
+ let st = sites.get(site);
947
+ if (st === undefined) {
948
+ st = { prevA: null, prevB: null, started: false };
949
+ sites.set(site, st);
950
+ }
951
+ const aF = finiteCell(a);
952
+ const bF = finiteCell(b);
953
+ let hit = 0;
954
+ if (st.started && st.prevA !== null && st.prevB !== null && aF !== null && bF !== null) {
955
+ const crossed = under
956
+ ? st.prevA >= st.prevB && aF < bF
957
+ : st.prevA <= st.prevB && aF > bF;
958
+ hit = crossed ? 1 : 0;
959
+ }
960
+ st.prevA = aF;
961
+ st.prevB = bF;
962
+ st.started = true;
963
+ return hit;
964
+ }
965
+
966
+ /** 1 if crossover or crossunder this bar. */
967
+ cross(site: string, a: Cell, b: Cell): Cell {
968
+ const up = this.crossover(`${site}:up`, a, b);
969
+ const down = this.crossunder(`${site}:down`, a, b);
970
+ return up === 1 || down === 1 ? 1 : 0;
971
+ }
972
+
973
+ /** `(up - lo) / mid` from `bb`. */
974
+ bbw(site: string, source: Cell, length: number, mult = 2): Cell {
975
+ const r = this.bb(site, source, length, mult);
976
+ if (r.mid === null || r.up === null || r.lo === null || r.mid === 0) return null;
977
+ return (r.up - r.lo) / r.mid;
978
+ }
979
+
980
+ /** `2*ema - ema(ema)`. */
981
+ dema(site: string, source: Cell, period: number): Cell {
982
+ const e1 = this.ema(`${site}:e1`, source, period);
983
+ const e2 = this.ema(`${site}:e2`, e1, period);
984
+ if (e1 === null || e2 === null) return null;
985
+ return 2 * e1 - e2;
986
+ }
987
+
988
+ /** `3*ema - 3*ema(ema) + ema(ema(ema))`. */
989
+ tema(site: string, source: Cell, period: number): Cell {
990
+ const e1 = this.ema(`${site}:e1`, source, period);
991
+ const e2 = this.ema(`${site}:e2`, e1, period);
992
+ const e3 = this.ema(`${site}:e3`, e2, period);
993
+ if (e1 === null || e2 === null || e3 === null) return null;
994
+ return 3 * e1 - 3 * e2 + e3;
995
+ }
996
+
997
+ /**
998
+ * Offset (negative, TradingView style) of the highest in the window.
999
+ * `0` = current bar is the high; `-n+1` = oldest bar.
1000
+ */
1001
+ highestbars(site: string, source: Cell, period: number): Cell {
1002
+ return this.extremeBars(this.highestBarsSites, site, source, period, true);
1003
+ }
1004
+
1005
+ /** Offset of the lowest in the window (negative TV style). */
1006
+ lowestbars(site: string, source: Cell, period: number): Cell {
1007
+ return this.extremeBars(this.lowestBarsSites, site, source, period, false);
1008
+ }
1009
+
1010
+ private extremeBars(
1011
+ sites: Map<string, HighestLowestState>,
1012
+ site: string,
1013
+ source: Cell,
1014
+ period: number,
1015
+ highest: boolean,
1016
+ ): Cell {
1017
+ const n = pinePeriod(period);
1018
+ if (n === null) return null;
1019
+ let st = sites.get(site);
1020
+ if (st === undefined || st.period !== n) {
1021
+ st = { period: n, window: [] };
1022
+ sites.set(site, st);
1023
+ }
1024
+ if (st.window.length === n) st.window.shift();
1025
+ st.window.push(finiteCell(source));
1026
+ if (st.window.length < n) return null;
1027
+ let bestI = -1;
1028
+ let best: number | null = null;
1029
+ for (let i = 0; i < st.window.length; i++) {
1030
+ const v = st.window[i];
1031
+ if (v === null) continue;
1032
+ // Strict compare so ties keep the oldest bar (Python `_highestbars`).
1033
+ if (best === null || (highest ? v > best : v < best)) {
1034
+ best = v;
1035
+ bestI = i;
1036
+ }
1037
+ }
1038
+ if (bestI < 0) return null;
1039
+ return bestI - (st.window.length - 1);
1040
+ }
1041
+
1042
+ /** Arnaud Legoux MA. na in window → na. Defaults offset=0.85, sigma=6. */
1043
+ alma(site: string, source: Cell, period: number, offset = 0.85, sigma = 6): Cell {
1044
+ const n = pinePeriod(period);
1045
+ if (n === null) return null;
1046
+ if (!Number.isFinite(offset) || !Number.isFinite(sigma)) return null;
1047
+ const off = offset;
1048
+ const sig = sigma;
1049
+ let st = this.almaSites.get(site);
1050
+ if (st === undefined || st.period !== n || st.offset !== off || st.sigma !== sig) {
1051
+ const m = off * (n - 1);
1052
+ const s = sig === 0 ? 0 : n / sig;
1053
+ const weights: number[] = [];
1054
+ let wsum = 0;
1055
+ for (let i = 0; i < n; i++) {
1056
+ const w = s === 0 ? 1 : Math.exp(-((i - m) ** 2) / (2 * s * s));
1057
+ weights.push(w);
1058
+ wsum += w;
1059
+ }
1060
+ st = { period: n, offset: off, sigma: sig, window: [], weights, wsum };
1061
+ this.almaSites.set(site, st);
1062
+ }
1063
+ if (st.window.length === n) st.window.shift();
1064
+ st.window.push(finiteCell(source));
1065
+ if (st.window.length < n || st.wsum === 0) return null;
1066
+ let total = 0;
1067
+ for (let i = 0; i < n; i++) {
1068
+ const v = st.window[i];
1069
+ if (v == null) return null;
1070
+ total += v * st.weights[i]!;
1071
+ }
1072
+ return total / st.wsum;
1073
+ }
1074
+
1075
+ /** Chande Momentum Oscillator. Window of length+1; denom 0 → 0. */
1076
+ cmo(site: string, source: Cell, period: number): Cell {
1077
+ const n = pinePeriod(period);
1078
+ if (n === null) return null;
1079
+ let st = this.cmoSites.get(site);
1080
+ if (st === undefined || st.period !== n) {
1081
+ st = { period: n, window: [], up: 0, down: 0 };
1082
+ this.cmoSites.set(site, st);
1083
+ }
1084
+ const x = finiteCell(source);
1085
+ if (st.window.length === n + 1) {
1086
+ const old = st.window[0]!;
1087
+ const nxt = st.window[1]!;
1088
+ if (old !== null && nxt !== null) {
1089
+ const d = nxt - old;
1090
+ if (d > 0) st.up -= d;
1091
+ else st.down -= -d;
1092
+ }
1093
+ st.window.shift();
1094
+ }
1095
+ const prev = st.window.length === 0 ? null : st.window[st.window.length - 1]!;
1096
+ st.window.push(x);
1097
+ if (prev !== null && x !== null) {
1098
+ const d = x - prev;
1099
+ if (d > 0) st.up += d;
1100
+ else st.down += -d;
1101
+ }
1102
+ if (st.window.length < n + 1) return null;
1103
+ const denom = st.up + st.down;
1104
+ if (denom === 0) return 0;
1105
+ return (100 * (st.up - st.down)) / denom;
1106
+ }
1107
+
1108
+ /** Kaufman AMA. First output on bar index `length` (need length+1 samples). */
1109
+ kama(site: string, source: Cell, period: number, fast = 2, slow = 30): Cell {
1110
+ const n = pinePeriod(period);
1111
+ if (n === null) return null;
1112
+ const f = Number.isFinite(fast) ? Math.trunc(fast) : 2;
1113
+ const sl = Number.isFinite(slow) ? Math.trunc(slow) : 30;
1114
+ // 2/(fast+1) / 2/(slow+1) — reject the singular -1 case (NaN-poison).
1115
+ if (f === -1 || sl === -1) return null;
1116
+ let st = this.kamaSites.get(site);
1117
+ if (st === undefined || st.period !== n || st.fast !== f || st.slow !== sl) {
1118
+ st = {
1119
+ period: n,
1120
+ fast: f,
1121
+ slow: sl,
1122
+ prices: [],
1123
+ diffs: [],
1124
+ vol: 0,
1125
+ kama: null,
1126
+ seeded: false,
1127
+ bars: 0,
1128
+ };
1129
+ this.kamaSites.set(site, st);
1130
+ }
1131
+ const x = finiteCell(source);
1132
+ if (x === null) return null;
1133
+ const prev = st.prices.length === 0 ? null : st.prices[st.prices.length - 1]!;
1134
+ if (st.prices.length === n + 1) st.prices.shift();
1135
+ st.prices.push(x);
1136
+ st.bars += 1;
1137
+ if (prev !== null) {
1138
+ const d = Math.abs(x - prev);
1139
+ if (st.diffs.length === n) st.vol -= st.diffs.shift()!;
1140
+ st.diffs.push(d);
1141
+ st.vol += d;
1142
+ }
1143
+ if (!st.seeded) {
1144
+ if (st.bars < n) return null;
1145
+ st.kama = x;
1146
+ st.seeded = true;
1147
+ return null;
1148
+ }
1149
+ const oldest = st.prices[0]!;
1150
+ const change = Math.abs(x - oldest);
1151
+ const volatility = st.vol;
1152
+ let sc: number;
1153
+ if (volatility !== 0) {
1154
+ const efficiency = change / volatility;
1155
+ const fastest = 2 / (f + 1);
1156
+ const slowest = 2 / (sl + 1);
1157
+ const smoothing = efficiency * (fastest - slowest) + slowest;
1158
+ sc = smoothing * smoothing;
1159
+ } else {
1160
+ sc = (2 / (sl + 1)) ** 2;
1161
+ }
1162
+ st.kama = (st.kama ?? x) + sc * (x - (st.kama ?? x));
1163
+ return st.kama;
1164
+ }
1165
+
1166
+ /**
1167
+ * On-Balance Volume. Python `_obv`: 0 until 3 samples; then signed volume
1168
+ * from index 2 onward.
1169
+ */
1170
+ obv(site: string, close: Cell, volume: Cell): Cell {
1171
+ let st = this.obvSites.get(site);
1172
+ if (st === undefined) {
1173
+ st = { prev: null, obv: 0, bars: 0 };
1174
+ this.obvSites.set(site, st);
1175
+ }
1176
+ const c = finiteCell(close);
1177
+ const v = finiteCell(volume) ?? 0;
1178
+ st.bars += 1;
1179
+ if (st.bars < 3) {
1180
+ st.prev = c;
1181
+ return 0;
1182
+ }
1183
+ if (c !== null && st.prev !== null) {
1184
+ if (c > st.prev) st.obv += v;
1185
+ else if (c < st.prev) st.obv -= v;
1186
+ }
1187
+ st.prev = c;
1188
+ return st.obv;
1189
+ }
1190
+
1191
+ /** Left-only pivothigh (Python incremental). */
1192
+ pivothigh(site: string, source: Cell, left: number, right: number): Cell {
1193
+ return this.pivotStep(this.pivotHighSites, site, source, left, right, true);
1194
+ }
1195
+
1196
+ /** Left-only pivotlow (Python incremental). */
1197
+ pivotlow(site: string, source: Cell, left: number, right: number): Cell {
1198
+ return this.pivotStep(this.pivotLowSites, site, source, left, right, false);
1199
+ }
1200
+
1201
+ private pivotStep(
1202
+ sites: Map<string, PivotState>,
1203
+ site: string,
1204
+ source: Cell,
1205
+ left: number,
1206
+ right: number,
1207
+ high: boolean,
1208
+ ): Cell {
1209
+ if (!Number.isFinite(left) || !Number.isFinite(right) || left < 0 || right < 0) return null;
1210
+ const L = Math.trunc(left);
1211
+ const R = Math.trunc(right);
1212
+ let st = sites.get(site);
1213
+ if (st === undefined || st.left !== L || st.right !== R) {
1214
+ st = { left: L, right: R, window: [], n: 0 };
1215
+ sites.set(site, st);
1216
+ }
1217
+ const need = L + 1;
1218
+ if (st.window.length === need) st.window.shift();
1219
+ st.window.push(finiteCell(source));
1220
+ st.n += 1;
1221
+ if (st.n <= L + R || st.window.length < need) return null;
1222
+ const current = st.window[st.window.length - 1];
1223
+ if (current === null) return null;
1224
+ for (let i = 1; i <= L; i++) {
1225
+ const leftVal = st.window[st.window.length - 1 - i];
1226
+ if (leftVal === null) continue;
1227
+ if (high ? leftVal >= current : leftVal <= current) return null;
1228
+ }
1229
+ return current;
1230
+ }
1231
+
1232
+ /**
1233
+ * Hull MA: WMA(2*WMA(n/2)-WMA(n), sqrt(n)).
1234
+ * Nested wma sites `${site}:half` / `:full` / `:sqrt` (`_hma_inc_update`).
1235
+ */
1236
+ hma(site: string, source: Cell, period: number): Cell {
1237
+ const n = pinePeriod(period);
1238
+ if (n === null) return null;
1239
+ const half = Math.max(1, Math.trunc(n / 2));
1240
+ const sqrtN = Math.max(1, Math.trunc(Math.sqrt(n)));
1241
+ const wh = this.wma(`${site}:half`, source, half);
1242
+ const wf = this.wma(`${site}:full`, source, n);
1243
+ const raw = wh === null || wf === null ? null : 2 * wh - wf;
1244
+ return this.wma(`${site}:sqrt`, raw, sqrtN);
1245
+ }
1246
+
1247
+ /**
1248
+ * Money Flow Index. Needs `period+1` typical prices; equal TP is neither side.
1249
+ * Only +MF → 100; only −MF → 0; both 0 → 50 (`_mfi_inc_update`).
1250
+ */
1251
+ mfi(
1252
+ site: string,
1253
+ high: Cell,
1254
+ low: Cell,
1255
+ close: Cell,
1256
+ volume: Cell,
1257
+ period: number,
1258
+ ): Cell {
1259
+ const n = pinePeriod(period);
1260
+ if (n === null) return null;
1261
+ let st = this.mfiSites.get(site);
1262
+ if (st === undefined || st.period !== n) {
1263
+ st = { period: n, tps: [], mfs: [] };
1264
+ this.mfiSites.set(site, st);
1265
+ }
1266
+ const h = finiteCell(high);
1267
+ const l = finiteCell(low);
1268
+ const c = finiteCell(close);
1269
+ if (st.tps.length === n + 1) {
1270
+ st.tps.shift();
1271
+ st.mfs.shift();
1272
+ }
1273
+ if (h === null || l === null || c === null) {
1274
+ st.tps.push(null);
1275
+ st.mfs.push(null);
1276
+ return null;
1277
+ }
1278
+ const vol = finiteCell(volume) ?? 0;
1279
+ const tp = (h + l + c) / 3;
1280
+ st.tps.push(tp);
1281
+ st.mfs.push(tp * vol);
1282
+ if (st.tps.length <= n) return null;
1283
+
1284
+ let pos = 0;
1285
+ let neg = 0;
1286
+ const startK = st.tps.length - n;
1287
+ for (let k = startK; k < st.tps.length; k++) {
1288
+ const tpCur = st.tps[k]!;
1289
+ const tpPrev = st.tps[k - 1]!;
1290
+ const mf = st.mfs[k]!;
1291
+ if (tpCur === null || tpPrev === null || mf === null) return null;
1292
+ if (tpCur > tpPrev) pos += mf;
1293
+ else if (tpCur < tpPrev) neg += mf;
1294
+ }
1295
+ if (neg === 0) return pos === 0 ? 50 : 100;
1296
+ const ratio = pos / neg;
1297
+ return 100 - 100 / (1 + ratio);
1298
+ }
1299
+
1300
+ /**
1301
+ * Parabolic SAR. First valid bar is low; leading na stays na (`_sar_inc_update`).
1302
+ */
1303
+ sar(
1304
+ site: string,
1305
+ high: Cell,
1306
+ low: Cell,
1307
+ start = 0.02,
1308
+ increment = 0.02,
1309
+ maximum = 0.2,
1310
+ ): Cell {
1311
+ if (!Number.isFinite(start) || !Number.isFinite(increment) || !Number.isFinite(maximum)) {
1312
+ return null;
1313
+ }
1314
+ const startF = start;
1315
+ const incF = increment;
1316
+ const maxF = maximum;
1317
+ let st = this.sarSites.get(site);
1318
+ if (
1319
+ st === undefined ||
1320
+ st.start !== startF ||
1321
+ st.increment !== incF ||
1322
+ st.maximum !== maxF
1323
+ ) {
1324
+ st = {
1325
+ start: startF,
1326
+ increment: incF,
1327
+ maximum: maxF,
1328
+ started: false,
1329
+ sar: null,
1330
+ ep: null,
1331
+ trend: 1,
1332
+ af: startF,
1333
+ };
1334
+ this.sarSites.set(site, st);
1335
+ }
1336
+ const h = finiteCell(high);
1337
+ const l = finiteCell(low);
1338
+ if (!st.started) {
1339
+ if (h === null || l === null) return null;
1340
+ st.started = true;
1341
+ st.sar = l;
1342
+ st.ep = h;
1343
+ st.trend = 1;
1344
+ st.af = startF;
1345
+ return l;
1346
+ }
1347
+ const previous = st.sar;
1348
+ const ep = st.ep;
1349
+ if (previous === null || h === null || l === null || ep === null) return previous;
1350
+ let trend = st.trend;
1351
+ let af = st.af;
1352
+ let epF = ep;
1353
+ let sarV: number;
1354
+ if (trend === 1) {
1355
+ sarV = previous + af * (epF - previous);
1356
+ if (h > epF) {
1357
+ epF = h;
1358
+ af = Math.min(af + incF, maxF);
1359
+ }
1360
+ if (sarV > l) {
1361
+ trend = -1;
1362
+ sarV = epF;
1363
+ epF = l;
1364
+ af = startF;
1365
+ }
1366
+ } else {
1367
+ sarV = previous - af * (previous - epF);
1368
+ if (l < epF) {
1369
+ epF = l;
1370
+ af = Math.min(af + incF, maxF);
1371
+ }
1372
+ if (sarV < h) {
1373
+ trend = 1;
1374
+ sarV = epF;
1375
+ epF = h;
1376
+ af = startF;
1377
+ }
1378
+ }
1379
+ st.sar = sarV;
1380
+ st.ep = epF;
1381
+ st.trend = trend;
1382
+ st.af = af;
1383
+ return Number.isFinite(sarV) ? sarV : null;
1384
+ }
1385
+
1386
+ /**
1387
+ * ADX (nan-first DM). Early bars and unseeded ATR/DX → 0 (`_adx_inc_update`).
1388
+ */
1389
+ adx(site: string, high: Cell, low: Cell, close: Cell, length: number): Cell {
1390
+ const n = pinePeriod(length);
1391
+ if (n === null) return 0;
1392
+ let st = this.adxSites.get(site);
1393
+ if (st === undefined || st.period !== n) {
1394
+ st = { period: n, prevH: null, prevL: null, prevC: null, n: 0 };
1395
+ this.adxSites.set(site, st);
1396
+ }
1397
+ let tr: Cell;
1398
+ let plusDm: Cell;
1399
+ let minusDm: Cell;
1400
+ if (st.prevH === null || st.prevL === null || st.prevC === null) {
1401
+ tr = null;
1402
+ plusDm = null;
1403
+ minusDm = null;
1404
+ } else {
1405
+ const hf = high !== null && Number.isFinite(high) ? high : Number.NaN;
1406
+ const lf = low !== null && Number.isFinite(low) ? low : Number.NaN;
1407
+ const trRaw = Math.max(
1408
+ hf - lf,
1409
+ Math.abs(hf - st.prevC),
1410
+ Math.abs(lf - st.prevC),
1411
+ );
1412
+ tr = Number.isFinite(trRaw) ? trRaw : null;
1413
+ const highDiff = hf - st.prevH;
1414
+ const lowDiff = st.prevL - lf;
1415
+ plusDm = highDiff > lowDiff && highDiff > 0 ? highDiff : 0;
1416
+ minusDm = lowDiff > highDiff && lowDiff > 0 ? lowDiff : 0;
1417
+ }
1418
+ st.prevH = finiteCell(high);
1419
+ st.prevL = finiteCell(low);
1420
+ st.prevC = finiteCell(close);
1421
+ st.n += 1;
1422
+
1423
+ const atr = this.rma(`${site}:tr`, tr, n);
1424
+ const pd = this.rma(`${site}:pdm`, plusDm, n);
1425
+ const md = this.rma(`${site}:mdm`, minusDm, n);
1426
+ if (st.n < n) return 0;
1427
+ if (atr === null) return 0;
1428
+
1429
+ let dxIn: Cell;
1430
+ if (pd === null || md === null) {
1431
+ dxIn = null;
1432
+ } else {
1433
+ const plusDi = atr !== 0 ? 100 * pd / atr : 0;
1434
+ const minusDi = atr !== 0 ? 100 * md / atr : 0;
1435
+ const denom = plusDi + minusDi;
1436
+ dxIn = denom !== 0 ? 100 * Math.abs(plusDi - minusDi) / denom : 0;
1437
+ }
1438
+ const adxV = this.rma(`${site}:dx`, dxIn, n);
1439
+ return adxV === null ? 0 : adxV;
1440
+ }
1441
+
1442
+ /**
1443
+ * DMI: 0-first +DI/−DI (RMA diLength) + nan-first ADX via `${site}:adx`.
1444
+ */
1445
+ dmi(
1446
+ site: string,
1447
+ high: Cell,
1448
+ low: Cell,
1449
+ close: Cell,
1450
+ diLength: number,
1451
+ adxSmoothing: number,
1452
+ ): { plus: Cell; minus: Cell; adx: Cell } {
1453
+ const diN = pinePeriod(diLength);
1454
+ if (diN === null) return { plus: null, minus: null, adx: null };
1455
+ let st = this.dmiSites.get(site);
1456
+ if (st === undefined || st.diLen !== diN) {
1457
+ st = { diLen: diN, prevH: null, prevL: null, prevC: null };
1458
+ this.dmiSites.set(site, st);
1459
+ }
1460
+ let tr: Cell;
1461
+ let plusDm: number;
1462
+ let minusDm: number;
1463
+ if (st.prevH === null || st.prevL === null || st.prevC === null) {
1464
+ tr = null;
1465
+ plusDm = 0;
1466
+ minusDm = 0;
1467
+ } else {
1468
+ const hf = finiteCell(high) ?? 0;
1469
+ const lf = finiteCell(low) ?? 0;
1470
+ const ph = st.prevH;
1471
+ const pl = st.prevL;
1472
+ const pc = st.prevC;
1473
+ tr = Math.max(hf - lf, Math.abs(hf - pc), Math.abs(lf - pc));
1474
+ const highDiff = hf - ph;
1475
+ const lowDiff = pl - lf;
1476
+ plusDm = highDiff > lowDiff && highDiff > 0 ? highDiff : 0;
1477
+ minusDm = lowDiff > highDiff && lowDiff > 0 ? lowDiff : 0;
1478
+ }
1479
+ st.prevH = finiteCell(high);
1480
+ st.prevL = finiteCell(low);
1481
+ st.prevC = finiteCell(close);
1482
+
1483
+ const atr = this.rma(`${site}:tr`, tr, diN);
1484
+ const pd = this.rma(`${site}:pdm`, plusDm, diN);
1485
+ const md = this.rma(`${site}:mdm`, minusDm, diN);
1486
+ let plus: Cell;
1487
+ let minus: Cell;
1488
+ if (atr === null) {
1489
+ plus = null;
1490
+ minus = null;
1491
+ } else if (atr === 0) {
1492
+ plus = 0;
1493
+ minus = 0;
1494
+ } else {
1495
+ plus = 100 * (pd ?? 0) / atr;
1496
+ minus = 100 * (md ?? 0) / atr;
1497
+ }
1498
+ const adx = this.adx(`${site}:adx`, high, low, close, adxSmoothing);
1499
+ return { plus, minus, adx };
1500
+ }
1501
+
1502
+ /**
1503
+ * Pearson correlation over `length` samples (na pairs skipped).
1504
+ * `length<2`, short window, <2 pairs, or zero variance → na.
1505
+ */
1506
+ correlation(site: string, a: Cell, b: Cell, length: number): Cell {
1507
+ const n = pinePeriod(length);
1508
+ if (n === null || n < 2) return null;
1509
+ let st = this.correlationSites.get(site);
1510
+ if (st === undefined || st.length !== n) {
1511
+ st = { length: n, a: [], b: [] };
1512
+ this.correlationSites.set(site, st);
1513
+ }
1514
+ if (st.a.length === n) {
1515
+ st.a.shift();
1516
+ st.b.shift();
1517
+ }
1518
+ st.a.push(finiteCell(a));
1519
+ st.b.push(finiteCell(b));
1520
+ if (st.a.length < n) return null;
1521
+
1522
+ const xs: number[] = [];
1523
+ const ys: number[] = [];
1524
+ for (let i = 0; i < n; i++) {
1525
+ const x = st.a[i]!;
1526
+ const y = st.b[i]!;
1527
+ if (x !== null && y !== null) {
1528
+ xs.push(x);
1529
+ ys.push(y);
1530
+ }
1531
+ }
1532
+ if (xs.length < 2) return null;
1533
+ const m = xs.length;
1534
+ let mx = 0;
1535
+ let my = 0;
1536
+ for (let i = 0; i < m; i++) {
1537
+ mx += xs[i]!;
1538
+ my += ys[i]!;
1539
+ }
1540
+ mx /= m;
1541
+ my /= m;
1542
+ let num = 0;
1543
+ let denx = 0;
1544
+ let deny = 0;
1545
+ for (let i = 0; i < m; i++) {
1546
+ const dx = xs[i]! - mx;
1547
+ const dy = ys[i]! - my;
1548
+ num += dx * dy;
1549
+ denx += dx * dx;
1550
+ deny += dy * dy;
1551
+ }
1552
+ const sx = Math.sqrt(denx);
1553
+ const sy = Math.sqrt(deny);
1554
+ if (sx === 0 || sy === 0) return null;
1555
+ return num / (sx * sy);
1556
+ }
1557
+
1558
+ /**
1559
+ * Cumulative A/D: CLV * volume. H/L/C na keeps previous ad; vol na → 0;
1560
+ * zero range → CLV 0 (`_accdist_inc_update`).
1561
+ */
1562
+ accdist(site: string, high: Cell, low: Cell, close: Cell, volume: Cell): Cell {
1563
+ let st = this.accdistSites.get(site);
1564
+ if (st === undefined) {
1565
+ st = { ad: 0 };
1566
+ this.accdistSites.set(site, st);
1567
+ }
1568
+ const h = finiteCell(high);
1569
+ const l = finiteCell(low);
1570
+ const c = finiteCell(close);
1571
+ if (h === null || l === null || c === null) return st.ad;
1572
+ const vv = finiteCell(volume) ?? 0;
1573
+ const rng = h - l;
1574
+ const clv = rng === 0 ? 0 : ((c - l) - (h - c)) / rng;
1575
+ st.ad += clv * vv;
1576
+ return st.ad;
1577
+ }
1578
+
1579
+ /**
1580
+ * Price-Volume Trend. Adds vol*(c-prev)/prev when prev is finite and != 0.
1581
+ * First bar is 0; na close or vol skips the add.
1582
+ */
1583
+ pvt(site: string, close: Cell, volume: Cell): Cell {
1584
+ let st = this.pvtSites.get(site);
1585
+ if (st === undefined) {
1586
+ st = { pvt: 0, prev: null };
1587
+ this.pvtSites.set(site, st);
1588
+ }
1589
+ const c = finiteCell(close);
1590
+ const prev = st.prev;
1591
+ if (prev !== null && prev !== 0 && c !== null) {
1592
+ const vv = finiteCell(volume) ?? 0;
1593
+ st.pvt += (vv * (c - prev)) / prev;
1594
+ }
1595
+ if (c !== null) st.prev = c;
1596
+ return st.pvt;
1597
+ }
1598
+
1599
+ /**
1600
+ * Williams AD. First bar 0; close>prev → +(close-low); close<prev →
1601
+ * -(high-close). H/L/C na keeps previous (`_wad`).
1602
+ */
1603
+ wad(site: string, high: Cell, low: Cell, close: Cell): Cell {
1604
+ let st = this.wadSites.get(site);
1605
+ if (st === undefined) {
1606
+ st = { wad: 0, prev: null, started: false };
1607
+ this.wadSites.set(site, st);
1608
+ }
1609
+ const h = finiteCell(high);
1610
+ const l = finiteCell(low);
1611
+ const c = finiteCell(close);
1612
+ if (h === null || l === null || c === null) return st.wad;
1613
+ if (!st.started) {
1614
+ st.started = true;
1615
+ st.prev = c;
1616
+ return st.wad;
1617
+ }
1618
+ const prev = st.prev;
1619
+ if (prev !== null) {
1620
+ if (c > prev) st.wad += c - l;
1621
+ else if (c < prev) st.wad -= h - c;
1622
+ }
1623
+ st.prev = c;
1624
+ return st.wad;
1625
+ }
1626
+
1627
+ /**
1628
+ * Negative Volume Index. Starts at 1000; updates when volume < prev volume.
1629
+ */
1630
+ nvi(site: string, close: Cell, volume: Cell): Cell {
1631
+ return this.volIndexStep(this.nviSites, site, close, volume, (vol, prevVol) => vol < prevVol);
1632
+ }
1633
+
1634
+ /**
1635
+ * Positive Volume Index. Starts at 1000; updates when volume > prev volume.
1636
+ */
1637
+ pvi(site: string, close: Cell, volume: Cell): Cell {
1638
+ return this.volIndexStep(this.pviSites, site, close, volume, (vol, prevVol) => vol > prevVol);
1639
+ }
1640
+
1641
+ private volIndexStep(
1642
+ sites: Map<string, VolIndexState>,
1643
+ site: string,
1644
+ close: Cell,
1645
+ volume: Cell,
1646
+ trigger: (vol: number, prevVol: number) => boolean,
1647
+ ): Cell {
1648
+ let st = sites.get(site);
1649
+ if (st === undefined) {
1650
+ st = { value: 1000, prevClose: null, prevVol: null, started: false };
1651
+ sites.set(site, st);
1652
+ }
1653
+ const c = finiteCell(close);
1654
+ const vol = finiteCell(volume) ?? 0;
1655
+ if (!st.started) {
1656
+ st.started = true;
1657
+ if (c !== null) st.prevClose = c;
1658
+ st.prevVol = vol;
1659
+ return st.value;
1660
+ }
1661
+ if (c !== null && st.prevClose !== null && trigger(vol, st.prevVol ?? 0)) {
1662
+ const change = st.prevClose !== 0 ? (c - st.prevClose) / st.prevClose : 0;
1663
+ const next = st.value * (1 + change);
1664
+ if (Number.isFinite(next)) st.value = next;
1665
+ }
1666
+ if (c !== null) st.prevClose = c;
1667
+ st.prevVol = vol;
1668
+ return st.value;
1669
+ }
1670
+
1671
+ /**
1672
+ * Intraday Intensity: ((2c-h-l)/(h-l))*volume. Zero range → 0; H/L/C na → na;
1673
+ * vol na → 0 (`_builtin_ta_iii` * volume).
1674
+ */
1675
+ iii(site: string, high: Cell, low: Cell, close: Cell, volume: Cell): Cell {
1676
+ const h = finiteCell(high);
1677
+ const l = finiteCell(low);
1678
+ const c = finiteCell(close);
1679
+ if (h === null || l === null || c === null) return null;
1680
+ const rng = h - l;
1681
+ if (rng === 0) return 0;
1682
+ const vv = finiteCell(volume) ?? 0;
1683
+ return ((2 * c - h - l) / rng) * vv;
1684
+ }
1685
+
1686
+ /**
1687
+ * Williams Variable AD: cumulative ((c-o)/(h-l))*volume. O/H/L/C na keeps
1688
+ * previous; vol na → 0; zero range adds 0.
1689
+ */
1690
+ wvad(site: string, open: Cell, high: Cell, low: Cell, close: Cell, volume: Cell): Cell {
1691
+ let st = this.wvadSites.get(site);
1692
+ if (st === undefined) {
1693
+ st = { value: 0 };
1694
+ this.wvadSites.set(site, st);
1695
+ }
1696
+ const o = finiteCell(open);
1697
+ const h = finiteCell(high);
1698
+ const l = finiteCell(low);
1699
+ const c = finiteCell(close);
1700
+ if (o === null || h === null || l === null || c === null) return st.value;
1701
+ const rng = h - l;
1702
+ const vv = finiteCell(volume) ?? 0;
1703
+ if (rng !== 0) st.value += ((c - o) / rng) * vv;
1704
+ return st.value;
1705
+ }
1706
+
1707
+ /**
1708
+ * Classic floor pivots. na high/low/close → all na.
1709
+ * Optional `type` is accepted; classic levels are always returned.
1710
+ */
1711
+ pivotPoints(site: string, high: Cell, low: Cell, close: Cell, type?: string): PivotPoints {
1712
+ const h = finiteCell(high);
1713
+ const l = finiteCell(low);
1714
+ const c = finiteCell(close);
1715
+ if (h === null || l === null || c === null) {
1716
+ return { pp: null, r1: null, s1: null, r2: null, s2: null };
1717
+ }
1718
+ const pp = (h + l + c) / 3;
1719
+ const rng = h - l;
1720
+ return {
1721
+ pp,
1722
+ r1: 2 * pp - l,
1723
+ s1: 2 * pp - h,
1724
+ r2: pp + rng,
1725
+ s2: pp - rng,
1726
+ };
1727
+ }
1728
+
1729
+ /**
1730
+ * Symmetric WMA. Omitted/NaN period → fixed 4-sample weights 1/2/2/1.
1731
+ * Else `_builtin_ta_swma` 2-arg weights. Any na in window → na.
1732
+ */
1733
+ swma(site: string, source: Cell, period?: number): Cell {
1734
+ const useFixed = period === undefined || Number.isNaN(period);
1735
+ const n = useFixed ? 4 : pinePeriod(period);
1736
+ if (n === null) return null;
1737
+ let st = this.swmaSites.get(site);
1738
+ if (st === undefined || st.period !== n) {
1739
+ const weights: number[] = [];
1740
+ let wsum = 0;
1741
+ for (let i = 0; i < n; i++) {
1742
+ const w = i < (n >> 1) ? i + 1 : i > ((n - 1) >> 1) ? n - i : (n >> 1) + 1;
1743
+ weights.push(w);
1744
+ wsum += w;
1745
+ }
1746
+ st = { period: n, window: [], weights, wsum };
1747
+ this.swmaSites.set(site, st);
1748
+ }
1749
+ if (st.window.length === n) st.window.shift();
1750
+ st.window.push(finiteCell(source));
1751
+ if (st.window.length < n || st.wsum === 0) return null;
1752
+ let acc = 0;
1753
+ for (let i = 0; i < n; i++) {
1754
+ const v = st.window[i];
1755
+ if (v == null) return null;
1756
+ acc += v * st.weights[i]!;
1757
+ }
1758
+ return acc / st.wsum;
1759
+ }
1760
+
1761
+ /** Center of Gravity: `-sum((i+1)*x_rev) / sum(x)` over `period`. den=0 → na. */
1762
+ cog(site: string, source: Cell, period: number): Cell {
1763
+ const n = pinePeriod(period);
1764
+ if (n === null) return null;
1765
+ let st = this.cogSites.get(site);
1766
+ if (st === undefined || st.period !== n) {
1767
+ st = { period: n, window: [] };
1768
+ this.cogSites.set(site, st);
1769
+ }
1770
+ if (st.window.length === n) st.window.shift();
1771
+ st.window.push(finiteCell(source));
1772
+ if (st.window.length < n) return null;
1773
+ let num = 0;
1774
+ let den = 0;
1775
+ for (let i = 0; i < n; i++) {
1776
+ const v = st.window[n - 1 - i];
1777
+ if (v === null) continue;
1778
+ num += (i + 1) * v;
1779
+ den += v;
1780
+ }
1781
+ if (den === 0) return null;
1782
+ return -num / den;
1783
+ }
1784
+
1785
+ /** TSI: 100 * EMA(EMA(mom, long), short) / EMA(EMA(|mom|, long), short). */
1786
+ tsi(site: string, source: Cell, longPeriod: number, shortPeriod: number): Cell {
1787
+ const longN = pinePeriod(longPeriod);
1788
+ const shortN = pinePeriod(shortPeriod);
1789
+ if (longN === null || shortN === null) return null;
1790
+ let st = this.tsiSites.get(site);
1791
+ if (st === undefined || st.longPeriod !== longN || st.shortPeriod !== shortN) {
1792
+ st = { longPeriod: longN, shortPeriod: shortN, prev: null, nBars: 0, value: null };
1793
+ this.tsiSites.set(site, st);
1794
+ }
1795
+ const x = finiteCell(source);
1796
+ st.nBars += 1;
1797
+ const prev = st.prev;
1798
+ st.prev = x;
1799
+ if (prev === null || x === null) {
1800
+ if (st.nBars < longN + shortN) {
1801
+ st.value = null;
1802
+ return null;
1803
+ }
1804
+ return st.value;
1805
+ }
1806
+ const mom = x - prev;
1807
+ const tag = `${site}:${longN}:${shortN}`;
1808
+ const e1 = this.ema(`${tag}:mom`, mom, longN);
1809
+ const e2 = this.ema(`${tag}:mom2`, e1, shortN);
1810
+ const a1 = this.ema(`${tag}:abs`, Math.abs(mom), longN);
1811
+ const a2 = this.ema(`${tag}:abs2`, a1, shortN);
1812
+ if (st.nBars < longN + shortN) {
1813
+ st.value = null;
1814
+ return null;
1815
+ }
1816
+ if (a2 === null || a2 === 0) {
1817
+ st.value = a2 === 0 ? 0 : null;
1818
+ return st.value;
1819
+ }
1820
+ if (e2 === null) {
1821
+ st.value = null;
1822
+ return null;
1823
+ }
1824
+ st.value = 100 * (e2 / a2);
1825
+ return st.value;
1826
+ }
1827
+
1828
+ /** `(up - lo) / mid` from `kc`. na if mid is 0/null. */
1829
+ kcw(
1830
+ site: string,
1831
+ high: Cell,
1832
+ low: Cell,
1833
+ close: Cell,
1834
+ length: number,
1835
+ mult: number,
1836
+ ): Cell {
1837
+ const r = this.kc(site, high, low, close, length, mult);
1838
+ if (r.mid === null || r.up === null || r.lo === null || r.mid === 0) return null;
1839
+ return (r.up - r.lo) / r.mid;
1840
+ }
1841
+
1842
+ /** Mean absolute deviation. Full window; any na → na. */
1843
+ dev(site: string, source: Cell, period: number): Cell {
1844
+ const n = pinePeriod(period);
1845
+ if (n === null) return null;
1846
+ let st = this.devSites.get(site);
1847
+ if (st === undefined || st.period !== n) {
1848
+ st = { period: n, window: [], sum: 0, count: 0 };
1849
+ this.devSites.set(site, st);
1850
+ }
1851
+ const x = finiteCell(source);
1852
+ if (st.window.length === n) {
1853
+ const old = st.window.shift()!;
1854
+ if (old !== null) {
1855
+ st.sum -= old;
1856
+ st.count -= 1;
1857
+ }
1858
+ }
1859
+ st.window.push(x);
1860
+ if (x !== null) {
1861
+ st.sum += x;
1862
+ st.count += 1;
1863
+ }
1864
+ if (st.window.length < n || st.count !== n) return null;
1865
+ const mean = st.sum / n;
1866
+ let acc = 0;
1867
+ for (const v of st.window) {
1868
+ if (v === null) return null;
1869
+ acc += Math.abs(v - mean);
1870
+ }
1871
+ return acc / n;
1872
+ }
1873
+
1874
+ /** Sample variance (ddof=1). Any na in window → na; period<=1 → na. */
1875
+ variance(site: string, source: Cell, period: number): Cell {
1876
+ const n = pinePeriod(period);
1877
+ if (n === null || n <= 1) return null;
1878
+ let st = this.varianceSites.get(site);
1879
+ if (st === undefined || st.period !== n) {
1880
+ st = { period: n, window: [], sum: 0, sumsq: 0, count: 0 };
1881
+ this.varianceSites.set(site, st);
1882
+ }
1883
+ const x = finiteCell(source);
1884
+ if (st.window.length === n) {
1885
+ const old = st.window.shift()!;
1886
+ if (old !== null) {
1887
+ st.sum -= old;
1888
+ st.sumsq -= old * old;
1889
+ st.count -= 1;
1890
+ }
1891
+ }
1892
+ st.window.push(x);
1893
+ if (x !== null) {
1894
+ st.sum += x;
1895
+ st.sumsq += x * x;
1896
+ st.count += 1;
1897
+ }
1898
+ if (st.window.length < n || st.count !== n) return null;
1899
+ let v = (st.sumsq - (st.sum * st.sum) / n) / (n - 1);
1900
+ if (v < 0) v = 0;
1901
+ return v;
1902
+ }
1903
+
1904
+ /** Median of finite samples. Needs a full `period` window. */
1905
+ median(site: string, source: Cell, period: number): Cell {
1906
+ const n = pinePeriod(period);
1907
+ if (n === null) return null;
1908
+ let st = this.medianSites.get(site);
1909
+ if (st === undefined || st.period !== n) {
1910
+ st = { period: n, window: [] };
1911
+ this.medianSites.set(site, st);
1912
+ }
1913
+ if (st.window.length === n) st.window.shift();
1914
+ st.window.push(finiteCell(source));
1915
+ if (st.window.length < n) return null;
1916
+ const valid: number[] = [];
1917
+ for (const v of st.window) {
1918
+ if (v !== null) valid.push(v);
1919
+ }
1920
+ if (valid.length === 0) return null;
1921
+ valid.sort((a, b) => a - b);
1922
+ const mid = valid.length >> 1;
1923
+ if (valid.length % 2 === 1) return valid[mid]!;
1924
+ return (valid[mid - 1]! + valid[mid]!) / 2;
1925
+ }
1926
+
1927
+ /** Most frequent finite value. Ties keep the first in-window value. */
1928
+ mode(site: string, source: Cell, period: number): Cell {
1929
+ const n = pinePeriod(period);
1930
+ if (n === null) return null;
1931
+ let st = this.modeSites.get(site);
1932
+ if (st === undefined || st.period !== n) {
1933
+ st = { period: n, window: [] };
1934
+ this.modeSites.set(site, st);
1935
+ }
1936
+ if (st.window.length === n) st.window.shift();
1937
+ st.window.push(finiteCell(source));
1938
+ if (st.window.length < n) return null;
1939
+ const counts = new Map<number, number>();
1940
+ let best: number | null = null;
1941
+ let bestC = 0;
1942
+ for (const v of st.window) {
1943
+ if (v === null) continue;
1944
+ const c = (counts.get(v) ?? 0) + 1;
1945
+ counts.set(v, c);
1946
+ if (c > bestC) {
1947
+ bestC = c;
1948
+ best = v;
1949
+ }
1950
+ }
1951
+ return best;
1952
+ }
1953
+
1954
+ /** `100 * count(x < current) / count`. <2 finite samples → 50; current na → na. */
1955
+ percentrank(site: string, source: Cell, period: number): Cell {
1956
+ const n = pinePeriod(period);
1957
+ if (n === null) return null;
1958
+ let st = this.percentrankSites.get(site);
1959
+ if (st === undefined || st.period !== n) {
1960
+ st = { period: n, window: [] };
1961
+ this.percentrankSites.set(site, st);
1962
+ }
1963
+ const x = finiteCell(source);
1964
+ if (st.window.length === n) st.window.shift();
1965
+ st.window.push(x);
1966
+ if (st.window.length < n) return null;
1967
+ const valid: number[] = [];
1968
+ for (const v of st.window) {
1969
+ if (v !== null) valid.push(v);
1970
+ }
1971
+ if (valid.length < 2) return 50;
1972
+ if (x === null) return null;
1973
+ let below = 0;
1974
+ for (const v of valid) {
1975
+ if (v < x) below += 1;
1976
+ }
1977
+ return (below / valid.length) * 100;
1978
+ }
1979
+
1980
+ /** Nearest-rank percentile. Rank = ceil(p/100 * n), 1-indexed. */
1981
+ percentileNearest(site: string, source: Cell, period: number, percentage: number): Cell {
1982
+ return this.percentileStep(this.percentileNearestSites, site, source, period, percentage, false);
1983
+ }
1984
+
1985
+ /** Linear-interpolation percentile at rank (p/100)*(n-1). */
1986
+ percentileLinear(site: string, source: Cell, period: number, percentage: number): Cell {
1987
+ return this.percentileStep(this.percentileLinearSites, site, source, period, percentage, true);
1988
+ }
1989
+
1990
+ private percentileStep(
1991
+ sites: Map<string, PercentileState>,
1992
+ site: string,
1993
+ source: Cell,
1994
+ period: number,
1995
+ percentage: number,
1996
+ linear: boolean,
1997
+ ): Cell {
1998
+ const n = pinePeriod(period);
1999
+ if (n === null || !Number.isFinite(percentage)) return null;
2000
+ let st = sites.get(site);
2001
+ if (st === undefined || st.period !== n || st.percentage !== percentage) {
2002
+ st = { period: n, percentage, window: [] };
2003
+ sites.set(site, st);
2004
+ }
2005
+ if (st.window.length === n) st.window.shift();
2006
+ st.window.push(finiteCell(source));
2007
+ if (st.window.length < n) return null;
2008
+ const sorted: number[] = [];
2009
+ for (const v of st.window) {
2010
+ if (v !== null) sorted.push(v);
2011
+ }
2012
+ if (sorted.length === 0) return null;
2013
+ sorted.sort((a, b) => a - b);
2014
+ const m = sorted.length;
2015
+ if (linear) {
2016
+ if (m === 1) return sorted[0]!;
2017
+ const rank = (percentage / 100) * (m - 1);
2018
+ const lo = Math.trunc(rank);
2019
+ const hi = Math.min(lo + 1, m - 1);
2020
+ const frac = rank - lo;
2021
+ return sorted[lo]! * (1 - frac) + sorted[hi]! * frac;
2022
+ }
2023
+ let rank = Math.trunc((percentage / 100) * m + 0.999999);
2024
+ if (rank < 1) rank = 1;
2025
+ if (rank > m) rank = m;
2026
+ return sorted[rank - 1]!;
2027
+ }
2028
+
2029
+ /** Running sum. na / non-finite contributes 0 (`_cum_inc_update`). */
2030
+ cum(site: string, source: Cell): Cell {
2031
+ let st = this.cumSites.get(site);
2032
+ if (st === undefined) {
2033
+ st = { total: 0 };
2034
+ this.cumSites.set(site, st);
2035
+ }
2036
+ const x = finiteCell(source);
2037
+ st.total += x ?? 0;
2038
+ return st.total;
2039
+ }
2040
+
2041
+ /** Bars since last truthy cond (non-null, !=0). Never true → na. */
2042
+ barssince(site: string, cond: Cell): Cell {
2043
+ let st = this.barssinceSites.get(site);
2044
+ if (st === undefined) {
2045
+ st = { everTrue: false, barsSince: 0 };
2046
+ this.barssinceSites.set(site, st);
2047
+ }
2048
+ const c = finiteCell(cond);
2049
+ if (c !== null && c !== 0) {
2050
+ st.everTrue = true;
2051
+ st.barsSince = 0;
2052
+ return 0;
2053
+ }
2054
+ st.barsSince += 1;
2055
+ return st.everTrue ? st.barsSince : null;
2056
+ }
2057
+
2058
+ /** Source at the `occurrence`-th most recent true cond (0 = latest). */
2059
+ valuewhen(site: string, cond: Cell, source: Cell, occurrence = 0): Cell {
2060
+ if (!Number.isFinite(occurrence) || occurrence < 0) return null;
2061
+ const occ = Math.trunc(occurrence);
2062
+ let st = this.valuewhenSites.get(site);
2063
+ if (st === undefined || st.occurrence !== occ) {
2064
+ st = { occurrence: occ, hits: [] };
2065
+ this.valuewhenSites.set(site, st);
2066
+ }
2067
+ const c = finiteCell(cond);
2068
+ if (c !== null && c !== 0) {
2069
+ if (st.hits.length === occ + 1) st.hits.shift();
2070
+ st.hits.push(finiteCell(source));
2071
+ }
2072
+ if (st.hits.length <= occ) return null;
2073
+ return st.hits[st.hits.length - 1 - occ]!;
2074
+ }
2075
+
2076
+ /** `highest - lowest` over `period` (nested highest/lowest sites). */
2077
+ range(site: string, source: Cell, period: number): Cell {
2078
+ const hi = this.highest(`${site}:hi`, source, period);
2079
+ const lo = this.lowest(`${site}:lo`, source, period);
2080
+ if (hi === null || lo === null) return null;
2081
+ return hi - lo;
2082
+ }
2083
+
2084
+ /** Alias of `highest`. */
2085
+ max(site: string, source: Cell, period: number): Cell {
2086
+ return this.highest(site, source, period);
2087
+ }
2088
+
2089
+ /** Alias of `lowest`. */
2090
+ min(site: string, source: Cell, period: number): Cell {
2091
+ return this.lowest(site, source, period);
2092
+ }
2093
+ }