@hoox-sh/pynets 0.3.0 → 0.5.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 +30 -0
- package/README.md +7 -5
- package/dist/browser.js +66 -66
- package/dist/index.js +2009 -47
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/runtime/compile/collections.ts +4 -0
- package/src/runtime/compile/draw.ts +609 -19
- package/src/runtime/compile/emit.ts +98 -1
- package/src/runtime/compile/emit_call.ts +366 -10
- package/src/runtime/compile/runtime.ts +71 -1
- package/src/runtime/compile/strategy.ts +253 -1
- package/src/runtime/footprint.ts +272 -0
- package/src/runtime/interpret.ts +325 -8
- package/src/runtime/request.ts +19 -4
- package/src/runtime/ta.ts +392 -0
package/src/runtime/ta.ts
CHANGED
|
@@ -2708,6 +2708,398 @@ export class TaEngine {
|
|
|
2708
2708
|
const direction = recentHigh === oldest ? 1 : -1;
|
|
2709
2709
|
return { high: recentHigh, low: recentLow, dir: pct > th ? 1 : direction };
|
|
2710
2710
|
}
|
|
2711
|
+
|
|
2712
|
+
private readonly accelSites = new Map<string, Cell[]>();
|
|
2713
|
+
|
|
2714
|
+
/** Python `pine_expect_int`: near-integers round, other finites floor. `na` throws. */
|
|
2715
|
+
private coerceTaInt(value: Cell, message: string): number {
|
|
2716
|
+
if (value === null || !Number.isFinite(value)) throw new Error(`${message}. Got: na`);
|
|
2717
|
+
const nearest = Math.round(value);
|
|
2718
|
+
if (Math.abs(value - nearest) <= PERIOD_INT_EPS) return nearest;
|
|
2719
|
+
return Math.floor(value);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* Current-bar hammer / doji (`patterns.py` `_hammer`). Both flags can be
|
|
2724
|
+
* true; strength prefers doji. `na` OHLC raises (Python `TypeError`).
|
|
2725
|
+
*/
|
|
2726
|
+
hammer(open: Cell, high: Cell, low: Cell, close: Cell): TaRecord {
|
|
2727
|
+
if (open === null || high === null || low === null || close === null) {
|
|
2728
|
+
throw new Error("bad operand type for abs(): 'NoneType'");
|
|
2729
|
+
}
|
|
2730
|
+
const body = Math.abs(close - open);
|
|
2731
|
+
const range = high - low;
|
|
2732
|
+
const lower = Math.min(open, close) - low;
|
|
2733
|
+
const upper = high - Math.max(open, close);
|
|
2734
|
+
const isDoji = body < range * 0.1;
|
|
2735
|
+
const isHammer = body > 0 && lower > body * 2 && upper < body;
|
|
2736
|
+
let strength = 0;
|
|
2737
|
+
if (isDoji) strength = 1 - body / (range + 0.0001);
|
|
2738
|
+
else if (isHammer) strength = Math.min(1, lower / (range + 0.0001));
|
|
2739
|
+
return taRec({
|
|
2740
|
+
is_hammer: isHammer,
|
|
2741
|
+
is_doji: isDoji,
|
|
2742
|
+
pattern_strength: Number.isFinite(strength) ? strength : null,
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
/**
|
|
2747
|
+
* Gap vs an explicit prev-close scalar (`patterns.py` `_gap_detector`).
|
|
2748
|
+
* `na` prev-close is the missing-scalar sentinel (zeros), not an error.
|
|
2749
|
+
* Numeric prev with `na` high/low raises, matching Python arithmetic.
|
|
2750
|
+
*/
|
|
2751
|
+
gapDetector(high: Cell, low: Cell, prevClose: Cell): TaRecord {
|
|
2752
|
+
const none = taRec({ gap_size: 0, gap_type: 0, gap_percent: 0 });
|
|
2753
|
+
if (prevClose === null) return none;
|
|
2754
|
+
if (high === null || low === null) {
|
|
2755
|
+
throw new Error("unsupported operand type(s) for -: 'float' and 'NoneType'");
|
|
2756
|
+
}
|
|
2757
|
+
const upside = Math.max(0, low - prevClose);
|
|
2758
|
+
const downside = Math.max(0, prevClose - high);
|
|
2759
|
+
if (upside > downside) {
|
|
2760
|
+
const pct = prevClose !== 0 ? (upside / prevClose) * 100 : 0;
|
|
2761
|
+
return taRec({
|
|
2762
|
+
gap_size: upside,
|
|
2763
|
+
gap_type: 1,
|
|
2764
|
+
gap_percent: Number.isFinite(pct) ? pct : null,
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
if (downside > 0) {
|
|
2768
|
+
const pct = prevClose !== 0 ? (downside / prevClose) * 100 : 0;
|
|
2769
|
+
return taRec({
|
|
2770
|
+
gap_size: downside,
|
|
2771
|
+
gap_type: -1,
|
|
2772
|
+
gap_percent: Number.isFinite(pct) ? pct : null,
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
return none;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
/** `macd_line - signal_line` (`oscillators.py`). Either `na` → `na`. */
|
|
2779
|
+
macdSignal(macdLine: Cell, signalLine: Cell): Cell {
|
|
2780
|
+
if (macdLine === null || signalLine === null) return null;
|
|
2781
|
+
return finiteOut(macdLine - signalLine);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
/**
|
|
2785
|
+
* `advanced.py` `_builtin_ta_acceleration_factor` (wins over economics).
|
|
2786
|
+
* `_expect_list` is newest-first PineSeries history, cap 1000, so
|
|
2787
|
+
* `[-period-1:]` is the oldest window — the value sticks once warm.
|
|
2788
|
+
*/
|
|
2789
|
+
accelerationFactor(site: string, source: Cell, period: Cell): Cell {
|
|
2790
|
+
const p = this.coerceTaInt(period, "ta.acceleration_factor() requires 2 arguments");
|
|
2791
|
+
let samples = this.accelSites.get(site);
|
|
2792
|
+
if (samples === undefined) {
|
|
2793
|
+
samples = [];
|
|
2794
|
+
this.accelSites.set(site, samples);
|
|
2795
|
+
}
|
|
2796
|
+
pushCap(samples, finiteCell(source), 1000);
|
|
2797
|
+
if (samples.length < p + 1 || p <= 0) return 0;
|
|
2798
|
+
const oldest = samples.slice(0, p + 1);
|
|
2799
|
+
const newestFirst: number[] = [];
|
|
2800
|
+
for (let i = oldest.length - 1; i >= 0; i--) {
|
|
2801
|
+
const v = oldest[i];
|
|
2802
|
+
if (v !== null) newestFirst.push(v);
|
|
2803
|
+
}
|
|
2804
|
+
if (newestFirst.length < 2) return 0;
|
|
2805
|
+
let oldSum = 0;
|
|
2806
|
+
for (let i = 0; i < newestFirst.length - 1; i++) oldSum += newestFirst[i]!;
|
|
2807
|
+
const oldMom = oldSum / (newestFirst.length - 1);
|
|
2808
|
+
if (Math.abs(oldMom) < 1e-10) return 0;
|
|
2809
|
+
const newMom = newestFirst[newestFirst.length - 1]!;
|
|
2810
|
+
let acc = (newMom - oldMom) / oldMom;
|
|
2811
|
+
if (acc < -2) acc = -2;
|
|
2812
|
+
if (acc > 2) acc = 2;
|
|
2813
|
+
return finiteOut(acc);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/**
|
|
2817
|
+
* Bar mode sees a 1-element OHLC list (`oscillators.py` `_builtin_ta_stoch_smooth`).
|
|
2818
|
+
* Raw %K is the current bar; `_ema` is `na` unless both smooth lengths are 1.
|
|
2819
|
+
* Period `<= 0` makes `max()` of an empty window raise.
|
|
2820
|
+
*/
|
|
2821
|
+
stochSmooth(high: Cell, low: Cell, close: Cell, period: Cell, smoothK: Cell, smoothD: Cell): Cell {
|
|
2822
|
+
const p = this.coerceTaInt(period, "ta.stoch_smooth period must be integer");
|
|
2823
|
+
const k = this.coerceTaInt(smoothK, "ta.stoch_smooth smooth_k must be integer");
|
|
2824
|
+
const d = this.coerceTaInt(smoothD, "ta.stoch_smooth smooth_d must be integer");
|
|
2825
|
+
if (p <= 0) throw new Error("max() iterable argument is empty");
|
|
2826
|
+
if (high === null || low === null || close === null) {
|
|
2827
|
+
throw new Error("unsupported operand type(s) for /: 'float' and 'NoneType'");
|
|
2828
|
+
}
|
|
2829
|
+
if (k !== 1 || d !== 1) return null;
|
|
2830
|
+
const span = high - low;
|
|
2831
|
+
const stoch = span !== 0 ? (100 * (close - low)) / span : 50;
|
|
2832
|
+
return finiteOut(stoch);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
/**
|
|
2836
|
+
* Bar-mode OHLC is a PineSeries, not a list (`len < 2`). Sentinel every bar
|
|
2837
|
+
* (`patterns.py` `_builtin_ta_engulfing`) — a lookback port would disagree.
|
|
2838
|
+
*/
|
|
2839
|
+
engulfing(): TaRecord {
|
|
2840
|
+
return taRec({ is_bullish: false, is_bearish: false, pattern_strength: 0 });
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
/** Same one-element series: `advanced.py` `_builtin_ta_inside_bar_pattern` is always false. */
|
|
2844
|
+
insideBarPattern(): boolean {
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
/**
|
|
2849
|
+
* One-element high/low never fills the window (`patterns.py`
|
|
2850
|
+
* `_builtin_ta_double_top_bottom`). Any finite period is the none-sentinel;
|
|
2851
|
+
* `na` period still raises.
|
|
2852
|
+
*/
|
|
2853
|
+
doubleTopBottom(period: Cell): TaRecord {
|
|
2854
|
+
if (period === null || !Number.isFinite(period)) {
|
|
2855
|
+
throw new Error("period must be integer. Got: na");
|
|
2856
|
+
}
|
|
2857
|
+
return taRec({ pattern_type: "none", strength: 0, breakout_level: 0 });
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
// `_expect_list` materializes PineSeries history newest-first (cap 1000).
|
|
2861
|
+
// Negative indexes are the oldest samples, not the latest bars.
|
|
2862
|
+
private readonly cumDeltaSites = new Map<string, { close: Cell[]; volume: Cell[] }>();
|
|
2863
|
+
private readonly volMomSites = new Map<string, { volume: Cell[] }>();
|
|
2864
|
+
private readonly volProfileLowSites = new Map<string, { close: Cell[]; volume: Cell[] }>();
|
|
2865
|
+
private readonly volThrustSites = new Map<string, { close: Cell[]; volume: Cell[]; sma: Cell[] }>();
|
|
2866
|
+
|
|
2867
|
+
private asCount(value: number): number | null {
|
|
2868
|
+
if (!Number.isFinite(value)) return null;
|
|
2869
|
+
const nearest = Math.round(value);
|
|
2870
|
+
return Math.abs(value - nearest) <= PERIOD_INT_EPS ? nearest : Math.floor(value);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
private pushChrono(buf: Cell[], value: Cell): void {
|
|
2874
|
+
buf.push(value !== null && Number.isFinite(value) ? value : null);
|
|
2875
|
+
if (buf.length > 1000) buf.shift();
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
/** Newest-first negative index into a chronological buffer. */
|
|
2879
|
+
private nf(buf: Cell[], i: number): Cell {
|
|
2880
|
+
const v = buf[-i - 1];
|
|
2881
|
+
return v === undefined ? null : v;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
/** `ta.voi` — non-numeric inputs are 0 (`volume.py`). */
|
|
2885
|
+
voi(buy: Cell, sell: Cell): Cell {
|
|
2886
|
+
const b = buy !== null && Number.isFinite(buy) ? buy : 0;
|
|
2887
|
+
const s = sell !== null && Number.isFinite(sell) ? sell : 0;
|
|
2888
|
+
const total = b + s;
|
|
2889
|
+
if (total === 0) return 0;
|
|
2890
|
+
return finiteOut((b - s) / total);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/** `ta.bid_ask_imbalance` — `{imbalance_ratio, spread}` (`volume.py`). */
|
|
2894
|
+
bidAskImbalance(bidSize: Cell, askSize: Cell, bidPrice: Cell, askPrice: Cell): TaRecord {
|
|
2895
|
+
const bid = bidSize !== null && Number.isFinite(bidSize) ? bidSize : 0;
|
|
2896
|
+
const ask = askSize !== null && Number.isFinite(askSize) ? askSize : 0;
|
|
2897
|
+
const bidPx = bidPrice !== null && Number.isFinite(bidPrice) ? bidPrice : 0;
|
|
2898
|
+
const askPx = askPrice !== null && Number.isFinite(askPrice) ? askPrice : 0;
|
|
2899
|
+
const total = bid + ask;
|
|
2900
|
+
if (total === 0) return taRec({ imbalance_ratio: 0, spread: 0 });
|
|
2901
|
+
return taRec({
|
|
2902
|
+
imbalance_ratio: finiteOut((bid - ask) / total),
|
|
2903
|
+
spread: finiteOut(bidPx > 0 ? askPx - bidPx : 0),
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
/** `ta.rsi_oversold_overbought` — current sample only (`oscillators.py`). */
|
|
2908
|
+
rsiOversoldOverbought(rsi: Cell, oversold: number, overbought: number): TaRecord {
|
|
2909
|
+
if (rsi === null || !Number.isFinite(rsi)) {
|
|
2910
|
+
return taRec({ is_oversold: false, is_overbought: false, rsi: null });
|
|
2911
|
+
}
|
|
2912
|
+
const os = this.asCount(oversold);
|
|
2913
|
+
const ob = this.asCount(overbought);
|
|
2914
|
+
if (os === null || ob === null) {
|
|
2915
|
+
return taRec({ is_oversold: false, is_overbought: false, rsi });
|
|
2916
|
+
}
|
|
2917
|
+
return taRec({ is_oversold: rsi < os, is_overbought: rsi > ob, rsi });
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
/**
|
|
2921
|
+
* `ta.rsi_divergence` — last element of the list Python returns.
|
|
2922
|
+
* A scalar is a one-element series (`i < period` → 0). Do not accumulate.
|
|
2923
|
+
*/
|
|
2924
|
+
rsiDivergence(series: Cell[], period: number): Cell {
|
|
2925
|
+
const p = this.asCount(period);
|
|
2926
|
+
if (p === null || series.length === 0) return null;
|
|
2927
|
+
let last = 0;
|
|
2928
|
+
for (let i = 0; i < series.length; i++) {
|
|
2929
|
+
if (i < p) {
|
|
2930
|
+
last = 0;
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
const start = Math.max(0, i - p);
|
|
2934
|
+
const vals: number[] = [];
|
|
2935
|
+
for (let j = start; j <= i; j++) {
|
|
2936
|
+
const v = series[j];
|
|
2937
|
+
if (v !== null && Number.isFinite(v)) vals.push(v);
|
|
2938
|
+
}
|
|
2939
|
+
if (vals.length < 2) {
|
|
2940
|
+
last = 0;
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
let rsiMin = vals[0]!;
|
|
2944
|
+
let rsiMax = vals[0]!;
|
|
2945
|
+
for (const v of vals) {
|
|
2946
|
+
if (v < rsiMin) rsiMin = v;
|
|
2947
|
+
if (v > rsiMax) rsiMax = v;
|
|
2948
|
+
}
|
|
2949
|
+
const cur = series[i];
|
|
2950
|
+
const range = rsiMax - rsiMin;
|
|
2951
|
+
if (cur === null || !Number.isFinite(cur) || range <= 0) {
|
|
2952
|
+
last = 0;
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
last = ((cur - rsiMin) / range) * 2 - 1;
|
|
2956
|
+
}
|
|
2957
|
+
return finiteOut(last);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
/** `ta.cumulative_delta` — oldest `period` bars (`advanced.py`). */
|
|
2961
|
+
cumulativeDelta(site: string, close: Cell, volume: Cell, period: number): Cell {
|
|
2962
|
+
const n = this.asCount(period);
|
|
2963
|
+
let st = this.cumDeltaSites.get(site);
|
|
2964
|
+
if (st === undefined) {
|
|
2965
|
+
st = { close: [], volume: [] };
|
|
2966
|
+
this.cumDeltaSites.set(site, st);
|
|
2967
|
+
}
|
|
2968
|
+
this.pushChrono(st.close, close);
|
|
2969
|
+
this.pushChrono(st.volume, volume);
|
|
2970
|
+
if (n === null || n <= 0 || st.close.length < n || st.volume.length < n) return 0;
|
|
2971
|
+
let delta = 0;
|
|
2972
|
+
for (let i = -n; i < 0; i++) {
|
|
2973
|
+
const cRaw = this.nf(st.close, i);
|
|
2974
|
+
const vRaw = this.nf(st.volume, i);
|
|
2975
|
+
const c = cRaw !== null && Number.isFinite(cRaw) ? cRaw : 0;
|
|
2976
|
+
const v = vRaw !== null && Number.isFinite(vRaw) ? vRaw : 0;
|
|
2977
|
+
if (i > -n) {
|
|
2978
|
+
const prevRaw = this.nf(st.close, i - 1);
|
|
2979
|
+
const prev = prevRaw !== null && Number.isFinite(prevRaw) ? prevRaw : c;
|
|
2980
|
+
if (c > prev) delta += v;
|
|
2981
|
+
else if (c < prev) delta -= v;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return finiteOut(delta);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
/** `ta.volume_momentum` — oldest `period + 1` volumes (`advanced.py`). */
|
|
2988
|
+
volumeMomentum(site: string, volume: Cell, period: number): Cell {
|
|
2989
|
+
const n = this.asCount(period);
|
|
2990
|
+
let st = this.volMomSites.get(site);
|
|
2991
|
+
if (st === undefined) {
|
|
2992
|
+
st = { volume: [] };
|
|
2993
|
+
this.volMomSites.set(site, st);
|
|
2994
|
+
}
|
|
2995
|
+
this.pushChrono(st.volume, volume);
|
|
2996
|
+
if (n === null || n <= 0 || st.volume.length < n + 1) return 0;
|
|
2997
|
+
const clean: number[] = [];
|
|
2998
|
+
for (let i = 0; i < n + 1; i++) {
|
|
2999
|
+
const v = st.volume[i];
|
|
3000
|
+
if (v !== null && Number.isFinite(v)) clean.push(v);
|
|
3001
|
+
}
|
|
3002
|
+
if (clean.length < 2) return 0;
|
|
3003
|
+
const newVol = clean[0]!;
|
|
3004
|
+
let sum = 0;
|
|
3005
|
+
for (let i = 1; i < clean.length; i++) sum += clean[i]!;
|
|
3006
|
+
const oldVol = sum / (clean.length - 1);
|
|
3007
|
+
if (oldVol === 0) return 0;
|
|
3008
|
+
let momentum = ((newVol - oldVol) / oldVol) * 100;
|
|
3009
|
+
if (momentum > 100) momentum = 100;
|
|
3010
|
+
if (momentum < -100) momentum = -100;
|
|
3011
|
+
return finiteOut(momentum);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
/**
|
|
3015
|
+
* `ta.volume_profile_high` — numeric list only (`advanced.py`).
|
|
3016
|
+
* A one-element pair is that price when its volume is the strict max.
|
|
3017
|
+
*/
|
|
3018
|
+
volumeProfileHigh(prices: Cell[], volumes: Cell[]): Cell {
|
|
3019
|
+
if (prices.length === 0 || volumes.length === 0 || prices.length !== volumes.length) return 0;
|
|
3020
|
+
let maxVol = 0;
|
|
3021
|
+
let maxPrice = 0;
|
|
3022
|
+
for (let i = 0; i < prices.length; i++) {
|
|
3023
|
+
const v = volumes[i];
|
|
3024
|
+
if (v === null || !Number.isFinite(v) || !(v > maxVol)) continue;
|
|
3025
|
+
maxVol = v;
|
|
3026
|
+
const p = prices[i];
|
|
3027
|
+
maxPrice = p !== null && Number.isFinite(p) ? p : 0;
|
|
3028
|
+
}
|
|
3029
|
+
return finiteOut(maxPrice);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
/** `ta.volume_profile_low` — buckets over the oldest `period` pairs (`advanced.py`). */
|
|
3033
|
+
volumeProfileLow(site: string, close: Cell, volume: Cell, period: number, levels: number): Cell {
|
|
3034
|
+
const p = this.asCount(period);
|
|
3035
|
+
const lv = this.asCount(levels);
|
|
3036
|
+
let st = this.volProfileLowSites.get(site);
|
|
3037
|
+
if (st === undefined) {
|
|
3038
|
+
st = { close: [], volume: [] };
|
|
3039
|
+
this.volProfileLowSites.set(site, st);
|
|
3040
|
+
}
|
|
3041
|
+
this.pushChrono(st.close, close);
|
|
3042
|
+
this.pushChrono(st.volume, volume);
|
|
3043
|
+
const oldest = st.close.length > 0 ? st.close[0]! : null;
|
|
3044
|
+
const fallback: Cell = st.close.length === 0 ? 100 : oldest !== null && Number.isFinite(oldest) ? oldest : null;
|
|
3045
|
+
if (p === null || lv === null || p <= 0 || lv <= 0 || st.close.length < p) return fallback;
|
|
3046
|
+
const prices: number[] = [];
|
|
3047
|
+
const vols: number[] = [];
|
|
3048
|
+
for (let i = -p; i < 0; i++) {
|
|
3049
|
+
const c = this.nf(st.close, i);
|
|
3050
|
+
const v = this.nf(st.volume, i);
|
|
3051
|
+
if (c !== null && Number.isFinite(c) && v !== null && Number.isFinite(v)) {
|
|
3052
|
+
prices.push(c);
|
|
3053
|
+
vols.push(v);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
if (prices.length === 0) return fallback;
|
|
3057
|
+
let minP = prices[0]!;
|
|
3058
|
+
let maxP = prices[0]!;
|
|
3059
|
+
for (const c of prices) {
|
|
3060
|
+
if (c < minP) minP = c;
|
|
3061
|
+
if (c > maxP) maxP = c;
|
|
3062
|
+
}
|
|
3063
|
+
if (minP === maxP) return minP;
|
|
3064
|
+
const bucket = (maxP - minP) / lv;
|
|
3065
|
+
const buckets = new Array<number>(lv).fill(0);
|
|
3066
|
+
for (let k = 0; k < prices.length; k++) {
|
|
3067
|
+
let idx = Math.trunc((prices[k]! - minP) / bucket);
|
|
3068
|
+
if (idx > lv - 1) idx = lv - 1;
|
|
3069
|
+
if (idx < 0) idx = 0;
|
|
3070
|
+
buckets[idx] = (buckets[idx] ?? 0) + vols[k]!;
|
|
3071
|
+
}
|
|
3072
|
+
let minIdx = 0;
|
|
3073
|
+
for (let i = 1; i < lv; i++) {
|
|
3074
|
+
if ((buckets[i] ?? 0) < (buckets[minIdx] ?? 0)) minIdx = i;
|
|
3075
|
+
}
|
|
3076
|
+
return finiteOut(minP + minIdx * bucket);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* `ta.volume_thrust` — bool as 1/0. `[-1]`/`[-2]` are the oldest two
|
|
3081
|
+
* samples (`advanced.py`), not the current bar.
|
|
3082
|
+
*/
|
|
3083
|
+
volumeThrust(site: string, close: Cell, volume: Cell, volSma: Cell, sensitivity: number): Cell {
|
|
3084
|
+
const sens = Number.isFinite(sensitivity) ? sensitivity : 0.3;
|
|
3085
|
+
let st = this.volThrustSites.get(site);
|
|
3086
|
+
if (st === undefined) {
|
|
3087
|
+
st = { close: [], volume: [], sma: [] };
|
|
3088
|
+
this.volThrustSites.set(site, st);
|
|
3089
|
+
}
|
|
3090
|
+
this.pushChrono(st.close, close);
|
|
3091
|
+
this.pushChrono(st.volume, volume);
|
|
3092
|
+
this.pushChrono(st.sma, volSma);
|
|
3093
|
+
if (st.close.length === 0 || st.volume.length === 0 || st.sma.length === 0) return 0;
|
|
3094
|
+
const numOr = (v: Cell, fallback: number): number => (v !== null && Number.isFinite(v) ? v : fallback);
|
|
3095
|
+
const cVal = numOr(this.nf(st.close, -1), 100);
|
|
3096
|
+
const cPrev = st.close.length > 1 ? numOr(this.nf(st.close, -2), 100) : 100;
|
|
3097
|
+
const vVal = numOr(this.nf(st.volume, -1), 1000);
|
|
3098
|
+
const vsVal = numOr(this.nf(st.sma, -1), 1000);
|
|
3099
|
+
const spike = vVal > vsVal * (1 + sens);
|
|
3100
|
+
const move = cPrev !== 0 ? Math.abs(cVal - cPrev) / cPrev > 0.01 : false;
|
|
3101
|
+
return spike && move ? 1 : 0;
|
|
3102
|
+
}
|
|
2711
3103
|
}
|
|
2712
3104
|
|
|
2713
3105
|
/** Spearman rho. Rank is the stable sort position (`_rci_spearman`). */
|