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::traits::Next;
4use talib_rs::MaType;
5
6/// Moving average with variable period — matches `talib_rs::overlap::mavp` (SMA path).
7#[derive(Debug, Clone)]
8#[allow(non_camel_case_types)]
9pub struct MAVP {
10    pub minperiod: usize,
11    pub maxperiod: usize,
12    pub matype: MaType,
13    prices: Vec<f64>,
14}
15
16impl MAVP {
17    pub fn new(minperiod: usize, maxperiod: usize, matype: MaType) -> Self {
18        Self {
19            minperiod,
20            maxperiod,
21            matype,
22            prices: Vec::new(),
23        }
24    }
25}
26
27impl Next<(f64, f64)> for MAVP {
28    type Output = f64;
29
30    fn next(&mut self, (price, period): (f64, f64)) -> Self::Output {
31        let _ = self.matype;
32        self.prices.push(price);
33        let i = self.prices.len() - 1;
34        let maxp = self.maxperiod;
35        if maxp == 0 || i < maxp - 1 {
36            return f64::NAN;
37        }
38        let p = (period.round() as usize).clamp(self.minperiod, self.maxperiod);
39        if i + 1 >= p {
40            let start = i + 1 - p;
41            let sum: f64 = self.prices[start..=i].iter().sum();
42            sum / p as f64
43        } else {
44            f64::NAN
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use proptest::prelude::*;
53
54    proptest! {
55        #[test]
56        fn test_mavp_parity(
57            h in prop::collection::vec(10.0..100.0, 10..100),
58            l in prop::collection::vec(10.0..100.0, 10..100)
59        ) {
60            let len = h.len().min(l.len());
61            let in1: Vec<f64> = (0..len)
62                .map(|i| {
63                    let hi: f64 = h[i];
64                    let lo: f64 = l[i];
65                    hi.max(lo)
66                })
67                .collect();
68            let in2: Vec<f64> = (0..len)
69                .map(|i| {
70                    let hi: f64 = h[i];
71                    let lo: f64 = l[i];
72                    hi.min(lo)
73                })
74                .collect();
75            let minperiod = 2usize;
76            let maxperiod = 30usize;
77            let matype = MaType::Sma;
78            let mut mavp = MAVP::new(minperiod, maxperiod, matype);
79            let streaming: Vec<f64> = (0..len)
80                .map(|i| mavp.next((in1[i], in2[i])))
81                .collect();
82            let batch = talib_rs::overlap::mavp(&in1, &in2, minperiod, maxperiod, matype)
83                .unwrap_or_else(|_| vec![f64::NAN; len]);
84            for (s, b) in streaming.iter().zip(batch.iter()) {
85                if s.is_nan() {
86                    assert!(b.is_nan());
87                } else {
88                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
89                }
90            }
91        }
92    }
93}