use std::collections::VecDeque;
use serde::{Deserialize, Serialize};
use crate::indicator::{
config::{EmaWindow, SmaWindow},
streaming::StreamingIndicator,
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub(crate) struct StreamingEwm {
alpha: f64,
current_mean: Option<f64>,
window_size: usize,
count: usize,
}
impl StreamingEwm {
pub(crate) const fn new(alpha: f64, window_size: usize) -> Self {
Self {
alpha,
current_mean: None,
window_size,
count: 0,
}
}
}
impl StreamingIndicator for StreamingEwm {
type Input = f64;
type Output<'a> = Option<f64>;
fn update(&mut self, value: Self::Input) -> Self::Output<'_> {
self.count += 1;
match self.current_mean {
None => {
self.current_mean = Some(value);
}
Some(prev) => {
self.current_mean = Some((1.0 - self.alpha).mul_add(prev, self.alpha * value));
}
}
if self.count >= self.window_size {
self.current_mean
} else {
None
}
}
fn reset(&mut self) {
self.current_mean = None;
self.count = 0;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamingSma {
window_size: usize,
buffer: VecDeque<f64>,
sum: f64,
}
impl StreamingSma {
#[must_use]
pub fn new(window_size: SmaWindow) -> Self {
let size = window_size.0 as usize;
Self {
window_size: size,
buffer: VecDeque::with_capacity(size + 1),
sum: 0.0,
}
}
}
impl StreamingIndicator for StreamingSma {
type Input = f64;
type Output<'a> = Option<f64>;
#[expect(
clippy::expect_used,
reason = "the ring-buffer length is bounded by the window size, which always fits in u32"
)]
fn update(&mut self, value: Self::Input) -> Self::Output<'_> {
self.buffer.push_back(value);
self.sum += value;
if self.buffer.len() > self.window_size
&& let Some(removed) = self.buffer.pop_front()
{
self.sum -= removed;
}
if self.buffer.len() >= self.window_size {
let len_u32 =
u32::try_from(self.buffer.len()).expect("SMA window length exceeds u32 range");
Some(self.sum / f64::from(len_u32))
} else {
None
}
}
fn reset(&mut self) {
self.buffer.clear();
self.sum = 0.0;
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct StreamingEma {
inner: StreamingEwm,
}
impl StreamingEma {
#[must_use]
pub fn new(window_size: EmaWindow) -> Self {
let size = window_size.0 as usize;
let size_u32 = u32::from(window_size.0);
let alpha = 2.0 / (f64::from(size_u32) + 1.0);
Self {
inner: StreamingEwm::new(alpha, size),
}
}
}
impl StreamingIndicator for StreamingEma {
type Input = f64;
type Output<'a> = Option<f64>;
fn update(&mut self, value: Self::Input) -> Self::Output<'_> {
self.inner.update(value)
}
fn reset(&mut self) {
self.inner.reset();
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests assert against known-valid fixtures; unwrap surfaces failures as panics that fail the test"
)]
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
#[test]
fn sma_returns_none_until_window_is_full() {
let mut sma = StreamingSma::new(SmaWindow(3));
assert_eq!(sma.update(1.0), None); assert_eq!(sma.update(2.0), None); assert_eq!(sma.update(3.0), Some(2.0)); }
#[test]
fn sma_slides_the_window() {
let mut sma = StreamingSma::new(SmaWindow(3));
sma.update(1.0);
sma.update(2.0);
sma.update(3.0); assert_eq!(sma.update(4.0), Some(3.0)); assert_eq!(sma.update(5.0), Some(4.0)); }
#[test]
fn sma_window_of_one_returns_each_value() {
let mut sma = StreamingSma::new(SmaWindow(1));
assert_eq!(sma.update(7.0), Some(7.0));
assert_eq!(sma.update(9.0), Some(9.0));
}
#[test]
fn sma_reset_clears_state() {
let mut sma = StreamingSma::new(SmaWindow(2));
sma.update(10.0);
sma.update(20.0); sma.reset();
assert_eq!(sma.update(1.0), None);
assert_eq!(sma.update(3.0), Some(2.0)); }
#[test]
fn ema_returns_none_until_window_is_reached() {
let mut ema = StreamingEma::new(EmaWindow(3));
assert_eq!(ema.update(1.0), None);
assert_eq!(ema.update(2.0), None);
assert!(ema.update(3.0).is_some()); }
#[test]
fn ema_seeds_with_first_value() {
let mut ema = StreamingEma::new(EmaWindow(1));
assert_eq!(ema.update(5.0), Some(5.0));
}
#[test]
fn ema_applies_alpha_recursively() {
let mut ema = StreamingEma::new(EmaWindow(2));
let alpha = 2.0 / 3.0;
assert_eq!(ema.update(10.0), None);
let expected = (1.0_f64 - alpha).mul_add(10.0, alpha * 20.0);
approx(ema.update(20.0).unwrap(), expected);
}
#[test]
fn ema_constant_input_converges_to_that_constant() {
let mut ema = StreamingEma::new(EmaWindow(5));
let mut last = None;
for _ in 0..20 {
last = ema.update(42.0);
}
approx(last.unwrap(), 42.0);
}
#[test]
fn ema_reset_clears_state() {
let mut ema = StreamingEma::new(EmaWindow(2));
ema.update(100.0);
ema.update(200.0); ema.reset();
assert_eq!(ema.update(1.0), None); approx(
ema.update(3.0).unwrap(),
(1.0_f64 / 3.0).mul_add(1.0, (2.0 / 3.0) * 3.0),
);
}
#[test]
fn ewm_with_alpha_one_tracks_latest_value() {
let mut ewm = StreamingEwm::new(1.0, 1);
assert_eq!(ewm.update(5.0), Some(5.0));
assert_eq!(ewm.update(9.0), Some(9.0));
assert_eq!(ewm.update(2.0), Some(2.0));
}
#[test]
fn ewm_with_alpha_zero_holds_first_value() {
let mut ewm = StreamingEwm::new(0.0, 1);
assert_eq!(ewm.update(7.0), Some(7.0));
assert_eq!(ewm.update(100.0), Some(7.0));
}
#[test]
fn ewm_respects_window_warmup() {
let mut ewm = StreamingEwm::new(0.5, 3);
assert_eq!(ewm.update(1.0), None);
assert_eq!(ewm.update(2.0), None);
assert!(ewm.update(3.0).is_some());
}
}