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.
Files changed (77) hide show
  1. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/CHANGELOG.md +12 -0
  2. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/Cargo.lock +1 -1
  3. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/Cargo.toml +1 -1
  4. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/PKG-INFO +2 -1
  5. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/README.md +1 -0
  6. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo/algo_gen.py +24 -0
  7. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo.md +1 -0
  8. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/context.py +43 -0
  9. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/mod.rs +2 -0
  10. py_alpha_lib-0.2.3/src/algo/quantile.rs +224 -0
  11. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.agent/skills/add_algo/SKILL.md +0 -0
  12. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.github/workflows/CI.yml +0 -0
  13. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.gitignore +0 -0
  14. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/.nwa-config.yaml +0 -0
  15. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/LICENSE +0 -0
  16. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/articles/001.md +0 -0
  17. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/articles/COMPARISON.md +0 -0
  18. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/benchmarks/benchmark_alpha101.py +0 -0
  19. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/benchmarks/benchmark_pandas.py +0 -0
  20. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/benchmarks/benchmark_polars_ta.py +0 -0
  21. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/build.rs +0 -0
  22. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/al/__init__.py +0 -0
  23. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/al/alpha191.py +0 -0
  24. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/al/alpha191_context.py +0 -0
  25. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/alpha191.txt +0 -0
  26. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/gtja191/main.py +0 -0
  27. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/al/__init__.py +0 -0
  28. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/al/alpha101.py +0 -0
  29. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/al/alpha101_context.py +0 -0
  30. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/alpha101.txt +0 -0
  31. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/main.py +0 -0
  32. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pd_/__init__.py +0 -0
  33. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pd_/alpha101_adjusted.py +0 -0
  34. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pl_/__init__.py +0 -0
  35. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/pl_/alpha101_adjusted.py +0 -0
  36. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/examples/wq101/result.md +0 -0
  37. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/pyproject.toml +0 -0
  38. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/__init__.py +0 -0
  39. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo/__init__.py +0 -0
  40. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/algo/algo.py +0 -0
  41. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/__init__.py +0 -0
  42. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/__main__.py +0 -0
  43. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/alpha.lark +0 -0
  44. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/parser.py +0 -0
  45. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/alpha/lang/to_python.py +0 -0
  46. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/conftest.py +0 -0
  47. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_grammar.py +0 -0
  48. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_rank.py +0 -0
  49. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_talib.py +0 -0
  50. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/python/tests/test_to_python.py +0 -0
  51. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/rustfmt.toml +0 -0
  52. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/backfill.rs +0 -0
  53. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/context.rs +0 -0
  54. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/cross.rs +0 -0
  55. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/ema.rs +0 -0
  56. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/entropy.rs +0 -0
  57. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/error.rs +0 -0
  58. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/extremum.rs +0 -0
  59. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/group.rs +0 -0
  60. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/ma.rs +0 -0
  61. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/misc.rs +0 -0
  62. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/moments.rs +0 -0
  63. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/neutralize.rs +0 -0
  64. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/rank.rs +0 -0
  65. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/returns.rs +0 -0
  66. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/scan.rs +0 -0
  67. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/series.rs +0 -0
  68. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/skip_nan_window.rs +0 -0
  69. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/slope.rs +0 -0
  70. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/stats.rs +0 -0
  71. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/stddev.rs +0 -0
  72. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/sum.rs +0 -0
  73. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/algo/zscore.rs +0 -0
  74. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/src/lib.rs +0 -0
  75. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/tests/rank.py +0 -0
  76. {py_alpha_lib-0.2.1 → py_alpha_lib-0.2.3}/tests/usage.py +0 -0
  77. {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
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "alpha"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  dependencies = [
9
9
  "anyhow",
10
10
  "log",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "alpha"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  edition = "2024"
5
5
  authors = ["ElseJJ"]
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-alpha-lib
3
- Version: 0.2.1
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