Skip to main content

wickra_core/indicators/
empirical_mode_decomposition.rs

1//! Ehlers Empirical Mode Decomposition (bandpass trend component).
2
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6use crate::error::{Error, Result};
7use crate::traits::Indicator;
8
9/// Ehlers' half-bandwidth `Delta` of the bandpass (his published default).
10const DELTA: f64 = 0.1;
11
12/// Length of the peak / valley averages.
13const PEAK_AVG_LEN: usize = 50;
14
15/// Ehlers' adaptation of Empirical Mode Decomposition (EMD).
16///
17/// From John Ehlers & Ric Way, *"Empirical Mode Decomposition"*, Technical
18/// Analysis of Stocks & Commodities, March 2010:
19///
20/// ```text
21/// β  = cos(2π / Period),  γ = 1 / cos(4π · Delta / Period),  α = γ − √(γ² − 1)
22/// BP = 0.5·(1 − α)·(Price − Price[2]) + β·(1 + α)·BP[1] − α·BP[2]
23/// Mean   = SMA(BP, 2 · Period)                          (the trend component)
24/// Peak   = BP[1] at a local maximum of BP, otherwise the previous Peak
25/// Valley = BP[1] at a local minimum of BP, otherwise the previous Valley
26/// Upper  = Fraction · SMA(Peak, 50)
27/// Lower  = Fraction · SMA(Valley, 50)
28/// ```
29///
30/// `Delta` is Ehlers' `0.1`. The output is `Mean`; the two thresholds are
31/// available from [`upper`](Self::upper) and [`lower`](Self::lower) after each
32/// update. The market is in a trend mode while `Mean` sits above `Upper`
33/// (bullish) or below `Lower` (bearish), and in a cycle mode between them.
34///
35/// # Example
36///
37/// ```
38/// use wickra_core::{Indicator, EmpiricalModeDecomposition};
39///
40/// let mut emd = EmpiricalModeDecomposition::new(20, 0.1).unwrap();
41/// let mut last = None;
42/// for i in 0..200 {
43///     last = emd.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
44/// }
45/// assert!(last.is_some());
46/// ```
47#[derive(Debug, Clone)]
48pub struct EmpiricalModeDecomposition {
49    period: usize,
50    fraction: f64,
51    beta: f64,
52    alpha: f64,
53    prev_in_1: Option<f64>,
54    prev_in_2: Option<f64>,
55    prev_bp_1: f64,
56    prev_bp_2: f64,
57    peak: f64,
58    valley: f64,
59    bp_window: VecDeque<f64>,
60    bp_sum: f64,
61    peak_window: VecDeque<f64>,
62    peak_sum: f64,
63    valley_window: VecDeque<f64>,
64    valley_sum: f64,
65    upper: f64,
66    lower: f64,
67    last_value: Option<f64>,
68}
69
70impl EmpiricalModeDecomposition {
71    /// Construct with the bandpass centre period and the threshold fraction.
72    ///
73    /// `fraction` scales the averaged peaks and valleys into the trend-mode
74    /// thresholds; Ehlers uses `0.1`.
75    ///
76    /// # Errors
77    ///
78    /// Returns [`Error::PeriodZero`] if `period == 0`, and
79    /// [`Error::InvalidPeriod`] if `fraction` is not in `(0, 1]`.
80    pub fn new(period: usize, fraction: f64) -> Result<Self> {
81        if period == 0 {
82            return Err(Error::PeriodZero);
83        }
84        if period > crate::error::MAX_PERIOD {
85            return Err(Error::InvalidPeriod {
86                message: crate::error::PERIOD_ABOVE_MAX,
87            });
88        }
89        if !fraction.is_finite() || fraction <= 0.0 || fraction > 1.0 {
90            return Err(Error::InvalidPeriod {
91                message: "fraction must be in (0, 1]",
92            });
93        }
94        let beta = (2.0 * PI / period as f64).cos();
95        let gamma = 1.0 / (4.0 * PI * DELTA / period as f64).cos();
96        let alpha = gamma - (gamma * gamma - 1.0).sqrt();
97        Ok(Self {
98            period,
99            fraction,
100            beta,
101            alpha,
102            prev_in_1: None,
103            prev_in_2: None,
104            prev_bp_1: 0.0,
105            prev_bp_2: 0.0,
106            peak: 0.0,
107            valley: 0.0,
108            bp_window: VecDeque::with_capacity(2 * period),
109            bp_sum: 0.0,
110            peak_window: VecDeque::with_capacity(PEAK_AVG_LEN),
111            peak_sum: 0.0,
112            valley_window: VecDeque::with_capacity(PEAK_AVG_LEN),
113            valley_sum: 0.0,
114            upper: 0.0,
115            lower: 0.0,
116            last_value: None,
117        })
118    }
119
120    /// Configured period.
121    pub const fn period(&self) -> usize {
122        self.period
123    }
124
125    /// Configured fraction.
126    pub const fn fraction(&self) -> f64 {
127        self.fraction
128    }
129
130    /// Current value (the trend component `Mean`) if available.
131    pub const fn value(&self) -> Option<f64> {
132        self.last_value
133    }
134
135    /// Upper trend threshold `Fraction · SMA(Peak, 50)` after the last update.
136    pub const fn upper(&self) -> f64 {
137        self.upper
138    }
139
140    /// Lower trend threshold `Fraction · SMA(Valley, 50)` after the last update.
141    pub const fn lower(&self) -> f64 {
142        self.lower
143    }
144
145    /// Push `x` into a fixed-length window with a running sum.
146    fn push(window: &mut VecDeque<f64>, sum: &mut f64, len: usize, x: f64) {
147        if window.len() == len {
148            *sum -= window.pop_front().expect("window is non-empty");
149        }
150        window.push_back(x);
151        *sum += x;
152    }
153}
154
155impl Indicator for EmpiricalModeDecomposition {
156    type Input = f64;
157    type Output = f64;
158
159    fn update(&mut self, input: f64) -> Option<f64> {
160        if !input.is_finite() {
161            return None;
162        }
163        // 2nd-order resonant bandpass.
164        let bp = if let Some(x2) = self.prev_in_2 {
165            0.5 * (1.0 - self.alpha) * (input - x2)
166                + self.beta * (1.0 + self.alpha) * self.prev_bp_1
167                - self.alpha * self.prev_bp_2
168        } else {
169            0.0
170        };
171        // Peak / valley of the previous bandpass value.
172        if self.prev_bp_1 > bp && self.prev_bp_1 > self.prev_bp_2 {
173            self.peak = self.prev_bp_1;
174        }
175        if self.prev_bp_1 < bp && self.prev_bp_1 < self.prev_bp_2 {
176            self.valley = self.prev_bp_1;
177        }
178        self.prev_bp_2 = self.prev_bp_1;
179        self.prev_bp_1 = bp;
180        self.prev_in_2 = self.prev_in_1;
181        self.prev_in_1 = Some(input);
182
183        Self::push(&mut self.bp_window, &mut self.bp_sum, 2 * self.period, bp);
184        Self::push(
185            &mut self.peak_window,
186            &mut self.peak_sum,
187            PEAK_AVG_LEN,
188            self.peak,
189        );
190        Self::push(
191            &mut self.valley_window,
192            &mut self.valley_sum,
193            PEAK_AVG_LEN,
194            self.valley,
195        );
196        if self.bp_window.len() < 2 * self.period || self.peak_window.len() < PEAK_AVG_LEN {
197            return None;
198        }
199        let n = PEAK_AVG_LEN as f64;
200        self.upper = self.fraction * self.peak_sum / n;
201        self.lower = self.fraction * self.valley_sum / n;
202        let mean = self.bp_sum / (2 * self.period) as f64;
203        self.last_value = Some(mean);
204        Some(mean)
205    }
206
207    fn reset(&mut self) {
208        self.prev_in_1 = None;
209        self.prev_in_2 = None;
210        self.prev_bp_1 = 0.0;
211        self.prev_bp_2 = 0.0;
212        self.peak = 0.0;
213        self.valley = 0.0;
214        self.bp_window.clear();
215        self.bp_sum = 0.0;
216        self.peak_window.clear();
217        self.peak_sum = 0.0;
218        self.valley_window.clear();
219        self.valley_sum = 0.0;
220        self.upper = 0.0;
221        self.lower = 0.0;
222        self.last_value = None;
223    }
224
225    #[inline]
226    fn warmup_period(&self) -> usize {
227        (2 * self.period).max(PEAK_AVG_LEN)
228    }
229
230    #[inline]
231    fn is_ready(&self) -> bool {
232        self.last_value.is_some()
233    }
234
235    #[inline]
236    fn name(&self) -> &'static str {
237        "EmpiricalModeDecomposition"
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::traits::BatchExt;
245
246    #[test]
247    fn new_rejects_invalid_params() {
248        assert!(matches!(
249            EmpiricalModeDecomposition::new(0, 0.5),
250            Err(Error::PeriodZero)
251        ));
252        assert!(matches!(
253            EmpiricalModeDecomposition::new(20, 0.0),
254            Err(Error::InvalidPeriod { .. })
255        ));
256        assert!(matches!(
257            EmpiricalModeDecomposition::new(20, 1.5),
258            Err(Error::InvalidPeriod { .. })
259        ));
260        assert!(matches!(
261            EmpiricalModeDecomposition::new(20, f64::NAN),
262            Err(Error::InvalidPeriod { .. })
263        ));
264    }
265
266    #[test]
267    fn accessors_and_metadata() {
268        let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
269        assert_eq!(emd.period(), 20);
270        assert!((emd.fraction() - 0.5).abs() < 1e-15);
271        assert_eq!(emd.name(), "EmpiricalModeDecomposition");
272        assert!(emd.warmup_period() >= 1);
273        assert!(!emd.is_ready());
274        let prices: Vec<f64> = (0..200)
275            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
276            .collect();
277        emd.batch(&prices);
278        assert!(emd.is_ready());
279        assert!(emd.value().is_some());
280    }
281
282    #[test]
283    fn batch_equals_streaming() {
284        let prices: Vec<f64> = (0..200)
285            .map(|i| 100.0 + (f64::from(i) * 0.2).cos() * 5.0)
286            .collect();
287        let mut a = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
288        let mut b = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
289        let batch = a.batch(&prices);
290        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
291        assert_eq!(batch, streamed);
292    }
293
294    #[test]
295    fn ignores_non_finite_input() {
296        let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
297        let prices: Vec<f64> = (0..200)
298            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
299            .collect();
300        emd.batch(&prices);
301        let before = emd.value();
302        assert!(before.is_some());
303        assert_eq!(emd.update(f64::NAN), None);
304    }
305
306    #[test]
307    fn reset_clears_state() {
308        let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
309        let prices: Vec<f64> = (0..200)
310            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
311            .collect();
312        emd.batch(&prices);
313        assert!(emd.is_ready());
314        emd.reset();
315        assert!(!emd.is_ready());
316    }
317
318    #[test]
319    fn rejects_period_above_maximum() {
320        assert!(matches!(
321            EmpiricalModeDecomposition::new(crate::error::MAX_PERIOD + 1, 0.5),
322            Err(Error::InvalidPeriod { .. })
323        ));
324        assert!(matches!(
325            EmpiricalModeDecomposition::new(20, -0.1),
326            Err(Error::InvalidPeriod { .. })
327        ));
328        assert!(matches!(
329            EmpiricalModeDecomposition::new(20, f64::INFINITY),
330            Err(Error::InvalidPeriod { .. })
331        ));
332        // fraction == 1 is the inclusive upper bound.
333        assert!(EmpiricalModeDecomposition::new(20, 1.0).is_ok());
334    }
335
336    #[test]
337    fn warmup_is_max_of_two_period_and_fifty() {
338        let prices: Vec<f64> = (0..150)
339            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
340            .collect();
341        for (period, expected) in [(1usize, 50usize), (10, 50), (25, 50), (26, 52), (40, 80)] {
342            let mut emd = EmpiricalModeDecomposition::new(period, 0.1).unwrap();
343            assert_eq!(emd.warmup_period(), expected);
344            let out = emd.batch(&prices);
345            assert!(out[..expected - 1].iter().all(Option::is_none));
346            assert!(out[expected - 1].is_some());
347        }
348    }
349
350    #[test]
351    fn constant_series_has_zero_mean_and_thresholds() {
352        // Price - Price[2] == 0 every bar -> BP == 0 -> Mean, Peak, Valley all 0.
353        let mut emd = EmpiricalModeDecomposition::new(10, 0.5).unwrap();
354        let out = emd.batch(&[42.0; 80]);
355        assert!(out
356            .iter()
357            .flatten()
358            .all(|v| v.to_bits() == 0.0f64.to_bits()));
359        assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
360        assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
361    }
362
363    #[test]
364    fn hand_computed_step_peak_valley_and_thresholds() {
365        // period 5: beta = cos(2*pi/5) = 0.309_017; gamma = 1 / cos(4*pi*0.1/5) = 1.032_436;
366        // alpha = gamma - sqrt(gamma^2 - 1) = 0.775_680.
367        let mut emd = EmpiricalModeDecomposition::new(5, 0.5).unwrap();
368        let (alpha, beta) = (emd.alpha, emd.beta);
369        assert!((beta - 0.309_016_994_374_947_45).abs() < 1e-15);
370        assert!((alpha - 0.775_679_511_049_613_4).abs() < 1e-12);
371        // 60 flat bars at 0 -> BP == 0, warmup (50) complete, thresholds 0.
372        for _ in 0..60 {
373            emd.update(0.0);
374        }
375        assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
376        assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
377        // Step to 1 at bar 60. With c = 0.5 * (1 - alpha) and k = beta * (1 + alpha):
378        // bp60 = c * (1 - 0)                         = 0.112_160
379        // bp61 = c * (1 - 0) + k * bp60              = 0.173_704
380        // bp62 = c * (1 - 1) + k * bp61 - alpha*bp60 = 0.008_314
381        // bp63 = k * bp62 - alpha * bp61             = -0.130_177
382        // bp64 = k * bp63 - alpha * bp62             = -0.077_879
383        let c = 0.5 * (1.0 - alpha);
384        let k = beta * (1.0 + alpha);
385        let bp60 = c;
386        let bp61 = c + k * bp60;
387        let bp62 = k * bp61 - alpha * bp60;
388        let bp63 = k * bp62 - alpha * bp61;
389        let bp64 = k * bp63 - alpha * bp62;
390        assert!((bp61 - 0.173_704_269_339_216_5).abs() < 1e-12);
391        assert!((bp63 + 0.130_176_956_775_418_27).abs() < 1e-12);
392        // Bar 60: BP rises from a flat 0 -> neither a peak nor a valley.
393        let m60 = emd.update(1.0).unwrap();
394        assert!((m60 - bp60 / 10.0).abs() < 1e-15);
395        assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
396        assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
397        emd.update(1.0);
398        // Bar 62: bp61 > bp62 and bp61 > bp60 -> Peak = bp61. One of the 50 peak
399        // slots holds it -> Upper = 0.5 * bp61 / 50 = 0.001_737; no valley yet.
400        let m62 = emd.update(1.0).unwrap();
401        assert!((emd.upper() - 0.5 * bp61 / 50.0).abs() < 1e-15);
402        assert!((emd.upper() - 0.001_737_042_693_392_165).abs() < 1e-12);
403        assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
404        // Mean = SMA(BP, 10) = (bp60 + bp61 + bp62) / 10.
405        assert!((m62 - (bp60 + bp61 + bp62) / 10.0).abs() < 1e-15);
406        // Bar 63: BP still falling -> no new peak/valley; the peak is held.
407        emd.update(1.0);
408        assert!((emd.upper() - 0.5 * 2.0 * bp61 / 50.0).abs() < 1e-15);
409        assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
410        // Bar 64: bp63 < bp64 and bp63 < bp62 -> Valley = bp63.
411        // Upper = 0.5 * 3 * bp61 / 50 = 0.005_211; Lower = 0.5 * bp63 / 50 = -0.001_302.
412        let m64 = emd.update(1.0).unwrap();
413        assert!((emd.upper() - 0.5 * 3.0 * bp61 / 50.0).abs() < 1e-15);
414        assert!((emd.lower() - 0.5 * bp63 / 50.0).abs() < 1e-15);
415        assert!((emd.lower() + 0.001_301_769_567_754_182_7).abs() < 1e-12);
416        assert!((m64 - (bp60 + bp61 + bp62 + bp63 + bp64) / 10.0).abs() < 1e-15);
417        assert_eq!(emd.value(), Some(m64));
418    }
419
420    #[test]
421    fn thresholds_bracket_zero_on_an_oscillation() {
422        // A sine at the centre period produces repeated peaks (> 0) and valleys (< 0).
423        let mut emd = EmpiricalModeDecomposition::new(20, 0.3).unwrap();
424        for i in 0..400 {
425            emd.update((f64::from(i) * 2.0 * PI / 20.0).sin() * 10.0 + 100.0);
426        }
427        assert!(emd.upper() > 0.0);
428        assert!(emd.lower() < 0.0);
429        let mean = emd.value().unwrap();
430        assert!(mean.abs() < emd.upper());
431    }
432
433    #[test]
434    fn reset_replays_identically_and_batch_nan_into_matches() {
435        let prices: Vec<f64> = (0..200)
436            .map(|i| 100.0 + (f64::from(i) * 0.17).sin() * 3.0 + f64::from(i) * 0.02)
437            .collect();
438        let mut emd = EmpiricalModeDecomposition::new(12, 0.2).unwrap();
439        let first = emd.batch(&prices);
440        let (up, lo) = (emd.upper(), emd.lower());
441        emd.reset();
442        assert_eq!(emd.upper().to_bits(), 0.0f64.to_bits());
443        assert_eq!(emd.lower().to_bits(), 0.0f64.to_bits());
444        assert_eq!(emd.value(), None);
445        let second = emd.batch(&prices);
446        assert_eq!(first, second);
447        assert_eq!(emd.upper().to_bits(), up.to_bits());
448        assert_eq!(emd.lower().to_bits(), lo.to_bits());
449        let mut fresh = EmpiricalModeDecomposition::new(12, 0.2).unwrap();
450        let mut out = vec![0.0; prices.len()];
451        fresh.batch_nan_into(&prices, &mut out);
452        assert!(out
453            .iter()
454            .zip(&first)
455            .all(|(a, b)| a.to_bits() == b.unwrap_or(f64::NAN).to_bits()));
456    }
457}