@hoox-sh/pynets 0.5.0 → 0.6.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,22 @@ 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>();
462
598
 
463
599
  sma(site: string, source: Cell, period: number): Cell {
464
600
  const n = pinePeriod(period);
@@ -527,7 +663,7 @@ export class TaEngine {
527
663
  return rmaStep(st, finiteCell(source), n);
528
664
  }
529
665
 
530
- /** `ta.atr(length)` ≡ `ta.rma(ta.tr, length)`. First bar is na (no prev close). */
666
+ /** `ta.atr` ≡ `ta.rma(ta.tr, length)`. Bar 0 seeds TR with `high - low` (0.6.32). */
531
667
  atr(site: string, high: Cell, low: Cell, close: Cell, period: number): Cell {
532
668
  const n = pinePeriod(period);
533
669
  if (n === null) return null;
@@ -536,17 +672,23 @@ export class TaEngine {
536
672
  st = {
537
673
  period: n,
538
674
  prevClose: null,
675
+ started: false,
539
676
  rma: { period: n, rma: null, seeded: false, started: false, seedBuf: [] },
540
677
  };
541
678
  this.atrSites.set(site, st);
542
679
  }
543
- const prev = st.prevClose;
544
- st.prevClose = finiteCell(close);
545
- if (prev === null) return null;
546
680
  const h = finiteCell(high);
547
681
  const l = finiteCell(low);
548
682
  const c = finiteCell(close);
549
- if (h === null || l === null || c === null) return null;
683
+ if (!st.started) {
684
+ st.started = true;
685
+ st.prevClose = c;
686
+ if (h === null || l === null) return null;
687
+ return rmaStep(st.rma, h - l, n);
688
+ }
689
+ const prev = st.prevClose;
690
+ st.prevClose = c;
691
+ if (prev === null || h === null || l === null || c === null) return null;
550
692
  const tr = Math.max(h - l, Math.abs(h - prev), Math.abs(l - prev));
551
693
  return rmaStep(st.rma, tr, n);
552
694
  }
@@ -709,7 +851,7 @@ export class TaEngine {
709
851
  return best;
710
852
  }
711
853
 
712
- /** Sample stdev (ddof=1). Any na in window → na; period<=1 → na. */
854
+ /** Population stdev (ddof=0, Python `_stdev_inc_update`). Any na in window → na; period<=1 → na. */
713
855
  stdev(site: string, source: Cell, period: number): Cell {
714
856
  const n = pinePeriod(period);
715
857
  if (n === null || n <= 1) return null;
@@ -734,7 +876,7 @@ export class TaEngine {
734
876
  st.count += 1;
735
877
  }
736
878
  if (st.window.length < n || st.count !== n) return null;
737
- let v = (st.sumsq - (st.sum * st.sum) / n) / (n - 1);
879
+ let v = (st.sumsq - (st.sum * st.sum) / n) / n;
738
880
  if (v < 0) v = 0;
739
881
  return Math.sqrt(v);
740
882
  }
@@ -813,7 +955,7 @@ export class TaEngine {
813
955
  }
814
956
 
815
957
  /**
816
- * Bollinger: mid=SMA, dev=sample stdev, bands=mid ± mult*dev.
958
+ * Bollinger: mid=SMA, dev=population stdev, bands=mid ± mult*dev.
817
959
  * All null if mid or stdev null.
818
960
  */
819
961
  bb(
@@ -2076,7 +2218,7 @@ export class TaEngine {
2076
2218
  st.count += 1;
2077
2219
  }
2078
2220
  if (st.window.length < n || st.count !== n) return null;
2079
- let v = (st.sumsq - (st.sum * st.sum) / n) / (n - 1);
2221
+ let v = (st.sumsq - (st.sum * st.sum) / n) / n;
2080
2222
  if (v < 0) v = 0;
2081
2223
  return v;
2082
2224
  }
@@ -2683,9 +2825,9 @@ export class TaEngine {
2683
2825
  }
2684
2826
 
2685
2827
  /**
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.
2828
+ * Zigzag tuple `(high, low, direction)`. Incremental SoT
2829
+ * `_zigzag_inc_update`: keep the last two numeric samples (`deque(maxlen=2)`),
2830
+ * skip `na` samples, and compare the threshold against their percent span.
2689
2831
  */
2690
2832
  zigzag(site: string, source: Cell, threshold: number): { high: Cell; low: Cell; dir: number } {
2691
2833
  const th = Number.isFinite(threshold) ? threshold : 5;
@@ -2694,18 +2836,15 @@ export class TaEngine {
2694
2836
  st = { samples: [] };
2695
2837
  this.zigzagSites.set(site, st);
2696
2838
  }
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);
2839
+ const x = finiteCell(source);
2840
+ if (x !== null) pushCap(st.samples, x, 2);
2841
+ if (st.samples.length < 2) return { high: null, low: null, dir: 0 };
2842
+ const oldest = st.samples[0]!;
2843
+ const newest = st.samples[1]!;
2844
+ const recentHigh = Math.max(oldest, newest);
2845
+ const recentLow = Math.min(oldest, newest);
2707
2846
  const pct = recentLow !== 0 ? ((recentHigh - recentLow) / recentLow) * 100 : 0;
2708
- const direction = recentHigh === oldest ? 1 : -1;
2847
+ const direction = recentHigh === newest ? 1 : -1;
2709
2848
  return { high: recentHigh, low: recentLow, dir: pct > th ? 1 : direction };
2710
2849
  }
2711
2850
 
@@ -2782,31 +2921,31 @@ export class TaEngine {
2782
2921
  }
2783
2922
 
2784
2923
  /**
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.
2924
+ * `advanced.py` `_acceleration_factor_inc_update`: last `period + 1`
2925
+ * samples (including `na`), clean numeric, compare the newest against the
2926
+ * mean of the preceding window, clamp to `[-2, 2]`.
2788
2927
  */
2789
2928
  accelerationFactor(site: string, source: Cell, period: Cell): Cell {
2790
2929
  const p = this.coerceTaInt(period, "ta.acceleration_factor() requires 2 arguments");
2791
- let samples = this.accelSites.get(site);
2930
+ if (p <= 0) return 0;
2931
+ const key = `${site}:${p}`;
2932
+ let samples = this.accelSites.get(key);
2792
2933
  if (samples === undefined) {
2793
2934
  samples = [];
2794
- this.accelSites.set(site, samples);
2935
+ this.accelSites.set(key, samples);
2795
2936
  }
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);
2937
+ pushCap(samples, finiteCell(source), p + 1);
2938
+ if (samples.length < p + 1) return 0;
2939
+ const clean: number[] = [];
2940
+ for (const v of samples) {
2941
+ if (v !== null) clean.push(v);
2803
2942
  }
2804
- if (newestFirst.length < 2) return 0;
2943
+ if (clean.length < 2) return 0;
2805
2944
  let oldSum = 0;
2806
- for (let i = 0; i < newestFirst.length - 1; i++) oldSum += newestFirst[i]!;
2807
- const oldMom = oldSum / (newestFirst.length - 1);
2945
+ for (let i = 0; i < clean.length - 1; i++) oldSum += clean[i]!;
2946
+ const oldMom = oldSum / (clean.length - 1);
2947
+ const newMom = clean[clean.length - 1]!;
2808
2948
  if (Math.abs(oldMom) < 1e-10) return 0;
2809
- const newMom = newestFirst[newestFirst.length - 1]!;
2810
2949
  let acc = (newMom - oldMom) / oldMom;
2811
2950
  if (acc < -2) acc = -2;
2812
2951
  if (acc > 2) acc = 2;
@@ -2814,55 +2953,162 @@ export class TaEngine {
2814
2953
  }
2815
2954
 
2816
2955
  /**
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.
2956
+ * `core.py` `_stoch_smooth_inc_update`: raw %K over the last `period`
2957
+ * highs/lows (all must be numeric), then EMA(%K, smooth_k) and
2958
+ * EMA(that, smooth_d) with SMA-seeded EMA state.
2820
2959
  */
