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