wickra_core/indicators/
smma.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
30pub struct Smma {
31 period: usize,
32 seed: VecDeque<f64>,
34 seed_sum: f64,
35 current: Option<f64>,
36}
37
38impl Smma {
39 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 pub const fn period(&self) -> usize {
63 self.period
64 }
65
66 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 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 fn batch_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
123 assert_eq!(
124 inputs.len(),
125 out.len(),
126 "batch output length must equal input length"
127 );
128 let p = self.period;
129 if !self.seed.is_empty()
130 || self.current.is_some()
131 || inputs.len() < p
132 || !crate::fast::in_range(inputs)
133 {
134 self.batch_nan_into(inputs, out);
135 return;
136 }
137 let seed_sum = inputs[..p].iter().fold(0.0, |acc, &x| acc + x);
138 let last = wickra_simd::dispatch(crate::fast::SmmaFast {
139 x: inputs,
140 period: p,
141 seed_sum,
142 out,
143 _borrow: std::marker::PhantomData,
144 });
145 self.seed.extend(&inputs[..p]);
146 self.seed_sum = seed_sum;
147 self.current = Some(last);
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::BatchExt;
155 use approx::assert_relative_eq;
156
157 #[test]
158 fn new_rejects_zero_period() {
159 assert!(matches!(Smma::new(0), Err(Error::PeriodZero)));
160 }
161
162 #[test]
167 fn accessors_and_metadata() {
168 let mut smma = Smma::new(7).unwrap();
169 assert_eq!(smma.period(), 7);
170 assert_eq!(smma.warmup_period(), 7);
171 assert_eq!(smma.name(), "SMMA");
172 assert_eq!(smma.value(), None);
174 for i in 1..=7 {
175 smma.update(f64::from(i));
176 }
177 assert!(smma.value().is_some());
178 }
179
180 #[test]
181 fn warmup_then_recurrence() {
182 let mut smma = Smma::new(3).unwrap();
184 assert_eq!(smma.update(1.0), None);
185 assert_eq!(smma.update(2.0), None);
186 assert_eq!(smma.update(3.0), Some(2.0));
187 assert_relative_eq!(
188 smma.update(4.0).unwrap(),
189 (2.0 * 2.0 + 4.0) / 3.0,
190 epsilon = 1e-12
191 );
192 assert_relative_eq!(
193 smma.update(5.0).unwrap(),
194 ((2.0 * 2.0 + 4.0) / 3.0 * 2.0 + 5.0) / 3.0,
195 epsilon = 1e-12
196 );
197 }
198
199 #[test]
200 fn period_one_is_pass_through() {
201 let mut smma = Smma::new(1).unwrap();
202 assert_eq!(smma.update(5.0), Some(5.0));
203 assert_eq!(smma.update(10.0), Some(10.0));
204 }
205
206 #[test]
207 fn constant_series_yields_the_constant() {
208 let mut smma = Smma::new(5).unwrap();
209 let out = smma.batch(&[7.0; 20]);
210 for x in out.iter().skip(4) {
211 assert_relative_eq!(x.unwrap(), 7.0, epsilon = 1e-12);
212 }
213 }
214
215 #[test]
216 fn ignores_non_finite_input() {
217 let mut smma = Smma::new(3).unwrap();
218 smma.batch(&[1.0, 2.0, 3.0]);
219 assert_eq!(smma.update(f64::NAN), None);
220 assert_eq!(smma.update(f64::INFINITY), None);
221 }
222
223 #[test]
224 fn reset_clears_state() {
225 let mut smma = Smma::new(3).unwrap();
226 smma.batch(&[1.0, 2.0, 3.0, 4.0]);
227 assert!(smma.is_ready());
228 smma.reset();
229 assert!(!smma.is_ready());
230 assert_eq!(smma.update(10.0), None);
231 }
232
233 #[test]
234 fn batch_equals_streaming() {
235 let prices: Vec<f64> = (1..=30).map(f64::from).collect();
236 let batch = Smma::new(7).unwrap().batch(&prices);
237 let mut b = Smma::new(7).unwrap();
238 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
239 assert_eq!(batch, streamed);
240 }
241}