Skip to main content

quant_indicators/
ema.rs

1//! Exponential Moving Average (EMA) indicator.
2
3use quant_primitives::Candle;
4use rust_decimal::Decimal;
5
6use crate::error::IndicatorError;
7use crate::indicator::Indicator;
8use crate::series::Series;
9
10/// Exponential Moving Average indicator.
11///
12/// Computes a weighted moving average that gives more weight to recent prices.
13/// The weighting decreases exponentially for older prices.
14///
15/// # Formula
16///
17/// multiplier = 2 / (period + 1)
18/// EMA_today = (Close - EMA_yesterday) * multiplier + EMA_yesterday
19///
20/// The first EMA value is the SMA of the first `period` candles.
21///
22/// # Example
23///
24/// ```
25/// use quant_indicators::{Indicator, Ema};
26/// use quant_primitives::Candle;
27/// use chrono::Utc;
28/// use rust_decimal_macros::dec;
29///
30/// let ts = Utc::now();
31/// let candles: Vec<Candle> = (0..20).map(|i| {
32///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
33/// }).collect();
34/// let ema = Ema::new(20).unwrap();
35/// let series = ema.compute(&candles).unwrap();
36/// ```
37#[derive(Debug, Clone)]
38pub struct Ema {
39    period: usize,
40    multiplier: Decimal,
41    name: String,
42}
43
44impl Ema {
45    /// Create a new EMA indicator with the specified period.
46    ///
47    /// # Errors
48    ///
49    /// Returns `InvalidParameter` if period is 0.
50    pub fn new(period: usize) -> Result<Self, IndicatorError> {
51        if period == 0 {
52            return Err(IndicatorError::InvalidParameter {
53                message: "EMA period must be > 0".to_string(),
54            });
55        }
56
57        // multiplier = 2 / (period + 1)
58        let multiplier = Decimal::TWO / Decimal::from(period as u64 + 1);
59
60        Ok(Self {
61            period,
62            multiplier,
63            name: format!("EMA({})", period),
64        })
65    }
66
67    /// Get the smoothing multiplier.
68    pub fn multiplier(&self) -> Decimal {
69        self.multiplier
70    }
71}
72
73impl Indicator for Ema {
74    fn name(&self) -> &str {
75        &self.name
76    }
77
78    fn warmup_period(&self) -> usize {
79        self.period
80    }
81
82    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
83        if candles.len() < self.period {
84            return Err(IndicatorError::InsufficientData {
85                required: self.period,
86                actual: candles.len(),
87            });
88        }
89
90        let mut values = Vec::with_capacity(candles.len() - self.period + 1);
91        let period_dec = Decimal::from(self.period as u64);
92
93        // First EMA is SMA of first `period` candles
94        let initial_sum: Decimal = candles[..self.period].iter().map(|c| c.close()).sum();
95        let mut ema = initial_sum / period_dec;
96        let ts = candles[self.period - 1].timestamp();
97        values.push((ts, ema));
98
99        // Subsequent EMAs use the recursive formula
100        for candle in candles.iter().skip(self.period) {
101            let close = candle.close();
102            // EMA = (Close - EMA_prev) * multiplier + EMA_prev
103            ema = (close - ema) * self.multiplier + ema;
104            values.push((candle.timestamp(), ema));
105        }
106
107        Ok(Series::new(values))
108    }
109}
110
111#[cfg(test)]
112#[path = "ema_tests.rs"]
113mod tests;