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}