Skip to main content

quantwave_core/indicators/incremental/
mavp.rs

1//! Native O(1) MAVP — variable-period SMA per bar (TA-Lib parity).
2
3use crate::indicators::ma_type::MaType;
4use crate::traits::Next;
5
6/// Moving average with variable period — matches `talib_rs::overlap::mavp`.
7///
8/// `matype` is retained for API compatibility but does not change the result:
9/// the batch implementation also always uses SMA ("变周期场景下无法高效使用其他
10/// MA" — a variable window makes the recursive MA families ill-defined), so
11/// honouring it here would *break* batch↔streaming parity rather than fix it.
12#[derive(Debug, Clone)]
13#[allow(non_camel_case_types)]
14pub struct MAVP {
15    pub minperiod: usize,
16    pub maxperiod: usize,
17    pub matype: MaType,
18    prices: Vec<f64>,
19}
20
21impl MAVP {
22    pub fn new(minperiod: usize, maxperiod: usize, matype: MaType) -> Self {
23        Self {
24            minperiod,
25            maxperiod,
26            matype,
27            prices: Vec::new(),
28        }
29    }
30}
31
32impl Next<(f64, f64)> for MAVP {
33    type Output = f64;
34
35    fn next(&mut self, (price, period): (f64, f64)) -> Self::Output {
36        let _ = self.matype;
37        self.prices.push(price);
38        let i = self.prices.len() - 1;
39        let maxp = self.maxperiod;
40        if maxp == 0 || i < maxp - 1 {
41            return f64::NAN;
42        }
43        let p = (period.round() as usize).clamp(self.minperiod, self.maxperiod);
44        if i + 1 >= p {
45            let start = i + 1 - p;
46            let sum: f64 = self.prices[start..=i].iter().sum();
47            sum / p as f64
48        } else {
49            f64::NAN
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use proptest::prelude::*;
58
59    proptest! {
60        #[test]
61        fn test_mavp_parity(
62            h in prop::collection::vec(10.0..100.0, 10..100),
63            l in prop::collection::vec(10.0..100.0, 10..100)
64        ) {
65            let len = h.len().min(l.len());
66            let in1: Vec<f64> = (0..len)
67                .map(|i| {
68                    let hi: f64 = h[i];
69                    let lo: f64 = l[i];
70                    hi.max(lo)
71                })
72                .collect();
73            let in2: Vec<f64> = (0..len)
74                .map(|i| {
75                    let hi: f64 = h[i];
76                    let lo: f64 = l[i];
77                    hi.min(lo)
78                })
79                .collect();
80            let minperiod = 2usize;
81            let maxperiod = 30usize;
82            let matype = MaType::Sma;
83            let mut mavp = MAVP::new(minperiod, maxperiod, matype);
84            let streaming: Vec<f64> = (0..len)
85                .map(|i| mavp.next((in1[i], in2[i])))
86                .collect();
87            let batch = talib_rs::overlap::mavp(&in1, &in2, minperiod, maxperiod, matype.into())
88                .unwrap_or_else(|_| vec![f64::NAN; len]);
89            for (s, b) in streaming.iter().zip(batch.iter()) {
90                if s.is_nan() {
91                    assert!(b.is_nan());
92                } else {
93                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
94                }
95            }
96        }
97    }
98}