Skip to main content

wickra_core/indicators/
smma.rs

1//! Smoothed Moving Average (Wilder's RMA).
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8/// Smoothed Moving Average — Wilder's running moving average, also known as
9/// RMA.
10///
11/// Seeded with the simple average of the first `period` inputs, then advanced
12/// by `SMMA_t = (SMMA_{t-1} * (period - 1) + price_t) / period`. This is an
13/// exponential average with a slow `1 / period` smoothing factor and is the
14/// average underlying Wilder's RSI and ATR. The first output lands after
15/// exactly `period` inputs.
16///
17/// # Example
18///
19/// ```
20/// use wickra_core::{Indicator, Smma};
21///
22/// let mut indicator = Smma::new(3).unwrap();
23/// let mut last = None;
24/// for i in 0..80 {
25///     last = indicator.update(100.0 + f64::from(i));
26/// }
27/// assert!(last.is_some());
28/// ```
29#[derive(Debug, Clone)]
30pub struct Smma {
31    period: usize,
32    /// Inputs collected while seeding (before the first value is produced).
33    seed: VecDeque<f64>,
34    seed_sum: f64,
35    current: Option<f64>,
36}
37
38impl Smma {
39    /// Construct a new SMMA with the given period.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`Error::PeriodZero`] if `period == 0`.
44    pub fn new(period: usize) -> Result<Self> {
45        if period == 0 {
46            return Err(Error::PeriodZero);
47        }
48        if period > crate::error::MAX_PERIOD {
49            return Err(Error::InvalidPeriod {
50                message: crate::error::PERIOD_ABOVE_MAX,
51            });
52        }
53        Ok(Self {
54            period,
55            seed: VecDeque::with_capacity(period),
56            seed_sum: 0.0,
57            current: None,
58        })
59    }
60
61    /// Configured period.
62    pub const fn period(&self) -> usize {
63        self.period
64    }
65
66    /// Current value if available.
67    pub const fn value(&self) -> Option<f64> {
68        self.current
69    }
70}
71
72impl Indicator for Smma {
73    type Input = f64;
74    type Output = f64;
75
76    #[inline]
77    fn update(&mut self, input: f64) -> Option<f64> {
78        if !input.is_finite() {
79            // Non-finite input is ignored, leaving state untouched.
80            return None;
81        }
82        if let Some(prev) = self.current {
83            let period = self.period as f64;
84            self.current = Some((prev * (period - 1.0) + input) / period);
85        } else {
86            self.seed.push_back(input);
87            self.seed_sum += input;
88            if self.seed.len() == self.period {
89                self.current = Some(self.seed_sum / self.period as f64);
90            }
91        }
92        self.current
93    }
94
95    fn reset(&mut self) {
96        self.seed.clear();
97        self.seed_sum = 0.0;
98        self.current = None;
99    }
100
101    #[inline]
102    fn warmup_period(&self) -> usize {
103        self.period
104    }
105
106    #[inline]
107    fn is_ready(&self) -> bool {
108        self.current.is_some()
109    }
110
111    #[inline]
112    fn name(&self) -> &'static str {
113        "SMMA"
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::traits::BatchExt;
121    use approx::assert_relative_eq;
122
123    #[test]
124    fn new_rejects_zero_period() {
125        assert!(matches!(Smma::new(0), Err(Error::PeriodZero)));
126    }
127
128    /// Cover the const accessors `period` / `value` and the Indicator-impl
129    /// `warmup_period` / `name` methods. Existing tests only exercise the
130    /// numeric output of `update` / `batch` / `reset`, never query the
131    /// metadata surface.
132    #[test]
133    fn accessors_and_metadata() {
134        let mut smma = Smma::new(7).unwrap();
135        assert_eq!(smma.period(), 7);
136        assert_eq!(smma.warmup_period(), 7);
137        assert_eq!(smma.name(), "SMMA");
138        // value() must report both the pre-warmup None and post-warmup Some branches.
139        assert_eq!(smma.value(), None);
140        for i in 1..=7 {
141            smma.update(f64::from(i));
142        }
143        assert!(smma.value().is_some());
144    }
145
146    #[test]
147    fn warmup_then_recurrence() {
148        // SMMA(3): seed = SMA(1,2,3) = 2.0; then (prev*2 + x) / 3.
149        let mut smma = Smma::new(3).unwrap();
150        assert_eq!(smma.update(1.0), None);
151        assert_eq!(smma.update(2.0), None);
152        assert_eq!(smma.update(3.0), Some(2.0));
153        assert_relative_eq!(
154            smma.update(4.0).unwrap(),
155            (2.0 * 2.0 + 4.0) / 3.0,
156            epsilon = 1e-12
157        );
158        assert_relative_eq!(
159            smma.update(5.0).unwrap(),
160            ((2.0 * 2.0 + 4.0) / 3.0 * 2.0 + 5.0) / 3.0,
161            epsilon = 1e-12
162        );
163    }
164
165    #[test]
166    fn period_one_is_pass_through() {
167        let mut smma = Smma::new(1).unwrap();
168        assert_eq!(smma.update(5.0), Some(5.0));
169        assert_eq!(smma.update(10.0), Some(10.0));
170    }
171
172    #[test]
173    fn constant_series_yields_the_constant() {
174        let mut smma = Smma::new(5).unwrap();
175        let out = smma.batch(&[7.0; 20]);
176        for x in out.iter().skip(4) {
177            assert_relative_eq!(x.unwrap(), 7.0, epsilon = 1e-12);
178        }
179    }
180
181    #[test]
182    fn ignores_non_finite_input() {
183        let mut smma = Smma::new(3).unwrap();
184        smma.batch(&[1.0, 2.0, 3.0]);
185        assert_eq!(smma.update(f64::NAN), None);
186        assert_eq!(smma.update(f64::INFINITY), None);
187    }
188
189    #[test]
190    fn reset_clears_state() {
191        let mut smma = Smma::new(3).unwrap();
192        smma.batch(&[1.0, 2.0, 3.0, 4.0]);
193        assert!(smma.is_ready());
194        smma.reset();
195        assert!(!smma.is_ready());
196        assert_eq!(smma.update(10.0), None);
197    }
198
199    #[test]
200    fn batch_equals_streaming() {
201        let prices: Vec<f64> = (1..=30).map(f64::from).collect();
202        let batch = Smma::new(7).unwrap().batch(&prices);
203        let mut b = Smma::new(7).unwrap();
204        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
205        assert_eq!(batch, streamed);
206    }
207}