@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/CHANGELOG.md +35 -0
- package/README.md +6 -4
- package/dist/browser.js +68 -68
- package/dist/index.js +2551 -205
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/generated/PinescriptLexerBase.ts +22 -4
- package/src/parser/LexerBase.ts +22 -4
- package/src/runtime/compile/draw.ts +26 -2
- package/src/runtime/compile/emit_call.ts +485 -6
- package/src/runtime/compile/helpers.ts +2 -1
- package/src/runtime/compile/runtime.ts +20 -5
- package/src/runtime/compile/strategy.ts +5 -2
- package/src/runtime/interpret.ts +680 -41
- package/src/runtime/str.ts +3 -2
- package/src/runtime/strategy.ts +164 -12
- package/src/runtime/ta.ts +1667 -149
- package/src/runtime/time.ts +12 -0
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
|
|
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 (
|
|
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
|
-
/**
|
|
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) /
|
|
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=
|
|
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) /
|
|
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)`.
|
|
2687
|
-
*
|
|
2688
|
-
*
|
|
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
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
const
|
|
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 ===
|
|
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` `
|
|
2786
|
-
* `
|
|
2787
|
-
*
|
|
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
|
-
|
|
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(
|
|
2939
|
+
this.accelSites.set(key, samples);
|
|
2795
2940
|
}
|
|
2796
|
-
pushCap(samples, finiteCell(source),
|
|
2797
|
-
if (samples.length < p + 1
|
|
2798
|
-
const
|
|
2799
|
-
const
|
|
2800
|
-
|
|
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 (
|
|
2947
|
+
if (clean.length < 2) return 0;
|
|
2805
2948
|
let oldSum = 0;
|
|
2806
|
-
for (let i = 0; i <
|
|
2807
|
-
const oldMom = oldSum / (
|
|
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
|
-
*
|
|
2818
|
-
*
|
|
2819
|
-
*
|
|
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(
|
|
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
|
|
2826
|
-
|
|
2827
|
-
|
|
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
|
-
|
|
2830
|
-
|
|
2831
|
-
const
|
|
2832
|
-
|
|
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
|
-
*
|
|
2837
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
2844
|
-
insideBarPattern(): boolean {
|
|
2845
|
-
|
|
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
|
-
*
|
|
2850
|
-
*
|
|
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
|
-
|
|
2855
|
-
|
|
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({
|
|
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
|
-
// `
|
|
2861
|
-
|
|
2862
|
-
private readonly
|
|
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,
|
|
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
|
-
* `
|
|
2922
|
-
*
|
|
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(
|
|
3224
|
+
rsiDivergence(site: string, value: Cell, period: number): Cell {
|
|
2925
3225
|
const p = this.asCount(period);
|
|
2926
|
-
if (p === null ||
|
|
2927
|
-
let
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
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
|
-
|
|
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
|
-
/** `
|
|
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
|
-
|
|
3254
|
+
if (n === null || n <= 0) return 0;
|
|
3255
|
+
let st = this.cumDeltaSites.get(`${site}:${n}`);
|
|
2964
3256
|
if (st === undefined) {
|
|
2965
|
-
st = {
|
|
2966
|
-
this.cumDeltaSites.set(site
|
|
3257
|
+
st = { closes: [], volumes: [], count: 0 };
|
|
3258
|
+
this.cumDeltaSites.set(`${site}:${n}`, st);
|
|
2967
3259
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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
|
-
|
|
2973
|
-
|
|
2974
|
-
const
|
|
2975
|
-
const
|
|
2976
|
-
const
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
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
|
-
/** `
|
|
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
|
-
|
|
3284
|
+
if (n === null || n <= 0) return 0;
|
|
3285
|
+
let st = this.volMomSites.get(`${site}:${n}`);
|
|
2991
3286
|
if (st === undefined) {
|
|
2992
|
-
st = {
|
|
2993
|
-
this.volMomSites.set(site
|
|
3287
|
+
st = { window: [] };
|
|
3288
|
+
this.volMomSites.set(`${site}:${n}`, st);
|
|
2994
3289
|
}
|
|
2995
|
-
|
|
2996
|
-
if (
|
|
2997
|
-
|
|
2998
|
-
for (let i = 0; i < n
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
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
|
-
* `
|
|
3081
|
-
*
|
|
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 = {
|
|
4597
|
+
st = { closes: [], volume: null, sma: null, nClose: 0, nVolume: 0, nSma: 0 };
|
|
3088
4598
|
this.volThrustSites.set(site, st);
|
|
3089
4599
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
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;
|