|
| 1 | +//! Calculate the exponential moving average (EMA) on the series of stock prices. |
| 2 | +//! |
| 3 | +//! Wikipedia Reference: <https://en.wikipedia.org/wiki/Exponential_smoothing> |
| 4 | +//! Investopedia Reference: <https://www.investopedia.com/terms/e/ema.asp> |
| 5 | +//! |
| 6 | +//! Exponential moving average is used in finance to analyze changes in stock prices. |
| 7 | +//! EMA is used in conjunction with Simple Moving Average (SMA). EMA reacts to |
| 8 | +//! changes in value quicker than SMA, which is one of its key advantages. |
| 9 | +//! |
| 10 | +//! # Formula |
| 11 | +//! ```text |
| 12 | +//! st = alpha * xt + (1 - alpha) * st_prev |
| 13 | +//! ``` |
| 14 | +//! Where: |
| 15 | +//! - `st` : Exponential moving average at timestamp t |
| 16 | +//! - `xt` : Stock price at timestamp t |
| 17 | +//! - `st_prev` : Exponential moving average at timestamp t-1 |
| 18 | +//! - `alpha` : 2 / (1 + window_size) — the smoothing factor |
| 19 | +
|
| 20 | +/// Returns an iterator of exponential moving averages over the given stock prices. |
| 21 | +/// |
| 22 | +/// # Errors |
| 23 | +/// Returns an error string if `window_size` is zero. |
| 24 | +/// |
| 25 | +/// # Examples |
| 26 | +/// ``` |
| 27 | +/// use the_algorithms_rust::financial::exponential_moving_average; |
| 28 | +/// let prices = vec![2.0, 5.0, 3.0, 8.2, 6.0, 9.0, 10.0]; |
| 29 | +/// let result: Vec<f64> = exponential_moving_average(prices.into_iter(), 3).unwrap().collect(); |
| 30 | +/// let expected = vec![2.0, 3.5, 3.25, 5.725, 5.8625, 7.43125, 8.715625]; |
| 31 | +/// for (r, e) in result.iter().zip(expected.iter()) { |
| 32 | +/// assert!((r - e).abs() < 1e-9); |
| 33 | +/// } |
| 34 | +/// ``` |
| 35 | +pub fn exponential_moving_average( |
| 36 | + stock_prices: impl Iterator<Item = f64>, |
| 37 | + window_size: usize, |
| 38 | +) -> Result<impl Iterator<Item = f64>, &'static str> { |
| 39 | + if window_size == 0 { |
| 40 | + return Err("window_size must be > 0"); |
| 41 | + } |
| 42 | + |
| 43 | + let alpha = 2.0 / (1.0 + window_size as f64); |
| 44 | + |
| 45 | + let iter = stock_prices |
| 46 | + .enumerate() |
| 47 | + .scan(0.0_f64, move |moving_average, (i, stock_price)| { |
| 48 | + *moving_average = if i <= window_size { |
| 49 | + // Use simple moving average until window_size is first reached |
| 50 | + if i == 0 { |
| 51 | + stock_price |
| 52 | + } else { |
| 53 | + (*moving_average + stock_price) * 0.5 |
| 54 | + } |
| 55 | + } else { |
| 56 | + // Apply exponential smoothing formula |
| 57 | + alpha * stock_price + (1.0 - alpha) * *moving_average |
| 58 | + }; |
| 59 | + Some(*moving_average) |
| 60 | + }); |
| 61 | + |
| 62 | + Ok(iter) |
| 63 | +} |
| 64 | + |
| 65 | +#[cfg(test)] |
| 66 | +mod tests { |
| 67 | + use super::*; |
| 68 | + |
| 69 | + fn approx_eq(a: f64, b: f64) -> bool { |
| 70 | + (a - b).abs() < 1e-9 |
| 71 | + } |
| 72 | + |
| 73 | + #[test] |
| 74 | + fn test_basic_ema() { |
| 75 | + let prices = vec![2.0, 5.0, 3.0, 8.2, 6.0, 9.0, 10.0]; |
| 76 | + let expected = [2.0, 3.5, 3.25, 5.725, 5.8625, 7.43125, 8.715625]; |
| 77 | + let result: Vec<f64> = exponential_moving_average(prices.into_iter(), 3) |
| 78 | + .unwrap() |
| 79 | + .collect(); |
| 80 | + |
| 81 | + assert_eq!(result.len(), expected.len()); |
| 82 | + for (r, e) in result.iter().zip(expected.iter()) { |
| 83 | + assert!(approx_eq(*r, *e), "Expected {e}, got {r}"); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + #[test] |
| 88 | + fn test_window_size_one() { |
| 89 | + // With window_size=1, alpha=1.0, so EMA should equal each price after the first |
| 90 | + let prices = vec![1.0, 2.0, 3.0]; |
| 91 | + let result: Vec<f64> = exponential_moving_average(prices.into_iter(), 1) |
| 92 | + .unwrap() |
| 93 | + .collect(); |
| 94 | + // i=0: price (window boundary), i=1: SMA=(1+2)*0.5=1.5, i=2: alpha=1.0 => 3.0 |
| 95 | + assert!(approx_eq(result[0], 1.0)); |
| 96 | + assert!(approx_eq(result[1], 1.5)); |
| 97 | + assert!(approx_eq(result[2], 3.0)); |
| 98 | + } |
| 99 | + |
| 100 | + #[test] |
| 101 | + fn test_single_price() { |
| 102 | + let prices = vec![42.0]; |
| 103 | + let result: Vec<f64> = exponential_moving_average(prices.into_iter(), 3) |
| 104 | + .unwrap() |
| 105 | + .collect(); |
| 106 | + assert_eq!(result, vec![42.0]); |
| 107 | + } |
| 108 | + |
| 109 | + #[test] |
| 110 | + fn test_empty_prices() { |
| 111 | + let prices: Vec<f64> = vec![]; |
| 112 | + assert!(exponential_moving_average(prices.into_iter(), 3) |
| 113 | + .unwrap() |
| 114 | + .next() |
| 115 | + .is_none()); |
| 116 | + } |
| 117 | + |
| 118 | + #[test] |
| 119 | + fn test_zero_window_size_returns_error() { |
| 120 | + let prices = vec![1.0, 2.0, 3.0]; |
| 121 | + assert!(exponential_moving_average(prices.into_iter(), 0).is_err()); |
| 122 | + } |
| 123 | +} |
0 commit comments