2821
- stochSmooth(high: Cell, low: Cell, close: Cell, period: Cell, smoothK: Cell, smoothD: Cell): Cell {
2960
+ stochSmooth(
2961
+ site: string,
2962
+ high: Cell,
2963
+ low: Cell,
2964
+ close: Cell,
2965
+ period: Cell,
2966
+ smoothK: Cell,
2967
+ smoothD: Cell,
2968
+ ): Cell {
2822
2969
  const p = this.coerceTaInt(period, "ta.stoch_smooth period must be integer");
2823
2970
  const k = this.coerceTaInt(smoothK, "ta.stoch_smooth smooth_k must be integer");
2824
2971
  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'");
2972
+ if (p < 1 || k < 1 || d < 1) return null;
2973
+ const key = `${site}:${p}:${k}:${d}`;
2974
+ let st = this.stochSmoothSites.get(key);
2975
+ if (st === undefined) {
2976
+ st = { highs: [], lows: [] };
2977
+ this.stochSmoothSites.set(key, st);
2828
2978
  }
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);
2979
+ pushCap(st.highs, finiteCell(high), p);
2980
+ pushCap(st.lows, finiteCell(low), p);
2981
+ const c = finiteCell(close);
2982
+ let stoch: Cell = null;
2983
+ if (c !== null && !st.highs.includes(null) && !st.lows.includes(null)) {
2984
+ let hh = -Infinity;
2985
+ let ll = Infinity;
2986
+ for (const v of st.highs) {
2987
+ if (v! > hh) hh = v!;
2988
+ }
2989
+ for (const v of st.lows) {
2990
+ if (v! < ll) ll = v!;
2991
+ }
2992
+ stoch = hh === ll ? 50 : (100 * (c - ll)) / (hh - ll);
2993
+ }
2994
+ const kVal = this.ema(`${site}:k`, stoch, k);
2995
+ return this.ema(`${site}:d`, kVal, d);
2833
2996
  }
2834
2997
 
2835
2998
  /**
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.
2999
+ * `core.py` `_engulfing_inc_update`: last two opens/closes; warmup is two
3000
+ * bars. High/low are still required by the caller but unused.
2838
3001
  */
