use std::collections::VecDeque;
use std::f64::consts::PI;
use crate::error::{Error, Result};
use crate::traits::Indicator;
const DELTA: f64 = 0.1;
const PEAK_AVG_LEN: usize = 50;
#[derive(Debug, Clone)]
pub struct EmpiricalModeDecomposition {
period: usize,
fraction: f64,
beta: f64,
alpha: f64,
prev_in_1: Option<f64>,
prev_in_2: Option<f64>,
prev_bp_1: f64,
prev_bp_2: f64,
peak: f64,
valley: f64,
bp_window: VecDeque<f64>,
bp_sum: f64,
peak_window: VecDeque<f64>,
peak_sum: f64,
valley_window: VecDeque<f64>,
valley_sum: f64,
upper: f64,
lower: f64,
last_value: Option<f64>,
}
impl EmpiricalModeDecomposition {
pub fn new(period: usize, fraction: f64) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
if period > crate::error::MAX_PERIOD {
return Err(Error::InvalidPeriod {
message: crate::error::PERIOD_ABOVE_MAX,
});
}
if !fraction.is_finite() || fraction <= 0.0 || fraction > 1.0 {
return Err(Error::InvalidPeriod {
message: "fraction must be in (0, 1]",
});
}
let beta = (2.0 * PI / period as f64).cos();
let gamma = 1.0 / (4.0 * PI * DELTA / period as f64).cos();
let alpha = gamma - (gamma * gamma - 1.0).sqrt();
Ok(Self {
period,
fraction,
beta,
alpha,
prev_in_1: None,
prev_in_2: None,
prev_bp_1: 0.0,
prev_bp_2: 0.0,
peak: 0.0,
valley: 0.0,
bp_window: VecDeque::with_capacity(2 * period),
bp_sum: 0.0,
peak_window: VecDeque::with_capacity(PEAK_AVG_LEN),
peak_sum: 0.0,
valley_window: VecDeque::with_capacity(PEAK_AVG_LEN),
valley_sum: 0.0,
upper: 0.0,
lower: 0.0,
last_value: None,
})
}
pub const fn period(&self) -> usize {
self.period
}
pub const fn fraction(&self) -> f64 {
self.fraction
}
pub const fn value(&self) -> Option<f64> {
self.last_value
}
pub const fn upper(&self) -> f64 {
self.upper
}
pub const fn lower(&self) -> f64 {
self.lower
}
fn push(window: &mut VecDeque<f64>, sum: &mut f64, len: usize, x: f64) {
if window.len() == len {
*sum -= window.pop_front().expect("window is non-empty");
}
window.push_back(x);
*sum += x;
}
}
impl Indicator for EmpiricalModeDecomposition {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return None;
}
let bp = if let Some(x2) = self.prev_in_2 {
0.5 * (1.0 - self.alpha) * (input - x2)
+ self.beta * (1.0 + self.alpha) * self.prev_bp_1
- self.alpha * self.prev_bp_2
} else {
0.0
};
if self.prev_bp_1 > bp && self.prev_bp_1 > self.prev_bp_2 {
self.peak = self.prev_bp_1;
}
if self.prev_bp_1 < bp && self.prev_bp_1 < self.prev_bp_2 {
self.valley = self.prev_bp_1;
}
self.prev_bp_2 = self.prev_bp_1;
self.prev_bp_1 = bp;
self.prev_in_2 = self.prev_in_1;
self.prev_in_1 = Some(input);
Self::push(&mut self.bp_window, &mut self.bp_sum, 2 * self.period, bp);
Self::push(
&mut self.peak_window,
&mut self.peak_sum,
PEAK_AVG_LEN,
self.peak,
);
Self::push(
&mut self.valley_window,
&mut self.valley_sum,
PEAK_AVG_LEN,
self.valley,
);
if self.bp_window.len() < 2 * self.period || self.peak_window.len() < PEAK_AVG_LEN {
return None;
}
let n = PEAK_AVG_LEN as f64;
self.upper = self.fraction * self.peak_sum / n;
self.lower = self.fraction * self.valley_sum / n;
let mean = self.bp_sum / (2 * self.period) as f64;
self.last_value = Some(mean);
Some(mean)
}
fn reset(&mut self) {
self.prev_in_1 = None;
self.prev_in_2 = None;
self.prev_bp_1 = 0.0;
self.prev_bp_2 = 0.0;
self.peak = 0.0;
self.valley = 0.0;
self.bp_window.clear();
self.bp_sum = 0.0;
self.peak_window.clear();
self.peak_sum = 0.0;
self.valley_window.clear();
self.valley_sum = 0.0;
self.upper = 0.0;
self.lower = 0.0;
self.last_value = None;
}
#[inline]
fn warmup_period(&self) -> usize {
(2 * self.period).max(PEAK_AVG_LEN)
}
#[inline]
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
#[inline]
fn name(&self) -> &'static str {
"EmpiricalModeDecomposition"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
#[test]
fn new_rejects_invalid_params() {
assert!(matches!(
EmpiricalModeDecomposition::new(0, 0.5),
Err(Error::PeriodZero)
));
assert!(matches!(
EmpiricalModeDecomposition::new(20, 0.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
EmpiricalModeDecomposition::new(20, 1.5),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
EmpiricalModeDecomposition::new(20, f64::NAN),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
assert_eq!(emd.period(), 20);
assert!((emd.fraction() - 0.5).abs() < 1e-15);
assert_eq!(emd.name(), "EmpiricalModeDecomposition");
assert!(emd.warmup_period() >= 1);
assert!(!emd.is_ready());
let prices: Vec<f64> = (0..200)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
emd.batch(&prices);
assert!(emd.is_ready());
assert!(emd.value().is_some());
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (0..200)
.map(|i| 100.0 + (f64::from(i) * 0.2).cos() * 5.0)
.collect();
let mut a = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
let mut b = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
let batch = a.batch(&prices);
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn ignores_non_finite_input() {
let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
let prices: Vec<f64> = (0..200)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
emd.batch(&prices);
let before = emd.value();
assert!(before.is_some());
assert_eq!(emd.update(f64::NAN), None);
}
#[test]
fn reset_clears_state() {
let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
let prices: Vec<f64> = (0..200)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
emd.batch(&prices);
assert!(emd.is_ready());
emd.reset();
assert!(!emd.is_ready());
}
#[test]
fn rejects_period_above_maximum() {
assert!(matches!(
EmpiricalModeDecomposition::new(crate::error::MAX_PERIOD + 1, 0.5),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
EmpiricalModeDecomposition::new(20, -0.1),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
EmpiricalModeDecomposition::new(20, f64::INFINITY),
Err(Error::InvalidPeriod { .. })
));
assert!(EmpiricalModeDecomposition::new(20, 1.0).is_ok());
}
#[test]
fn warmup_is_max_of_two_period_and_fifty() {
let prices: Vec<f64> = (0..150)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
for (period, expected) in [(1usize, 50usize), (10, 50), (25, 50), (26, 52), (40, 80)] {
let mut emd = EmpiricalModeDecomposition::new(period, 0.1).unwrap();
assert_eq!(emd.warmup_period(), expected);
let out = emd.batch(&prices);
assert!(out[..expected - 1].iter().all(Option::is_none));
assert!(out[expected - 1].is_some());
}
}
#[test]
fn constant_series_has_zero_mean_and_thresholds() {
let mut emd = EmpiricalModeDecomposition::new(10, 0.5).unwrap();
let out = emd.batch(&[42.0; 80]);
assert!(out
.iter()
.flatten()
.all(|v| v.to_bits() == 0.0f64.to_bits()));
assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
}
#[test]
fn hand_computed_step_peak_valley_and_thresholds() {
let mut emd = EmpiricalModeDecomposition::new(5, 0.5).unwrap();
let (alpha, beta) = (emd.alpha, emd.beta);
assert!((beta - 0.309_016_994_374_947_45).abs() < 1e-15);
assert!((alpha - 0.775_679_511_049_613_4).abs() < 1e-12);
for _ in 0..60 {
emd.update(0.0);
}
assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
let c = 0.5 * (1.0 - alpha);
let k = beta * (1.0 + alpha);
let bp60 = c;
let bp61 = c + k * bp60;
let bp62 = k * bp61 - alpha * bp60;
let bp63 = k * bp62 - alpha * bp61;
let bp64 = k * bp63 - alpha * bp62;
assert!((bp61 - 0.173_704_269_339_216_5).abs() < 1e-12);
assert!((bp63 + 0.130_176_956_775_418_27).abs() < 1e-12);
let m60 = emd.update(1.0).unwrap();
assert!((m60 - bp60 / 10.0).abs() < 1e-15);
assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
emd.update(1.0);
let m62 = emd.update(1.0).unwrap();
assert!((emd.upper() - 0.5 * bp61 / 50.0).abs() < 1e-15);
assert!((emd.upper() - 0.001_737_042_693_392_165).abs() < 1e-12);
assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
assert!((m62 - (bp60 + bp61 + bp62) / 10.0).abs() < 1e-15);
emd.update(1.0);
assert!((emd.upper() - 0.5 * 2.0 * bp61 / 50.0).abs() < 1e-15);
assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
let m64 = emd.update(1.0).unwrap();
assert!((emd.upper() - 0.5 * 3.0 * bp61 / 50.0).abs() < 1e-15);
assert!((emd.lower() - 0.5 * bp63 / 50.0).abs() < 1e-15);
assert!((emd.lower() + 0.001_301_769_567_754_182_7).abs() < 1e-12);
assert!((m64 - (bp60 + bp61 + bp62 + bp63 + bp64) / 10.0).abs() < 1e-15);
assert_eq!(emd.value(), Some(m64));
}
#[test]
fn thresholds_bracket_zero_on_an_oscillation() {
let mut emd = EmpiricalModeDecomposition::new(20, 0.3).unwrap();
for i in 0..400 {
emd.update((f64::from(i) * 2.0 * PI / 20.0).sin() * 10.0 + 100.0);
}
assert!(emd.upper() > 0.0);
assert!(emd.lower() < 0.0);
let mean = emd.value().unwrap();
assert!(mean.abs() < emd.upper());
}
#[test]
fn reset_replays_identically_and_batch_nan_into_matches() {
let prices: Vec<f64> = (0..200)
.map(|i| 100.0 + (f64::from(i) * 0.17).sin() * 3.0 + f64::from(i) * 0.02)
.collect();
let mut emd = EmpiricalModeDecomposition::new(12, 0.2).unwrap();
let first = emd.batch(&prices);
let (up, lo) = (emd.upper(), emd.lower());
emd.reset();
assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
assert_eq!(emd.value(), None);
let second = emd.batch(&prices);
assert_eq!(first, second);
assert_eq!(emd.upper().to_bits(), up.to_bits());
assert_eq!(emd.lower().to_bits(), lo.to_bits());
let mut fresh = EmpiricalModeDecomposition::new(12, 0.2).unwrap();
let mut out = vec![0.0; prices.len()];
fresh.batch_nan_into(&prices, &mut out);
assert!(out
.iter()
.zip(&first)
.all(|(a, b)| a.to_bits() == b.unwrap_or(f64::NAN).to_bits()));
}
}