mantis_ta/indicators/trend/
ema.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5#[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 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}