use wide::f64x4;
const LANES: usize = 4;
pub fn ema(values: &[f64], k: f64) -> Vec<f64> {
let mut out = vec![0.0; values.len()];
ema_into(values, k, &mut out);
out
}
pub fn ema_into(values: &[f64], k: f64, out: &mut [f64]) {
assert_eq!(
values.len(),
out.len(),
"ema_into: out.len() must equal values.len()"
);
if values.is_empty() {
return;
}
out[0] = values[0];
if values.len() == 1 {
return;
}
ema_continuation_into(&values[1..], k, values[0], &mut out[1..]);
}
pub fn ema_continuation_into(values: &[f64], k: f64, s_prev: f64, out: &mut [f64]) {
assert_eq!(
values.len(),
out.len(),
"ema_continuation_into: out.len() must equal values.len()"
);
if values.is_empty() {
return;
}
let a = 1.0 - k;
let a2 = a * a;
let a3 = a2 * a;
let a4 = a3 * a;
let col0 = f64x4::new([1.0, a, a2, a3]);
let col1 = f64x4::new([0.0, 1.0, a, a2]);
let col2 = f64x4::new([0.0, 0.0, 1.0, a]);
let col3 = f64x4::new([0.0, 0.0, 0.0, 1.0]);
let a_powers = f64x4::new([a, a2, a3, a4]);
let k_vec = f64x4::splat(k);
let mut s_prev = s_prev;
let n = values.len();
let mut i = 0;
while i + LANES <= n {
let x0 = f64x4::splat(values[i]);
let x1 = f64x4::splat(values[i + 1]);
let x2 = f64x4::splat(values[i + 2]);
let x3 = f64x4::splat(values[i + 3]);
let xc = x0 * col0 + x1 * col1 + x2 * col2 + x3 * col3;
let block = k_vec * xc + a_powers * f64x4::splat(s_prev);
let arr = block.to_array();
out[i] = arr[0];
out[i + 1] = arr[1];
out[i + 2] = arr[2];
out[i + 3] = arr[3];
s_prev = arr[3];
i += LANES;
}
while i < n {
s_prev = k * values[i] + a * s_prev;
out[i] = s_prev;
i += 1;
}
}
#[cfg(test)]
pub(crate) fn ema_scalar(values: &[f64], k: f64) -> Vec<f64> {
let n = values.len();
let mut out = vec![0.0; n];
if n == 0 {
return out;
}
out[0] = values[0];
let a = 1.0 - k;
let mut s = values[0];
for i in 1..n {
s = k * values[i] + a * s;
out[i] = s;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_slice_eq(a: &[f64], b: &[f64], rel: f64, abs: f64) {
assert_eq!(a.len(), b.len());
for (i, (&x, &y)) in a.iter().zip(b.iter()).enumerate() {
let diff = (x - y).abs();
let tol = abs.max(rel * x.abs().max(y.abs()));
assert!(
diff <= tol,
"mismatch at index {}: simd={} scalar={} diff={}",
i,
x,
y,
diff
);
}
}
#[test]
fn known_values() {
let v = [2.0, 5.0, 1.0, 6.25];
let out = ema(&v, 0.5);
let expected = [2.0, 3.5, 2.25, 4.25];
for (a, b) in out.iter().zip(expected.iter()) {
assert!((a - b).abs() < 1e-12, "got {} expected {}", a, b);
}
}
#[test]
fn empty_and_single() {
assert_eq!(ema(&[], 0.5), Vec::<f64>::new());
assert_eq!(ema(&[3.14], 0.5), vec![3.14]);
}
#[test]
fn parity_random() {
for n in [0, 1, 2, 3, 4, 5, 7, 8, 16, 17, 31, 100, 1000, 10_000] {
let values: Vec<f64> = (0..n)
.map(|i| 100.0 + ((i as f64) * 0.07).sin() * 10.0)
.collect();
for k in [0.01, 0.1, 0.0645, 0.5, 0.9] {
let s = ema_scalar(&values, k);
let v = ema(&values, k);
approx_slice_eq(&v, &s, 1e-10, 1e-12);
}
}
}
#[test]
fn matches_streaming_indicator() {
use crate::indicators::ExponentialMovingAverage;
use crate::Next;
use chrono::{TimeZone, Utc};
use std::time::Duration;
let mut streaming = ExponentialMovingAverage::new(Duration::from_secs(30 * 86400)).unwrap();
let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let values: Vec<f64> = (0..200).map(|i| 50.0 + (i as f64) * 0.3).collect();
let streaming_out: Vec<f64> = values
.iter()
.enumerate()
.map(|(i, &v)| streaming.next((start + chrono::Duration::days(i as i64), v)))
.collect();
let k = 2.0 / 31.0;
let batch_out = ema(&values, k);
for (i, (s, b)) in streaming_out.iter().zip(batch_out.iter()).enumerate() {
let diff = (s - b).abs();
assert!(
diff <= 1e-10 * s.abs().max(b.abs()).max(1e-12),
"index {}: streaming={} batch={}",
i,
s,
b
);
}
}
#[test]
fn continuation_resumes_correctly() {
let values: Vec<f64> = (0..50).map(|i| 10.0 + (i as f64).sin()).collect();
let k = 0.2;
let full = ema(&values, k);
let split = 7;
let prefix = ema(&values[..split], k);
let s_prev = *prefix.last().unwrap();
let mut cont = vec![0.0; values.len() - split];
ema_continuation_into(&values[split..], k, s_prev, &mut cont);
let mut joined = prefix;
joined.extend(cont);
for (i, (a, b)) in full.iter().zip(joined.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-12,
"index {}: full={} joined={}",
i,
a,
b
);
}
}
}