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 Ok(Self {
49 period,
50 seed: VecDeque::with_capacity(period),
51 seed_sum: 0.0,
52 current: None,
53 })
54 }
55
56 pub const fn period(&self) -> usize {
58 self.period
59 }
60
61 pub const fn value(&self) -> Option<f64> {
63 self.current
64 }
65}
66
67impl Indicator for Smma {
68 type Input = f64;
69 type Output = f64;
70
71 fn update(&mut self, input: f64) -> Option<f64> {
72 if !input.is_finite() {
73 return self.current;
75 }
76 if let Some(prev) = self.current {
77 let period = self.period as f64;
78 self.current = Some((prev * (period - 1.0) + input) / period);
79 } else {
80 self.seed.push_back(input);
81 self.seed_sum += input;
82 if self.seed.len() == self.period {
83 self.current = Some(self.seed_sum / self.period as f64);
84 }
85 }
86 self.current
87 }
88
89 fn reset(&mut self) {
90 self.seed.clear();
91 self.seed_sum = 0.0;
92 self.current = None;
93 }
94
95 fn warmup_period(&self) -> usize {
96 self.period
97 }
98
99 fn is_ready(&self) -> bool {
100 self.current.is_some()
101 }
102
103 fn name(&self) -> &'static str {
104 "SMMA"
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::traits::BatchExt;
112 use approx::assert_relative_eq;
113
114 #[test]
115 fn new_rejects_zero_period() {
116 assert!(matches!(Smma::new(0), Err(Error::PeriodZero)));
117 }
118
119 #[test]
124 fn accessors_and_metadata() {
125 let mut smma = Smma::new(7).unwrap();
126 assert_eq!(smma.period(), 7);
127 assert_eq!(smma.warmup_period(), 7);
128 assert_eq!(smma.name(), "SMMA");
129 assert_eq!(smma.value(), None);
131 for i in 1..=7 {
132 smma.update(f64::from(i));
133 }
134 assert!(smma.value().is_some());
135 }
136
137 #[test]
138 fn warmup_then_recurrence() {
139 let mut smma = Smma::new(3).unwrap();
141 assert_eq!(smma.update(1.0), None);
142 assert_eq!(smma.update(2.0), None);
143 assert_eq!(smma.update(3.0), Some(2.0));
144 assert_relative_eq!(
145 smma.update(4.0).unwrap(),
146 (2.0 * 2.0 + 4.0) / 3.0,
147 epsilon = 1e-12
148 );
149 assert_relative_eq!(
150 smma.update(5.0).unwrap(),
151 ((2.0 * 2.0 + 4.0) / 3.0 * 2.0 + 5.0) / 3.0,
152 epsilon = 1e-12
153 );
154 }
155
156 #[test]
157 fn period_one_is_pass_through() {
158 let mut smma = Smma::new(1).unwrap();
159 assert_eq!(smma.update(5.0), Some(5.0));
160 assert_eq!(smma.update(10.0), Some(10.0));
161 }
162
163 #[test]
164 fn constant_series_yields_the_constant() {
165 let mut smma = Smma::new(5).unwrap();
166 let out = smma.batch(&[7.0; 20]);
167 for x in out.iter().skip(4) {
168 assert_relative_eq!(x.unwrap(), 7.0, epsilon = 1e-12);
169 }
170 }
171
172 #[test]
173 fn ignores_non_finite_input() {
174 let mut smma = Smma::new(3).unwrap();
175 smma.batch(&[1.0, 2.0, 3.0]);
176 assert_eq!(smma.update(f64::NAN), Some(2.0));
177 assert_eq!(smma.update(f64::INFINITY), Some(2.0));
178 }
179
180 #[test]
181 fn reset_clears_state() {
182 let mut smma = Smma::new(3).unwrap();
183 smma.batch(&[1.0, 2.0, 3.0, 4.0]);
184 assert!(smma.is_ready());
185 smma.reset();
186 assert!(!smma.is_ready());
187 assert_eq!(smma.update(10.0), None);
188 }
189
190 #[test]
191 fn batch_equals_streaming() {
192 let prices: Vec<f64> = (1..=30).map(f64::from).collect();
193 let batch = Smma::new(7).unwrap().batch(&prices);
194 let mut b = Smma::new(7).unwrap();
195 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
196 assert_eq!(batch, streamed);
197 }
198}