py-alpha-lib 0.2.1__tar.gz → 0.2.3__tar.gz
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.
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/CHANGELOG.md +12 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/Cargo.lock +1 -1
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/Cargo.toml +1 -1
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/PKG-INFO +2 -1
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/README.md +1 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo/algo_gen.py +24 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo.md +1 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/context.py +43 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/mod.rs +2 -0
- py_alpha_lib-0.2.3/src/algo/quantile.rs +224 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.agent/skills/add_algo/SKILL.md +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.github/workflows/CI.yml +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.gitignore +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.nwa-config.yaml +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/LICENSE +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/articles/001.md +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/articles/COMPARISON.md +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/benchmarks/benchmark_alpha101.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/benchmarks/benchmark_pandas.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/benchmarks/benchmark_polars_ta.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/build.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/al/__init__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/al/alpha191.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/al/alpha191_context.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/alpha191.txt +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/main.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/al/__init__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/al/alpha101.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/al/alpha101_context.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/alpha101.txt +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/main.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pd_/__init__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pd_/alpha101_adjusted.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pl_/__init__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pl_/alpha101_adjusted.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/result.md +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/pyproject.toml +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/__init__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo/__init__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo/algo.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/__init__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/__main__.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/alpha.lark +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/parser.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/to_python.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/conftest.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_grammar.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_rank.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_talib.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_to_python.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/rustfmt.toml +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/backfill.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/context.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/cross.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/ema.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/entropy.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/error.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/extremum.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/group.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/ma.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/misc.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/moments.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/neutralize.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/rank.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/returns.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/scan.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/series.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/skip_nan_window.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/slope.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/stats.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/stddev.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/sum.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/zscore.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/lib.rs +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/tests/rank.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/tests/usage.py +0 -0
- {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/tests/verify_sumif.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# ChangeLog
|
|
2
2
|
|
|
3
|
+
## [0.2.3] - 2026-04-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- ExecContext: TS_QUANTILE, SLOPE, INTERCEPT, DMA, SUMBARS, BARSLAST, BARSSINCE, BINS, FRET, RCROSS, RLONGCROSS
|
|
8
|
+
|
|
9
|
+
## [0.2.2] - 2026-04-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- QUANTILE
|
|
14
|
+
|
|
3
15
|
## [0.2.1] - 2026-04-06
|
|
4
16
|
|
|
5
17
|
### Fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-alpha-lib
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -203,6 +203,7 @@ Naming Rules:
|
|
|
203
203
|
| MOMENT | Calculate rolling k-th central moment over a moving window |
|
|
204
204
|
| NEUTRALIZE | Neutralize the effect of a categorical variable on a numeric variable |
|
|
205
205
|
| PRODUCT | Calculate product of values in preceding `periods` window |
|
|
206
|
+
| QUANTILE | Calculate rolling quantile over a moving window |
|
|
206
207
|
| RANK | Calculate rank in a sliding window with size `periods` |
|
|
207
208
|
| RCROSS | For 2 arrays A and B, return true if A[i-1] > B[i-1] and A[i] <= B[i] alias: death_cross, cross_le |
|
|
208
209
|
| REF | Right shift input array by `periods`, r[i] = input[i - periods] |
|
|
@@ -188,6 +188,7 @@ Naming Rules:
|
|
|
188
188
|
| MOMENT | Calculate rolling k-th central moment over a moving window |
|
|
189
189
|
| NEUTRALIZE | Neutralize the effect of a categorical variable on a numeric variable |
|
|
190
190
|
| PRODUCT | Calculate product of values in preceding `periods` window |
|
|
191
|
+
| QUANTILE | Calculate rolling quantile over a moving window |
|
|
191
192
|
| RANK | Calculate rank in a sliding window with size `periods` |
|
|
192
193
|
| RCROSS | For 2 arrays A and B, return true if A[i-1] > B[i-1] and A[i] <= B[i] alias: death_cross, cross_le |
|
|
193
194
|
| REF | Right shift input array by `periods`, r[i] = input[i - periods] |
|
|
@@ -623,6 +623,30 @@ def PRODUCT(
|
|
|
623
623
|
_algo.product(r, input, periods)
|
|
624
624
|
return r
|
|
625
625
|
|
|
626
|
+
def QUANTILE(
|
|
627
|
+
input: np.ndarray | list[np.ndarray], periods: int, q: float
|
|
628
|
+
) -> np.ndarray | list[np.ndarray]:
|
|
629
|
+
"""
|
|
630
|
+
Calculate rolling quantile over a moving window
|
|
631
|
+
|
|
632
|
+
QUANTILE(x, d, q) returns the q-th quantile (0 <= q <= 1) of values
|
|
633
|
+
in the preceding d periods. Uses linear interpolation between data points
|
|
634
|
+
(matching numpy/pandas percentile with interpolation='linear').
|
|
635
|
+
NaN values are excluded from the computation. Requires at least 1 valid value.
|
|
636
|
+
|
|
637
|
+
Ref: https://numpy.org/doc/stable/reference/generated/numpy.quantile.html
|
|
638
|
+
"""
|
|
639
|
+
if isinstance(input, list):
|
|
640
|
+
input = [_to_f64(x) for x in input]
|
|
641
|
+
r = [np.empty_like(x) for x in input]
|
|
642
|
+
_algo.quantile(r, input, periods, q)
|
|
643
|
+
return r
|
|
644
|
+
else:
|
|
645
|
+
input = _to_f64(input)
|
|
646
|
+
r = np.empty_like(input)
|
|
647
|
+
_algo.quantile(r, input, periods, q)
|
|
648
|
+
return r
|
|
649
|
+
|
|
626
650
|
def RANK(
|
|
627
651
|
input: np.ndarray | list[np.ndarray], periods: int
|
|
628
652
|
) -> np.ndarray | list[np.ndarray]:
|
|
@@ -33,6 +33,7 @@ the `np.ndarray` is `ndarray` type in `numpy` package
|
|
|
33
33
|
- MOMENT(input: np.ndarray[float], periods: int, k: int): Calculate rolling k-th central moment over a moving window MOMENT(x, d, k) = mean((x - mean)^k) over window of d periods. This is the raw (non-adjusted) sample moment. k=2 gives variance (population), k=3 gives raw third moment, etc.
|
|
34
34
|
- NEUTRALIZE(category: np.ndarray[float], input: np.ndarray[float]): Neutralize the effect of a categorical variable on a numeric variable
|
|
35
35
|
- PRODUCT(input: np.ndarray[float], periods: int): Calculate product of values in preceding `periods` window If periods is 0, it calculates the cumulative product from the first valid value.
|
|
36
|
+
- QUANTILE(input: np.ndarray[float], periods: int, q: float): Calculate rolling quantile over a moving window QUANTILE(x, d, q) returns the q-th quantile (0 <= q <= 1) of values in the preceding d periods. Uses linear interpolation between data points (matching numpy/pandas percentile with interpolation='linear'). NaN values are excluded from the computation. Requires at least 1 valid value.
|
|
36
37
|
- RANK(input: np.ndarray[float], periods: int): Calculate rank in a sliding window with size `periods` Uses min-rank method for ties (same as pandas rankdata method='min'). NaN values are treated as larger than all non-NaN values.
|
|
37
38
|
- RCROSS(a: np.ndarray[float], b: np.ndarray[float]): For 2 arrays A and B, return true if A[i-1] > B[i-1] and A[i] <= B[i] alias: death_cross, cross_le
|
|
38
39
|
- REF(input: np.ndarray[float], periods: int): Right shift input array by `periods`, r[i] = input[i - periods]
|
|
@@ -249,6 +249,9 @@ class ExecContext:
|
|
|
249
249
|
else:
|
|
250
250
|
return alpha.SMA(a, int(args[0]), int(args[1]))
|
|
251
251
|
|
|
252
|
+
def DMA(self, a: np.ndarray, weight: float) -> np.ndarray:
|
|
253
|
+
return alpha.DMA(a, float(weight))
|
|
254
|
+
|
|
252
255
|
# ── TS: Std Dev / Variance ─────────────────────────────────────────
|
|
253
256
|
# BRAIN: ts_std_dev GTJA: STD wq101: STDDEV
|
|
254
257
|
|
|
@@ -315,6 +318,11 @@ class ExecContext:
|
|
|
315
318
|
|
|
316
319
|
TSRANK = TS_RANK # gtja191
|
|
317
320
|
|
|
321
|
+
# ── TS: Quantile ────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
def TS_QUANTILE(self, a: np.ndarray, w: int, q: float) -> np.ndarray:
|
|
324
|
+
return alpha.QUANTILE(a, int(w), q)
|
|
325
|
+
|
|
318
326
|
# ── TS: Delay / Delta ──────────────────────────────────────────────
|
|
319
327
|
# BRAIN: ts_delay, ts_delta
|
|
320
328
|
# wq101: DELAY, DELTA
|
|
@@ -364,6 +372,14 @@ class ExecContext:
|
|
|
364
372
|
|
|
365
373
|
REGRESI = TS_REGRESI # gtja191
|
|
366
374
|
|
|
375
|
+
# ── TS: Slope / Intercept ───────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
def SLOPE(self, a: np.ndarray, w: int) -> np.ndarray:
|
|
378
|
+
return alpha.SLOPE(a, int(w))
|
|
379
|
+
|
|
380
|
+
def INTERCEPT(self, a: np.ndarray, w: int) -> np.ndarray:
|
|
381
|
+
return alpha.INTERCEPT(a, int(w))
|
|
382
|
+
|
|
367
383
|
# ── TS: Counting / Conditional ─────────────────────────────────────
|
|
368
384
|
# GTJA: COUNT, SUMIF
|
|
369
385
|
|
|
@@ -379,6 +395,17 @@ class ExecContext:
|
|
|
379
395
|
|
|
380
396
|
SUMIF = TS_SUMIF # gtja191
|
|
381
397
|
|
|
398
|
+
def SUMBARS(self, a: np.ndarray, amount: float) -> np.ndarray:
|
|
399
|
+
return alpha.SUMBARS(a, amount)
|
|
400
|
+
|
|
401
|
+
# ── TS: Bar Counting ──────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
def BARSLAST(self, cond: np.ndarray) -> np.ndarray:
|
|
404
|
+
return alpha.BARSLAST(np.asarray(cond, dtype=bool))
|
|
405
|
+
|
|
406
|
+
def BARSSINCE(self, cond: np.ndarray) -> np.ndarray:
|
|
407
|
+
return alpha.BARSSINCE(np.asarray(cond, dtype=bool))
|
|
408
|
+
|
|
382
409
|
# ── TS: Conditional Scan (SELF recursion) ───────────────────────────
|
|
383
410
|
# GTJA: SELF-referencing patterns
|
|
384
411
|
|
|
@@ -440,6 +467,16 @@ class ExecContext:
|
|
|
440
467
|
def TS_MOMENT(self, a: np.ndarray, w: int, k: int = 2) -> np.ndarray:
|
|
441
468
|
return alpha.MOMENT(a, int(w), int(k))
|
|
442
469
|
|
|
470
|
+
# ── TS: Binning ────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
def BINS(self, a: np.ndarray, n: int) -> np.ndarray:
|
|
473
|
+
return alpha.BINS(a, int(n))
|
|
474
|
+
|
|
475
|
+
# ── TS: Future Return ──────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
def FRET(self, open: np.ndarray, close: np.ndarray, is_calc: np.ndarray, delay: int, periods: int) -> np.ndarray:
|
|
478
|
+
return alpha.FRET(open, close, is_calc, int(delay), int(periods))
|
|
479
|
+
|
|
443
480
|
# ── TS: Cross Detection ────────────────────────────────────────────
|
|
444
481
|
# AmiBroker/GTJA: CROSS, LONGCROSS
|
|
445
482
|
|
|
@@ -449,6 +486,12 @@ class ExecContext:
|
|
|
449
486
|
def LONGCROSS(self, a: np.ndarray, b: np.ndarray, n: int) -> np.ndarray:
|
|
450
487
|
return alpha.LONGCROSS(a, b, int(n))
|
|
451
488
|
|
|
489
|
+
def RCROSS(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
490
|
+
return alpha.RCROSS(a, b)
|
|
491
|
+
|
|
492
|
+
def RLONGCROSS(self, a: np.ndarray, b: np.ndarray, n: int) -> np.ndarray:
|
|
493
|
+
return alpha.RLONGCROSS(a, b, int(n))
|
|
494
|
+
|
|
452
495
|
# ====================================================================
|
|
453
496
|
# Cross-Sectional Operators (no prefix)
|
|
454
497
|
#
|
|
@@ -13,6 +13,7 @@ mod ma;
|
|
|
13
13
|
mod misc;
|
|
14
14
|
mod moments;
|
|
15
15
|
mod neutralize;
|
|
16
|
+
mod quantile;
|
|
16
17
|
mod rank;
|
|
17
18
|
mod returns;
|
|
18
19
|
mod scan;
|
|
@@ -36,6 +37,7 @@ pub use ma::*;
|
|
|
36
37
|
pub use misc::*;
|
|
37
38
|
pub use moments::*;
|
|
38
39
|
pub use neutralize::*;
|
|
40
|
+
pub use quantile::*;
|
|
39
41
|
pub use rank::*;
|
|
40
42
|
pub use returns::*;
|
|
41
43
|
pub use scan::*;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Copyright 2026 MSD-RS Project LiJia
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
|
|
4
|
+
use num_traits::Float;
|
|
5
|
+
use rayon::prelude::*;
|
|
6
|
+
|
|
7
|
+
use crate::algo::{Context, Error, is_normal};
|
|
8
|
+
|
|
9
|
+
/// Calculate rolling quantile over a moving window
|
|
10
|
+
///
|
|
11
|
+
/// QUANTILE(x, d, q) returns the q-th quantile (0 <= q <= 1) of values
|
|
12
|
+
/// in the preceding d periods. Uses linear interpolation between data points
|
|
13
|
+
/// (matching numpy/pandas percentile with interpolation='linear').
|
|
14
|
+
/// NaN values are excluded from the computation. Requires at least 1 valid value.
|
|
15
|
+
///
|
|
16
|
+
/// Ref: https://numpy.org/doc/stable/reference/generated/numpy.quantile.html
|
|
17
|
+
pub fn ta_quantile<NumT: Float + Send + Sync>(
|
|
18
|
+
ctx: &Context,
|
|
19
|
+
r: &mut [NumT],
|
|
20
|
+
input: &[NumT],
|
|
21
|
+
periods: usize,
|
|
22
|
+
q: NumT,
|
|
23
|
+
) -> Result<(), Error> {
|
|
24
|
+
if r.len() != input.len() {
|
|
25
|
+
return Err(Error::LengthMismatch(r.len(), input.len()));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if periods == 0 {
|
|
29
|
+
r.fill(NumT::nan());
|
|
30
|
+
return Ok(());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if q < NumT::zero() || q > NumT::one() || q.is_nan() {
|
|
34
|
+
r.fill(NumT::nan());
|
|
35
|
+
return Ok(());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
r.par_chunks_mut(ctx.chunk_size(r.len()))
|
|
39
|
+
.zip(input.par_chunks(ctx.chunk_size(input.len())))
|
|
40
|
+
.for_each(|(r, x)| {
|
|
41
|
+
let start = ctx.start(r.len());
|
|
42
|
+
let end = ctx.end(r.len());
|
|
43
|
+
r.fill(NumT::nan());
|
|
44
|
+
|
|
45
|
+
// Allocate a buffer for sorting within the window
|
|
46
|
+
let mut buf = vec![NumT::zero(); periods];
|
|
47
|
+
|
|
48
|
+
for i in start..end {
|
|
49
|
+
let val = x[i];
|
|
50
|
+
if !is_normal(&val) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Determine window boundaries
|
|
55
|
+
let win_start = if i >= periods { i - periods + 1 } else { 0 };
|
|
56
|
+
let win_end = i + 1;
|
|
57
|
+
|
|
58
|
+
// Collect valid (non-NaN) values into buffer
|
|
59
|
+
let mut count = 0;
|
|
60
|
+
for k in win_start..win_end {
|
|
61
|
+
let v = x[k];
|
|
62
|
+
if is_normal(&v) {
|
|
63
|
+
buf[count] = v;
|
|
64
|
+
count += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if count == 0 {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check strict cycle: require full window of `periods` valid values
|
|
73
|
+
if ctx.is_strictly_cycle() {
|
|
74
|
+
if i < periods - 1 || count != periods || (win_end - win_start) != periods {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Sort the buffer (insertion sort for small windows, efficient for typical d=5..50)
|
|
80
|
+
let slice = &mut buf[..count];
|
|
81
|
+
for j in 1..count {
|
|
82
|
+
let key = slice[j];
|
|
83
|
+
let mut k = j;
|
|
84
|
+
while k > 0 && slice[k - 1] > key {
|
|
85
|
+
slice[k] = slice[k - 1];
|
|
86
|
+
k -= 1;
|
|
87
|
+
}
|
|
88
|
+
slice[k] = key;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Linear interpolation quantile (matching numpy/pandas)
|
|
92
|
+
let pos = q * NumT::from(count - 1).unwrap();
|
|
93
|
+
let lo = pos.floor();
|
|
94
|
+
let hi = lo + NumT::one();
|
|
95
|
+
let lo_idx = lo.to_usize().unwrap_or(0).min(count - 1);
|
|
96
|
+
let hi_idx = hi.to_usize().unwrap_or(0).min(count - 1);
|
|
97
|
+
let frac = pos - lo;
|
|
98
|
+
r[i] = slice[lo_idx] * (NumT::one() - frac) + slice[hi_idx] * frac;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[cfg(test)]
|
|
106
|
+
mod tests {
|
|
107
|
+
use super::*;
|
|
108
|
+
use crate::algo::{assert_vec_eq_nan, context::FLAG_STRICTLY_CYCLE};
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn test_quantile_median() {
|
|
112
|
+
// Median (q=0.5) of [1,2,3] = 2.0
|
|
113
|
+
let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
|
114
|
+
let periods = 3;
|
|
115
|
+
let mut r = vec![0.0; input.len()];
|
|
116
|
+
let ctx = Context::new(0, 0, 0);
|
|
117
|
+
ta_quantile(&ctx, &mut r, &input, periods, 0.5).unwrap();
|
|
118
|
+
|
|
119
|
+
// i=0: [1] -> median=1.0
|
|
120
|
+
// i=1: [1,2] -> median=1.5
|
|
121
|
+
// i=2: [1,2,3] -> median=2.0
|
|
122
|
+
// i=3: [2,3,4] -> median=3.0
|
|
123
|
+
// i=4: [3,4,5] -> median=4.0
|
|
124
|
+
assert_vec_eq_nan(&r, &vec![1.0, 1.5, 2.0, 3.0, 4.0]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn test_quantile_strict() {
|
|
129
|
+
let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
|
130
|
+
let periods = 3;
|
|
131
|
+
let mut r = vec![0.0; input.len()];
|
|
132
|
+
let ctx = Context::new(0, 0, FLAG_STRICTLY_CYCLE);
|
|
133
|
+
ta_quantile(&ctx, &mut r, &input, periods, 0.5).unwrap();
|
|
134
|
+
|
|
135
|
+
assert_vec_eq_nan(
|
|
136
|
+
&r,
|
|
137
|
+
&vec![f64::NAN, f64::NAN, 2.0, 3.0, 4.0],
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn test_quantile_q0_q1() {
|
|
143
|
+
let input = vec![3.0, 1.0, 4.0, 1.0, 5.0];
|
|
144
|
+
let periods = 3;
|
|
145
|
+
let mut r = vec![0.0; input.len()];
|
|
146
|
+
let ctx = Context::new(0, 0, FLAG_STRICTLY_CYCLE);
|
|
147
|
+
|
|
148
|
+
// q=0 -> min
|
|
149
|
+
ta_quantile(&ctx, &mut r, &input, periods, 0.0).unwrap();
|
|
150
|
+
assert_vec_eq_nan(
|
|
151
|
+
&r,
|
|
152
|
+
&vec![f64::NAN, f64::NAN, 1.0, 1.0, 1.0],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// q=1 -> max
|
|
156
|
+
ta_quantile(&ctx, &mut r, &input, periods, 1.0).unwrap();
|
|
157
|
+
assert_vec_eq_nan(
|
|
158
|
+
&r,
|
|
159
|
+
&vec![f64::NAN, f64::NAN, 4.0, 4.0, 5.0],
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn test_quantile_with_nan() {
|
|
165
|
+
let input = vec![1.0, f64::NAN, 3.0, 4.0, 5.0];
|
|
166
|
+
let periods = 3;
|
|
167
|
+
let mut r = vec![0.0; input.len()];
|
|
168
|
+
let ctx = Context::new(0, 0, 0);
|
|
169
|
+
ta_quantile(&ctx, &mut r, &input, periods, 0.5).unwrap();
|
|
170
|
+
|
|
171
|
+
// i=0: [1] -> 1.0
|
|
172
|
+
// i=1: NaN -> NaN
|
|
173
|
+
// i=2: [1, 3] (NaN skipped) -> median of [1,3] = 2.0
|
|
174
|
+
// i=3: [3, 4] (NaN skipped) -> median of [3,4] = 3.5
|
|
175
|
+
// i=4: [3, 4, 5] -> median = 4.0
|
|
176
|
+
assert_vec_eq_nan(&r, &vec![1.0, f64::NAN, 2.0, 3.5, 4.0]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn test_quantile_groups() {
|
|
181
|
+
// 2 groups of 3 each
|
|
182
|
+
let input = vec![1.0, 2.0, 3.0, 10.0, 20.0, 30.0];
|
|
183
|
+
let periods = 3;
|
|
184
|
+
let mut r = vec![0.0; input.len()];
|
|
185
|
+
let ctx = Context::new(0, 2, FLAG_STRICTLY_CYCLE);
|
|
186
|
+
ta_quantile(&ctx, &mut r, &input, periods, 0.5).unwrap();
|
|
187
|
+
|
|
188
|
+
// Group 1: [1,2,3] -> median=2.0
|
|
189
|
+
// Group 2: [10,20,30] -> median=20.0
|
|
190
|
+
assert_vec_eq_nan(
|
|
191
|
+
&r,
|
|
192
|
+
&vec![f64::NAN, f64::NAN, 2.0, f64::NAN, f64::NAN, 20.0],
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn test_quantile_periods_zero() {
|
|
198
|
+
let input = vec![1.0, 2.0, 3.0];
|
|
199
|
+
let mut r = vec![0.0; input.len()];
|
|
200
|
+
let ctx = Context::new(0, 0, 0);
|
|
201
|
+
ta_quantile(&ctx, &mut r, &input, 0, 0.5).unwrap();
|
|
202
|
+
// periods=0 → all NaN
|
|
203
|
+
assert!(r.iter().all(|x| x.is_nan()));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn test_quantile_invalid_q() {
|
|
208
|
+
let input = vec![1.0, 2.0, 3.0];
|
|
209
|
+
let mut r = vec![0.0; input.len()];
|
|
210
|
+
let ctx = Context::new(0, 0, 0);
|
|
211
|
+
|
|
212
|
+
// q < 0
|
|
213
|
+
ta_quantile(&ctx, &mut r, &input, 3, -0.1).unwrap();
|
|
214
|
+
assert!(r.iter().all(|x| x.is_nan()));
|
|
215
|
+
|
|
216
|
+
// q > 1
|
|
217
|
+
ta_quantile(&ctx, &mut r, &input, 3, 1.5).unwrap();
|
|
218
|
+
assert!(r.iter().all(|x| x.is_nan()));
|
|
219
|
+
|
|
220
|
+
// q = NaN
|
|
221
|
+
ta_quantile(&ctx, &mut r, &input, 3, f64::NAN).unwrap();
|
|
222
|
+
assert!(r.iter().all(|x| x.is_nan()));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|