use std::fmt;
use std::time::Duration;
use crate::errors::{Result, TaError};
use crate::indicators::AdaptiveTimeDetector;
use crate::simd::ema::ema_continuation_into;
use crate::{Next, NextBatch, Reset};
use chrono::{DateTime, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[doc(alias = "EMA")]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ExponentialMovingAverage {
duration: Duration,
k: f64,
current: f64,
is_new: bool,
detector: AdaptiveTimeDetector,
last_value: f64,
}
impl ExponentialMovingAverage {
pub fn new(duration: Duration) -> Result<Self> {
if duration.as_secs() == 0 && duration.subsec_nanos() == 0 {
Err(TaError::InvalidParameter)
} else {
let unit_seconds = if duration < Duration::from_secs(86400) {
60.0
} else {
86400.0
};
let periods = duration.as_secs() as f64 / unit_seconds;
Ok(Self {
duration,
k: 2.0 / (periods + 1.0),
current: 0.0,
is_new: true,
detector: AdaptiveTimeDetector::new(duration),
last_value: 0.0,
})
}
}
}
impl Next<f64> for ExponentialMovingAverage {
type Output = f64;
fn next(&mut self, (timestamp, value): (DateTime<Utc>, f64)) -> Self::Output {
let should_replace = self.detector.should_replace(timestamp);
if should_replace && !self.is_new {
let old_current = if (1.0 - self.k) != 0.0 {
(self.current - self.k * self.last_value) / (1.0 - self.k)
} else {
self.current
};
self.current = (self.k * value) + ((1.0 - self.k) * old_current);
} else {
if self.is_new {
self.is_new = false;
self.current = value;
} else {
self.current = (self.k * value) + ((1.0 - self.k) * self.current);
}
}
self.last_value = value;
self.current
}
}
impl NextBatch<f64> for ExponentialMovingAverage {
fn next_batch(&mut self, inputs: &[(DateTime<Utc>, f64)]) -> Vec<f64> {
if inputs.is_empty() {
return Vec::new();
}
let mut probe = self.detector.clone();
for &(ts, _) in inputs {
if probe.should_replace(ts) {
return inputs.iter().map(|&i| self.next(i)).collect();
}
}
let values: Vec<f64> = inputs.iter().map(|&(_, v)| v).collect();
let mut out = vec![0.0; values.len()];
let cont_start = if self.is_new {
out[0] = values[0];
self.is_new = false;
self.current = values[0];
1
} else {
0
};
if cont_start < values.len() {
ema_continuation_into(
&values[cont_start..],
self.k,
self.current,
&mut out[cont_start..],
);
self.current = *out.last().expect("len > 0");
}
for &(ts, _) in inputs {
self.detector.should_replace(ts);
}
self.last_value = values[values.len() - 1];
out
}
}
impl Reset for ExponentialMovingAverage {
fn reset(&mut self) {
self.current = 0.0;
self.is_new = true;
self.detector.reset();
self.last_value = 0.0;
}
}
impl Default for ExponentialMovingAverage {
fn default() -> Self {
Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() }
}
impl fmt::Display for ExponentialMovingAverage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let days = self.duration.as_secs() / 86400;
if days > 0 && self.duration.as_secs() % 86400 == 0 {
write!(f, "EMA({} days)", days)
} else {
write!(f, "EMA({}s)", self.duration.as_secs())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_new() {
assert!(ExponentialMovingAverage::new(Duration::from_secs(0)).is_err());
assert!(ExponentialMovingAverage::new(Duration::from_secs(86400)).is_ok());
}
#[test]
fn test_next() {
let mut ema = ExponentialMovingAverage::new(Duration::from_secs(3 * 86400)).unwrap(); let now = Utc::now();
assert_eq!(ema.next((now, 2.0)), 2.0);
assert_eq!(ema.next((now + chrono::Duration::days(1), 5.0)), 3.5);
assert_eq!(ema.next((now + chrono::Duration::days(2), 1.0)), 2.25);
assert_eq!(ema.next((now + chrono::Duration::days(3), 6.25)), 4.25);
}
#[test]
fn test_reset() {
let mut ema = ExponentialMovingAverage::new(Duration::from_secs(5 * 86400)).unwrap(); let now = Utc::now();
assert_eq!(ema.next((now, 4.0)), 4.0);
ema.next((now + chrono::Duration::days(1), 10.0));
ema.next((now + chrono::Duration::days(2), 15.0));
ema.next((now + chrono::Duration::days(3), 20.0));
assert_ne!(ema.next((now + chrono::Duration::days(4), 4.0)), 4.0);
ema.reset();
assert_eq!(ema.next((now, 4.0)), 4.0);
}
#[test]
fn test_default() {
let _ema = ExponentialMovingAverage::default();
}
#[test]
fn test_display() {
let ema = ExponentialMovingAverage::new(Duration::from_secs(7 * 86400)).unwrap(); assert_eq!(format!("{}", ema), "EMA(7 days)");
}
#[test]
fn test_next_batch_matches_next_loop() {
for n in [0usize, 1, 4, 5, 16, 17, 100, 1000] {
for period_days in [1u64, 3, 7, 30, 90] {
let duration = Duration::from_secs(period_days * 86400);
let mut a = ExponentialMovingAverage::new(duration).unwrap();
let mut b = ExponentialMovingAverage::new(duration).unwrap();
let start = Utc::now();
let inputs: Vec<(DateTime<Utc>, f64)> = (0..n)
.map(|i| {
(
start + chrono::Duration::days(i as i64),
100.0 + ((i as f64) * 0.13).sin() * 5.0,
)
})
.collect();
let scalar_out: Vec<f64> = inputs.iter().map(|&i| a.next(i)).collect();
let simd_out = b.next_batch(&inputs);
assert_eq!(scalar_out.len(), simd_out.len());
for (i, (s, v)) in scalar_out.iter().zip(simd_out.iter()).enumerate() {
let diff = (s - v).abs();
let tol = 1e-10 * s.abs().max(v.abs()).max(1.0);
assert!(
diff <= tol,
"n={} period={}d index={}: next()={} next_batch()={} diff={}",
n,
period_days,
i,
s,
v,
diff
);
}
let extra_inputs: Vec<(DateTime<Utc>, f64)> = (n..n + 5)
.map(|i| {
(
start + chrono::Duration::days(i as i64),
42.0 + (i as f64).cos(),
)
})
.collect();
for &inp in &extra_inputs {
let s = a.next(inp);
let v = b.next(inp);
let diff = (s - v).abs();
let tol = 1e-10 * s.abs().max(v.abs()).max(1.0);
assert!(
diff <= tol,
"post-batch state diverged: scalar={} simd={}",
s,
v
);
}
}
}
}
#[test]
fn test_next_batch_falls_back_on_replacement() {
let duration = Duration::from_secs(60 * 60); let mut a = ExponentialMovingAverage::new(duration).unwrap();
let mut b = ExponentialMovingAverage::new(duration).unwrap();
let start = Utc::now();
let inputs = vec![
(start, 100.0),
(start + chrono::Duration::minutes(1), 101.0),
(start + chrono::Duration::minutes(1), 102.0), (start + chrono::Duration::minutes(2), 103.0),
];
let scalar: Vec<f64> = inputs.iter().map(|&i| a.next(i)).collect();
let batch = b.next_batch(&inputs);
for (s, v) in scalar.iter().zip(batch.iter()) {
assert!((s - v).abs() < 1e-12, "scalar={} batch={}", s, v);
}
}
#[test]
fn test_intraday_instability() {
let mut ema = ExponentialMovingAverage::new(Duration::from_secs(30 * 60)).unwrap();
let now = Utc::now();
ema.next((now, 100.0));
let val_step = ema.next((now + chrono::Duration::minutes(1), 110.0));
assert!(
val_step < 110.0,
"EMA overshot the target value! Value: {}",
val_step
);
assert!(
val_step > 100.0,
"EMA did not increase! Value: {}",
val_step
);
}
}