Skip to main content

wickra_core/indicators/
dema.rs

1//! Double Exponential Moving Average (DEMA).
2
3use crate::error::Result;
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7/// Double Exponential Moving Average: `2 * EMA - EMA(EMA)`.
8///
9/// Designed by Patrick Mulloy to reduce the lag of a single EMA while keeping
10/// the smoothing benefit.
11///
12/// # Example
13///
14/// ```
15/// use wickra_core::{Indicator, Dema};
16///
17/// let mut indicator = Dema::new(3).unwrap();
18/// let mut last = None;
19/// for i in 0..80 {
20///     last = indicator.update(100.0 + f64::from(i));
21/// }
22/// assert!(last.is_some());
23/// ```
24#[derive(Debug, Clone)]
25pub struct Dema {
26    ema1: Ema,
27    ema2: Ema,
28    period: usize,
29}
30
31impl Dema {
32    /// # Errors
33    /// Returns [`crate::Error::PeriodZero`] if `period == 0`.
34    pub fn new(period: usize) -> Result<Self> {
35        Ok(Self {
36            ema1: Ema::new(period)?,
37            ema2: Ema::new(period)?,
38            period,
39        })
40    }
41
42    /// Configured period.
43    pub const fn period(&self) -> usize {
44        self.period
45    }
46}
47
48impl Indicator for Dema {
49    type Input = f64;
50    type Output = f64;
51
52    #[inline]
53    fn update(&mut self, input: f64) -> Option<f64> {
54        let e1 = self.ema1.update(input)?;
55        let e2 = self.ema2.update(e1)?;
56        Some(2.0 * e1 - e2)
57    }
58
59    fn reset(&mut self) {
60        self.ema1.reset();
61        self.ema2.reset();
62    }
63
64    #[inline]
65    fn warmup_period(&self) -> usize {
66        // EMA1 seeds at period, then EMA2 needs another (period - 1) values to seed.
67        2 * self.period - 1
68    }
69
70    #[inline]
71    fn is_ready(&self) -> bool {
72        self.ema2.is_ready()
73    }
74
75    #[inline]
76    fn name(&self) -> &'static str {
77        "DEMA"
78    }
79
80    /// SIMD kernel: the warmup runs the exact EMA recurrence until both EMAs
81    /// are seeded (those values match the exact batch to the bit), then both
82    /// EMAs run as chained linear-recurrence scans over stack blocks. Agrees
83    /// with the exact batch to within a few units in the last place; warmup
84    /// `NaN`s and length are identical. Afterwards both EMAs continue
85    /// streaming from the kernel's last values.
86    fn batch_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
87        assert_eq!(
88            inputs.len(),
89            out.len(),
90            "batch output length must equal input length"
91        );
92        let p = self.period;
93        if !(self.ema1.is_fresh() && self.ema2.is_fresh())
94            || inputs.len() < 2 * p - 1
95            || !crate::fast::in_range(inputs)
96        {
97            self.batch_nan_into(inputs, out);
98            return;
99        }
100        let (e1, e2) = wickra_simd::dispatch(crate::fast::DemaFast {
101            x: inputs,
102            period: p,
103            alpha: self.ema1.alpha(),
104            out,
105            _borrow: std::marker::PhantomData,
106        });
107        self.ema1.seed_to(e1);
108        self.ema2.seed_to(e2);
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::traits::BatchExt;
116    use approx::assert_relative_eq;
117
118    #[test]
119    fn constant_series_yields_constant_dema() {
120        let mut dema = Dema::new(5).unwrap();
121        let out = dema.batch(&[100.0_f64; 60]);
122        let last = out.iter().rev().flatten().next().unwrap();
123        assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
124    }
125
126    #[test]
127    fn linear_uptrend_dema_above_ema_eventually() {
128        // On a linear uptrend DEMA should be ahead of (greater than) a plain EMA,
129        // because the second-order correction removes lag.
130        let prices: Vec<f64> = (1..=200).map(f64::from).collect();
131        let mut dema = Dema::new(20).unwrap();
132        let mut ema = Ema::new(20).unwrap();
133        let dema_out = dema.batch(&prices);
134        let ema_out = ema.batch(&prices);
135        // Compare at the last index where both are ready.
136        let d = dema_out.last().unwrap().unwrap();
137        let e = ema_out.last().unwrap().unwrap();
138        assert!(d > e, "DEMA={d} should exceed EMA={e} on uptrend");
139    }
140
141    #[test]
142    fn batch_equals_streaming() {
143        let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
144        let mut a = Dema::new(7).unwrap();
145        let mut b = Dema::new(7).unwrap();
146        assert_eq!(
147            a.batch(&prices),
148            prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
149        );
150    }
151
152    #[test]
153    fn reset_clears_state() {
154        let mut dema = Dema::new(5).unwrap();
155        dema.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
156        assert!(dema.is_ready());
157        dema.reset();
158        assert!(!dema.is_ready());
159    }
160
161    #[test]
162    fn rejects_zero_period() {
163        assert!(Dema::new(0).is_err());
164    }
165
166    /// Cover the const accessor `period` (43-45) and the Indicator-impl
167    /// `warmup_period` (63-66) + `name` (72-74). Existing tests never
168    /// inspect these metadata methods.
169    #[test]
170    fn accessors_and_metadata() {
171        let dema = Dema::new(5).unwrap();
172        assert_eq!(dema.period(), 5);
173        // EMA1 seeds at period (5), EMA2 needs another (period - 1) = 4 ->
174        // total warmup = 2*period - 1 = 9.
175        assert_eq!(dema.warmup_period(), 9);
176        assert_eq!(dema.name(), "DEMA");
177    }
178}