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
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::traits::BatchExt;
85    use approx::assert_relative_eq;
86
87    #[test]
88    fn constant_series_yields_constant_dema() {
89        let mut dema = Dema::new(5).unwrap();
90        let out = dema.batch(&[100.0_f64; 60]);
91        let last = out.iter().rev().flatten().next().unwrap();
92        assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
93    }
94
95    #[test]
96    fn linear_uptrend_dema_above_ema_eventually() {
97        // On a linear uptrend DEMA should be ahead of (greater than) a plain EMA,
98        // because the second-order correction removes lag.
99        let prices: Vec<f64> = (1..=200).map(f64::from).collect();
100        let mut dema = Dema::new(20).unwrap();
101        let mut ema = Ema::new(20).unwrap();
102        let dema_out = dema.batch(&prices);
103        let ema_out = ema.batch(&prices);
104        // Compare at the last index where both are ready.
105        let d = dema_out.last().unwrap().unwrap();
106        let e = ema_out.last().unwrap().unwrap();
107        assert!(d > e, "DEMA={d} should exceed EMA={e} on uptrend");
108    }
109
110    #[test]
111    fn batch_equals_streaming() {
112        let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
113        let mut a = Dema::new(7).unwrap();
114        let mut b = Dema::new(7).unwrap();
115        assert_eq!(
116            a.batch(&prices),
117            prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
118        );
119    }
120
121    #[test]
122    fn reset_clears_state() {
123        let mut dema = Dema::new(5).unwrap();
124        dema.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
125        assert!(dema.is_ready());
126        dema.reset();
127        assert!(!dema.is_ready());
128    }
129
130    #[test]
131    fn rejects_zero_period() {
132        assert!(Dema::new(0).is_err());
133    }
134
135    /// Cover the const accessor `period` (43-45) and the Indicator-impl
136    /// `warmup_period` (63-66) + `name` (72-74). Existing tests never
137    /// inspect these metadata methods.
138    #[test]
139    fn accessors_and_metadata() {
140        let dema = Dema::new(5).unwrap();
141        assert_eq!(dema.period(), 5);
142        // EMA1 seeds at period (5), EMA2 needs another (period - 1) = 4 ->
143        // total warmup = 2*period - 1 = 9.
144        assert_eq!(dema.warmup_period(), 9);
145        assert_eq!(dema.name(), "DEMA");
146    }
147}