Skip to main content

mantis_ta/indicators/trend/
dema.rs

1use super::EMA;
2use crate::indicators::Indicator;
3use crate::types::Candle;
4
5/// Double Exponential Moving Average over closing prices.
6///
7/// DEMA = 2 * EMA - EMA(EMA), reducing lag compared to single EMA.
8///
9/// # Examples
10/// ```rust
11/// use mantis_ta::indicators::{Indicator, DEMA};
12/// use mantis_ta::types::Candle;
13///
14/// let prices = [1.0, 2.0, 3.0, 4.0, 5.0];
15/// let candles: Vec<Candle> = prices
16///     .iter()
17///     .enumerate()
18///     .map(|(i, p)| Candle {
19///         timestamp: i as i64,
20///         open: *p,
21///         high: *p,
22///         low: *p,
23///         close: *p,
24///         volume: 0.0,
25///     })
26///     .collect();
27///
28/// let out = DEMA::new(3).calculate(&candles);
29/// assert!(out.iter().take(4).all(|v| v.is_none()));
30/// assert!(out.iter().skip(4).any(|v| v.is_some()));
31/// ```
32#[derive(Debug, Clone)]
33pub struct DEMA {
34    period: usize,
35    ema1: EMA,
36    ema2: EMA,
37}
38
39impl DEMA {
40    pub fn new(period: usize) -> Self {
41        assert!(period > 0, "period must be > 0");
42        Self {
43            period,
44            ema1: EMA::new(period),
45            ema2: EMA::new(period),
46        }
47    }
48
49    #[inline]
50    fn update(&mut self, value: f64) -> Option<f64> {
51        let ema1_val = self.ema1.next(&Candle {
52            timestamp: 0,
53            open: value,
54            high: value,
55            low: value,
56            close: value,
57            volume: 0.0,
58        });
59
60        if let Some(ema1) = ema1_val {
61            let ema2_val = self.ema2.next(&Candle {
62                timestamp: 0,
63                open: ema1,
64                high: ema1,
65                low: ema1,
66                close: ema1,
67                volume: 0.0,
68            });
69
70            if let Some(ema2) = ema2_val {
71                return Some(2.0 * ema1 - ema2);
72            }
73        }
74
75        None
76    }
77}
78
79impl Indicator for DEMA {
80    type Output = f64;
81
82    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
83        self.update(candle.close)
84    }
85
86    fn reset(&mut self) {
87        self.ema1.reset();
88        self.ema2.reset();
89    }
90
91    fn warmup_period(&self) -> usize {
92        self.period * 2 - 1
93    }
94
95    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
96        Box::new(self.clone())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn dema_emits_after_warmup() {
106        let prices = [1.0, 2.0, 3.0, 4.0, 5.0];
107        let candles: Vec<Candle> = prices
108            .iter()
109            .enumerate()
110            .map(|(i, p)| Candle {
111                timestamp: i as i64,
112                open: *p,
113                high: *p,
114                low: *p,
115                close: *p,
116                volume: 0.0,
117            })
118            .collect();
119
120        let out = DEMA::new(3).calculate(&candles);
121        assert!(out.iter().take(4).all(|v| v.is_none()));
122        assert!(out.iter().skip(4).any(|v| v.is_some()));
123    }
124}