Skip to main content

mantis_ta/indicators/trend/
ema.rs

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