1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone)]
32pub struct Sma {
33 period: usize,
34 buf: Box<[f64]>,
38 head: usize,
40 count: usize,
42 sum: f64,
43 updates_since_recompute: usize,
47}
48
49const RECOMPUTE_EVERY: usize = 16;
55
56impl Sma {
57 pub fn new(period: usize) -> Result<Self> {
63 if period == 0 {
64 return Err(Error::PeriodZero);
65 }
66 if period > crate::error::MAX_PERIOD {
67 return Err(Error::InvalidPeriod {
68 message: crate::error::PERIOD_ABOVE_MAX,
69 });
70 }
71 Ok(Self {
72 period,
73 buf: vec![0.0; period].into_boxed_slice(),
74 head: 0,
75 count: 0,
76 sum: 0.0,
77 updates_since_recompute: 0,
78 })
79 }
80
81 pub const fn period(&self) -> usize {
83 self.period
84 }
85
86 pub fn value(&self) -> Option<f64> {
88 if self.count == self.period {
89 Some(self.sum / self.period as f64)
90 } else {
91 None
92 }
93 }
94
95 pub fn batch_nan(&mut self, inputs: &[f64]) -> Vec<f64> {
106 let p = self.period;
107 if self.count != 0
108 || self.updates_since_recompute != 0
109 || !inputs.iter().all(|x| x.is_finite())
110 {
111 return inputs
112 .iter()
113 .map(|&x| self.update(x).unwrap_or(f64::NAN))
114 .collect();
115 }
116
117 let p_f64 = p as f64;
118 let mut out = vec![f64::NAN; inputs.len()];
119 let mut rest = inputs;
133 let mut written: &mut [f64] = &mut out;
134 let mut lap = 0_usize;
135 while !rest.is_empty() {
136 let take = rest.len().min(p);
137 let (chunk, tail) = rest.split_at(take);
138 rest = tail;
139 let (lap_out, out_tail) = written.split_at_mut(take);
140 written = out_tail;
141 if lap == 0 {
142 for ((slot, &x), cell) in self.buf.iter_mut().zip(chunk).zip(lap_out.iter_mut()) {
143 *slot = x;
144 self.sum += x;
145 self.count += 1;
146 if self.count == p {
147 *cell = self.sum / p_f64;
148 }
149 }
150 } else {
151 for ((slot, &x), cell) in self.buf.iter_mut().zip(chunk).zip(lap_out.iter_mut()) {
152 self.sum -= *slot;
153 *slot = x;
154 self.sum += x;
155 *cell = self.sum / p_f64;
156 }
157 }
158 self.updates_since_recompute += take;
159 if self.updates_since_recompute >= RECOMPUTE_EVERY * p {
160 self.sum = self.buf.iter().copied().sum();
161 self.updates_since_recompute = 0;
162 *lap_out
168 .last_mut()
169 .expect("a lap writes at least one value before it can reseed") =
170 self.sum / p_f64;
171 }
172 lap += 1;
173 }
174 self.head = inputs.len() % p;
175 out
176 }
177}
178
179impl Indicator for Sma {
180 type Input = f64;
181 type Output = f64;
182
183 #[inline]
184 fn update(&mut self, input: f64) -> Option<f64> {
185 if !input.is_finite() {
186 return None;
187 }
188 if self.count == self.period {
189 self.sum -= self.buf[self.head];
193 self.buf[self.head] = input;
194 self.sum += input;
195 } else {
196 self.buf[self.head] = input;
197 self.sum += input;
198 self.count += 1;
199 }
200 self.head += 1;
202 if self.head == self.period {
203 self.head = 0;
204 }
205 self.updates_since_recompute += 1;
206 if self.updates_since_recompute >= RECOMPUTE_EVERY * self.period {
207 self.sum = self.buf[self.head..]
210 .iter()
211 .chain(&self.buf[..self.head])
212 .copied()
213 .sum();
214 self.updates_since_recompute = 0;
215 }
216 self.value()
217 }
218
219 fn reset(&mut self) {
220 self.head = 0;
221 self.count = 0;
222 self.sum = 0.0;
223 self.updates_since_recompute = 0;
224 }
225
226 #[inline]
227 fn warmup_period(&self) -> usize {
228 self.period
229 }
230
231 #[inline]
232 fn is_ready(&self) -> bool {
233 self.count == self.period
234 }
235
236 #[inline]
237 fn name(&self) -> &'static str {
238 "SMA"
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
249 fn rejects_a_period_above_the_maximum() {
250 assert!(matches!(
251 Sma::new(usize::MAX),
252 Err(Error::InvalidPeriod { .. })
253 ));
254 assert!(matches!(
255 Sma::new(crate::error::MAX_PERIOD + 1),
256 Err(Error::InvalidPeriod { .. })
257 ));
258 assert!(Sma::new(20).is_ok());
259 }
260 use crate::traits::BatchExt;
261 use approx::assert_relative_eq;
262 use std::collections::VecDeque;
263
264 #[test]
265 fn new_rejects_zero_period() {
266 assert!(matches!(Sma::new(0), Err(Error::PeriodZero)));
267 }
268
269 #[test]
273 fn accessors_and_metadata() {
274 let sma = Sma::new(20).unwrap();
275 assert_eq!(sma.period(), 20);
276 assert_eq!(sma.warmup_period(), 20);
277 assert_eq!(sma.name(), "SMA");
278 }
279
280 #[test]
281 fn warmup_returns_none() {
282 let mut sma = Sma::new(3).unwrap();
283 assert_eq!(sma.update(1.0), None);
284 assert_eq!(sma.update(2.0), None);
285 assert_eq!(sma.update(3.0), Some(2.0));
286 }
287
288 #[test]
289 fn rolls_window_after_full() {
290 let mut sma = Sma::new(3).unwrap();
291 let out: Vec<_> = [1.0, 2.0, 3.0, 4.0, 5.0]
292 .iter()
293 .map(|p| sma.update(*p))
294 .collect();
295 assert_eq!(out, vec![None, None, Some(2.0), Some(3.0), Some(4.0)]);
296 }
297
298 #[test]
299 fn period_one_is_pass_through() {
300 let mut sma = Sma::new(1).unwrap();
301 assert_eq!(sma.update(5.0), Some(5.0));
302 assert_eq!(sma.update(10.0), Some(10.0));
303 }
304
305 #[test]
306 fn ignores_non_finite_input_but_keeps_state() {
307 let mut sma = Sma::new(3).unwrap();
308 sma.update(1.0);
309 sma.update(2.0);
310 sma.update(3.0);
311 assert_eq!(sma.update(f64::NAN), None);
312 assert_eq!(sma.update(f64::INFINITY), None);
313 assert_eq!(sma.update(6.0), Some((2.0 + 3.0 + 6.0) / 3.0));
315 }
316
317 #[test]
318 fn reset_clears_state() {
319 let mut sma = Sma::new(3).unwrap();
320 sma.batch(&[1.0, 2.0, 3.0]);
321 assert!(sma.is_ready());
322 sma.reset();
323 assert!(!sma.is_ready());
324 assert_eq!(sma.update(10.0), None);
325 }
326
327 #[test]
328 fn batch_equals_streaming() {
329 let prices: Vec<f64> = (1..=20).map(f64::from).collect();
330 let mut a = Sma::new(5).unwrap();
331 let batch = a.batch(&prices);
332 let mut b = Sma::new(5).unwrap();
333 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
334 assert_eq!(batch, streamed);
335 }
336
337 #[test]
338 fn known_reference_values() {
339 let mut sma = Sma::new(3).unwrap();
341 let out = sma.batch(&[2.0, 4.0, 6.0, 8.0, 10.0]);
342 assert_eq!(out[2], Some(4.0));
343 assert_eq!(out[3], Some(6.0));
344 assert_eq!(out[4], Some(8.0));
345 }
346
347 #[test]
348 fn constant_series_yields_constant_sma() {
349 let mut sma = Sma::new(5).unwrap();
350 let v = sma.batch(&[7.0; 10]);
351 for x in v.iter().skip(4) {
352 assert_relative_eq!(x.unwrap(), 7.0, epsilon = 1e-12);
353 }
354 }
355
356 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
358 a.len() == b.len()
359 && a.iter()
360 .zip(b)
361 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
362 }
363
364 fn sma_replay(period: usize, series: &[f64]) -> Vec<f64> {
365 let mut s = Sma::new(period).unwrap();
366 series
367 .iter()
368 .map(|&x| s.update(x).unwrap_or(f64::NAN))
369 .collect()
370 }
371
372 #[test]
373 fn batch_nan_fast_path_is_bit_identical_with_reseed() {
374 let series: Vec<f64> = (0..500)
376 .map(|i| (f64::from(i) * 0.2).sin() * 10.0 + 50.0)
377 .collect();
378 let mut sma = Sma::new(14).unwrap();
379 let got = sma.batch_nan(&series);
380 assert!(bits_eq(&got, &sma_replay(14, &series)));
381 let mut ref_sma = Sma::new(14).unwrap();
383 for &x in &series {
384 ref_sma.update(x);
385 }
386 assert_eq!(sma.update(42.0), ref_sma.update(42.0));
387 }
388
389 #[test]
390 fn batch_nan_falls_back_on_non_finite() {
391 let series = [1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0];
392 let mut sma = Sma::new(3).unwrap();
393 assert!(bits_eq(&sma.batch_nan(&series), &sma_replay(3, &series)));
394 }
395
396 #[test]
397 fn batch_nan_falls_back_when_not_fresh() {
398 let mut sma = Sma::new(3).unwrap();
399 sma.update(99.0);
400 let series = [1.0, 2.0, 3.0, 4.0];
401 let mut ref_sma = Sma::new(3).unwrap();
402 ref_sma.update(99.0);
403 let want: Vec<f64> = series
404 .iter()
405 .map(|&x| ref_sma.update(x).unwrap_or(f64::NAN))
406 .collect();
407 assert!(bits_eq(&sma.batch_nan(&series), &want));
408 }
409
410 #[test]
411 fn batch_nan_sub_period_slice_is_all_nan() {
412 let series = [1.0, 2.0, 3.0];
413 let mut sma = Sma::new(10).unwrap();
414 let got = sma.batch_nan(&series);
415 assert!(bits_eq(&got, &sma_replay(10, &series)));
416 assert!(got.iter().all(|x| x.is_nan()));
417 }
418
419 proptest::proptest! {
420 #![proptest_config(proptest::test_runner::Config::with_cases(64))]
421 #[test]
422 fn sma_matches_naive_definition(
423 period in 1usize..20,
424 prices in proptest::collection::vec(-1000.0_f64..1000.0, 0..200),
425 ) {
426 let mut sma = Sma::new(period).unwrap();
427 let stream: Vec<_> = prices.iter().map(|p| sma.update(*p)).collect();
428 for (i, got) in stream.iter().enumerate() {
429 if i + 1 < period {
430 proptest::prop_assert!(got.is_none());
431 } else {
432 let window = &prices[i + 1 - period..=i];
433 let expected = window.iter().sum::<f64>() / period as f64;
434 let actual = got.expect("ready");
435 proptest::prop_assert!(
436 (actual - expected).abs() < 1e-9,
437 "i={i} actual={actual} expected={expected}"
438 );
439 }
440 }
441 }
442 }
443
444 #[test]
451 fn long_stream_drift_stays_bounded() {
452 let period = 20;
453 let mut sma = Sma::new(period).unwrap();
454 let mut window: VecDeque<f64> = VecDeque::with_capacity(period);
455 let n_updates = 16 * period * 5;
457 for i in 0..n_updates {
458 let v = if i % 2 == 0 { 1e9 } else { 1.0 };
459 sma.update(v);
460 if window.len() == period {
461 window.pop_front();
462 }
463 window.push_back(v);
464 }
465 let from_scratch: f64 = window.iter().sum::<f64>() / period as f64;
466 let got = sma.value().expect("warmed up");
467 assert!(
468 (got - from_scratch).abs() < 1e-6,
469 "SMA drift exceeds 1e-6 over {n_updates} updates: got={got}, scratch={from_scratch}"
470 );
471 }
472}