use crate::Price;
#[derive(Clone, Debug)]
pub(crate) struct EmaCore {
length: usize,
value: Price,
previous_value: Price,
current_price: Price,
sma_sum: f64,
seen_bars: usize,
converged: bool,
alpha: f64,
length_reciprocal: f64,
}
impl EmaCore {
pub(crate) fn bars_to_converge(length: usize) -> usize {
3 * (length + 1)
}
pub(crate) fn new(length: usize) -> Self {
#[allow(clippy::cast_precision_loss)]
let alpha = 2.0 / (length + 1) as f64;
Self::with_alpha(length, alpha)
}
pub(crate) fn with_alpha(length: usize, alpha: f64) -> Self {
Self {
length,
value: 0.0,
current_price: 0.0,
previous_value: 0.0,
seen_bars: 0,
sma_sum: 0.0,
converged: false,
alpha,
#[allow(clippy::cast_precision_loss)]
length_reciprocal: 1.0 / length as f64,
}
}
pub(crate) fn push(&mut self, price: Price) -> Option<Price> {
if self.converged {
if self.seen_bars == self.length {
self.seen_bars = self.length + 1;
}
self.previous_value = self.value;
self.value = self
.alpha
.mul_add(price - self.previous_value, self.previous_value);
return Some(self.value);
}
if self.seen_bars < self.length {
self.current_price = price;
self.sma_sum += price;
self.seen_bars += 1;
if self.seen_bars == self.length {
self.value = self.sma_sum * self.length_reciprocal;
} else {
return None;
}
} else {
self.seen_bars += 1;
self.previous_value = self.value;
self.value = self
.alpha
.mul_add(price - self.previous_value, self.previous_value);
}
self.converged = self.seen_bars >= self.length;
self.value()
}
pub(crate) fn replace(&mut self, price: Price) -> Option<Price> {
if self.seen_bars <= self.length {
self.sma_sum = self.sma_sum - self.current_price + price;
self.current_price = price;
if self.seen_bars == self.length {
self.value = self.sma_sum * self.length_reciprocal;
} else {
return None;
}
} else {
self.value = self
.alpha
.mul_add(price - self.previous_value, self.previous_value);
}
self.value()
}
pub(crate) fn value(&self) -> Option<Price> {
if self.converged {
Some(self.value)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn raw_ema(length: usize) -> EmaCore {
EmaCore::new(length)
}
mod seeding {
use super::*;
#[test]
fn none_before_seed_complete() {
let mut ema = raw_ema(3);
assert_eq!(ema.push(10.0), None);
assert_eq!(ema.push(20.0), None);
}
#[test]
fn first_value_is_sma() {
let mut ema = raw_ema(3);
ema.push(10.0);
ema.push(20.0);
assert_eq!(ema.push(30.0), Some(20.0));
}
#[test]
fn length_one_immediate() {
let mut ema = raw_ema(1);
assert_eq!(ema.push(42.0), Some(42.0));
}
}
mod steady_state {
use super::*;
#[test]
fn applies_ema_after_seed() {
let mut ema = raw_ema(3);
ema.push(2.0);
ema.push(4.0);
ema.push(6.0); assert_eq!(ema.push(8.0), Some(6.0));
}
#[test]
fn continues_computation() {
let mut ema = raw_ema(3);
ema.push(2.0);
ema.push(4.0);
ema.push(6.0); ema.push(8.0); assert_eq!(ema.push(10.0), Some(8.0));
}
#[test]
fn constant_input_stays_constant() {
let mut ema = raw_ema(3);
for _ in 0..20 {
ema.push(50.0);
}
assert_eq!(ema.push(50.0), Some(50.0));
}
}
mod convergence {
use super::*;
#[test]
fn emits_at_seed_without_enforcement() {
let mut ema = raw_ema(3);
ema.push(10.0);
ema.push(20.0);
assert!(ema.push(30.0).is_some());
}
}
mod replace {
use super::*;
#[test]
fn none_during_seed() {
let mut ema = raw_ema(3);
ema.push(10.0);
ema.push(20.0);
assert_eq!(ema.replace(25.0), None);
}
#[test]
fn seed_replace_adjusts_sum() {
let mut ema = raw_ema(3);
ema.push(10.0);
ema.push(20.0);
ema.replace(25.0); let val = ema.push(30.0).unwrap();
let expected = (10.0 + 25.0 + 30.0) / 3.0;
assert!((val - expected).abs() < 1e-10);
}
#[test]
fn seed_bar_replace_recomputes_sma() {
let mut ema = raw_ema(3);
ema.push(10.0);
ema.push(20.0);
assert_eq!(ema.push(30.0), Some(20.0)); assert_eq!(ema.replace(60.0), Some(30.0));
}
#[test]
fn seed_bar_replace_uses_sma_not_ema() {
let mut ema = raw_ema(3);
ema.push(10.0);
ema.push(20.0);
ema.push(30.0); assert_eq!(ema.replace(45.0), Some(25.0));
}
#[test]
fn multiple_replaces_during_seed() {
let mut ema = raw_ema(3);
ema.push(10.0);
ema.push(20.0);
ema.replace(22.0);
ema.replace(25.0); let val = ema.push(30.0).unwrap();
let expected = (10.0 + 25.0 + 30.0) / 3.0;
assert!((val - expected).abs() < 1e-10);
}
#[test]
fn steady_state_recomputes_from_previous() {
let mut ema = raw_ema(3);
ema.push(2.0);
ema.push(4.0);
ema.push(6.0); ema.push(8.0);
assert_eq!(ema.replace(12.0), Some(8.0));
}
#[test]
fn multiple_replaces_stable() {
let mut ema = raw_ema(3);
ema.push(2.0);
ema.push(4.0);
ema.push(6.0); ema.push(8.0);
ema.replace(10.0);
ema.replace(12.0);
assert_eq!(ema.replace(12.0), Some(8.0));
}
#[test]
fn push_after_replace_uses_replaced_value() {
let mut ema = raw_ema(3);
ema.push(2.0);
ema.push(4.0);
ema.push(6.0); ema.push(8.0); ema.replace(10.0);
assert_eq!(ema.push(12.0), Some(9.5));
} #[test]
fn replace_matches_clean_computation() {
let mut repainted = raw_ema(3);
repainted.push(2.0);
repainted.push(4.0);
repainted.push(6.0);
repainted.push(8.0);
repainted.replace(10.0); let val = repainted.push(12.0);
let mut clean = raw_ema(3);
clean.push(2.0);
clean.push(4.0);
clean.push(6.0);
clean.push(10.0);
let expected = clean.push(12.0);
assert_eq!(val, expected);
}
}
}