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