@hoox-sh/pynets 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/runtime/ta.ts CHANGED
@@ -33,6 +33,7 @@ interface RmaState {
33
33
  interface AtrState {
34
34
  period: number;
35
35
  prevClose: Cell;
36
+ started: boolean;
36
37
  rma: RmaState;
37
38
  }
38
39
 
@@ -331,6 +332,96 @@ interface ZigzagState {
331
332
  samples: Cell[];
332
333
  }
333
334
 
335
+ interface EngulfingState {
336
+ prevOpen: Cell;
337
+ prevClose: Cell;
338
+ count: number;
339
+ }
340
+
341
+ interface InsideBarState {
342
+ prevHigh: Cell;
343
+ prevLow: Cell;
344
+ count: number;
345
+ }
346
+
347
+ interface DoubleTopBottomState {
348
+ highs: Cell[];
349
+ lows: Cell[];
350
+ count: number;
351
+ }
352
+
353
+ interface RsiDivergenceState {
354
+ window: Cell[];
355
+ count: number;
356
+ }
357
+
358
+ interface CumulativeDeltaState {
359
+ closes: Cell[];
360
+ volumes: Cell[];
361
+ count: number;
362
+ }
363
+
364
+ interface VolumeMomentumState {
365
+ window: number[];
366
+ }
367
+
368
+ interface VolumeThrustState {
369
+ closes: Cell[];
370
+ volume: Cell;
371
+ sma: Cell;
372
+ nClose: number;
373
+ nVolume: number;
374
+ nSma: number;
375
+ }
376
+
377
+ interface StochSmoothState {
378
+ highs: Cell[];
379
+ lows: Cell[];
380
+ }
381
+
382
+ interface SmaWeightedState {
383
+ window: Cell[];
384
+ weights: number[];
385
+ wsum: number;
386
+ }
387
+
388
+ interface PairWindowState {
389
+ a: Cell[];
390
+ b: Cell[];
391
+ }
392
+
393
+ interface SingleWindowState {
394
+ window: Cell[];
395
+ }
396
+
397
+ interface MarketConditionState {
398
+ window: Cell[];
399
+ atr: Cell;
400
+ }
401
+
402
+ interface OrderFlowState {
403
+ highs: Cell[];
404
+ lows: Cell[];
405
+ closes: Cell[];
406
+ volumes: Cell[];
407
+ }
408
+
409
+ interface LiquidityState {
410
+ volume: Cell[];
411
+ volatility: Cell[];
412
+ spread: Cell[];
413
+ }
414
+
415
+ interface MeanReversionState {
416
+ countClose: number;
417
+ countSma: number;
418
+ }
419
+
420
+ interface VolumeWeightedMomentumState {
421
+ prices: Cell[];
422
+ volumes: Cell[];
423
+ }
424
+
334
425
  export type PivotPoints = {
335
426
  pp: Cell;
336
427
  r1: Cell;
@@ -362,6 +453,35 @@ function finiteOut(value: number): Cell {
362
453
  return Number.isFinite(value) ? value : null;
363
454
  }
364
455
 
456
+ /** Python `_sma` last sample: strict window, any `na` → `na`. */
457
+ function smaOf(series: Cell[], period: number): Cell {
458
+ if (period <= 0 || series.length < period) return null;
459
+ const window = series.slice(series.length - period);
460
+ let sum = 0;
461
+ for (const v of window) {
462
+ if (v === null) return null;
463
+ sum += v;
464
+ }
465
+ return sum / period;
466
+ }
467
+
468
+ /** Python `_stdev` last sample: population stdev, strict window. */
469
+ function stdevOf(series: Cell[], period: number): Cell {
470
+ if (period <= 1 || series.length < period) return null;
471
+ const window = series.slice(series.length - period);
472
+ const vals: number[] = [];
473
+ for (const v of window) {
474
+ if (v === null) return null;
475
+ vals.push(v);
476
+ }
477
+ let mean = 0;
478
+ for (const v of vals) mean += v;
479
+ mean /= period;
480
+ let sumSq = 0;
481
+ for (const v of vals) sumSq += (v - mean) ** 2;
482
+ return Math.sqrt(sumSq / period);
483
+ }
484
+
365
485
  /** Near-integer floats round (14.0000000001 → 14); else floor. Python `_float_to_period_int`. */
366
486
  const PERIOD_INT_EPS = 1e-9;
367
487
 
@@ -459,6 +579,26 @@ export class TaEngine {
459
579
  private readonly emvSites = new Map<string, EmvState>();
460
580
  private readonly fractalSites = new Map<string, FractalState>();
461
581
  private readonly zigzagSites = new Map<string, ZigzagState>();
582
+ private readonly engulfingSites = new Map<string, EngulfingState>();
583
+ private readonly insideBarSites = new Map<string, InsideBarState>();
584
+ private readonly doubleTopBottomSites = new Map<string, DoubleTopBottomState>();
585
+ private readonly rsiDivergenceSites = new Map<string, RsiDivergenceState>();
586
+ private readonly stochSmoothSites = new Map<string, StochSmoothState>();
587
+ private readonly smaWeightedSites = new Map<string, SmaWeightedState>();
588
+ private readonly betaSites = new Map<string, PairWindowState>();
589
+ private readonly rSquaredSites = new Map<string, PairWindowState>();
590
+ private readonly comovementSites = new Map<string, PairWindowState>();
591
+ private readonly skewnessSites = new Map<string, SingleWindowState>();
592
+ private readonly kurtosisSites = new Map<string, SingleWindowState>();
593
+ private readonly vwmSites = new Map<string, VolumeWeightedMomentumState>();
594
+ private readonly meanReversionSites = new Map<string, MeanReversionState>();
595
+ private readonly marketConditionSites = new Map<string, MarketConditionState>();
596
+ private readonly orderFlowSites = new Map<string, OrderFlowState>();
597
+ private readonly liquiditySites = new Map<string, LiquidityState>();
598
+ private readonly emaCrossSites = new Map<string, { prevFast: Cell; prevSlow: Cell; count: number }>();
599
+ private readonly historySites = new Map<string, Cell[]>();
600
+ private readonly momentumFilterSites = new Map<string, { momentum: Cell[]; volume: Cell[] }>();
601
+ private readonly volatilityRegimeSites = new Map<string, Cell[]>();
462
602
 
463
603
  sma(site: string, source: Cell, period: number): Cell {
464
604
  const n = pinePeriod(period);
@@ -527,7 +667,7 @@ export class TaEngine {
527
667
  return rmaStep(st, finiteCell(source), n);
528
668
  }
529
669
 
530
- /** `ta.atr(length)` ≡ `ta.rma(ta.tr, length)`. First bar is na (no prev close). */
670
+ /** `ta.atr` ≡ `ta.rma(ta.tr, length)`. Bar 0 seeds TR with `high - low` (0.6.32). */
531
671
  atr(site: string, high: Cell, low: Cell, close: Cell, period: number): Cell {
532
672
  const n = pinePeriod(period);
533
673
  if (n === null) return null;
@@ -536,17 +676,23 @@ export class TaEngine {
536
676
  st = {
537
677
  period: n,
538
678
  prevClose: null,
679
+ started: false,
539
680
  rma: { period: n, rma: null, seeded: false, started: false, seedBuf: [] },
540
681
  };
541
682
  this.atrSites.set(site, st);
542
683
  }
543
- const prev = st.prevClose;
544
- st.prevClose = finiteCell(close);
545
- if (prev === null) return null;
546
684
  const h = finiteCell(high);
547
685
  const l = finiteCell(low);
548
686
  const c = finiteCell(close);
549
- if (h === null || l === null || c === null) return null;
687
+ if (!st.started) {
688
+ st.started = true;
689
+ st.prevClose = c;
690
+ if (h === null || l === null) return null;
691
+ return rmaStep(st.rma, h - l, n);
692
+ }
693
+ const prev = st.prevClose;
694
+ st.prevClose = c;
695
+ if (prev === null || h === null || l === null || c === null) return null;
550
696
  const tr = Math.max(h - l, Math.abs(h - prev), Math.abs(l - prev));
551
697
  return rmaStep(st.rma, tr, n);
552
698
  }
@@ -709,7 +855,7 @@ export class TaEngine {
709
855
  return best;
710
856
  }
711
857
 
712
- /** Sample stdev (ddof=1). Any na in window → na; period<=1 → na. */
858
+ /** Population stdev (ddof=0, Python `_stdev_inc_update`). Any na in window → na; period<=1 → na. */
713
859
  stdev(site: string, source: Cell, period: number): Cell {
714
860
  const n = pinePeriod(period);
715
861
  if (n === null || n <= 1) return null;
@@ -734,7 +880,7 @@ export class TaEngine {
734
880
  st.count += 1;
735
881
  }
736
882
  if (st.window.length < n || st.count !== n) return null;
737
- let v = (st.sumsq - (st.sum * st.sum) / n) / (n - 1);
883
+ let v = (st.sumsq - (st.sum * st.sum) / n) / n;
738
884
  if (v < 0) v = 0;
739
885
  return Math.sqrt(v);
740
886
  }
@@ -813,7 +959,7 @@ export class TaEngine {
813
959
  }
814
960
 
815
961
  /**
816
- * Bollinger: mid=SMA, dev=sample stdev, bands=mid ± mult*dev.
962
+ * Bollinger: mid=SMA, dev=population stdev, bands=mid ± mult*dev.
817
963
  * All null if mid or stdev null.
818
964
  */
819
965
  bb(
@@ -2076,7 +2222,7 @@ export class TaEngine {
2076
2222
  st.count += 1;
2077
2223
  }
2078
2224
  if (st.window.length < n || st.count !== n) return null;
2079
- let v = (st.sumsq - (st.sum * st.sum) / n) / (n - 1);
2225
+ let v = (st.sumsq - (st.sum * st.sum) / n) / n;
2080
2226
  if (v < 0) v = 0;
2081
2227
  return v;
2082
2228
  }
@@ -2683,9 +2829,9 @@ export class TaEngine {
2683
2829
  }
2684
2830
 
2685
2831
  /**
2686
- * Zigzag tuple `(high, low, direction)`. `_expect_list` materializes
2687
- * newest-first history, so `[-2:]` is the two oldest finite samples
2688
- * (PineSeries cap 1000), not the last two bars.
2832
+ * Zigzag tuple `(high, low, direction)`. Incremental SoT
2833
+ * `_zigzag_inc_update`: keep the last two numeric samples (`deque(maxlen=2)`),
2834
+ * skip `na` samples, and compare the threshold against their percent span.
2689
2835
  */
2690
2836
  zigzag(site: string, source: Cell, threshold: number): { high: Cell; low: Cell; dir: number } {
2691
2837
  const th = Number.isFinite(threshold) ? threshold : 5;
@@ -2694,18 +2840,15 @@ export class TaEngine {
2694
2840
  st = { samples: [] };
2695
2841
  this.zigzagSites.set(site, st);
2696
2842
  }
2697
- pushCap(st.samples, finiteCell(source), 1000);
2698
- const finite: number[] = [];
2699
- for (const v of st.samples) {
2700
- if (v !== null) finite.push(v);
2701
- }
2702
- if (finite.length < 2) return { high: null, low: null, dir: 0 };
2703
- const oldest = finite[0]!;
2704
- const second = finite[1]!;
2705
- const recentHigh = Math.max(oldest, second);
2706
- const recentLow = Math.min(oldest, second);
2843
+ const x = finiteCell(source);
2844
+ if (x !== null) pushCap(st.samples, x, 2);
2845
+ if (st.samples.length < 2) return { high: null, low: null, dir: 0 };
2846
+ const oldest = st.samples[0]!;
2847
+ const newest = st.samples[1]!;
2848
+ const recentHigh = Math.max(oldest, newest);
2849
+ const recentLow = Math.min(oldest, newest);
2707
2850
  const pct = recentLow !== 0 ? ((recentHigh - recentLow) / recentLow) * 100 : 0;
2708
- const direction = recentHigh === oldest ? 1 : -1;
2851
+ const direction = recentHigh === newest ? 1 : -1;
2709
2852
  return { high: recentHigh, low: recentLow, dir: pct > th ? 1 : direction };
2710
2853
  }
2711
2854
 
@@ -2782,31 +2925,31 @@ export class TaEngine {
2782
2925
  }
2783
2926
 
2784
2927
  /**
2785
- * `advanced.py` `_builtin_ta_acceleration_factor` (wins over economics).
2786
- * `_expect_list` is newest-first PineSeries history, cap 1000, so
2787
- * `[-period-1:]` is the oldest window — the value sticks once warm.
2928
+ * `advanced.py` `_acceleration_factor_inc_update`: last `period + 1`
2929
+ * samples (including `na`), clean numeric, compare the newest against the
2930
+ * mean of the preceding window, clamp to `[-2, 2]`.
2788
2931
  */
2789
2932
  accelerationFactor(site: string, source: Cell, period: Cell): Cell {
2790
2933
  const p = this.coerceTaInt(period, "ta.acceleration_factor() requires 2 arguments");
2791
- let samples = this.accelSites.get(site);
2934
+ if (p <= 0) return 0;
2935
+ const key = `${site}:${p}`;
2936
+ let samples = this.accelSites.get(key);
2792
2937
  if (samples === undefined) {
2793
2938
  samples = [];
2794
- this.accelSites.set(site, samples);
2939
+ this.accelSites.set(key, samples);
2795
2940
  }
2796
- pushCap(samples, finiteCell(source), 1000);
2797
- if (samples.length < p + 1 || p <= 0) return 0;
2798
- const oldest = samples.slice(0, p + 1);
2799
- const newestFirst: number[] = [];
2800
- for (let i = oldest.length - 1; i >= 0; i--) {
2801
- const v = oldest[i];
2802
- if (v !== null) newestFirst.push(v);
2941
+ pushCap(samples, finiteCell(source), p + 1);
2942
+ if (samples.length < p + 1) return 0;
2943
+ const clean: number[] = [];
2944
+ for (const v of samples) {
2945
+ if (v !== null) clean.push(v);
2803
2946
  }
2804
- if (newestFirst.length < 2) return 0;
2947
+ if (clean.length < 2) return 0;
2805
2948
  let oldSum = 0;
2806
- for (let i = 0; i < newestFirst.length - 1; i++) oldSum += newestFirst[i]!;
2807
- const oldMom = oldSum / (newestFirst.length - 1);
2949
+ for (let i = 0; i < clean.length - 1; i++) oldSum += clean[i]!;
2950
+ const oldMom = oldSum / (clean.length - 1);
2951
+ const newMom = clean[clean.length - 1]!;
2808
2952
  if (Math.abs(oldMom) < 1e-10) return 0;
2809
- const newMom = newestFirst[newestFirst.length - 1]!;
2810
2953
  let acc = (newMom - oldMom) / oldMom;
2811
2954
  if (acc < -2) acc = -2;
2812
2955
  if (acc > 2) acc = 2;
@@ -2814,55 +2957,162 @@ export class TaEngine {
2814
2957
  }
2815
2958
 
2816
2959
  /**
2817
- * Bar mode sees a 1-element OHLC list (`oscillators.py` `_builtin_ta_stoch_smooth`).
2818
- * Raw %K is the current bar; `_ema` is `na` unless both smooth lengths are 1.
2819
- * Period `<= 0` makes `max()` of an empty window raise.
2960
+ * `core.py` `_stoch_smooth_inc_update`: raw %K over the last `period`
2961
+ * highs/lows (all must be numeric), then EMA(%K, smooth_k) and
2962
+ * EMA(that, smooth_d) with SMA-seeded EMA state.
2820
2963
  */
2821
- stochSmooth(high: Cell, low: Cell, close: Cell, period: Cell, smoothK: Cell, smoothD: Cell): Cell {
2964
+ stochSmooth(
2965
+ site: string,
2966
+ high: Cell,
2967
+ low: Cell,
2968
+ close: Cell,
2969
+ period: Cell,
2970
+ smoothK: Cell,
2971
+ smoothD: Cell,
2972
+ ): Cell {
2822
2973
  const p = this.coerceTaInt(period, "ta.stoch_smooth period must be integer");
2823
2974
  const k = this.coerceTaInt(smoothK, "ta.stoch_smooth smooth_k must be integer");
2824
2975
  const d = this.coerceTaInt(smoothD, "ta.stoch_smooth smooth_d must be integer");
2825
- if (p <= 0) throw new Error("max() iterable argument is empty");
2826
- if (high === null || low === null || close === null) {
2827
- throw new Error("unsupported operand type(s) for /: 'float' and 'NoneType'");
2976
+ if (p < 1 || k < 1 || d < 1) return null;
2977
+ const key = `${site}:${p}:${k}:${d}`;
2978
+ let st = this.stochSmoothSites.get(key);
2979
+ if (st === undefined) {
2980
+ st = { highs: [], lows: [] };
2981
+ this.stochSmoothSites.set(key, st);
2828
2982
  }
2829
- if (k !== 1 || d !== 1) return null;
2830
- const span = high - low;
2831
- const stoch = span !== 0 ? (100 * (close - low)) / span : 50;
2832
- return finiteOut(stoch);
2983
+ pushCap(st.highs, finiteCell(high), p);
2984
+ pushCap(st.lows, finiteCell(low), p);
2985
+ const c = finiteCell(close);
2986
+ let stoch: Cell = null;
2987
+ if (c !== null && !st.highs.includes(null) && !st.lows.includes(null)) {
2988
+ let hh = -Infinity;
2989
+ let ll = Infinity;
2990
+ for (const v of st.highs) {
2991
+ if (v! > hh) hh = v!;
2992
+ }
2993
+ for (const v of st.lows) {
2994
+ if (v! < ll) ll = v!;
2995
+ }
2996
+ stoch = hh === ll ? 50 : (100 * (c - ll)) / (hh - ll);
2997
+ }
2998
+ const kVal = this.ema(`${site}:k`, stoch, k);
2999
+ return this.ema(`${site}:d`, kVal, d);
2833
3000
  }
2834
3001
 
2835
3002
  /**
2836
- * Bar-mode OHLC is a PineSeries, not a list (`len < 2`). Sentinel every bar
2837
- * (`patterns.py` `_builtin_ta_engulfing`) — a lookback port would disagree.
3003
+ * `core.py` `_engulfing_inc_update`: last two opens/closes; warmup is two
3004
+ * bars. High/low are still required by the caller but unused.
2838
3005
  */
2839
- engulfing(): TaRecord {
2840
- return taRec({ is_bullish: false, is_bearish: false, pattern_strength: 0 });
3006
+ engulfing(site: string, open: Cell, close: Cell): TaRecord {
3007
+ const off = taRec({ is_bullish: false, is_bearish: false, pattern_strength: 0 });
3008
+ let st = this.engulfingSites.get(site);
3009
+ if (st === undefined) {
3010
+ st = { prevOpen: null, prevClose: null, count: 0 };
3011
+ this.engulfingSites.set(site, st);
3012
+ }
3013
+ const o = finiteCell(open);
3014
+ const c = finiteCell(close);
3015
+ st.count += 1;
3016
+ const prevO = st.prevOpen;
3017
+ const prevC = st.prevClose;
3018
+ if (st.count < 2 || prevO === null || prevC === null || o === null || c === null) {
3019
+ st.prevOpen = o;
3020
+ st.prevClose = c;
3021
+ return off;
3022
+ }
3023
+ const isBullish = o < prevC && c > prevO && c > o;
3024
+ const isBearish = o > prevC && c < prevO && c < o;
3025
+ let engulf = 0;
3026
+ if (isBullish) {
3027
+ engulf = Math.max(0, Math.min(1, (c - prevO) / Math.abs(prevO - prevC + 0.0001)));
3028
+ } else if (isBearish) {
3029
+ engulf = Math.max(0, Math.min(1, (prevO - c) / Math.abs(prevC - prevO + 0.0001)));
3030
+ }
3031
+ st.prevOpen = o;
3032
+ st.prevClose = c;
3033
+ return taRec({
3034
+ is_bullish: isBullish,
3035
+ is_bearish: isBearish,
3036
+ pattern_strength: finiteOut(engulf) ?? 0,
3037
+ });
2841
3038
  }
2842
3039
 
2843
- /** Same one-element series: `advanced.py` `_builtin_ta_inside_bar_pattern` is always false. */
2844
- insideBarPattern(): boolean {
2845
- return false;
3040
+ /** `core.py` `_inside_bar_pattern_inc_update`: last two highs/lows. */
3041
+ insideBarPattern(site: string, high: Cell, low: Cell): boolean {
3042
+ let st = this.insideBarSites.get(site);
3043
+ if (st === undefined) {
3044
+ st = { prevHigh: null, prevLow: null, count: 0 };
3045
+ this.insideBarSites.set(site, st);
3046
+ }
3047
+ const currH = high !== null && Number.isFinite(high) ? high : 0;
3048
+ const currL = low !== null && Number.isFinite(low) ? low : 0;
3049
+ st.count += 1;
3050
+ const prevH = st.prevHigh;
3051
+ const prevL = st.prevLow;
3052
+ if (st.count < 2 || prevH === null || prevL === null) {
3053
+ st.prevHigh = currH;
3054
+ st.prevLow = currL;
3055
+ return false;
3056
+ }
3057
+ const val = currH < prevH && currL > prevL;
3058
+ st.prevHigh = currH;
3059
+ st.prevLow = currL;
3060
+ return val;
2846
3061
  }
2847
3062
 
2848
3063
  /**
2849
- * One-element high/low never fills the window (`patterns.py`
2850
- * `_builtin_ta_double_top_bottom`). Any finite period is the none-sentinel;
2851
- * `na` period still raises.
3064
+ * `core.py` `_double_top_bottom_inc_update`: last `period` highs/lows,
3065
+ * local-maxima peaks seeded with the first numeric high. `na` period raises.
2852
3066
  */
2853
- doubleTopBottom(period: Cell): TaRecord {
2854
- if (period === null || !Number.isFinite(period)) {
2855
- throw new Error("period must be integer. Got: na");
3067
+ doubleTopBottom(site: string, high: Cell, low: Cell, period: Cell): TaRecord {
3068
+ const n = this.coerceTaInt(period, "period must be integer");
3069
+ const off = taRec({ pattern_type: "none", strength: 0, breakout_level: 0 });
3070
+ if (n < 1) return off;
3071
+ const key = `${site}:${n}`;
3072
+ let st = this.doubleTopBottomSites.get(key);
3073
+ if (st === undefined) {
3074
+ st = { highs: [], lows: [], count: 0 };
3075
+ this.doubleTopBottomSites.set(key, st);
3076
+ }
3077
+ pushCap(st.highs, finiteCell(high), n);
3078
+ pushCap(st.lows, finiteCell(low), n);
3079
+ st.count += 1;
3080
+ if (st.count < n) return off;
3081
+ const recentHigh: number[] = [];
3082
+ for (const v of st.highs) {
3083
+ if (v !== null) recentHigh.push(v);
3084
+ }
3085
+ const recentLow: number[] = [];
3086
+ for (const v of st.lows) {
3087
+ if (v !== null) recentLow.push(v);
3088
+ }
3089
+ if (recentHigh.length < 3 || recentLow.length === 0) return off;
3090
+ const peaks = [recentHigh[0]!];
3091
+ for (let i = 1; i < recentHigh.length - 1; i++) {
3092
+ if (recentHigh[i]! > recentHigh[i - 1]! && recentHigh[i]! > recentHigh[i + 1]!) {
3093
+ peaks.push(recentHigh[i]!);
3094
+ }
3095
+ }
3096
+ if (peaks.length < 2) return off;
3097
+ const peakDiff = Math.abs(peaks[peaks.length - 1]! - peaks[peaks.length - 2]!);
3098
+ const avgPeak = (peaks[peaks.length - 1]! + peaks[peaks.length - 2]!) / 2;
3099
+ const strength = avgPeak > 0 ? 1 - Math.min(1, peakDiff / avgPeak) : 0;
3100
+ let minLow = recentLow[0]!;
3101
+ for (const v of recentLow) {
3102
+ if (v < minLow) minLow = v;
2856
3103
  }
2857
- return taRec({ pattern_type: "none", strength: 0, breakout_level: 0 });
3104
+ return taRec({
3105
+ pattern_type: "double_top",
3106
+ strength: finiteOut(strength) ?? 0,
3107
+ breakout_level: finiteOut(minLow - avgPeak * 0.1) ?? 0,
3108
+ });
2858
3109
  }
2859
3110
 
2860
- // `_expect_list` materializes PineSeries history newest-first (cap 1000).
2861
- // Negative indexes are the oldest samples, not the latest bars.
2862
- private readonly cumDeltaSites = new Map<string, { close: Cell[]; volume: Cell[] }>();
2863
- private readonly volMomSites = new Map<string, { volume: Cell[] }>();
3111
+ // `core.py` incremental call-site state for the wave10 detectors.
3112
+ private readonly cumDeltaSites = new Map<string, CumulativeDeltaState>();
3113
+ private readonly volMomSites = new Map<string, VolumeMomentumState>();
2864
3114
  private readonly volProfileLowSites = new Map<string, { close: Cell[]; volume: Cell[] }>();
2865
- private readonly volThrustSites = new Map<string, { close: Cell[]; volume: Cell[]; sma: Cell[] }>();
3115
+ private readonly volThrustSites = new Map<string, VolumeThrustState>();
2866
3116
 
2867
3117
  private asCount(value: number): number | null {
2868
3118
  if (!Number.isFinite(value)) return null;
@@ -2881,6 +3131,55 @@ export class TaEngine {
2881
3131
  return v === undefined ? null : v;
2882
3132
  }
2883
3133
 
3134
+ /**
3135
+ * Python `_expect_list(PineSeries)` history: newest-first, capped at 1000.
3136
+ * `h[0]` is the current bar, `h[h.length - 1]` the oldest retained sample.
3137
+ */
3138
+ private hist(site: string, value: Cell): Cell[] {
3139
+ let h = this.historySites.get(site);
3140
+ if (h === undefined) {
3141
+ h = [];
3142
+ this.historySites.set(site, h);
3143
+ }
3144
+ h.unshift(finiteCell(value));
3145
+ if (h.length > 1000) h.pop();
3146
+ return h;
3147
+ }
3148
+
3149
+ /**
3150
+ * Python `_expect_list` input: a Pine array is used as-is (last element is
3151
+ * the `[-1]` index); a series sample appends to the newest-first history.
3152
+ */
3153
+ private windowFor(site: string, value: unknown): Cell[] {
3154
+ if (Array.isArray(value)) {
3155
+ return value.map((v) => (typeof v === "number" ? finiteCell(v) : null));
3156
+ }
3157
+ if (
3158
+ value != null &&
3159
+ typeof value === "object" &&
3160
+ typeof (value as { size?: unknown }).size === "function" &&
3161
+ typeof (value as { get?: unknown }).get === "function"
3162
+ ) {
3163
+ const arr = value as { size: () => number; get: (i: number) => unknown };
3164
+ const out: Cell[] = [];
3165
+ const n = arr.size();
3166
+ for (let i = 0; i < n; i++) {
3167
+ const v = arr.get(i);
3168
+ out.push(typeof v === "number" ? finiteCell(v) : null);
3169
+ }
3170
+ return out;
3171
+ }
3172
+ return this.hist(site, typeof value === "number" ? value : null);
3173
+ }
3174
+
3175
+ private static oldest(h: Cell[]): Cell {
3176
+ return h.length === 0 ? null : h[h.length - 1]!;
3177
+ }
3178
+
3179
+ private static secondOldest(h: Cell[]): Cell {
3180
+ return h.length < 2 ? null : h[h.length - 2]!;
3181
+ }
3182
+
2884
3183
  /** `ta.voi` — non-numeric inputs are 0 (`volume.py`). */
2885
3184
  voi(buy: Cell, sell: Cell): Cell {
2886
3185
  const b = buy !== null && Number.isFinite(buy) ? buy : 0;
@@ -2918,92 +3217,84 @@ export class TaEngine {
2918
3217
  }
2919
3218
 
2920
3219
  /**
2921
- * `ta.rsi_divergence` — last element of the list Python returns.
2922
- * A scalar is a one-element series (`i < period` → 0). Do not accumulate.
3220
+ * `core.py` `_rsi_divergence_inc_update`: last `period + 1` RSI samples,
3221
+ * scaled to `[-1, 1]`. Warmup (`count <= period`) and degenerate windows
3222
+ * return `0`.
2923
3223
  */
2924
- rsiDivergence(series: Cell[], period: number): Cell {
3224
+ rsiDivergence(site: string, value: Cell, period: number): Cell {
2925
3225
  const p = this.asCount(period);
2926
- if (p === null || series.length === 0) return null;
2927
- let last = 0;
2928
- for (let i = 0; i < series.length; i++) {
2929
- if (i < p) {
2930
- last = 0;
2931
- continue;
2932
- }
2933
- const start = Math.max(0, i - p);
2934
- const vals: number[] = [];
2935
- for (let j = start; j <= i; j++) {
2936
- const v = series[j];
2937
- if (v !== null && Number.isFinite(v)) vals.push(v);
2938
- }
2939
- if (vals.length < 2) {
2940
- last = 0;
2941
- continue;
2942
- }
2943
- let rsiMin = vals[0]!;
2944
- let rsiMax = vals[0]!;
2945
- for (const v of vals) {
2946
- if (v < rsiMin) rsiMin = v;
2947
- if (v > rsiMax) rsiMax = v;
2948
- }
2949
- const cur = series[i];
2950
- const range = rsiMax - rsiMin;
2951
- if (cur === null || !Number.isFinite(cur) || range <= 0) {
2952
- last = 0;
2953
- continue;
2954
- }
2955
- last = ((cur - rsiMin) / range) * 2 - 1;
3226
+ if (p === null || p < 1) return 0;
3227
+ let st = this.rsiDivergenceSites.get(`${site}:${p}`);
3228
+ if (st === undefined) {
3229
+ st = { window: [], count: 0 };
3230
+ this.rsiDivergenceSites.set(`${site}:${p}`, st);
3231
+ }
3232
+ const x = finiteCell(value);
3233
+ pushCap(st.window, x, p + 1);
3234
+ st.count += 1;
3235
+ if (st.count <= p) return 0;
3236
+ const vals: number[] = [];
3237
+ for (const v of st.window) {
3238
+ if (v !== null) vals.push(v);
2956
3239
  }
2957
- return finiteOut(last);
3240
+ if (vals.length < 2 || x === null) return 0;
3241
+ let lo = vals[0]!;
3242
+ let hi = vals[0]!;
3243
+ for (const v of vals) {
3244
+ if (v < lo) lo = v;
3245
+ if (v > hi) hi = v;
3246
+ }
3247
+ const range = hi - lo;
3248
+ return range > 0 ? finiteOut(((x - lo) / range) * 2 - 1) : 0;
2958
3249
  }
2959
3250
 
2960
- /** `ta.cumulative_delta` — oldest `period` bars (`advanced.py`). */
3251
+ /** `core.py` `_cumulative_delta_inc_update`: signed volume over the last `period` bars. */
2961
3252
  cumulativeDelta(site: string, close: Cell, volume: Cell, period: number): Cell {
2962
3253
  const n = this.asCount(period);
2963
- let st = this.cumDeltaSites.get(site);
3254
+ if (n === null || n <= 0) return 0;
3255
+ let st = this.cumDeltaSites.get(`${site}:${n}`);
2964
3256
  if (st === undefined) {
2965
- st = { close: [], volume: [] };
2966
- this.cumDeltaSites.set(site, st);
3257
+ st = { closes: [], volumes: [], count: 0 };
3258
+ this.cumDeltaSites.set(`${site}:${n}`, st);
2967
3259
  }
2968
- this.pushChrono(st.close, close);
2969
- this.pushChrono(st.volume, volume);
2970
- if (n === null || n <= 0 || st.close.length < n || st.volume.length < n) return 0;
3260
+ const c = finiteCell(close);
3261
+ const v = volume !== null && Number.isFinite(volume) ? volume : 0;
3262
+ pushCap(st.closes, c, n + 1);
3263
+ pushCap(st.volumes, v, n);
3264
+ st.count += 1;
3265
+ if (st.count < n) return 0;
2971
3266
  let delta = 0;
2972
- for (let i = -n; i < 0; i++) {
2973
- const cRaw = this.nf(st.close, i);
2974
- const vRaw = this.nf(st.volume, i);
2975
- const c = cRaw !== null && Number.isFinite(cRaw) ? cRaw : 0;
2976
- const v = vRaw !== null && Number.isFinite(vRaw) ? vRaw : 0;
2977
- if (i > -n) {
2978
- const prevRaw = this.nf(st.close, i - 1);
2979
- const prev = prevRaw !== null && Number.isFinite(prevRaw) ? prevRaw : c;
2980
- if (c > prev) delta += v;
2981
- else if (c < prev) delta -= v;
2982
- }
3267
+ const offset = st.closes.length - st.volumes.length;
3268
+ for (let j = 0; j < st.volumes.length; j++) {
3269
+ const idx = offset + j;
3270
+ const cRaw = st.closes[idx] ?? null;
3271
+ const cur = cRaw ?? 0;
3272
+ const prevRaw = idx === 0 ? null : (st.closes[idx - 1] ?? null);
3273
+ const prev = prevRaw ?? cur;
3274
+ const vol = st.volumes[j]!;
3275
+ if (cur > prev) delta += vol;
3276
+ else if (cur < prev) delta -= vol;
2983
3277
  }
2984
3278
  return finiteOut(delta);
2985
3279
  }
2986
3280
 
2987
- /** `ta.volume_momentum` — oldest `period + 1` volumes (`advanced.py`). */
3281
+ /** `core.py` `_volume_momentum_inc_update`: ROC of the period SMA of volume. */
2988
3282
  volumeMomentum(site: string, volume: Cell, period: number): Cell {
2989
3283
  const n = this.asCount(period);
2990
- let st = this.volMomSites.get(site);
3284
+ if (n === null || n <= 0) return 0;
3285
+ let st = this.volMomSites.get(`${site}:${n}`);
2991
3286
  if (st === undefined) {
2992
- st = { volume: [] };
2993
- this.volMomSites.set(site, st);
3287
+ st = { window: [] };
3288
+ this.volMomSites.set(`${site}:${n}`, st);
2994
3289
  }
2995
- this.pushChrono(st.volume, volume);
2996
- if (n === null || n <= 0 || st.volume.length < n + 1) return 0;
2997
- const clean: number[] = [];
2998
- for (let i = 0; i < n + 1; i++) {
2999
- const v = st.volume[i];
3000
- if (v !== null && Number.isFinite(v)) clean.push(v);
3001
- }
3002
- if (clean.length < 2) return 0;
3003
- const newVol = clean[0]!;
3004
- let sum = 0;
3005
- for (let i = 1; i < clean.length; i++) sum += clean[i]!;
3006
- const oldVol = sum / (clean.length - 1);
3290
+ if (volume !== null && Number.isFinite(volume)) pushCap(st.window, volume, n + 1);
3291
+ if (st.window.length < n + 1) return 0;
3292
+ let oldSum = 0;
3293
+ for (let i = 0; i < n; i++) oldSum += st.window[i]!;
3294
+ let newSum = 0;
3295
+ for (let i = 1; i <= n; i++) newSum += st.window[i]!;
3296
+ const oldVol = oldSum / n;
3297
+ const newVol = newSum / n;
3007
3298
  if (oldVol === 0) return 0;
3008
3299
  let momentum = ((newVol - oldVol) / oldVol) * 100;
3009
3300
  if (momentum > 100) momentum = 100;
@@ -3011,6 +3302,1224 @@ export class TaEngine {
3011
3302
  return finiteOut(momentum);
3012
3303
  }
3013
3304
 
3305
+ /** `moving_averages.py` `_sma_weighted_inc_update`: linear / quadratic / sqrt weights. */
3306
+ smaWeighted(site: string, source: Cell, period: Cell, weightType: unknown): Cell {
3307
+ const n = this.coerceTaInt(period, "period must be integer");
3308
+ const kind = typeof weightType === "string" ? weightType : "linear";
3309
+ if (n < 1) return null;
3310
+ const key = `${site}:${n}:${kind}`;
3311
+ let st = this.smaWeightedSites.get(key);
3312
+ if (st === undefined) {
3313
+ const weights: number[] = [];
3314
+ for (let i = 0; i < n; i++) {
3315
+ if (kind === "quadratic") weights.push((i + 1) ** 2);
3316
+ else if (kind === "sqrt") weights.push((i + 1) ** 0.5);
3317
+ else weights.push(i + 1);
3318
+ }
3319
+ st = { window: [], weights, wsum: weights.reduce((a, b) => a + b, 0) };
3320
+ this.smaWeightedSites.set(key, st);
3321
+ }
3322
+ pushCap(st.window, finiteCell(source), n);
3323
+ if (st.window.length < n || st.window.includes(null) || st.wsum <= 0) return null;
3324
+ let total = 0;
3325
+ for (let i = 0; i < n; i++) total += st.window[i]! * st.weights[i]!;
3326
+ return finiteOut(total / st.wsum);
3327
+ }
3328
+
3329
+ /** `volume.py` `_apo_inc_update`: `EMA(fast) - EMA(slow)`. */
3330
+ apo(site: string, source: Cell, fast: Cell, slow: Cell): Cell {
3331
+ const f = this.coerceTaInt(fast, "ta.apo fast_period must be integer");
3332
+ const s = this.coerceTaInt(slow, "ta.apo slow_period must be integer");
3333
+ const fastV = this.ema(`${site}:fast`, source, f);
3334
+ const slowV = this.ema(`${site}:slow`, source, s);
3335
+ if (fastV === null || slowV === null) return null;
3336
+ return finiteOut(fastV - slowV);
3337
+ }
3338
+
3339
+ /** `volatility.py` `_atr_normalized_inc_update`: ATR as a percent of close. */
3340
+ atrNormalized(site: string, high: Cell, low: Cell, close: Cell, period: Cell): Cell {
3341
+ const n = this.coerceTaInt(period, "period must be integer");
3342
+ const atr = this.atr(`${site}:atr`, high, low, close, n);
3343
+ const c = finiteCell(close);
3344
+ if (c === null || c === 0 || atr === null) return null;
3345
+ return finiteOut((atr / c) * 100);
3346
+ }
3347
+
3348
+ private pairWindow(
3349
+ sites: Map<string, PairWindowState>,
3350
+ site: string,
3351
+ a: Cell,
3352
+ b: Cell,
3353
+ length: Cell,
3354
+ ): { a: number[]; b: number[] } | null {
3355
+ const n = this.coerceTaInt(length, "length must be integer");
3356
+ if (n < 1) return null;
3357
+ const key = `${site}:${n}`;
3358
+ let st = sites.get(key);
3359
+ if (st === undefined) {
3360
+ st = { a: [], b: [] };
3361
+ sites.set(key, st);
3362
+ }
3363
+ pushCap(st.a, finiteCell(a), n);
3364
+ pushCap(st.b, finiteCell(b), n);
3365
+ if (st.a.length < n || st.b.length < n) return null;
3366
+ const left: number[] = [];
3367
+ const right: number[] = [];
3368
+ for (let i = 0; i < n; i++) {
3369
+ const av = st.a[i]!;
3370
+ const bv = st.b[i]!;
3371
+ if (av === null || bv === null) return null;
3372
+ left.push(av);
3373
+ right.push(bv);
3374
+ }
3375
+ return { a: left, b: right };
3376
+ }
3377
+
3378
+ /** `volatility.py` `_beta_inc_update`: `cov(a,b) / var(b)`. */
3379
+ beta(site: string, a: Cell, b: Cell, length: Cell): Cell {
3380
+ const pair = this.pairWindow(this.betaSites, site, a, b, length);
3381
+ if (pair === null) return null;
3382
+ const n = pair.a.length;
3383
+ let meanA = 0;
3384
+ let meanB = 0;
3385
+ for (let i = 0; i < n; i++) {
3386
+ meanA += pair.a[i]!;
3387
+ meanB += pair.b[i]!;
3388
+ }
3389
+ meanA /= n;
3390
+ meanB /= n;
3391
+ let covariance = 0;
3392
+ let varianceB = 0;
3393
+ for (let i = 0; i < n; i++) {
3394
+ covariance += (pair.a[i]! - meanA) * (pair.b[i]! - meanB);
3395
+ varianceB += (pair.b[i]! - meanB) ** 2;
3396
+ }
3397
+ covariance /= n;
3398
+ varianceB /= n;
3399
+ if (varianceB === 0) return 0;
3400
+ return finiteOut(covariance / varianceB);
3401
+ }
3402
+
3403
+ /** `volatility.py` `_r_squared_inc_update`: `corr(a,b)^2` clipped to [0, 1]. */
3404
+ rSquared(site: string, a: Cell, b: Cell, length: Cell): Cell {
3405
+ const pair = this.pairWindow(this.rSquaredSites, site, a, b, length);
3406
+ if (pair === null) return null;
3407
+ const n = pair.a.length;
3408
+ let meanA = 0;
3409
+ let meanB = 0;
3410
+ for (let i = 0; i < n; i++) {
3411
+ meanA += pair.a[i]!;
3412
+ meanB += pair.b[i]!;
3413
+ }
3414
+ meanA /= n;
3415
+ meanB /= n;
3416
+ let covariance = 0;
3417
+ let varA = 0;
3418
+ let varB = 0;
3419
+ for (let i = 0; i < n; i++) {
3420
+ covariance += (pair.a[i]! - meanA) * (pair.b[i]! - meanB);
3421
+ varA += (pair.a[i]! - meanA) ** 2;
3422
+ varB += (pair.b[i]! - meanB) ** 2;
3423
+ }
3424
+ covariance /= n;
3425
+ varA /= n;
3426
+ varB /= n;
3427
+ if (varA === 0 || varB === 0) return 0;
3428
+ const corr = covariance / Math.sqrt(varA * varB);
3429
+ return finiteOut(Math.max(0, Math.min(1, corr ** 2)));
3430
+ }
3431
+
3432
+ /** `volatility.py` `_comovement_inc_update`: percent of same-sign bar deltas. */
3433
+ comovement(site: string, a: Cell, b: Cell, length: Cell): Cell {
3434
+ const pair = this.pairWindow(this.comovementSites, site, a, b, length);
3435
+ if (pair === null) return null;
3436
+ const n = pair.a.length;
3437
+ if (n <= 1) return 0;
3438
+ let same = 0;
3439
+ for (let i = 1; i < n; i++) {
3440
+ if ((pair.a[i]! - pair.a[i - 1]!) * (pair.b[i]! - pair.b[i - 1]!) > 0) same += 1;
3441
+ }
3442
+ return finiteOut((same / (n - 1)) * 100);
3443
+ }
3444
+
3445
+ /** `common.py` `_skewness_inc_update`: population third central moment / std³. */
3446
+ skewness(site: string, source: Cell, period: Cell): Cell {
3447
+ const n = this.coerceTaInt(period, "period must be integer");
3448
+ if (n < 1) return null;
3449
+ const key = `${site}:${n}`;
3450
+ let st = this.skewnessSites.get(key);
3451
+ if (st === undefined) {
3452
+ st = { window: [] };
3453
+ this.skewnessSites.set(key, st);
3454
+ }
3455
+ pushCap(st.window, finiteCell(source), n);
3456
+ if (st.window.length < n || st.window.includes(null)) return null;
3457
+ const vals = st.window as number[];
3458
+ let mean = 0;
3459
+ for (const v of vals) mean += v;
3460
+ mean /= n;
3461
+ let variance = 0;
3462
+ for (const v of vals) variance += (v - mean) ** 2;
3463
+ variance /= n;
3464
+ const std = Math.sqrt(variance);
3465
+ if (std === 0) return 0;
3466
+ let third = 0;
3467
+ for (const v of vals) third += (v - mean) ** 3;
3468
+ return finiteOut(third / (n * std ** 3));
3469
+ }
3470
+
3471
+ /** `common.py` `_kurtosis_inc_update`: excess kurtosis. */
3472
+ kurtosis(site: string, source: Cell, period: Cell): Cell {
3473
+ const n = this.coerceTaInt(period, "period must be integer");
3474
+ if (n < 1) return null;
3475
+ const key = `${site}:${n}`;
3476
+ let st = this.kurtosisSites.get(key);
3477
+ if (st === undefined) {
3478
+ st = { window: [] };
3479
+ this.kurtosisSites.set(key, st);
3480
+ }
3481
+ pushCap(st.window, finiteCell(source), n);
3482
+ if (st.window.length < n || st.window.includes(null)) return null;
3483
+ const vals = st.window as number[];
3484
+ let mean = 0;
3485
+ for (const v of vals) mean += v;
3486
+ mean /= n;
3487
+ let variance = 0;
3488
+ for (const v of vals) variance += (v - mean) ** 2;
3489
+ variance /= n;
3490
+ const std = Math.sqrt(variance);
3491
+ if (std === 0) return 0;
3492
+ let fourth = 0;
3493
+ for (const v of vals) fourth += (v - mean) ** 4;
3494
+ fourth /= n;
3495
+ return finiteOut(fourth / std ** 4 - 3);
3496
+ }
3497
+
3498
+ /** `volume.py` `_volume_weighted_momentum_inc_update`. */
3499
+ volumeWeightedMomentum(site: string, source: Cell, volume: Cell, length: Cell): Cell {
3500
+ const n = this.coerceTaInt(length, "length must be integer");
3501
+ if (n < 1) return null;
3502
+ const key = `${site}:${n}`;
3503
+ let st = this.vwmSites.get(key);
3504
+ if (st === undefined) {
3505
+ st = { prices: [], volumes: [] };
3506
+ this.vwmSites.set(key, st);
3507
+ }
3508
+ pushCap(st.prices, finiteCell(source), n + 1);
3509
+ pushCap(st.volumes, finiteCell(volume), n);
3510
+ if (st.volumes.length < n) return null;
3511
+ const ps = st.prices;
3512
+ const vs = st.volumes;
3513
+ let momentumSum = 0;
3514
+ let volumeSum = 0;
3515
+ for (let i = 0; i < n; i++) {
3516
+ const pEnd = ps.length - 1 - i;
3517
+ const pPrev = pEnd - 1;
3518
+ if (pPrev < 0) break;
3519
+ const pv = ps[pEnd] ?? null;
3520
+ const pp = ps[pPrev] ?? null;
3521
+ if (pv === null || pp === null) return null;
3522
+ const vol = vs[vs.length - 1 - i] ?? 0;
3523
+ momentumSum += (pv - pp) * vol;
3524
+ volumeSum += vol;
3525
+ }
3526
+ return finiteOut(volumeSum > 0 ? momentumSum / volumeSum : 0);
3527
+ }
3528
+
3529
+ /** `common.py` `_parkinson`: volatility from the high/low ratio. */
3530
+ parkinson(high: Cell, low: Cell): Cell {
3531
+ const h = finiteCell(high);
3532
+ const l = finiteCell(low);
3533
+ if (h === null || l === null || h <= l) return null;
3534
+ const ratio = h / l;
3535
+ if (ratio <= 0) return null;
3536
+ return finiteOut(Math.sqrt(Math.log(ratio) ** 2 / (4 * Math.log(2))));
3537
+ }
3538
+
3539
+ /** `common.py` `_garman_klass`: OHLC volatility. */
3540
+ garmanKlass(high: Cell, low: Cell, close: Cell, open: Cell): Cell {
3541
+ const h = finiteCell(high);
3542
+ const l = finiteCell(low);
3543
+ const c = finiteCell(close);
3544
+ const o = finiteCell(open);
3545
+ if (h === null || l === null || c === null || o === null) return null;
3546
+ if (h <= l || h <= 0 || c <= 0) return null;
3547
+ const term1 = 0.5 * Math.log(h / l) ** 2;
3548
+ const term2 = (2 * Math.log(2) - 1) * Math.log(c / o) ** 2;
3549
+ return finiteOut(Math.sqrt(term1 - term2));
3550
+ }
3551
+
3552
+ /** `core.py` `_mean_reversion_score_inc_update`: period is a warmup gate. */
3553
+ meanReversionScore(site: string, close: Cell, sma: Cell, stdev: Cell, period: Cell): number {
3554
+ const n = this.coerceTaInt(period, "ta.mean_reversion_score() requires 4 arguments");
3555
+ if (n <= 0) return 50;
3556
+ let st = this.meanReversionSites.get(`${site}:${n}`);
3557
+ if (st === undefined) {
3558
+ st = { countClose: 0, countSma: 0 };
3559
+ this.meanReversionSites.set(`${site}:${n}`, st);
3560
+ }
3561
+ st.countClose += 1;
3562
+ st.countSma += 1;
3563
+ if (st.countClose < n || st.countSma < n) return 50;
3564
+ const c = finiteCell(close) ?? 100;
3565
+ const s = finiteCell(sma) ?? 100;
3566
+ const sd = finiteCell(stdev) ?? 1;
3567
+ if (sd === 0) return 50;
3568
+ return Math.max(0, Math.min(100, Math.min(100, (Math.abs(c - s) / sd) * 20)));
3569
+ }
3570
+
3571
+ /** `core.py` `_market_condition_inc_update`. */
3572
+ marketCondition(site: string, close: Cell, atr: Cell, smaPeriod: Cell, stdevPeriod: Cell): string {
3573
+ const smaN = this.coerceTaInt(smaPeriod, "sma period must be integer");
3574
+ const stdevN = this.coerceTaInt(stdevPeriod, "stdev period must be integer");
3575
+ if (smaN <= 0 || stdevN <= 0) return "ranging";
3576
+ const win = Math.max(smaN, stdevN);
3577
+ const key = `${site}:${smaN}:${stdevN}`;
3578
+ let st = this.marketConditionSites.get(key);
3579
+ if (st === undefined) {
3580
+ st = { window: [], atr: null };
3581
+ this.marketConditionSites.set(key, st);
3582
+ }
3583
+ pushCap(st.window, finiteCell(close), win);
3584
+ st.atr = finiteCell(atr);
3585
+ if (st.window.length < win) return "ranging";
3586
+ const cLast = st.window[st.window.length - 1] ?? null;
3587
+ const currentClose = cLast ?? 0;
3588
+ const currentAtr = st.atr ?? 1;
3589
+ const currentSma = smaOf(st.window, smaN) ?? currentClose;
3590
+ const stdevVal = stdevOf(st.window, stdevN);
3591
+ if (stdevVal !== null && stdevVal > currentAtr * 1.5) return "volatile";
3592
+ if (currentClose > currentSma && currentAtr > 0.5) return "trending_up";
3593
+ if (currentClose < currentSma && currentAtr > 0.5) return "trending_down";
3594
+ return "ranging";
3595
+ }
3596
+
3597
+ /** `core.py` `_order_flow_imbalance_inc_update`. */
3598
+ orderFlowImbalance(
3599
+ site: string,
3600
+ high: Cell,
3601
+ low: Cell,
3602
+ close: Cell,
3603
+ volume: Cell,
3604
+ period: Cell,
3605
+ ): number {
3606
+ const n = this.coerceTaInt(period, "period must be integer");
3607
+ if (n <= 0) return 0;
3608
+ const key = `${site}:${n}`;
3609
+ let st = this.orderFlowSites.get(key);
3610
+ if (st === undefined) {
3611
+ st = { highs: [], lows: [], closes: [], volumes: [] };
3612
+ this.orderFlowSites.set(key, st);
3613
+ }
3614
+ pushCap(st.highs, finiteCell(high), n);
3615
+ pushCap(st.lows, finiteCell(low), n);
3616
+ pushCap(st.closes, finiteCell(close), n);
3617
+ pushCap(st.volumes, finiteCell(volume), n);
3618
+ if (st.highs.length < n || st.lows.length < n) return 0;
3619
+ let buy = 0;
3620
+ let sell = 0;
3621
+ for (let i = 0; i < n; i++) {
3622
+ const h = st.highs[i] ?? 0;
3623
+ const l = st.lows[i] ?? 0;
3624
+ const c = st.closes[i] ?? 0;
3625
+ const v = st.volumes[i] ?? 0;
3626
+ if (h > l) {
3627
+ const mid = (h + l) / 2;
3628
+ if (c > mid) buy += v;
3629
+ else sell += v;
3630
+ }
3631
+ }
3632
+ const total = buy + sell;
3633
+ if (total === 0) return 0;
3634
+ return finiteOut((buy - sell) / total) ?? 0;
3635
+ }
3636
+
3637
+ /** `core.py` `_liquidity_score_inc_update`. */
3638
+ liquidityScore(site: string, volume: Cell, volatility: Cell, spread: Cell, period: Cell): number {
3639
+ const n = this.coerceTaInt(period, "period must be integer");
3640
+ if (n <= 0) return 50;
3641
+ const key = `${site}:${n}`;
3642
+ let st = this.liquiditySites.get(key);
3643
+ if (st === undefined) {
3644
+ st = { volume: [], volatility: [], spread: [] };
3645
+ this.liquiditySites.set(key, st);
3646
+ }
3647
+ pushCap(st.volume, finiteCell(volume), n);
3648
+ pushCap(st.volatility, finiteCell(volatility), n);
3649
+ pushCap(st.spread, finiteCell(spread), n);
3650
+ if (st.volume.length < n || st.volatility.length < n || st.spread.length < n) return 50;
3651
+ let volSum = 0;
3652
+ for (const v of st.volume) if (v !== null) volSum += v;
3653
+ let volaSum = 0;
3654
+ for (const v of st.volatility) if (v !== null) volaSum += v;
3655
+ let spreadSum = 0;
3656
+ for (const v of st.spread) if (v !== null) spreadSum += v;
3657
+ const volScore = Math.min(100, volSum / n / 100);
3658
+ const volatilityScore = Math.max(0, 100 - (volaSum / n) * 50);
3659
+ const spreadScore = Math.max(0, 100 - (spreadSum / n) * 100);
3660
+ const liquidity = volScore * 0.4 + volatilityScore * 0.3 + spreadScore * 0.3;
3661
+ return Math.max(0, Math.min(100, liquidity));
3662
+ }
3663
+
3664
+ /** `core.py` `_fear_greed_index_inc_update` — last samples only. */
3665
+ fearGreedIndex(rsi: Cell, vix: Cell, putCall: Cell, breadth: Cell): number {
3666
+ const r = finiteCell(rsi) ?? 50;
3667
+ const v = finiteCell(vix) ?? 2;
3668
+ const pc = finiteCell(putCall) ?? 1;
3669
+ const b = finiteCell(breadth) ?? 0.5;
3670
+ const index = ((r - 50) * 1 + (v - 2) * 10 + (1 - pc) * 50 + (b - 0.5) * 100) / 4;
3671
+ return Math.max(-100, Math.min(100, index));
3672
+ }
3673
+
3674
+ /** `core.py` `_crowd_sentiment_inc_update` — last samples only. */
3675
+ crowdSentiment(priceAgreement: Cell, volumeAgreement: Cell, timeAgreement: Cell): number {
3676
+ const p = finiteCell(priceAgreement) ?? 0.5;
3677
+ const v = finiteCell(volumeAgreement) ?? 0.5;
3678
+ const t = finiteCell(timeAgreement) ?? 0.5;
3679
+ return Math.max(0, Math.min(100, ((p + v + t) / 3) * 100));
3680
+ }
3681
+
3682
+ /** `core.py` `_smart_money_flow_inc_update` — last samples only. */
3683
+ smartMoneyFlow(priceChange: Cell, volume: Cell, timeHigh: Cell, timeLow: Cell): number {
3684
+ const p = finiteCell(priceChange) ?? 0;
3685
+ const v = finiteCell(volume) ?? 1000;
3686
+ const th = finiteCell(timeHigh) ?? 10;
3687
+ const tl = finiteCell(timeLow) ?? 10;
3688
+ const volFactor = Math.min(1, v / 5000);
3689
+ let flow = 0;
3690
+ if (p > 0 && th < tl) flow = volFactor * 0.8;
3691
+ else if (p < 0 && tl < th) flow = -volFactor * 0.8;
3692
+ return Math.max(-1, Math.min(1, flow));
3693
+ }
3694
+
3695
+ /** `core.py` `_trend_confirmation_score_inc_update` — last samples only. */
3696
+ trendConfirmationScore(
3697
+ momentum: Cell,
3698
+ trendAlignment: Cell,
3699
+ strength: Cell,
3700
+ rsi: Cell,
3701
+ rsiAlignment: Cell,
3702
+ ): number {
3703
+ const m = finiteCell(momentum) ?? 0;
3704
+ const t = finiteCell(trendAlignment) ?? 0;
3705
+ const s = finiteCell(strength) ?? 1;
3706
+ const r = finiteCell(rsi) ?? 50;
3707
+ const a = finiteCell(rsiAlignment) ?? 0;
3708
+ const momentumScore = Math.min(100, Math.abs(m) * 20);
3709
+ const trendScore = Math.max(0, ((t + 1) / 2) * 100);
3710
+ const strengthScore = Math.min(100, s * 50);
3711
+ const rsiScore = Math.abs(r - 50) > 10 ? Math.min(100, Math.abs(r - 50) * 2) : 40;
3712
+ const alignmentBonus = Math.abs(a) > 0.5 ? 20 : 0;
3713
+ const total =
3714
+ momentumScore * 0.25 + trendScore * 0.3 + strengthScore * 0.25 + rsiScore * 0.15 + alignmentBonus;
3715
+ return Math.max(0, Math.min(100, total));
3716
+ }
3717
+
3718
+ /** `core.py` `_max_loss_level_inc_update` — last samples only. */
3719
+ maxLossLevel(entry: Cell, account: Cell, maxLossPct: Cell): number {
3720
+ const e = finiteCell(entry) ?? 100;
3721
+ const a = finiteCell(account) ?? 10000;
3722
+ const p = finiteCell(maxLossPct) ?? 1;
3723
+ const maxLossAmount = a * (p / 100);
3724
+ if (e <= 0) return 0;
3725
+ const shares = a / e;
3726
+ const stop = shares > 0 ? e - maxLossAmount / shares : 0;
3727
+ return Math.max(0, stop);
3728
+ }
3729
+
3730
+ /** `core.py` `_profit_lock_level_inc_update` — last samples only. */
3731
+ profitLockLevel(entry: Cell, current: Cell, trailPct: Cell, direction: Cell): number {
3732
+ const e = finiteCell(entry) ?? 100;
3733
+ const c = finiteCell(current) ?? 100;
3734
+ const t = Math.max(0, Math.min(1, finiteCell(trailPct) ?? 0.05));
3735
+ const d = finiteCell(direction) ?? 1;
3736
+ return d > 0 ? Math.max(e * 0.9, c - c * t) : Math.min(e * 1.1, c + c * t);
3737
+ }
3738
+
3739
+ /** `advanced.py` `_builtin_ta_position_sizing`. */
3740
+ positionSizing(account: Cell, riskPct: Cell, entry: Cell, stop: Cell): number {
3741
+ const a = finiteCell(account) ?? 10000;
3742
+ const r = finiteCell(riskPct) ?? 0.01;
3743
+ const e = finiteCell(entry) ?? 100;
3744
+ const s = finiteCell(stop) ?? 95;
3745
+ const riskAmount = a * (r / 100);
3746
+ const distance = e - s;
3747
+ if (Math.abs(distance) < 1e-10) return 0;
3748
+ return Math.max(0, riskAmount / Math.abs(distance));
3749
+ }
3750
+
3751
+ /** `advanced.py` `_builtin_ta_kelly_criterion`. */
3752
+ kellyCriterion(winRate: Cell, avgWin: Cell, avgLoss: Cell): number {
3753
+ const wr = Math.max(0, Math.min(1, finiteCell(winRate) ?? 0.5));
3754
+ const win = finiteCell(avgWin) ?? 1;
3755
+ const loss = finiteCell(avgLoss) ?? 1;
3756
+ if (Math.abs(win) < 1e-10) return 0;
3757
+ const kelly = (wr * win - (1 - wr) * loss) / win;
3758
+ return Math.max(0, kelly);
3759
+ }
3760
+
3761
+ /** `advanced.py` `_builtin_ta_risk_reward_ratio`. */
3762
+ riskRewardRatio(entry: Cell, stop: Cell, target: Cell): Cell {
3763
+ const e = finiteCell(entry) ?? 0;
3764
+ const s = finiteCell(stop) ?? 0;
3765
+ const t = finiteCell(target) ?? 0;
3766
+ const risk = e - s;
3767
+ if (Math.abs(risk) < 1e-10) return null;
3768
+ return finiteOut((t - e) / risk);
3769
+ }
3770
+
3771
+ /** `strategies.py` `_builtin_ta_risk_reward_asymmetry`. */
3772
+ riskRewardAsymmetry(entry: Cell, stop: Cell, target: Cell, winRate: Cell): TaRecord {
3773
+ const e = finiteCell(entry) ?? 0;
3774
+ const s = finiteCell(stop) ?? 0;
3775
+ const t = finiteCell(target) ?? 0;
3776
+ const wr = finiteCell(winRate) ?? 0.5;
3777
+ const risk = Math.abs(e - s);
3778
+ const reward = Math.abs(t - e);
3779
+ const ratio = risk > 0 ? reward / risk : 0;
3780
+ const ev = wr * reward - (1 - wr) * risk;
3781
+ const kelly = ratio > 0 ? wr - (1 - wr) / ratio : 0;
3782
+ return taRec({
3783
+ ratio: finiteOut(ratio) ?? 0,
3784
+ expected_value: finiteOut(ev) ?? 0,
3785
+ kelly_percent: finiteOut(kelly * 100) ?? 0,
3786
+ is_favorable: ev > 0,
3787
+ risk_reward_ratio: finiteOut(ratio) ?? 0,
3788
+ kelly_percentage: finiteOut(kelly * 100) ?? 0,
3789
+ });
3790
+ }
3791
+
3792
+ /** `advanced.py` `_builtin_ta_probability_of_movement`. */
3793
+ probabilityOfMovement(current: Cell, target: Cell, atr: Cell, period: Cell): number {
3794
+ const c = finiteCell(current) ?? 100;
3795
+ const t = finiteCell(target) ?? 100;
3796
+ const a = finiteCell(atr) ?? 1;
3797
+ const n = this.coerceTaInt(period, "period must be integer");
3798
+ if (Math.abs(c) < 1e-10 || Math.abs(a) < 1e-10) return 0.5;
3799
+ const distance = Math.abs(t - c);
3800
+ const expected = a * Math.sqrt(n);
3801
+ if (expected === 0) return 0.5;
3802
+ return Math.max(0, Math.min(1, Math.min(1, distance / expected) * 0.8 + 0.1));
3803
+ }
3804
+
3805
+ /** `advanced.py` `_builtin_ta_trend_strength`. */
3806
+ trendStrength(price: Cell, adx: Cell, rsi: Cell): number {
3807
+ const a = finiteCell(adx) ?? 20;
3808
+ const r = finiteCell(rsi) ?? 50;
3809
+ const adxNorm = Math.min(100, Math.max(0, a));
3810
+ const rsiExtremeness = Math.abs(r - 50) / 50;
3811
+ return Math.min(100, Math.max(0, adxNorm * 0.6 + rsiExtremeness * 40));
3812
+ }
3813
+
3814
+ /** `strategies.py` `_builtin_ta_breakeven_level`. */
3815
+ breakevenLevel(entry: Cell, qty: Cell, commission: Cell, slippage: Cell, direction: Cell): TaRecord {
3816
+ const e = finiteCell(entry) ?? 0;
3817
+ const q = finiteCell(qty) ?? 0;
3818
+ const comm = finiteCell(commission) ?? 0;
3819
+ const slip = finiteCell(slippage) ?? 0;
3820
+ const d = finiteCell(direction) ?? 1;
3821
+ const cost = comm + slip * q;
3822
+ const impact = q > 0 ? cost / q : 0;
3823
+ const breakeven = d >= 0 ? e + impact : e - impact;
3824
+ const move = e > 0 ? (Math.abs(breakeven - e) / e) * 100 : 0;
3825
+ return taRec({
3826
+ breakeven_price: finiteOut(breakeven) ?? 0,
3827
+ total_cost: finiteOut(cost) ?? 0,
3828
+ move_required_percent: finiteOut(move) ?? 0,
3829
+ });
3830
+ }
3831
+
3832
+ /**
3833
+ * `advanced.py` `_builtin_ta_market_structure_pivot` (wins MRO over the
3834
+ * strategies variant): oldest `period` highs/lows, midpoint pivot.
3835
+ */
3836
+ marketStructurePivot(
3837
+ site: string,
3838
+ high: unknown,
3839
+ low: unknown,
3840
+ period: Cell,
3841
+ mode: Cell,
3842
+ ): TaRecord {
3843
+ const hh = this.windowFor(`${site}:high`, high);
3844
+ const ll = this.windowFor(`${site}:low`, low);
3845
+ const n = this.coerceTaInt(period, "ta.market_structure_pivot() requires 5 arguments");
3846
+ const m = this.coerceTaInt(mode, "ta.market_structure_pivot() requires 5 arguments");
3847
+ const off = taRec({ pivot_price: 100, strength: 50, structure: "neutral" });
3848
+ if (hh.length === 0 || ll.length === 0 || hh.length < n || n <= 0) return off;
3849
+ const hVals = hh.slice(-n).filter((v): v is number => v !== null);
3850
+ const lVals = ll.slice(-n).filter((v): v is number => v !== null);
3851
+ if (hVals.length === 0 || lVals.length === 0) return off;
3852
+ let pivotHigh = hVals[0]!;
3853
+ let pivotLow = lVals[0]!;
3854
+ for (const v of hVals) if (v > pivotHigh) pivotHigh = v;
3855
+ for (const v of lVals) if (v < pivotLow) pivotLow = v;
3856
+ const price = (pivotHigh + pivotLow) / 2;
3857
+ const range = pivotHigh - pivotLow;
3858
+ let structure = "block";
3859
+ let strength = Math.min(100, range * 0.5);
3860
+ if (m === 0) {
3861
+ structure = "fractal";
3862
+ strength = Math.min(100, range * 2);
3863
+ } else if (m === 1) {
3864
+ structure = "swing";
3865
+ strength = Math.min(100, range * 1.5);
3866
+ }
3867
+ return taRec({
3868
+ pivot_price: finiteOut(price) ?? 0,
3869
+ strength: finiteOut(strength) ?? 0,
3870
+ structure,
3871
+ });
3872
+ }
3873
+
3874
+ /** `strategies.py` `_builtin_ta_position_sizing_score`. */
3875
+ positionSizingScore(risk: Cell, volatility: Cell, conviction: Cell, correlation: Cell): TaRecord {
3876
+ const r = finiteCell(risk) ?? 0;
3877
+ const v = finiteCell(volatility) ?? 0;
3878
+ const c = finiteCell(conviction) ?? 0;
3879
+ const corr = finiteCell(correlation) ?? 0;
3880
+ let score = (r / (v + 0.1)) * c * (1 - corr);
3881
+ score = Math.max(0, Math.min(1, score * 0.1));
3882
+ return taRec({
3883
+ score: finiteOut(score * 100) ?? 0,
3884
+ position_size_ratio: finiteOut(score) ?? 0,
3885
+ kelly_fraction: finiteOut(score * 0.5) ?? 0,
3886
+ correlation_adjustment: finiteOut(1 - corr) ?? 0,
3887
+ });
3888
+ }
3889
+
3890
+ /** `strategies.py` `_builtin_ta_trailing_exit_level`. */
3891
+ trailingExitLevel(entry: Cell, current: Cell, atr: Cell, mult: Cell, step: Cell): TaRecord {
3892
+ const e = finiteCell(entry) ?? 0;
3893
+ const c = finiteCell(current) ?? 0;
3894
+ const a = finiteCell(atr) ?? 0;
3895
+ const m = finiteCell(mult) ?? 0;
3896
+ void (finiteCell(step) ?? 0);
3897
+ const trail = c > e ? Math.max(e * 1.001, c - a * m) : c + a * m;
3898
+ return taRec({
3899
+ trail_stop: finiteOut(trail) ?? 0,
3900
+ protected_profit: c > e ? finiteOut(Math.max(0, c - e)) ?? 0 : 0,
3901
+ risk_reward_current: a > 0 ? finiteOut(Math.abs(c - e) / a) ?? 0 : 0,
3902
+ });
3903
+ }
3904
+
3905
+ /** `strategies.py` `_builtin_ta_optimal_entry_zone` (newest-first list: `[-1]` = oldest). */
3906
+ optimalEntryZone(site: string, close: unknown): TaRecord {
3907
+ const h = this.windowFor(`${site}:close`, close);
3908
+ const c = TaEngine.oldest(h) ?? 0;
3909
+ return taRec({
3910
+ min: finiteOut(c * 0.99) ?? 0,
3911
+ max: finiteOut(c * 1.01) ?? 0,
3912
+ optimal: c,
3913
+ entry_zone_low: finiteOut(c * 0.99) ?? 0,
3914
+ entry_zone_high: finiteOut(c * 1.01) ?? 0,
3915
+ best_entry: c,
3916
+ zone_strength: 85,
3917
+ });
3918
+ }
3919
+
3920
+ /** `strategies.py` `_builtin_ta_mean_reversion_entry` (newest-first lists: `[-1]` = oldest). */
3921
+ meanReversionEntry(
3922
+ site: string,
3923
+ price: unknown,
3924
+ sma: unknown,
3925
+ stdev: unknown,
3926
+ threshold: Cell,
3927
+ ): TaRecord {
3928
+ const ph = this.windowFor(`${site}:price`, price);
3929
+ const sh = this.windowFor(`${site}:sma`, sma);
3930
+ const dh = this.windowFor(`${site}:stdev`, stdev);
3931
+ const p = TaEngine.oldest(ph) ?? 0;
3932
+ const s = TaEngine.oldest(sh) ?? 0;
3933
+ const sd = TaEngine.oldest(dh) ?? 1;
3934
+ const th = finiteCell(threshold) ?? 0;
3935
+ const z = sd !== 0 ? (p - s) / sd : 0;
3936
+ return taRec({
3937
+ entry_price: 100,
3938
+ stop_loss: 95,
3939
+ take_profit: 105,
3940
+ probability: 0.7,
3941
+ reversion_probability: 0.75,
3942
+ target_price: 105,
3943
+ z_score: finiteOut(z) ?? 0,
3944
+ is_mean_reversion_setup: Math.abs(z) > th,
3945
+ });
3946
+ }
3947
+
3948
+ /** `strategies.py` `_builtin_ta_multi_timeframe_signal`. */
3949
+ multiTimeframeSignal(s1: Cell, s2: Cell, s3: Cell, w1: Cell, w2: Cell, w3: Cell): TaRecord {
3950
+ const score =
3951
+ (finiteCell(s1) ?? 0) * (finiteCell(w1) ?? 0) +
3952
+ (finiteCell(s2) ?? 0) * (finiteCell(w2) ?? 0) +
3953
+ (finiteCell(s3) ?? 0) * (finiteCell(w3) ?? 0);
3954
+ return taRec({ score: finiteOut(score) ?? 0, signal: score > 0.5 ? "buy" : "sell", agreement: true });
3955
+ }
3956
+
3957
+ /** `strategies.py` `_builtin_ta_market_timing_index`. */
3958
+ marketTimingIndex(trend: Cell, momentum: Cell, volatility: Cell, sentiment: Cell): TaRecord {
3959
+ const t = finiteCell(trend) ?? 0;
3960
+ const m = finiteCell(momentum) ?? 0;
3961
+ const v = finiteCell(volatility) ?? 0;
3962
+ const s = finiteCell(sentiment) ?? 0;
3963
+ const score = t * 0.3 + m * 0.3 + v * 0.2 + s * 0.2;
3964
+ let recommendation = "hold";
3965
+ if (score > 80) recommendation = "strong_buy";
3966
+ else if (score > 60) recommendation = "buy";
3967
+ else if (score < 20) recommendation = "strong_sell";
3968
+ else if (score < 40) recommendation = "sell";
3969
+ return taRec({
3970
+ score: finiteOut(score) ?? 0,
3971
+ recommendation,
3972
+ regime: Math.abs(t) > 50 ? "trending" : "ranging",
3973
+ timing_index: finiteOut(score) ?? 0,
3974
+ market_condition: score > 80 ? "optimal_long" : "neutral",
3975
+ confidence: 0.85,
3976
+ });
3977
+ }
3978
+
3979
+ /** `strategies.py` `_builtin_ta_regime_adaptive_signal`. */
3980
+ regimeAdaptiveSignal(
3981
+ strength: Cell,
3982
+ volRegime: unknown,
3983
+ trendRegime: unknown,
3984
+ ): TaRecord {
3985
+ const vol = typeof volRegime === "string" ? volRegime : "";
3986
+ const trend = typeof trendRegime === "string" ? trendRegime : "";
3987
+ let adapted = finiteCell(strength) ?? 0;
3988
+ let fit = 1;
3989
+ if (vol === "high") {
3990
+ adapted *= 0.8;
3991
+ fit *= 0.8;
3992
+ }
3993
+ if (trend === "ranging") {
3994
+ adapted *= 0.5;
3995
+ fit *= 0.5;
3996
+ }
3997
+ return taRec({
3998
+ signal: adapted > 0.5 ? "buy" : "sell",
3999
+ strength: finiteOut(adapted) ?? 0,
4000
+ confidence: finiteOut(adapted * 100) ?? 0,
4001
+ adapted_signal: finiteOut(adapted) ?? 0,
4002
+ regime_fit: finiteOut(fit) ?? 0,
4003
+ strategy_recommendation: trend === "trending" ? "trend_following" : "mean_reversion",
4004
+ signal_confidence: finiteOut(adapted) ?? 0,
4005
+ });
4006
+ }
4007
+
4008
+ /** `advanced.py` `_builtin_ta_strategy_score`. */
4009
+ strategyScore(rsi: Cell, macd: Cell, emaCross: Cell, trend: Cell): number {
4010
+ const r = finiteCell(rsi) ?? 50;
4011
+ const m = finiteCell(macd) ?? 0;
4012
+ const cross = emaCross != null && emaCross !== 0;
4013
+ const t = finiteCell(trend) ?? 50;
4014
+ const rsiNorm = ((r - 50) / 50) * 25;
4015
+ const macdNorm = Math.max(-25, Math.min(25, m * 50));
4016
+ const emaBonus = cross ? 25 : -25;
4017
+ const trendNorm = ((t - 50) / 50) * 25;
4018
+ return Math.max(-100, Math.min(100, rsiNorm + macdNorm + emaBonus + trendNorm));
4019
+ }
4020
+
4021
+ /** `advanced.py` `_builtin_ta_breakout_detection`. */
4022
+ breakoutDetection(close: Cell, resistance: Cell, support: Cell): TaRecord {
4023
+ const c = finiteCell(close) ?? 0;
4024
+ const r = finiteCell(resistance) ?? 0;
4025
+ const s = finiteCell(support) ?? 0;
4026
+ if (c > r) {
4027
+ const strength = r > 0 ? ((c - r) / r) * 100 : 0;
4028
+ return taRec({
4029
+ is_breakout: true,
4030
+ breakout_type: "resistance",
4031
+ breakout_strength: finiteOut(strength) ?? 0,
4032
+ });
4033
+ }
4034
+ if (c < s) {
4035
+ const strength = s > 0 ? ((s - c) / s) * 100 : 0;
4036
+ return taRec({
4037
+ is_breakout: true,
4038
+ breakout_type: "support",
4039
+ breakout_strength: finiteOut(strength) ?? 0,
4040
+ });
4041
+ }
4042
+ return taRec({ is_breakout: false, breakout_type: "none", breakout_strength: 0 });
4043
+ }
4044
+
4045
+ /** `core.py` `_ema_cross_signal_inc_update`. */
4046
+ emaCrossSignal(site: string, fast: Cell, slow: Cell, threshold: Cell): TaRecord {
4047
+ const off = taRec({ signal: "neutral", strength: 0, trend_direction: 0 });
4048
+ let st = this.emaCrossSites.get(site);
4049
+ if (st === undefined) {
4050
+ st = { prevFast: null, prevSlow: null, count: 0 };
4051
+ this.emaCrossSites.set(site, st);
4052
+ }
4053
+ const f = finiteCell(fast) ?? 0;
4054
+ const s = finiteCell(slow) ?? 0;
4055
+ const th = finiteCell(threshold) ?? 0;
4056
+ st.count += 1;
4057
+ const prevFast = st.prevFast;
4058
+ const prevSlow = st.prevSlow;
4059
+ if (st.count < 2 || prevFast === null || prevSlow === null) {
4060
+ st.prevFast = f;
4061
+ st.prevSlow = s;
4062
+ return off;
4063
+ }
4064
+ const diff = f - s;
4065
+ const prevDiff = prevFast - prevSlow;
4066
+ const crossover = prevDiff <= 0 && diff > 0;
4067
+ const crossunder = prevDiff >= 0 && diff < 0;
4068
+ const strength = Math.abs(diff);
4069
+ const trend = diff > 0 ? 1 : -1;
4070
+ let signal = "neutral";
4071
+ if (crossover && strength > th) signal = "buy";
4072
+ else if (crossunder && strength > th) signal = "sell";
4073
+ st.prevFast = f;
4074
+ st.prevSlow = s;
4075
+ return taRec({ signal, strength: finiteOut(strength) ?? 0, trend_direction: trend });
4076
+ }
4077
+
4078
+ /** `core.py` `_momentum_filter_inc_update`. */
4079
+ momentumFilter(site: string, momentum: Cell, volume: Cell, period: Cell): number {
4080
+ const n = this.coerceTaInt(period, "ta.momentum_filter() requires 3 arguments");
4081
+ if (n <= 0) return 0;
4082
+ let st = this.momentumFilterSites.get(`${site}:${n}`);
4083
+ if (st === undefined) {
4084
+ st = { momentum: [], volume: [] };
4085
+ this.momentumFilterSites.set(`${site}:${n}`, st);
4086
+ }
4087
+ pushCap(st.momentum, finiteCell(momentum), n);
4088
+ pushCap(st.volume, finiteCell(volume), n);
4089
+ if (st.momentum.length < n || st.volume.length < n) return 0;
4090
+ let volSum = 0;
4091
+ for (const v of st.volume) if (v !== null) volSum += v;
4092
+ if (volSum === 0) return 0;
4093
+ let numerator = 0;
4094
+ let paired = false;
4095
+ for (let i = 0; i < n; i++) {
4096
+ const m = st.momentum[i];
4097
+ const v = st.volume[i];
4098
+ if (m !== null && v !== null) {
4099
+ numerator += m * v;
4100
+ paired = true;
4101
+ }
4102
+ }
4103
+ if (!paired) return 0;
4104
+ return finiteOut(numerator / volSum) ?? 0;
4105
+ }
4106
+
4107
+ /** `advanced.py` `_volatility_regime_inc_update`. */
4108
+ volatilityRegime(site: string, atr: Cell, period: Cell): string {
4109
+ const n = this.coerceTaInt(period, "period must be integer");
4110
+ if (n < 1) return "medium";
4111
+ let w = this.volatilityRegimeSites.get(`${site}:${n}`);
4112
+ if (w === undefined) {
4113
+ w = [];
4114
+ this.volatilityRegimeSites.set(`${site}:${n}`, w);
4115
+ }
4116
+ pushCap(w, finiteCell(atr), n);
4117
+ if (w.length < n) return "medium";
4118
+ const recent: number[] = [];
4119
+ for (const v of w) if (v !== null) recent.push(v);
4120
+ if (recent.length === 0) return "medium";
4121
+ const current = recent[recent.length - 1]!;
4122
+ let sum = 0;
4123
+ for (const v of recent) sum += v;
4124
+ const avg = sum / recent.length;
4125
+ if (current < avg * 0.5) return "low";
4126
+ if (current > avg * 2) return "extreme";
4127
+ if (current > avg * 1.3) return "high";
4128
+ return "medium";
4129
+ }
4130
+
4131
+ /** `advanced.py` `_builtin_ta_economic_impact_score`. */
4132
+ economicImpactScore(priceChange: Cell, volatility: Cell, volumeChange: Cell): number {
4133
+ const pc = finiteCell(priceChange) ?? 0;
4134
+ const vol = finiteCell(volatility) ?? 0;
4135
+ const vc = finiteCell(volumeChange) ?? 0;
4136
+ const pcScore = Math.min(100, Math.abs(pc) * 20);
4137
+ const volScore = Math.min(100, vol * 30);
4138
+ const vcScore = Math.min(100, vc * 25);
4139
+ return Math.max(0, Math.min(100, (pcScore + volScore + vcScore) / 3));
4140
+ }
4141
+
4142
+ /** `advanced.py` `_builtin_ta_contrarian_signal`. */
4143
+ contrarianSignal(sentiment: Cell, volatility: Cell, timeSinceExtreme: Cell): TaRecord {
4144
+ const sent = finiteCell(sentiment) ?? 50;
4145
+ const vol = finiteCell(volatility) ?? 1;
4146
+ const time = finiteCell(timeSinceExtreme) ?? 10;
4147
+ let signal = "neutral";
4148
+ let strength = 0.5;
4149
+ let confidence = 0.5;
4150
+ if ((sent > 80 || sent < 20) && vol > 2 && time < 5) {
4151
+ signal = "strong_contrarian";
4152
+ strength = 0.9;
4153
+ confidence = 0.8;
4154
+ } else if (sent > 65 || sent < 35) {
4155
+ signal = "mild_contrarian";
4156
+ strength = 0.6;
4157
+ confidence = 0.6;
4158
+ } else if (sent > 45 && sent < 55) {
4159
+ signal = "follow_crowd";
4160
+ strength = 0.3;
4161
+ confidence = 0.4;
4162
+ }
4163
+ return taRec({ signal, strength, confidence });
4164
+ }
4165
+
4166
+ /** `strategies.py` `_builtin_ta_drawdown_recovery_level`. */
4167
+ drawdownRecoveryLevel(peak: Cell, current: Cell, factor: Cell, period: Cell): TaRecord {
4168
+ const p = finiteCell(peak) ?? 0;
4169
+ const c = finiteCell(current) ?? 0;
4170
+ const f = finiteCell(factor) ?? 0;
4171
+ const n = this.coerceTaInt(period, "ta.drawdown_recovery_level() requires 4 arguments");
4172
+ const drawdown = p > 0 ? (p - c) / p : 0;
4173
+ const recovery = drawdown < 1 ? drawdown / (1 - drawdown) : 0;
4174
+ return taRec({
4175
+ recovery_level: p,
4176
+ percent_needed: finiteOut(recovery * 100) ?? 0,
4177
+ estimated_time: finiteOut(n * f) ?? 0,
4178
+ confidence: 80,
4179
+ drawdown_percent: finiteOut(drawdown * 100) ?? 0,
4180
+ recovery_timeframe: finiteOut(n * f) ?? 0,
4181
+ recovery_confidence: 0.8,
4182
+ });
4183
+ }
4184
+
4185
+ /** `common.py` `_builtin_ta_pivot_point_levels` (high/low/close form). */
4186
+ pivotPointLevels(high: Cell, low: Cell, close: Cell, traditional: boolean): TaRecord {
4187
+ const h = finiteCell(high) ?? 0;
4188
+ const l = finiteCell(low) ?? 0;
4189
+ const c = finiteCell(close) ?? 0;
4190
+ const pivot = (h + l + c) / 3;
4191
+ let r1: number;
4192
+ let s1: number;
4193
+ let r2: number;
4194
+ let s2: number;
4195
+ let r3: number;
4196
+ let s3: number;
4197
+ if (traditional) {
4198
+ r1 = 2 * pivot - l;
4199
+ s1 = 2 * pivot - h;
4200
+ r2 = pivot + (h - l);
4201
+ s2 = pivot - (h - l);
4202
+ r3 = h + 2 * (pivot - l);
4203
+ s3 = l - 2 * (h - pivot);
4204
+ } else {
4205
+ const diff = h - l;
4206
+ r1 = pivot + 0.382 * diff;
4207
+ s1 = pivot - 0.382 * diff;
4208
+ r2 = pivot + 0.618 * diff;
4209
+ s2 = pivot - 0.618 * diff;
4210
+ r3 = pivot + diff;
4211
+ s3 = pivot - diff;
4212
+ }
4213
+ return taRec({
4214
+ pivot: finiteOut(pivot) ?? 0,
4215
+ r1: finiteOut(r1) ?? 0,
4216
+ s1: finiteOut(s1) ?? 0,
4217
+ r2: finiteOut(r2) ?? 0,
4218
+ s2: finiteOut(s2) ?? 0,
4219
+ r3: finiteOut(r3) ?? 0,
4220
+ s3: finiteOut(s3) ?? 0,
4221
+ });
4222
+ }
4223
+
4224
+ /** `advanced.py` `_builtin_ta_volatility_regime_score` (newest-first lists). */
4225
+ volatilityRegimeScore(
4226
+ site: string,
4227
+ atr: unknown,
4228
+ volatility: unknown,
4229
+ vix: unknown,
4230
+ threshold: Cell,
4231
+ ): TaRecord {
4232
+ const a = this.windowFor(`${site}:atr`, atr);
4233
+ const v = this.windowFor(`${site}:vol`, volatility);
4234
+ const x = this.windowFor(`${site}:vix`, vix);
4235
+ const th = finiteCell(threshold) ?? 50;
4236
+ if (a.length === 0 || v.length === 0 || x.length === 0) {
4237
+ return taRec({ regime: "normal", volatility_score: 50, momentum: "stable" });
4238
+ }
4239
+ const atrVal = TaEngine.oldest(a) ?? 2;
4240
+ const volVal = TaEngine.oldest(v) ?? 0.02;
4241
+ const vixVal = TaEngine.oldest(x) ?? 15;
4242
+ const atrScore = Math.min(100, atrVal * 20);
4243
+ const volScore = Math.min(100, volVal * 100);
4244
+ const vixScore = Math.min(100, vixVal * 2);
4245
+ const score = atrScore * 0.4 + volScore * 0.3 + vixScore * 0.3;
4246
+ let regime: string;
4247
+ if (score < th * 0.5) regime = "low";
4248
+ else if (score < th) regime = "normal";
4249
+ else if (score < th * 1.5) regime = "high";
4250
+ else regime = "extreme";
4251
+ let momentum = "stable";
4252
+ const prevAtr = TaEngine.secondOldest(a);
4253
+ if (a.length > 1 && prevAtr !== null) {
4254
+ if (atrVal > prevAtr * 1.05) momentum = "accelerating";
4255
+ else if (atrVal < prevAtr * 0.95) momentum = "decelerating";
4256
+ }
4257
+ return taRec({ regime, volatility_score: finiteOut(score) ?? 0, momentum });
4258
+ }
4259
+
4260
+ /** `advanced.py` `_builtin_ta_correlation_filter` (newest-first lists). */
4261
+ correlationFilter(
4262
+ site: string,
4263
+ s1: unknown,
4264
+ s2: unknown,
4265
+ s3: unknown,
4266
+ threshold: Cell,
4267
+ ): TaRecord {
4268
+ const h1 = this.windowFor(`${site}:s1`, s1);
4269
+ const h2 = this.windowFor(`${site}:s2`, s2);
4270
+ const h3 = this.windowFor(`${site}:s3`, s3);
4271
+ const histories = [h1, h2, h3].filter((h) => h.length > 0);
4272
+ const off = taRec({ is_correlated: false, signal_agreement: 0, divergence_count: 0 });
4273
+ if (histories.length < 2) return off;
4274
+ const lastVals: number[] = [];
4275
+ for (const h of histories) {
4276
+ const v = TaEngine.oldest(h);
4277
+ if (v !== null) lastVals.push(v);
4278
+ }
4279
+ if (lastVals.length === 0) return off;
4280
+ let agreement = 0;
4281
+ let divergence = 0;
4282
+ for (let i = 0; i < lastVals.length - 1; i++) {
4283
+ for (let j = i + 1; j < lastVals.length; j++) {
4284
+ if (lastVals[i]! * lastVals[j]! > 0) agreement += 1;
4285
+ else divergence += 1;
4286
+ }
4287
+ }
4288
+ const pairs = (lastVals.length * (lastVals.length - 1)) / 2;
4289
+ const pct = pairs > 0 ? (agreement / pairs) * 100 : 0;
4290
+ const th = finiteCell(threshold) ?? 0.7;
4291
+ return taRec({
4292
+ is_correlated: pct / 100 >= th,
4293
+ signal_agreement: finiteOut(pct) ?? 0,
4294
+ divergence_count: divergence,
4295
+ });
4296
+ }
4297
+
4298
+ /** `advanced.py` `_builtin_ta_advanced_breakout_detector` (newest-first lists). */
4299
+ advancedBreakoutDetector(
4300
+ site: string,
4301
+ price: unknown,
4302
+ volume: unknown,
4303
+ level: Cell,
4304
+ lookback: Cell,
4305
+ volumeMultiplier: Cell,
4306
+ ): TaRecord {
4307
+ const ph = this.windowFor(`${site}:price`, price);
4308
+ const vh = this.windowFor(`${site}:vol`, volume);
4309
+ const lvl = finiteCell(level) ?? 100;
4310
+ const n = this.coerceTaInt(lookback, "ta.advanced_breakout_detector() requires 5 arguments");
4311
+ const mult = finiteCell(volumeMultiplier) ?? 0.5;
4312
+ const off = taRec({ breakout_detected: false, breakout_type: "none", pullback_probability: 0.5 });
4313
+ if (ph.length === 0 || vh.length === 0 || ph.length < 2 || n <= 0) return off;
4314
+ const currentPrice = TaEngine.oldest(ph) ?? 100;
4315
+ const prevPrice = TaEngine.secondOldest(ph) ?? 100;
4316
+ const currentVol = TaEngine.oldest(vh) ?? 1000;
4317
+ const recentVol = vh.slice(-n).filter((v): v is number => v !== null);
4318
+ const avgVol = recentVol.length > 0 ? recentVol.reduce((a, b) => a + b, 0) / recentVol.length : 1000;
4319
+ const gap = currentPrice > lvl && prevPrice <= lvl;
4320
+ const closeBreak = currentPrice > lvl && Math.abs(currentPrice - lvl) < 0.5;
4321
+ const volumeBreak = currentVol > avgVol * (1 + mult);
4322
+ const detected = gap || closeBreak || volumeBreak;
4323
+ let type = "none";
4324
+ if (gap) type = "gap";
4325
+ else if (closeBreak && volumeBreak) type = "volume_break";
4326
+ else if (closeBreak) type = "close_above";
4327
+ const pullback = avgVol !== 0 ? Math.max(0.1, 0.8 - (currentVol / avgVol - 1) * 0.3) : 0.8;
4328
+ return taRec({
4329
+ breakout_detected: detected,
4330
+ breakout_type: type,
4331
+ pullback_probability: finiteOut(pullback) ?? 0.5,
4332
+ });
4333
+ }
4334
+
4335
+ /** `advanced.py` `_builtin_ta_pullback_bounce_level` (newest-first lists). */
4336
+ pullbackBounceLevel(
4337
+ site: string,
4338
+ high: unknown,
4339
+ low: unknown,
4340
+ trendDirection: Cell,
4341
+ lookback: Cell,
4342
+ ): TaRecord {
4343
+ const hh = this.windowFor(`${site}:high`, high);
4344
+ const ll = this.windowFor(`${site}:low`, low);
4345
+ const trend = this.coerceTaInt(trendDirection, "trend direction must be integer");
4346
+ const n = this.coerceTaInt(lookback, "ta.pullback_bounce_level() requires 5 arguments");
4347
+ const off = taRec({ primary_level: 100, bounce_probability: 0.5, support_strength: 50 });
4348
+ if (hh.length === 0 || ll.length === 0 || n <= 0) return off;
4349
+ const hVals = hh.slice(-n).filter((v): v is number => v !== null);
4350
+ const lVals = ll.slice(-n).filter((v): v is number => v !== null);
4351
+ if (hVals.length === 0 || lVals.length === 0) return off;
4352
+ let swingHigh = hVals[0]!;
4353
+ let swingLow = lVals[0]!;
4354
+ for (const v of hVals) if (v > swingHigh) swingHigh = v;
4355
+ for (const v of lVals) if (v < swingLow) swingLow = v;
4356
+ const range = swingHigh - swingLow;
4357
+ const fib = trend > 0 ? swingLow + range * 0.382 : swingHigh - range * 0.382;
4358
+ const bounce = swingHigh !== 0 ? Math.min(0.95, 0.5 + (range / swingHigh) * 1) : 0.5;
4359
+ const strength = swingHigh !== 0 ? Math.min(100, (range / swingHigh) * 100) : 0;
4360
+ return taRec({
4361
+ primary_level: finiteOut(fib) ?? 0,
4362
+ bounce_probability: finiteOut(bounce) ?? 0.5,
4363
+ support_strength: finiteOut(strength) ?? 0,
4364
+ });
4365
+ }
4366
+
4367
+ /** `advanced.py` `_builtin_ta_divergence_detector` (newest-first lists). */
4368
+ divergenceDetector(site: string, price: unknown, indicator: unknown, lookback: Cell): TaRecord {
4369
+ const ph = this.windowFor(`${site}:price`, price);
4370
+ const ih = this.windowFor(`${site}:ind`, indicator);
4371
+ const n = this.coerceTaInt(lookback, "lookback must be integer");
4372
+ const off = taRec({ is_bullish: false, is_bearish: false, strength: 0 });
4373
+ if (ph.length < n || ih.length < n) return off;
4374
+ const pv = ph.slice(-n).filter((v): v is number => v !== null);
4375
+ const iv = ih.slice(-n).filter((v): v is number => v !== null);
4376
+ if (pv.length < 2 || iv.length < 2) return off;
4377
+ const priceLower = pv[pv.length - 1]! < pv[0]!;
4378
+ const indHigher = iv[iv.length - 1]! > iv[0]!;
4379
+ const bullish = priceLower && indHigher;
4380
+ const priceHigher = pv[pv.length - 1]! > pv[0]!;
4381
+ const indLower = iv[iv.length - 1]! < iv[0]!;
4382
+ const bearish = priceHigher && indLower;
4383
+ const strength = Math.min(1, Math.abs(iv[iv.length - 1]! - iv[0]!) / 100);
4384
+ return taRec({
4385
+ is_bullish: bullish,
4386
+ is_bearish: bearish,
4387
+ strength: finiteOut(strength) ?? 0,
4388
+ });
4389
+ }
4390
+
4391
+ /** `advanced.py` `_builtin_ta_momentum_divergence` (newest-first lists). */
4392
+ momentumDivergence(
4393
+ site: string,
4394
+ price: unknown,
4395
+ momentumFast: unknown,
4396
+ momentumSlow: unknown,
4397
+ ): TaRecord {
4398
+ const ph = this.windowFor(`${site}:price`, price);
4399
+ const fh = this.windowFor(`${site}:fast`, momentumFast);
4400
+ const sh = this.windowFor(`${site}:slow`, momentumSlow);
4401
+ const off = taRec({ divergence_type: "none", strength: 0, bars_since: 0 });
4402
+ if (ph.length < 2 || fh.length < 2 || sh.length < 2) return off;
4403
+ const pv = ph.slice(-2).filter((v): v is number => v !== null);
4404
+ const fv = fh.slice(-2).filter((v): v is number => v !== null);
4405
+ const sv = sh.slice(-2).filter((v): v is number => v !== null);
4406
+ if (pv.length < 2 || fv.length < 2 || sv.length < 2) return off;
4407
+ const priceLower = pv[1]! < pv[0]!;
4408
+ const fastHigher = fv[1]! > fv[0]!;
4409
+ const slowHigher = sv[1]! > sv[0]!;
4410
+ const bullish = priceLower && fastHigher && slowHigher;
4411
+ const bearish = !priceLower && !fastHigher && !slowHigher;
4412
+ const type = bullish ? "bullish" : bearish ? "bearish" : "none";
4413
+ const strength = Math.min(1, Math.abs(fv[1]! - fv[0]!) / 100);
4414
+ return taRec({ divergence_type: type, strength: finiteOut(strength) ?? 0, bars_since: 1 });
4415
+ }
4416
+
4417
+ /** `economics.py` `_builtin_ta_spread_analysis` (newest-first lists). */
4418
+ spreadAnalysis(site: string, bid: unknown, ask: unknown, period: Cell): TaRecord {
4419
+ const bh = this.windowFor(`${site}:bid`, bid);
4420
+ const ah = this.windowFor(`${site}:ask`, ask);
4421
+ const n = this.coerceTaInt(period, "ta.spread_analysis() requires 3 arguments");
4422
+ const off = taRec({ avg_spread: 0, spread_percent: 0, spread_trend: "stable" });
4423
+ if (bh.length < n || ah.length < n || n <= 0) return off;
4424
+ const bSlice = bh.slice(-n);
4425
+ const aSlice = ah.slice(-n);
4426
+ const spreads: number[] = [];
4427
+ for (let i = 0; i < n; i++) {
4428
+ const b = bSlice[i] ?? 0;
4429
+ const a = aSlice[i] ?? 0;
4430
+ if (a > b && b > 0) spreads.push(a - b);
4431
+ }
4432
+ if (spreads.length === 0) return off;
4433
+ const avg = spreads.reduce((a, b) => a + b, 0) / spreads.length;
4434
+ const askOld = TaEngine.oldest(ah);
4435
+ const bidOld = TaEngine.oldest(bh);
4436
+ const mid = askOld !== null && bidOld !== null ? (askOld + bidOld) / 2 : 100;
4437
+ const pct = mid > 0 ? (avg / mid) * 100 : 0;
4438
+ let trend = "stable";
4439
+ if (spreads.length >= 2) {
4440
+ const first = spreads[0]!;
4441
+ const last = spreads[spreads.length - 1]!;
4442
+ if (last > first * 1.1) trend = "increasing";
4443
+ else if (last < first * 0.9) trend = "decreasing";
4444
+ }
4445
+ return taRec({
4446
+ avg_spread: finiteOut(avg) ?? 0,
4447
+ spread_percent: finiteOut(pct) ?? 0,
4448
+ spread_trend: trend,
4449
+ });
4450
+ }
4451
+
4452
+ /** `advanced.py` `_builtin_ta_employment_cycle_indicator` (newest-first lists). */
4453
+ employmentCycleIndicator(
4454
+ site: string,
4455
+ cyclical: unknown,
4456
+ defensive: unknown,
4457
+ unemployment: unknown,
4458
+ ): string {
4459
+ const ch = this.windowFor(`${site}:cyc`, cyclical);
4460
+ const dh = this.windowFor(`${site}:def`, defensive);
4461
+ const uh = this.windowFor(`${site}:unemp`, unemployment);
4462
+ if (ch.length === 0 || dh.length === 0 || uh.length === 0) return "mid_cycle";
4463
+ const cNew = ch[0];
4464
+ const cOld = TaEngine.oldest(ch);
4465
+ const dNew = dh[0];
4466
+ const dOld = TaEngine.oldest(dh);
4467
+ const cycPerf = cNew !== null && cOld !== null && cNew !== 0 ? (cOld - cNew) / cNew : 0;
4468
+ const defPerf = dNew !== null && dOld !== null && dNew !== 0 ? (dOld - dNew) / dNew : 0;
4469
+ const unemp = TaEngine.oldest(uh) ?? 0.05;
4470
+ if (cycPerf > defPerf && unemp < 0.04) return "early_cycle";
4471
+ if (cycPerf > defPerf && unemp < 0.06) return "mid_cycle";
4472
+ if (cycPerf < defPerf && unemp > 0.05) return "late_cycle";
4473
+ return "recession";
4474
+ }
4475
+
4476
+ /** `advanced.py` `_builtin_ta_gdp_growth_proxy` (newest-first lists). */
4477
+ gdpGrowthProxy(site: string, breadth: unknown, volume: unknown, momentum: unknown): number {
4478
+ const bh = this.windowFor(`${site}:breadth`, breadth);
4479
+ const vh = this.windowFor(`${site}:volume`, volume);
4480
+ const mh = this.windowFor(`${site}:momentum`, momentum);
4481
+ if (bh.length === 0 || vh.length === 0 || mh.length === 0) return 0;
4482
+ const b = TaEngine.oldest(bh) ?? 0.5;
4483
+ const bScore = b * 2 - 1;
4484
+ const vNew = vh[0];
4485
+ const vOld = TaEngine.oldest(vh);
4486
+ const vChange = vNew !== null && vOld !== null && vNew !== 0 ? ((vOld - vNew) / vNew) * 100 : 0;
4487
+ const m = TaEngine.oldest(mh) ?? 0;
4488
+ const gdp = bScore * 1.5 + vChange / 100 + m / 100;
4489
+ return Math.max(-2, Math.min(4, gdp));
4490
+ }
4491
+
4492
+ /** `advanced.py` `_builtin_ta_inflation_proxy_indicator` (newest-first lists). */
4493
+ inflationProxyIndicator(site: string, usdIndex: unknown, commodities: unknown, bondYields: unknown): number {
4494
+ const uh = this.windowFor(`${site}:usd`, usdIndex);
4495
+ const ch = this.windowFor(`${site}:comm`, commodities);
4496
+ const yh = this.windowFor(`${site}:yield`, bondYields);
4497
+ if (uh.length === 0 || ch.length === 0 || yh.length === 0) return 0;
4498
+ const uNew = uh[0];
4499
+ const uOld = TaEngine.oldest(uh);
4500
+ const usdChange =
4501
+ uNew !== null && uOld !== null && uNew !== 0 ? -((uOld - uNew) / uNew) * 100 : 0;
4502
+ const cNew = ch[0];
4503
+ const cOld = TaEngine.oldest(ch);
4504
+ const commChange = cNew !== null && cOld !== null && cNew !== 0 ? ((cOld - cNew) / cNew) * 100 : 0;
4505
+ const yNew = yh[0];
4506
+ const yOld = TaEngine.oldest(yh);
4507
+ const yieldsChange = yNew !== null && yOld !== null ? yOld - yNew : 0;
4508
+ const pressure = usdChange * 0.3 + commChange * 0.4 + yieldsChange * 0.3;
4509
+ return Math.max(-100, Math.min(100, pressure * 10));
4510
+ }
4511
+
4512
+ /** `common.py` `_builtin_ta_expected_value`. */
4513
+ expectedValue(returns: number[], probabilities: number[]): number {
4514
+ if (returns.length !== probabilities.length) return 0;
4515
+ let totalProb = 0;
4516
+ for (const p of probabilities) totalProb += p;
4517
+ if (totalProb === 0) return 0;
4518
+ let ev = 0;
4519
+ for (let i = 0; i < returns.length; i++) ev += returns[i]! * probabilities[i]!;
4520
+ return finiteOut(ev / totalProb) ?? 0;
4521
+ }
4522
+
3014
4523
  /**
3015
4524
  * `ta.volume_profile_high` — numeric list only (`advanced.py`).
3016
4525
  * A one-element pair is that price when its volume is the strict max.
@@ -3077,25 +4586,34 @@ export class TaEngine {
3077
4586
  }
3078
4587
 
3079
4588
  /**
3080
- * `ta.volume_thrust` — bool as 1/0. `[-1]`/`[-2]` are the oldest two
3081
- * samples (`advanced.py`), not the current bar.
4589
+ * `core.py` `_volume_thrust_inc_update`: volume spike plus a >1% price
4590
+ * move. `na` samples fall back to 100 / 1000; the first sample of each
4591
+ * series is skipped when it is `na`.
3082
4592
  */
3083
4593
  volumeThrust(site: string, close: Cell, volume: Cell, volSma: Cell, sensitivity: number): Cell {
3084
4594
  const sens = Number.isFinite(sensitivity) ? sensitivity : 0.3;
3085
4595
  let st = this.volThrustSites.get(site);
3086
4596
  if (st === undefined) {
3087
- st = { close: [], volume: [], sma: [] };
4597
+ st = { closes: [], volume: null, sma: null, nClose: 0, nVolume: 0, nSma: 0 };
3088
4598
  this.volThrustSites.set(site, st);
3089
4599
  }
3090
- this.pushChrono(st.close, close);
3091
- this.pushChrono(st.volume, volume);
3092
- this.pushChrono(st.sma, volSma);
3093
- if (st.close.length === 0 || st.volume.length === 0 || st.sma.length === 0) return 0;
3094
- const numOr = (v: Cell, fallback: number): number => (v !== null && Number.isFinite(v) ? v : fallback);
3095
- const cVal = numOr(this.nf(st.close, -1), 100);
3096
- const cPrev = st.close.length > 1 ? numOr(this.nf(st.close, -2), 100) : 100;
3097
- const vVal = numOr(this.nf(st.volume, -1), 1000);
3098
- const vsVal = numOr(this.nf(st.sma, -1), 1000);
4600
+ if (!(close === null && st.nClose === 0)) {
4601
+ pushCap(st.closes, finiteCell(close), 2);
4602
+ st.nClose += 1;
4603
+ }
4604
+ if (!(volume === null && st.nVolume === 0)) {
4605
+ st.volume = finiteCell(volume);
4606
+ st.nVolume += 1;
4607
+ }
4608
+ if (!(volSma === null && st.nSma === 0)) {
4609
+ st.sma = finiteCell(volSma);
4610
+ st.nSma += 1;
4611
+ }
4612
+ if (st.nClose < 1 || st.nVolume < 1 || st.nSma < 1) return 0;
4613
+ const cVal = st.closes[st.closes.length - 1] ?? 100;
4614
+ const cPrev = st.closes.length > 1 ? (st.closes[st.closes.length - 2] ?? 100) : 100;
4615
+ const vVal = st.volume ?? 1000;
4616
+ const vsVal = st.sma ?? 1000;
3099
4617
  const spike = vVal > vsVal * (1 + sens);
3100
4618
  const move = cPrev !== 0 ? Math.abs(cVal - cPrev) / cPrev > 0.01 : false;
3101
4619
  return spike && move ? 1 : 0;