@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/CHANGELOG.md +21 -0
- package/README.md +6 -4
- package/dist/browser.js +68 -68
- package/dist/index.js +1719 -197
- 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 +324 -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 +447 -28
- package/src/runtime/str.ts +3 -2
- package/src/runtime/strategy.ts +164 -12
- package/src/runtime/ta.ts +1090 -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,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
|
|
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 (
|
|
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
|
-
/**
|
|
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) /
|
|
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=
|
|
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) /
|
|
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)`.
|
|
2687
|
-
*
|
|
2688
|
-
*
|
|
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
|
-
|
|
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);
|
|
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 ===
|
|
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` `
|
|
2786
|
-
* `
|
|
2787
|
-
*
|
|
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
|
-
|
|
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(
|
|
2935
|
+
this.accelSites.set(key, samples);
|
|
2795
2936
|
}
|
|
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);
|
|
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 (
|
|
2943
|
+
if (clean.length < 2) return 0;
|
|
2805
2944
|
let oldSum = 0;
|
|
2806
|
-
for (let i = 0; i <
|
|
2807
|
-
const oldMom = oldSum / (
|
|
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
|
-
*
|
|
2818
|
-
*
|
|
2819
|
-
*
|
|
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(
|
|
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
|
|
2826
|
-
|
|
2827
|
-
|
|
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
|
-
|
|
2830
|
-
|
|
2831
|
-
const
|
|
2832
|
-
|
|
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
|
-
*
|
|
2837
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
2844
|
-
insideBarPattern(): boolean {
|
|
2845
|
-
|
|
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
|
-
*
|
|
2850
|
-
*
|
|
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
|
-
|
|
2855
|
-
|
|
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({
|
|
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
|
-
// `
|
|
2861
|
-
|
|
2862
|
-
private readonly
|
|
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,
|
|
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
|
-
* `
|
|
2922
|
-
*
|
|
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(
|
|
3171
|
+
rsiDivergence(site: string, value: Cell, period: number): Cell {
|
|
2925
3172
|
const p = this.asCount(period);
|
|
2926
|
-
if (p === null ||
|
|
2927
|
-
let
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
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
|
-
|
|
3194
|
+
const range = hi - lo;
|
|
3195
|
+
return range > 0 ? finiteOut(((x - lo) / range) * 2 - 1) : 0;
|
|
2958
3196
|
}
|
|
2959
3197
|
|
|
2960
|
-
/** `
|
|
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
|
-
|
|
3201
|
+
if (n === null || n <= 0) return 0;
|
|
3202
|
+
let st = this.cumDeltaSites.get(`${site}:${n}`);
|
|
2964
3203
|
if (st === undefined) {
|
|
2965
|
-
st = {
|
|
2966
|
-
this.cumDeltaSites.set(site
|
|
3204
|
+
st = { closes: [], volumes: [], count: 0 };
|
|
3205
|
+
this.cumDeltaSites.set(`${site}:${n}`, st);
|
|
2967
3206
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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
|
-
|
|
2973
|
-
|
|
2974
|
-
const
|
|
2975
|
-
const
|
|
2976
|
-
const
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
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
|
-
/** `
|
|
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
|
-
|
|
3231
|
+
if (n === null || n <= 0) return 0;
|
|
3232
|
+
let st = this.volMomSites.get(`${site}:${n}`);
|
|
2991
3233
|
if (st === undefined) {
|
|
2992
|
-
st = {
|
|
2993
|
-
this.volMomSites.set(site
|
|
3234
|
+
st = { window: [] };
|
|
3235
|
+
this.volMomSites.set(`${site}:${n}`, st);
|
|
2994
3236
|
}
|
|
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);
|
|
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
|
-
* `
|
|
3081
|
-
*
|
|
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 = {
|
|
4020
|
+
st = { closes: [], volume: null, sma: null, nClose: 0, nVolume: 0, nSma: 0 };
|
|
3088
4021
|
this.volThrustSites.set(site, st);
|
|
3089
4022
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
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;
|