2839
- engulfing(): TaRecord {
2840
- return taRec({ is_bullish: false, is_bearish: false, pattern_strength: 0 });
3002
+ engulfing(site: string, open: Cell, close: Cell): TaRecord {
3003
+ const off = taRec({ is_bullish: false, is_bearish: false, pattern_strength: 0 });
3004
+ let st = this.engulfingSites.get(site);
3005
+ if (st === undefined) {
3006
+ st = { prevOpen: null, prevClose: null, count: 0 };
3007
+ this.engulfingSites.set(site, st);
3008
+ }
3009
+ const o = finiteCell(open);
3010
+ const c = finiteCell(close);
3011
+ st.count += 1;
3012
+ const prevO = st.prevOpen;
3013
+ const prevC = st.prevClose;
3014
+ if (st.count < 2 || prevO === null || prevC === null || o === null || c === null) {
3015
+ st.prevOpen = o;
3016
+ st.prevClose = c;
3017
+ return off;
3018
+ }
3019
+ const isBullish = o < prevC && c > prevO && c > o;
3020
+ const isBearish = o > prevC && c < prevO && c < o;
3021
+ let engulf = 0;
3022
+ if (isBullish) {
3023
+ engulf = Math.max(0, Math.min(1, (c - prevO) / Math.abs(prevO - prevC + 0.0001)));
3024
+ } else if (isBearish) {
3025
+ engulf = Math.max(0, Math.min(1, (prevO - c) / Math.abs(prevC - prevO + 0.0001)));
3026
+ }
3027
+ st.prevOpen = o;
3028
+ st.prevClose = c;
3029
+ return taRec({
3030
+ is_bullish: isBullish,
3031
+ is_bearish: isBearish,
3032
+ pattern_strength: finiteOut(engulf) ?? 0,
3033
+ });
2841
3034
  }
2842
3035
 
2843
- /** Same one-element series: `advanced.py` `_builtin_ta_inside_bar_pattern` is always false. */
2844
- insideBarPattern(): boolean {
2845
- return false;
3036
+ /** `core.py` `_inside_bar_pattern_inc_update`: last two highs/lows. */
3037
+ insideBarPattern(site: string, high: Cell, low: Cell): boolean {
3038
+ let st = this.insideBarSites.get(site);
3039
+ if (st === undefined) {
3040
+ st = { prevHigh: null, prevLow: null, count: 0 };
3041
+ this.insideBarSites.set(site, st);
3042
+ }
3043
+ const currH = high !== null && Number.isFinite(high) ? high : 0;
3044
+ const currL = low !== null && Number.isFinite(low) ? low : 0;
3045
+ st.count += 1;
3046
+ const prevH = st.prevHigh;
3047
+ const prevL = st.prevLow;
3048
+ if (st.count < 2 || prevH === null || prevL === null) {
3049
+ st.prevHigh = currH;
3050
+ st.prevLow = currL;
3051
+ return false;
3052
+ }
3053
+ const val = currH < prevH && currL > prevL;
3054
+ st.prevHigh = currH;
3055
+ st.prevLow = currL;
3056
+ return val;
2846
3057
  }
2847
3058
 
2848
3059
  /**
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.
3060
+ * `core.py` `_double_top_bottom_inc_update`: last `period` highs/lows,
3061
+ * local-maxima peaks seeded with the first numeric high. `na` period raises.
2852
3062
  */
2853
- doubleTopBottom(period: Cell): TaRecord {
2854
- if (period === null || !Number.isFinite(period)) {
2855
- throw new Error("period must be integer. Got: na");
3063
+ doubleTopBottom(site: string, high: Cell, low: Cell, period: Cell): TaRecord {
3064
+ const n = this.coerceTaInt(period, "period must be integer");
3065
+ const off = taRec({ pattern_type: "none", strength: 0, breakout_level: 0 });
3066
+ if (n < 1) return off;
3067
+ const key = `${site}:${n}`;
3068
+ let st = this.doubleTopBottomSites.get(key);
3069
+ if (st === undefined) {
3070
+ st = { highs: [], lows: [], count: 0 };
3071
+ this.doubleTopBottomSites.set(key, st);
3072
+ }
3073
+ pushCap(st.highs, finiteCell(high), n);
3074
+ pushCap(st.lows, finiteCell(low), n);
3075
+ st.count += 1;
3076
+ if (st.count < n) return off;
3077
+ const recentHigh: number[] = [];
3078
+ for (const v of st.highs) {
3079
+ if (v !== null) recentHigh.push(v);
3080
+ }
3081
+ const recentLow: number[] = [];
3082
+ for (const v of st.lows) {
3083
+ if (v !== null) recentLow.push(v);
3084
+ }
3085
+ if (recentHigh.length < 3 || recentLow.length === 0) return off;
3086
+ const peaks = [recentHigh[0]!];
3087
+ for (let i = 1; i < recentHigh.length - 1; i++) {
3088
+ if (recentHigh[i]! > recentHigh[i - 1]! && recentHigh[i]! > recentHigh[i + 1]!) {
3089
+ peaks.push(recentHigh[i]!);
3090
+ }
3091
+ }
3092
+ if (peaks.length < 2) return off;
3093
+ const peakDiff = Math.abs(peaks[peaks.length - 1]! - peaks[peaks.length - 2]!);
3094
+ const avgPeak = (peaks[peaks.length - 1]! + peaks[peaks.length - 2]!) / 2;
3095
+ const strength = avgPeak > 0 ? 1 - Math.min(1, peakDiff / avgPeak) : 0;
3096
+ let minLow = recentLow[0]!;
3097
+ for (const v of recentLow) {
3098
+ if (v < minLow) minLow = v;
2856
3099
  }
2857
- return taRec({ pattern_type: "none", strength: 0, breakout_level: 0 });
3100
+ return taRec({
3101
+ pattern_type: "double_top",
3102
+ strength: finiteOut(strength) ?? 0,
3103
+ breakout_level: finiteOut(minLow - avgPeak * 0.1) ?? 0,
3104
+ });
2858
3105
  }
2859
3106
 
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[] }>();
3107
+ // `core.py` incremental call-site state for the wave10 detectors.
3108
+ private readonly cumDeltaSites = new Map<string, CumulativeDeltaState>();
3109
+ private readonly volMomSites = new Map<string, VolumeMomentumState>();
2864
3110
  private readonly volProfileLowSites = new Map<string, { close: Cell[]; volume: Cell[] }>();
2865
- private readonly volThrustSites = new Map<string, { close: Cell[]; volume: Cell[]; sma: Cell[] }>();
3111
+ private readonly volThrustSites = new Map<string, VolumeThrustState>();
2866
3112
 
2867
3113
  private asCount(value: number): number | null {
2868
3114
  if (!Number.isFinite(value)) return null;
@@ -2918,92 +3164,84 @@ export class TaEngine {
2918
3164
  }
2919
3165
 
2920
3166
  /**
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.
3167
+ * `core.py` `_rsi_divergence_inc_update`: last `period + 1` RSI samples,
3168
+ * scaled to `[-1, 1]`. Warmup (`count <= period`) and degenerate windows
3169
+ * return `0`.
2923
3170
  */
2924
- rsiDivergence(series: Cell[], period: number): Cell {
3171
+ rsiDivergence(site: string, value: Cell, period: number): Cell {
2925
3172
  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;
3173
+ if (p === null || p < 1) return 0;
3174
+ let st = this.rsiDivergenceSites.get(`${site}:${p}`);
3175
+ if (st === undefined) {
3176
+ st = { window: [], count: 0 };
3177
+ this.rsiDivergenceSites.set(`${site}:${p}`, st);
3178
+ }
3179
+ const x = finiteCell(value);
3180
+ pushCap(st.window, x, p + 1);
3181
+ st.count += 1;
3182
+ if (st.count <= p) return 0;
3183
+ const vals: number[] = [];
3184
+ for (const v of st.window) {
3185
+ if (v !== null) vals.push(v);
3186
+ }
3187
+ if (vals.length < 2 || x === null) return 0;
3188
+ let lo = vals[0]!;
3189
+ let hi = vals[0]!;
3190
+ for (const v of vals) {
3191
+ if (v < lo) lo = v;
3192
+ if (v > hi) hi = v;
2956
3193
  }
2957
- return finiteOut(last);
3194
+ const range = hi - lo;
3195
+ return range > 0 ? finiteOut(((x - lo) / range) * 2 - 1) : 0;
2958
3196
  }
2959
3197
 
2960
- /** `ta.cumulative_delta` — oldest `period` bars (`advanced.py`). */
3198
+ /** `core.py` `_cumulative_delta_inc_update`: signed volume over the last `period` bars. */
2961
3199
  cumulativeDelta(site: string, close: Cell, volume: Cell, period: number): Cell {
2962
3200
  const n = this.asCount(period);
2963
- let st = this.cumDeltaSites.get(site);
3201
+ if (n === null || n <= 0) return 0;
3202
+ let st = this.cumDeltaSites.get(`${site}:${n}`);
2964
3203
  if (st === undefined) {
2965
- st = { close: [], volume: [] };
2966
- this.cumDeltaSites.set(site, st);
3204
+ st = { closes: [], volumes: [], count: 0 };
3205
+ this.cumDeltaSites.set(`${site}:${n}`, st);
2967
3206
  }
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;
3207
+ const c = finiteCell(close);
3208
+ const v = volume !== null && Number.isFinite(volume) ? volume : 0;
3209
+ pushCap(st.closes, c, n + 1);
3210
+ pushCap(st.volumes, v, n);
3211
+ st.count += 1;
3212
+ if (st.count < n) return 0;
2971
3213
  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
- }
3214
+ const offset = st.closes.length - st.volumes.length;
3215
+ for (let j = 0; j < st.volumes.length; j++) {
3216
+ const idx = offset + j;
3217
+ const cRaw = st.closes[idx] ?? null;
3218
+ const cur = cRaw ?? 0;
3219
+ const prevRaw = idx === 0 ? null : (st.closes[idx - 1] ?? null);
3220
+ const prev = prevRaw ?? cur;
3221
+ const vol = st.volumes[j]!;
3222
+ if (cur > prev) delta += vol;
3223
+ else if (cur < prev) delta -= vol;
2983
3224
  }
2984
3225
  return finiteOut(delta);
2985
3226
  }
2986
3227
 
2987
- /** `ta.volume_momentum` — oldest `period + 1` volumes (`advanced.py`). */
3228
+ /** `core.py` `_volume_momentum_inc_update`: ROC of the period SMA of volume. */
2988
3229
  volumeMomentum(site: string, volume: Cell, period: number): Cell {
2989
3230
  const n = this.asCount(period);
2990
- let st = this.volMomSites.get(site);
3231
+ if (n === null || n <= 0) return 0;
3232
+ let st = this.volMomSites.get(`${site}:${n}`);
2991
3233
  if (st === undefined) {
2992
- st = { volume: [] };
2993
- this.volMomSites.set(site, st);
3234
+ st = { window: [] };
3235
+ this.volMomSites.set(`${site}:${n}`, st);
2994
3236
  }
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);
3237
+ if (volume !== null && Number.isFinite(volume)) pushCap(st.window, volume, n + 1);
3238
+ if (st.window.length < n + 1) return 0;
3239
+ let oldSum = 0;
3240
+ for (let i = 0; i < n; i++) oldSum += st.window[i]!;
3241
+ let newSum = 0;
3242
+ for (let i = 1; i <= n; i++) newSum += st.window[i]!;
3243
+ const oldVol = oldSum / n;
3244
+ const newVol = newSum / n;
3007
3245
  if (oldVol === 0) return 0;
3008
3246
  let momentum = ((newVol - oldVol) / oldVol) * 100;
3009
3247
  if (momentum > 100) momentum = 100;
@@ -3011,6 +3249,700 @@ export class TaEngine {
3011
3249
  return finiteOut(momentum);
3012
3250
  }
3013
3251
 
3252
+ /** `moving_averages.py` `_sma_weighted_inc_update`: linear / quadratic / sqrt weights. */
3253
+ smaWeighted(site: string, source: Cell, period: Cell, weightType: unknown): Cell {
3254
+ const n = this.coerceTaInt(period, "period must be integer");
3255
+ const kind = typeof weightType === "string" ? weightType : "linear";
3256
+ if (n < 1) return null;
3257
+ const key = `${site}:${n}:${kind}`;
3258
+ let st = this.smaWeightedSites.get(key);
3259
+ if (st === undefined) {
3260
+ const weights: number[] = [];
3261
+ for (let i = 0; i < n; i++) {
3262
+ if (kind === "quadratic") weights.push((i + 1) ** 2);
3263
+ else if (kind === "sqrt") weights.push((i + 1) ** 0.5);
3264
+ else weights.push(i + 1);
3265
+ }
3266
+ st = { window: [], weights, wsum: weights.reduce((a, b) => a + b, 0) };
3267
+ this.smaWeightedSites.set(key, st);
3268
+ }
3269
+ pushCap(st.window, finiteCell(source), n);
3270
+ if (st.window.length < n || st.window.includes(null) || st.wsum <= 0) return null;
3271
+ let total = 0;
3272
+ for (let i = 0; i < n; i++) total += st.window[i]! * st.weights[i]!;
3273
+ return finiteOut(total / st.wsum);
3274
+ }
3275
+
3276
+ /** `volume.py` `_apo_inc_update`: `EMA(fast) - EMA(slow)`. */
3277
+ apo(site: string, source: Cell, fast: Cell, slow: Cell): Cell {
3278
+ const f = this.coerceTaInt(fast, "ta.apo fast_period must be integer");
3279
+ const s = this.coerceTaInt(slow, "ta.apo slow_period must be integer");
3280
+ const fastV = this.ema(`${site}:fast`, source, f);
3281
+ const slowV = this.ema(`${site}:slow`, source, s);
3282
+ if (fastV === null || slowV === null) return null;
3283
+ return finiteOut(fastV - slowV);
3284
+ }
3285
+
3286
+ /** `volatility.py` `_atr_normalized_inc_update`: ATR as a percent of close. */
3287
+ atrNormalized(site: string, high: Cell, low: Cell, close: Cell, period: Cell): Cell {
3288
+ const n = this.coerceTaInt(period, "period must be integer");
3289
+ const atr = this.atr(`${site}:atr`, high, low, close, n);
3290
+ const c = finiteCell(close);
3291
+ if (c === null || c === 0 || atr === null) return null;
3292
+ return finiteOut((atr / c) * 100);
3293
+ }
3294
+
3295
+ private pairWindow(
3296
+ sites: Map<string, PairWindowState>,
3297
+ site: string,
3298
+ a: Cell,
3299
+ b: Cell,
3300
+ length: Cell,
3301
+ ): { a: number[]; b: number[] } | null {
3302
+ const n = this.coerceTaInt(length, "length must be integer");
3303
+ if (n < 1) return null;
3304
+ const key = `${site}:${n}`;
3305
+ let st = sites.get(key);
3306
+ if (st === undefined) {
3307
+ st = { a: [], b: [] };
3308
+ sites.set(key, st);
3309
+ }
3310
+ pushCap(st.a, finiteCell(a), n);
3311
+ pushCap(st.b, finiteCell(b), n);
3312
+ if (st.a.length < n || st.b.length < n) return null;
3313
+ const left: number[] = [];
3314
+ const right: number[] = [];
3315
+ for (let i = 0; i < n; i++) {
3316
+ const av = st.a[i]!;
3317
+ const bv = st.b[i]!;
3318
+ if (av === null || bv === null) return null;
3319
+ left.push(av);
3320
+ right.push(bv);
3321
+ }
3322
+ return { a: left, b: right };
3323
+ }
3324
+
3325
+ /** `volatility.py` `_beta_inc_update`: `cov(a,b) / var(b)`. */
3326
+ beta(site: string, a: Cell, b: Cell, length: Cell): Cell {
3327
+ const pair = this.pairWindow(this.betaSites, site, a, b, length);
3328
+ if (pair === null) return null;
3329
+ const n = pair.a.length;
3330
+ let meanA = 0;
3331
+ let meanB = 0;
3332
+ for (let i = 0; i < n; i++) {
3333
+ meanA += pair.a[i]!;
3334
+ meanB += pair.b[i]!;
3335
+ }
3336
+ meanA /= n;
3337
+ meanB /= n;
3338
+ let covariance = 0;
3339
+ let varianceB = 0;
3340
+ for (let i = 0; i < n; i++) {
3341
+ covariance += (pair.a[i]! - meanA) * (pair.b[i]! - meanB);
3342
+ varianceB += (pair.b[i]! - meanB) ** 2;
3343
+ }
3344
+ covariance /= n;
3345
+ varianceB /= n;
3346
+ if (varianceB === 0) return 0;
3347
+ return finiteOut(covariance / varianceB);
3348
+ }
3349
+
3350
+ /** `volatility.py` `_r_squared_inc_update`: `corr(a,b)^2` clipped to [0, 1]. */
3351
+ rSquared(site: string, a: Cell, b: Cell, length: Cell): Cell {
3352
+ const pair = this.pairWindow(this.rSquaredSites, site, a, b, length);
3353
+ if (pair === null) return null;
3354
+ const n = pair.a.length;
3355
+ let meanA = 0;
3356
+ let meanB = 0;
3357
+ for (let i = 0; i < n; i++) {
3358
+ meanA += pair.a[i]!;
3359
+ meanB += pair.b[i]!;
3360
+ }
3361
+ meanA /= n;
3362
+ meanB /= n;
3363
+ let covariance = 0;
3364
+ let varA = 0;
3365
+ let varB = 0;
3366
+ for (let i = 0; i < n; i++) {
3367
+ covariance += (pair.a[i]! - meanA) * (pair.b[i]! - meanB);
3368
+ varA += (pair.a[i]! - meanA) ** 2;
3369
+ varB += (pair.b[i]! - meanB) ** 2;
3370
+ }
3371
+ covariance /= n;
3372
+ varA /= n;
3373
+ varB /= n;
3374
+ if (varA === 0 || varB === 0) return 0;
3375
+ const corr = covariance / Math.sqrt(varA * varB);
3376
+ return finiteOut(Math.max(0, Math.min(1, corr ** 2)));
3377
+ }
3378
+
3379
+ /** `volatility.py` `_comovement_inc_update`: percent of same-sign bar deltas. */
3380
+ comovement(site: string, a: Cell, b: Cell, length: Cell): Cell {
3381
+ const pair = this.pairWindow(this.comovementSites, site, a, b, length);
3382
+ if (pair === null) return null;
3383
+ const n = pair.a.length;
3384
+ if (n <= 1) return 0;
3385
+ let same = 0;
3386
+ for (let i = 1; i < n; i++) {
3387
+ if ((pair.a[i]! - pair.a[i - 1]!) * (pair.b[i]! - pair.b[i - 1]!) > 0) same += 1;
3388
+ }
3389
+ return finiteOut((same / (n - 1)) * 100);
3390
+ }
3391
+
3392
+ /** `common.py` `_skewness_inc_update`: population third central moment / std³. */
3393
+ skewness(site: string, source: Cell, period: Cell): Cell {
3394
+ const n = this.coerceTaInt(period, "period must be integer");
3395
+ if (n < 1) return null;
3396
+ const key = `${site}:${n}`;
3397
+ let st = this.skewnessSites.get(key);
3398
+ if (st === undefined) {
3399
+ st = { window: [] };
3400
+ this.skewnessSites.set(key, st);
3401
+ }
3402
+ pushCap(st.window, finiteCell(source), n);
3403
+ if (st.window.length < n || st.window.includes(null)) return null;
3404
+ const vals = st.window as number[];
3405
+ let mean = 0;
3406
+ for (const v of vals) mean += v;
3407
+ mean /= n;
3408
+ let variance = 0;
3409
+ for (const v of vals) variance += (v - mean) ** 2;
3410
+ variance /= n;
3411
+ const std = Math.sqrt(variance);
3412
+ if (std === 0) return 0;
3413
+ let third = 0;
3414
+ for (const v of vals) third += (v - mean) ** 3;
3415
+ return finiteOut(third / (n * std ** 3));
3416
+ }
3417
+
3418
+ /** `common.py` `_kurtosis_inc_update`: excess kurtosis. */
3419
+ kurtosis(site: string, source: Cell, period: Cell): Cell {
3420
+ const n = this.coerceTaInt(period, "period must be integer");
3421
+ if (n < 1) return null;
3422
+ const key = `${site}:${n}`;
3423
+ let st = this.kurtosisSites.get(key);
3424
+ if (st === undefined) {
3425
+ st = { window: [] };
3426
+ this.kurtosisSites.set(key, st);
3427
+ }
3428
+ pushCap(st.window, finiteCell(source), n);
3429
+ if (st.window.length < n || st.window.includes(null)) return null;
3430
+ const vals = st.window as number[];
3431
+ let mean = 0;
3432
+ for (const v of vals) mean += v;
3433
+ mean /= n;
3434
+ let variance = 0;
3435
+ for (const v of vals) variance += (v - mean) ** 2;
3436
+ variance /= n;
3437
+ const std = Math.sqrt(variance);
3438
+ if (std === 0) return 0;
3439
+ let fourth = 0;
3440
+ for (const v of vals) fourth += (v - mean) ** 4;
3441
+ fourth /= n;
3442
+ return finiteOut(fourth / std ** 4 - 3);
3443
+ }
3444
+
3445
+ /** `volume.py` `_volume_weighted_momentum_inc_update`. */
3446
+ volumeWeightedMomentum(site: string, source: Cell, volume: Cell, length: Cell): Cell {
3447
+ const n = this.coerceTaInt(length, "length must be integer");
3448
+ if (n < 1) return null;
3449
+ const key = `${site}:${n}`;
3450
+ let st = this.vwmSites.get(key);
3451
+ if (st === undefined) {
3452
+ st = { prices: [], volumes: [] };
3453
+ this.vwmSites.set(key, st);
3454
+ }
3455
+ pushCap(st.prices, finiteCell(source), n + 1);
3456
+ pushCap(st.volumes, finiteCell(volume), n);
3457
+ if (st.volumes.length < n) return null;
3458
+ const ps = st.prices;
3459
+ const vs = st.volumes;
3460
+ let momentumSum = 0;
3461
+ let volumeSum = 0;
3462
+ for (let i = 0; i < n; i++) {
3463
+ const pEnd = ps.length - 1 - i;
3464
+ const pPrev = pEnd - 1;
3465
+ if (pPrev < 0) break;
3466
+ const pv = ps[pEnd] ?? null;
3467
+ const pp = ps[pPrev] ?? null;
3468
+ if (pv === null || pp === null) return null;
3469
+ const vol = vs[vs.length - 1 - i] ?? 0;
3470
+ momentumSum += (pv - pp) * vol;
3471
+ volumeSum += vol;
3472
+ }
3473
+ return finiteOut(volumeSum > 0 ? momentumSum / volumeSum : 0);
3474
+ }
3475
+
3476
+ /** `common.py` `_parkinson`: volatility from the high/low ratio. */
3477
+ parkinson(high: Cell, low: Cell): Cell {
3478
+ const h = finiteCell(high);
3479
+ const l = finiteCell(low);
3480
+ if (h === null || l === null || h <= l) return null;
3481
+ const ratio = h / l;
3482
+ if (ratio <= 0) return null;
3483
+ return finiteOut(Math.sqrt(Math.log(ratio) ** 2 / (4 * Math.log(2))));
3484
+ }
3485
+
3486
+ /** `common.py` `_garman_klass`: OHLC volatility. */
3487
+ garmanKlass(high: Cell, low: Cell, close: Cell, open: Cell): Cell {
3488
+ const h = finiteCell(high);
3489
+ const l = finiteCell(low);
3490
+ const c = finiteCell(close);
3491
+ const o = finiteCell(open);
3492
+ if (h === null || l === null || c === null || o === null) return null;
3493
+ if (h <= l || h <= 0 || c <= 0) return null;
3494
+ const term1 = 0.5 * Math.log(h / l) ** 2;
3495
+ const term2 = (2 * Math.log(2) - 1) * Math.log(c / o) ** 2;
3496
+ return finiteOut(Math.sqrt(term1 - term2));
3497
+ }
3498
+
3499
+ /** `core.py` `_mean_reversion_score_inc_update`: period is a warmup gate. */
3500
+ meanReversionScore(site: string, close: Cell, sma: Cell, stdev: Cell, period: Cell): number {
3501
+ const n = this.coerceTaInt(period, "ta.mean_reversion_score() requires 4 arguments");
3502
+ if (n <= 0) return 50;
3503
+ let st = this.meanReversionSites.get(`${site}:${n}`);
3504
+ if (st === undefined) {
3505
+ st = { countClose: 0, countSma: 0 };
3506
+ this.meanReversionSites.set(`${site}:${n}`, st);
3507
+ }
3508
+ st.countClose += 1;
3509
+ st.countSma += 1;
3510
+ if (st.countClose < n || st.countSma < n) return 50;
3511
+ const c = finiteCell(close) ?? 100;
3512
+ const s = finiteCell(sma) ?? 100;
3513
+ const sd = finiteCell(stdev) ?? 1;
3514
+ if (sd === 0) return 50;
3515
+ return Math.max(0, Math.min(100, Math.min(100, (Math.abs(c - s) / sd) * 20)));
3516
+ }
3517
+
3518
+ /** `core.py` `_market_condition_inc_update`. */
3519
+ marketCondition(site: string, close: Cell, atr: Cell, smaPeriod: Cell, stdevPeriod: Cell): string {
3520
+ const smaN = this.coerceTaInt(smaPeriod, "sma period must be integer");
3521
+ const stdevN = this.coerceTaInt(stdevPeriod, "stdev period must be integer");
3522
+ if (smaN <= 0 || stdevN <= 0) return "ranging";
3523
+ const win = Math.max(smaN, stdevN);
3524
+ const key = `${site}:${smaN}:${stdevN}`;
3525
+ let st = this.marketConditionSites.get(key);
3526
+ if (st === undefined) {
3527
+ st = { window: [], atr: null };
3528
+ this.marketConditionSites.set(key, st);
3529
+ }
3530
+ pushCap(st.window, finiteCell(close), win);
3531
+ st.atr = finiteCell(atr);
3532
+ if (st.window.length < win) return "ranging";
3533
+ const cLast = st.window[st.window.length - 1] ?? null;
3534
+ const currentClose = cLast ?? 0;
3535
+ const currentAtr = st.atr ?? 1;
3536
+ const currentSma = smaOf(st.window, smaN) ?? currentClose;
3537
+ const stdevVal = stdevOf(st.window, stdevN);
3538
+ if (stdevVal !== null && stdevVal > currentAtr * 1.5) return "volatile";
3539
+ if (currentClose > currentSma && currentAtr > 0.5) return "trending_up";
3540
+ if (currentClose < currentSma && currentAtr > 0.5) return "trending_down";
3541
+ return "ranging";
3542
+ }
3543
+
3544
+ /** `core.py` `_order_flow_imbalance_inc_update`. */
3545
+ orderFlowImbalance(
3546
+ site: string,
3547
+ high: Cell,
3548
+ low: Cell,
3549
+ close: Cell,
3550
+ volume: Cell,
3551
+ period: Cell,
3552
+ ): number {
3553
+ const n = this.coerceTaInt(period, "period must be integer");
3554
+ if (n <= 0) return 0;
3555
+ const key = `${site}:${n}`;
3556
+ let st = this.orderFlowSites.get(key);
3557
+ if (st === undefined) {
3558
+ st = { highs: [], lows: [], closes: [], volumes: [] };
3559
+ this.orderFlowSites.set(key, st);
3560
+ }
3561
+ pushCap(st.highs, finiteCell(high), n);
3562
+ pushCap(st.lows, finiteCell(low), n);
3563
+ pushCap(st.closes, finiteCell(close), n);
3564
+ pushCap(st.volumes, finiteCell(volume), n);
3565
+ if (st.highs.length < n || st.lows.length < n) return 0;
3566
+ let buy = 0;
3567
+ let sell = 0;
3568
+ for (let i = 0; i < n; i++) {
3569
+ const h = st.highs[i] ?? 0;
3570
+ const l = st.lows[i] ?? 0;
3571
+ const c = st.closes[i] ?? 0;
3572
+ const v = st.volumes[i] ?? 0;
3573
+ if (h > l) {
3574
+ const mid = (h + l) / 2;
3575
+ if (c > mid) buy += v;
3576
+ else sell += v;
3577
+ }
3578
+ }
3579
+ const total = buy + sell;
3580
+ if (total === 0) return 0;
3581
+ return finiteOut((buy - sell) / total) ?? 0;
3582
+ }
3583
+
3584
+ /** `core.py` `_liquidity_score_inc_update`. */
3585
+ liquidityScore(site: string, volume: Cell, volatility: Cell, spread: Cell, period: Cell): number {
3586
+ const n = this.coerceTaInt(period, "period must be integer");
3587
+ if (n <= 0) return 50;
3588
+ const key = `${site}:${n}`;
3589
+ let st = this.liquiditySites.get(key);
3590
+ if (st === undefined) {
3591
+ st = { volume: [], volatility: [], spread: [] };
3592
+ this.liquiditySites.set(key, st);
3593
+ }
3594
+ pushCap(st.volume, finiteCell(volume), n);
3595
+ pushCap(st.volatility, finiteCell(volatility), n);
3596
+ pushCap(st.spread, finiteCell(spread), n);
3597
+ if (st.volume.length < n || st.volatility.length < n || st.spread.length < n) return 50;
3598
+ let volSum = 0;
3599
+ for (const v of st.volume) if (v !== null) volSum += v;
3600
+ let volaSum = 0;
3601
+ for (const v of st.volatility) if (v !== null) volaSum += v;
3602
+ let spreadSum = 0;
3603
+ for (const v of st.spread) if (v !== null) spreadSum += v;
3604
+ const volScore = Math.min(100, volSum / n / 100);
3605
+ const volatilityScore = Math.max(0, 100 - (volaSum / n) * 50);
3606
+ const spreadScore = Math.max(0, 100 - (spreadSum / n) * 100);
3607
+ const liquidity = volScore * 0.4 + volatilityScore * 0.3 + spreadScore * 0.3;
3608
+ return Math.max(0, Math.min(100, liquidity));
3609
+ }
3610
+
3611
+ /** `core.py` `_fear_greed_index_inc_update` — last samples only. */
3612
+ fearGreedIndex(rsi: Cell, vix: Cell, putCall: Cell, breadth: Cell): number {
3613
+ const r = finiteCell(rsi) ?? 50;
3614
+ const v = finiteCell(vix) ?? 2;
3615
+ const pc = finiteCell(putCall) ?? 1;
3616
+ const b = finiteCell(breadth) ?? 0.5;
3617
+ const index = ((r - 50) * 1 + (v - 2) * 10 + (1 - pc) * 50 + (b - 0.5) * 100) / 4;
3618
+ return Math.max(-100, Math.min(100, index));
3619
+ }
3620
+
3621
+ /** `core.py` `_crowd_sentiment_inc_update` — last samples only. */
3622
+ crowdSentiment(priceAgreement: Cell, volumeAgreement: Cell, timeAgreement: Cell): number {
3623
+ const p = finiteCell(priceAgreement) ?? 0.5;
3624
+ const v = finiteCell(volumeAgreement) ?? 0.5;
3625
+ const t = finiteCell(timeAgreement) ?? 0.5;
3626
+ return Math.max(0, Math.min(100, ((p + v + t) / 3) * 100));
3627
+ }
3628
+
3629
+ /** `core.py` `_smart_money_flow_inc_update` — last samples only. */
3630
+ smartMoneyFlow(priceChange: Cell, volume: Cell, timeHigh: Cell, timeLow: Cell): number {
3631
+ const p = finiteCell(priceChange) ?? 0;
3632
+ const v = finiteCell(volume) ?? 1000;
3633
+ const th = finiteCell(timeHigh) ?? 10;
3634
+ const tl = finiteCell(timeLow) ?? 10;
3635
+ const volFactor = Math.min(1, v / 5000);
3636
+ let flow = 0;
3637
+ if (p > 0 && th < tl) flow = volFactor * 0.8;
3638
+ else if (p < 0 && tl < th) flow = -volFactor * 0.8;
3639
+ return Math.max(-1, Math.min(1, flow));
3640
+ }
3641
+
3642
+ /** `core.py` `_trend_confirmation_score_inc_update` — last samples only. */
3643
+ trendConfirmationScore(
3644
+ momentum: Cell,
3645
+ trendAlignment: Cell,
3646
+ strength: Cell,
3647
+ rsi: Cell,
3648
+ rsiAlignment: Cell,
3649
+ ): number {
3650
+ const m = finiteCell(momentum) ?? 0;
3651
+ const t = finiteCell(trendAlignment) ?? 0;
3652
+ const s = finiteCell(strength) ?? 1;
3653
+ const r = finiteCell(rsi) ?? 50;
3654
+ const a = finiteCell(rsiAlignment) ?? 0;
3655
+ const momentumScore = Math.min(100, Math.abs(m) * 20);
3656
+ const trendScore = Math.max(0, ((t + 1) / 2) * 100);
3657
+ const strengthScore = Math.min(100, s * 50);
3658
+ const rsiScore = Math.abs(r - 50) > 10 ? Math.min(100, Math.abs(r - 50) * 2) : 40;
3659
+ const alignmentBonus = Math.abs(a) > 0.5 ? 20 : 0;
3660
+ const total =
3661
+ momentumScore * 0.25 + trendScore * 0.3 + strengthScore * 0.25 + rsiScore * 0.15 + alignmentBonus;
3662
+ return Math.max(0, Math.min(100, total));
3663
+ }
3664
+
3665
+ /** `core.py` `_max_loss_level_inc_update` — last samples only. */
3666
+ maxLossLevel(entry: Cell, account: Cell, maxLossPct: Cell): number {
3667
+ const e = finiteCell(entry) ?? 100;
3668
+ const a = finiteCell(account) ?? 10000;
3669
+ const p = finiteCell(maxLossPct) ?? 1;
3670
+ const maxLossAmount = a * (p / 100);
3671
+ if (e <= 0) return 0;
3672
+ const shares = a / e;
3673
+ const stop = shares > 0 ? e - maxLossAmount / shares : 0;
3674
+ return Math.max(0, stop);
3675
+ }
3676
+
3677
+ /** `core.py` `_profit_lock_level_inc_update` — last samples only. */
3678
+ profitLockLevel(entry: Cell, current: Cell, trailPct: Cell, direction: Cell): number {
3679
+ const e = finiteCell(entry) ?? 100;
3680
+ const c = finiteCell(current) ?? 100;
3681
+ const t = Math.max(0, Math.min(1, finiteCell(trailPct) ?? 0.05));
3682
+ const d = finiteCell(direction) ?? 1;
3683
+ return d > 0 ? Math.max(e * 0.9, c - c * t) : Math.min(e * 1.1, c + c * t);
3684
+ }
3685
+
3686
+ /** `advanced.py` `_builtin_ta_position_sizing`. */
3687
+ positionSizing(account: Cell, riskPct: Cell, entry: Cell, stop: Cell): number {
3688
+ const a = finiteCell(account) ?? 10000;
3689
+ const r = finiteCell(riskPct) ?? 0.01;
3690
+ const e = finiteCell(entry) ?? 100;
3691
+ const s = finiteCell(stop) ?? 95;
3692
+ const riskAmount = a * (r / 100);
3693
+ const distance = e - s;
3694
+ if (Math.abs(distance) < 1e-10) return 0;
3695
+ return Math.max(0, riskAmount / Math.abs(distance));
3696
+ }
3697
+
3698
+ /** `advanced.py` `_builtin_ta_kelly_criterion`. */
3699
+ kellyCriterion(winRate: Cell, avgWin: Cell, avgLoss: Cell): number {
3700
+ const wr = Math.max(0, Math.min(1, finiteCell(winRate) ?? 0.5));
3701
+ const win = finiteCell(avgWin) ?? 1;
3702
+ const loss = finiteCell(avgLoss) ?? 1;
3703
+ if (Math.abs(win) < 1e-10) return 0;
3704
+ const kelly = (wr * win - (1 - wr) * loss) / win;
3705
+ return Math.max(0, kelly);
3706
+ }
3707
+
3708
+ /** `advanced.py` `_builtin_ta_risk_reward_ratio`. */
3709
+ riskRewardRatio(entry: Cell, stop: Cell, target: Cell): Cell {
3710
+ const e = finiteCell(entry) ?? 0;
3711
+ const s = finiteCell(stop) ?? 0;
3712
+ const t = finiteCell(target) ?? 0;
3713
+ const risk = e - s;
3714
+ if (Math.abs(risk) < 1e-10) return null;
3715
+ return finiteOut((t - e) / risk);
3716
+ }
3717
+
3718
+ /** `strategies.py` `_builtin_ta_risk_reward_asymmetry`. */
3719
+ riskRewardAsymmetry(entry: Cell, stop: Cell, target: Cell, winRate: Cell): TaRecord {
3720
+ const e = finiteCell(entry) ?? 0;
3721
+ const s = finiteCell(stop) ?? 0;
3722
+ const t = finiteCell(target) ?? 0;
3723
+ const wr = finiteCell(winRate) ?? 0.5;
3724
+ const risk = Math.abs(e - s);
3725
+ const reward = Math.abs(t - e);
3726
+ const ratio = risk > 0 ? reward / risk : 0;
3727
+ const ev = wr * reward - (1 - wr) * risk;
3728
+ const kelly = ratio > 0 ? wr - (1 - wr) / ratio : 0;
3729
+ return taRec({
3730
+ ratio: finiteOut(ratio) ?? 0,
3731
+ expected_value: finiteOut(ev) ?? 0,
3732
+ kelly_percent: finiteOut(kelly * 100) ?? 0,
3733
+ is_favorable: ev > 0,
3734
+ risk_reward_ratio: finiteOut(ratio) ?? 0,
3735
+ kelly_percentage: finiteOut(kelly * 100) ?? 0,
3736
+ });
3737
+ }
3738
+
3739
+ /** `advanced.py` `_builtin_ta_probability_of_movement`. */
3740
+ probabilityOfMovement(current: Cell, target: Cell, atr: Cell, period: Cell): number {
3741
+ const c = finiteCell(current) ?? 100;
3742
+ const t = finiteCell(target) ?? 100;
3743
+ const a = finiteCell(atr) ?? 1;
3744
+ const n = this.coerceTaInt(period, "period must be integer");
3745
+ if (Math.abs(c) < 1e-10 || Math.abs(a) < 1e-10) return 0.5;
3746
+ const distance = Math.abs(t - c);
3747
+ const expected = a * Math.sqrt(n);
3748
+ if (expected === 0) return 0.5;
3749
+ return Math.max(0, Math.min(1, Math.min(1, distance / expected) * 0.8 + 0.1));
3750
+ }
3751
+
3752
+ /** `advanced.py` `_builtin_ta_trend_strength`. */
3753
+ trendStrength(price: Cell, adx: Cell, rsi: Cell): number {
3754
+ const a = finiteCell(adx) ?? 20;
3755
+ const r = finiteCell(rsi) ?? 50;
3756
+ const adxNorm = Math.min(100, Math.max(0, a));
3757
+ const rsiExtremeness = Math.abs(r - 50) / 50;
3758
+ return Math.min(100, Math.max(0, adxNorm * 0.6 + rsiExtremeness * 40));
3759
+ }
3760
+
3761
+ /** `strategies.py` `_builtin_ta_breakeven_level`. */
3762
+ breakevenLevel(entry: Cell, qty: Cell, commission: Cell, slippage: Cell, direction: Cell): TaRecord {
3763
+ const e = finiteCell(entry) ?? 0;
3764
+ const q = finiteCell(qty) ?? 0;
3765
+ const comm = finiteCell(commission) ?? 0;
3766
+ const slip = finiteCell(slippage) ?? 0;
3767
+ const d = finiteCell(direction) ?? 1;
3768
+ const cost = comm + slip * q;
3769
+ const impact = q > 0 ? cost / q : 0;
3770
+ const breakeven = d >= 0 ? e + impact : e - impact;
3771
+ const move = e > 0 ? (Math.abs(breakeven - e) / e) * 100 : 0;
3772
+ return taRec({
3773
+ breakeven_price: finiteOut(breakeven) ?? 0,
3774
+ total_cost: finiteOut(cost) ?? 0,
3775
+ move_required_percent: finiteOut(move) ?? 0,
3776
+ });
3777
+ }
3778
+
3779
+ /** `strategies.py` `_builtin_ta_market_structure_pivot` (bar-mode last close). */
3780
+ marketStructurePivot(close: Cell): TaRecord {
3781
+ const c = finiteCell(close) ?? 0;
3782
+ return taRec({ pivot_price: c, structure: "fractal", type: "high" });
3783
+ }
3784
+
3785
+ /** `strategies.py` `_builtin_ta_position_sizing_score`. */
3786
+ positionSizingScore(risk: Cell, volatility: Cell, conviction: Cell, correlation: Cell): TaRecord {
3787
+ const r = finiteCell(risk) ?? 0;
3788
+ const v = finiteCell(volatility) ?? 0;
3789
+ const c = finiteCell(conviction) ?? 0;
3790
+ const corr = finiteCell(correlation) ?? 0;
3791
+ let score = (r / (v + 0.1)) * c * (1 - corr);
3792
+ score = Math.max(0, Math.min(1, score * 0.1));
3793
+ return taRec({
3794
+ score: finiteOut(score * 100) ?? 0,
3795
+ position_size_ratio: finiteOut(score) ?? 0,
3796
+ kelly_fraction: finiteOut(score * 0.5) ?? 0,
3797
+ correlation_adjustment: finiteOut(1 - corr) ?? 0,
3798
+ });
3799
+ }
3800
+
3801
+ /** `strategies.py` `_builtin_ta_trailing_exit_level`. */
3802
+ trailingExitLevel(entry: Cell, current: Cell, atr: Cell, mult: Cell, step: Cell): TaRecord {
3803
+ const e = finiteCell(entry) ?? 0;
3804
+ const c = finiteCell(current) ?? 0;
3805
+ const a = finiteCell(atr) ?? 0;
3806
+ const m = finiteCell(mult) ?? 0;
3807
+ void (finiteCell(step) ?? 0);
3808
+ const trail = c > e ? Math.max(e * 1.001, c - a * m) : c + a * m;
3809
+ return taRec({
3810
+ trail_stop: finiteOut(trail) ?? 0,
3811
+ protected_profit: c > e ? finiteOut(Math.max(0, c - e)) ?? 0 : 0,
3812
+ risk_reward_current: a > 0 ? finiteOut(Math.abs(c - e) / a) ?? 0 : 0,
3813
+ });
3814
+ }
3815
+
3816
+ /** `strategies.py` `_builtin_ta_optimal_entry_zone` (bar-mode last close). */
3817
+ optimalEntryZone(close: Cell): TaRecord {
3818
+ const c = finiteCell(close) ?? 0;
3819
+ return taRec({
3820
+ min: finiteOut(c * 0.99) ?? 0,
3821
+ max: finiteOut(c * 1.01) ?? 0,
3822
+ optimal: c,
3823
+ entry_zone_low: finiteOut(c * 0.99) ?? 0,
3824
+ entry_zone_high: finiteOut(c * 1.01) ?? 0,
3825
+ best_entry: c,
3826
+ zone_strength: 85,
3827
+ });
3828
+ }
3829
+
3830
+ /** `strategies.py` `_builtin_ta_mean_reversion_entry` (bar-mode last samples). */
3831
+ meanReversionEntry(price: Cell, sma: Cell, stdev: Cell, threshold: Cell): TaRecord {
3832
+ const p = finiteCell(price) ?? 0;
3833
+ const s = finiteCell(sma) ?? 0;
3834
+ const sd = finiteCell(stdev) ?? 1;
3835
+ const th = finiteCell(threshold) ?? 0;
3836
+ const z = sd !== 0 ? (p - s) / sd : 0;
3837
+ return taRec({
3838
+ entry_price: 100,
3839
+ stop_loss: 95,
3840
+ take_profit: 105,
3841
+ probability: 0.7,
3842
+ reversion_probability: 0.75,
3843
+ target_price: 105,
3844
+ z_score: finiteOut(z) ?? 0,
3845
+ is_mean_reversion_setup: Math.abs(z) > th,
3846
+ });
3847
+ }
3848
+
3849
+ /** `strategies.py` `_builtin_ta_multi_timeframe_signal`. */
3850
+ multiTimeframeSignal(s1: Cell, s2: Cell, s3: Cell, w1: Cell, w2: Cell, w3: Cell): TaRecord {
3851
+ const score =
3852
+ (finiteCell(s1) ?? 0) * (finiteCell(w1) ?? 0) +
3853
+ (finiteCell(s2) ?? 0) * (finiteCell(w2) ?? 0) +
3854
+ (finiteCell(s3) ?? 0) * (finiteCell(w3) ?? 0);
3855
+ return taRec({ score: finiteOut(score) ?? 0, signal: score > 0.5 ? "buy" : "sell", agreement: true });
3856
+ }
3857
+
3858
+ /** `strategies.py` `_builtin_ta_market_timing_index`. */
3859
+ marketTimingIndex(trend: Cell, momentum: Cell, volatility: Cell, sentiment: Cell): TaRecord {
3860
+ const t = finiteCell(trend) ?? 0;
3861
+ const m = finiteCell(momentum) ?? 0;
3862
+ const v = finiteCell(volatility) ?? 0;
3863
+ const s = finiteCell(sentiment) ?? 0;
3864
+ const score = t * 0.3 + m * 0.3 + v * 0.2 + s * 0.2;
3865
+ let recommendation = "hold";
3866
+ if (score > 80) recommendation = "strong_buy";
3867
+ else if (score > 60) recommendation = "buy";
3868
+ else if (score < 20) recommendation = "strong_sell";
3869
+ else if (score < 40) recommendation = "sell";
3870
+ return taRec({
3871
+ score: finiteOut(score) ?? 0,
3872
+ recommendation,
3873
+ regime: Math.abs(t) > 50 ? "trending" : "ranging",
3874
+ timing_index: finiteOut(score) ?? 0,
3875
+ market_condition: score > 80 ? "optimal_long" : "neutral",
3876
+ confidence: 0.85,
3877
+ });
3878
+ }
3879
+
3880
+ /** `strategies.py` `_builtin_ta_regime_adaptive_signal`. */
3881
+ regimeAdaptiveSignal(
3882
+ strength: Cell,
3883
+ volRegime: unknown,
3884
+ trendRegime: unknown,
3885
+ ): TaRecord {
3886
+ const vol = typeof volRegime === "string" ? volRegime : "";
3887
+ const trend = typeof trendRegime === "string" ? trendRegime : "";
3888
+ let adapted = finiteCell(strength) ?? 0;
3889
+ let fit = 1;
3890
+ if (vol === "high") {
3891
+ adapted *= 0.8;
3892
+ fit *= 0.8;
3893
+ }
3894
+ if (trend === "ranging") {
3895
+ adapted *= 0.5;
3896
+ fit *= 0.5;
3897
+ }
3898
+ return taRec({
3899
+ signal: adapted > 0.5 ? "buy" : "sell",
3900
+ strength: finiteOut(adapted) ?? 0,
3901
+ confidence: finiteOut(adapted * 100) ?? 0,
3902
+ adapted_signal: finiteOut(adapted) ?? 0,
3903
+ regime_fit: finiteOut(fit) ?? 0,
3904
+ strategy_recommendation: trend === "trending" ? "trend_following" : "mean_reversion",
3905
+ signal_confidence: finiteOut(adapted) ?? 0,
3906
+ });
3907
+ }
3908
+
3909
+ /** `advanced.py` `_builtin_ta_strategy_score`. */
3910
+ strategyScore(rsi: Cell, macd: Cell, emaCross: Cell, trend: Cell): number {
3911
+ const r = finiteCell(rsi) ?? 50;
3912
+ const m = finiteCell(macd) ?? 0;
3913
+ const cross = emaCross != null && emaCross !== 0;
3914
+ const t = finiteCell(trend) ?? 50;
3915
+ const rsiNorm = ((r - 50) / 50) * 25;
3916
+ const macdNorm = Math.max(-25, Math.min(25, m * 50));
3917
+ const emaBonus = cross ? 25 : -25;
3918
+ const trendNorm = ((t - 50) / 50) * 25;
3919
+ return Math.max(-100, Math.min(100, rsiNorm + macdNorm + emaBonus + trendNorm));
3920
+ }
3921
+
3922
+ /** `advanced.py` `_builtin_ta_breakout_detection`. */
3923
+ breakoutDetection(close: Cell, resistance: Cell, support: Cell): TaRecord {
3924
+ const c = finiteCell(close) ?? 0;
3925
+ const r = finiteCell(resistance) ?? 0;
3926
+ const s = finiteCell(support) ?? 0;
3927
+ if (c > r) {
3928
+ const strength = r > 0 ? ((c - r) / r) * 100 : 0;
3929
+ return taRec({
3930
+ is_breakout: true,
3931
+ breakout_type: "resistance",
3932
+ breakout_strength: finiteOut(strength) ?? 0,
3933
+ });
3934
+ }
3935
+ if (c < s) {
3936
+ const strength = s > 0 ? ((s - c) / s) * 100 : 0;
3937
+ return taRec({
3938
+ is_breakout: true,
3939
+ breakout_type: "support",
3940
+ breakout_strength: finiteOut(strength) ?? 0,
3941
+ });
3942
+ }
3943
+ return taRec({ is_breakout: false, breakout_type: "none", breakout_strength: 0 });
3944
+ }
3945
+
3014
3946
  /**
3015
3947
  * `ta.volume_profile_high` — numeric list only (`advanced.py`).
3016
3948
  * A one-element pair is that price when its volume is the strict max.
@@ -3077,25 +4009,34 @@ export class TaEngine {
3077
4009
  }
3078
4010
 
3079
4011
  /**
3080
- * `ta.volume_thrust` — bool as 1/0. `[-1]`/`[-2]` are the oldest two
3081
- * samples (`advanced.py`), not the current bar.
4012
+ * `core.py` `_volume_thrust_inc_update`: volume spike plus a >1% price
4013
+ * move. `na` samples fall back to 100 / 1000; the first sample of each
4014
+ * series is skipped when it is `na`.
3082
4015
  */
3083
4016
  volumeThrust(site: string, close: Cell, volume: Cell, volSma: Cell, sensitivity: number): Cell {
3084
4017
  const sens = Number.isFinite(sensitivity) ? sensitivity : 0.3;
3085
4018
  let st = this.volThrustSites.get(site);
3086
4019
  if (st === undefined) {
3087
- st = { close: [], volume: [], sma: [] };
4020
+ st = { closes: [], volume: null, sma: null, nClose: 0, nVolume: 0, nSma: 0 };
3088
4021
  this.volThrustSites.set(site, st);
3089
4022
  }
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);
4023
+ if (!(close === null && st.nClose === 0)) {
4024
+ pushCap(st.closes, finiteCell(close), 2);
4025
+ st.nClose += 1;
4026
+ }
4027
+ if (!(volume === null && st.nVolume === 0)) {
4028
+ st.volume = finiteCell(volume);
4029
+ st.nVolume += 1;
4030
+ }
4031
+ if (!(volSma === null && st.nSma === 0)) {
4032
+ st.sma = finiteCell(volSma);
4033
+ st.nSma += 1;
4034
+ }
4035
+ if (st.nClose < 1 || st.nVolume < 1 || st.nSma < 1) return 0;
4036
+ const cVal = st.closes[st.closes.length - 1] ?? 100;
4037
+ const cPrev = st.closes.length > 1 ? (st.closes[st.closes.length - 2] ?? 100) : 100;
4038
+ const vVal = st.volume ?? 1000;
4039
+ const vsVal = st.sma ?? 1000;
3099
4040
  const spike = vVal > vsVal * (1 + sens);
3100
4041
  const move = cPrev !== 0 ? Math.abs(cVal - cPrev) / cPrev > 0.01 : false;
3101
4042
  return spike && move ? 1 : 0;