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
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 #[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 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 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}