Skip to content

Commit 8856177

Browse files
feat: add exponential moving average to financial (#1021)
1 parent d14a4e2 commit 8856177

File tree

3 files changed

+126
-0
lines changed

3 files changed

+126
-0
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
* Financial
152152
* [Compound Interest](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/compound_interest.rs)
153153
* [Equated Monthly Installments](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/equated_monthly_installments.rs)
154+
* [Exponential Moving Average](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/exponential_moving_average.rs)
154155
* [Finance Ratios](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/finance_ratios.rs)
155156
* [Net Present Value](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/npv.rs)
156157
* [NPV Sensitivity](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/npv_sensitivity.rs)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
}

src/financial/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod compound_interest;
22
mod equated_monthly_installments;
3+
mod exponential_moving_average;
34
mod finance_ratios;
45
mod npv;
56
mod npv_sensitivity;
@@ -9,6 +10,7 @@ mod treynor_ratio;
910

1011
pub use self::compound_interest::compound_interest;
1112
pub use self::equated_monthly_installments::equated_monthly_installments;
13+
pub use self::exponential_moving_average::exponential_moving_average;
1214
pub use self::finance_ratios::{
1315
debt_to_equity, earnings_per_sale, gross_profit_margin, return_on_investment,
1416
};

0 commit comments

Comments
 (0)