Skip to main content

finance_query/indicators/
ema.rs

1//! Exponential Moving Average (EMA) indicator.
2
3use super::sma::sma;
4
5/// Calculate Exponential Moving Average (EMA).
6///
7/// EMA gives more weight to recent prices, making it more responsive than SMA.
8/// The first value is calculated as an SMA, then subsequent values use the EMA formula.
9///
10/// # Arguments
11///
12/// * `data` - Price data (typically close prices)
13/// * `period` - Number of periods for the moving average
14///
15/// # Formula
16///
17/// - First EMA = SMA(period)
18/// - Multiplier = 2 / (period + 1)
19/// - EMA = (Close - Previous EMA) × Multiplier + Previous EMA
20///
21/// # Example
22///
23/// ```
24/// use finance_query::indicators::ema;
25///
26/// let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0];
27/// let result = ema(&prices, 3);
28///
29/// // First 2 values are None (insufficient data)
30/// assert!(result[0].is_none());
31/// assert!(result[1].is_none());
32/// // Subsequent values are calculated using EMA formula
33/// assert!(result[2].is_some());
34/// ```
35pub fn ema(data: &[f64], period: usize) -> Vec<Option<f64>> {
36    if period == 0 || data.is_empty() || data.len() < period {
37        return vec![None; data.len()];
38    }
39
40    let multiplier = 2.0 / (period as f64 + 1.0);
41    let mut result = Vec::with_capacity(data.len());
42
43    // Calculate initial SMA for the first EMA value
44    let sma_values = sma(data, period);
45
46    for (i, &sma_val) in sma_values.iter().enumerate() {
47        match (sma_val, i) {
48            (Some(sma), idx) if idx == period - 1 => {
49                // First EMA value is the SMA
50                result.push(Some(sma));
51            }
52            (_, idx) if idx < period - 1 => {
53                // Not enough data yet
54                result.push(None);
55            }
56            _ => {
57                // Calculate EMA using previous EMA
58                if let Some(prev_ema) = result.last().and_then(|&v| v) {
59                    let ema_val = (data[i] - prev_ema) * multiplier + prev_ema;
60                    result.push(Some(ema_val));
61                } else {
62                    result.push(None);
63                }
64            }
65        }
66    }
67
68    result
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_ema_basic() {
77        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
78        let result = ema(&data, 3);
79
80        assert_eq!(result.len(), 5);
81        assert_eq!(result[0], None);
82        assert_eq!(result[1], None);
83        assert!(result[2].is_some());
84        assert!(result[3].is_some());
85        assert!(result[4].is_some());
86
87        // EMA should be more responsive to recent price changes
88        // Later values should be closer to actual price than SMA
89    }
90
91    #[test]
92    fn test_ema_period_1() {
93        let data = vec![10.0, 20.0, 30.0];
94        let result = ema(&data, 1);
95
96        // Period 1 EMA should equal the price itself
97        assert_eq!(result[0], Some(10.0));
98        assert_eq!(result[1], Some(20.0));
99        assert_eq!(result[2], Some(30.0));
100    }
101
102    #[test]
103    fn test_ema_insufficient_data() {
104        let data = vec![1.0, 2.0];
105        let result = ema(&data, 5);
106
107        assert_eq!(result.len(), 2);
108        assert_eq!(result[0], None);
109        assert_eq!(result[1], None);
110    }
111}