Skip to main content

quantwave_core/indicators/incremental/
talib_sma.rs

1//! TA-Lib compatible SMA (NaN until lookback, then sliding window).
2
3use crate::traits::Next;
4use crate::utils::RingBuffer;
5
6/// Simple moving average matching `talib_rs::overlap::sma`.
7#[derive(Debug, Clone)]
8pub struct TalibSma {
9    period: usize,
10    window: RingBuffer<f64>,
11    sum: f64,
12}
13
14impl TalibSma {
15    pub fn new(period: usize) -> Self {
16        Self {
17            period,
18            window: RingBuffer::with_capacity(period),
19            sum: 0.0,
20        }
21    }
22}
23
24impl Next<f64> for TalibSma {
25    type Output = f64;
26
27    fn next(&mut self, input: f64) -> Self::Output {
28        let p = self.period;
29        if p == 0 {
30            return f64::NAN;
31        }
32
33        if self.window.len() >= p {
34            if let Some(old) = self.window.pop_front() {
35                self.sum -= old;
36            }
37        }
38        self.window.push_back(input);
39        self.sum += input;
40
41        if self.window.len() < p {
42            return f64::NAN;
43        }
44        self.sum / p as f64
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use proptest::prelude::*;
52
53    proptest! {
54        #[test]
55        fn test_talib_sma_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
56            let period = 10;
57            let mut sma = TalibSma::new(period);
58            let streaming: Vec<f64> = input.iter().map(|&x| sma.next(x)).collect();
59            let batch = talib_rs::overlap::sma(&input, period)
60                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
61            for (s, b) in streaming.iter().zip(batch.iter()) {
62                if s.is_nan() { assert!(b.is_nan()); }
63                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
64            }
65        }
66    }
67}