Skip to main content

quantwave_core/indicators/
smoothing.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Simple Moving Average (SMA)
6#[derive(Debug, Clone)]
7pub struct SMA {
8    period: usize,
9    window: VecDeque<f64>,
10    sum: f64,
11}
12
13impl SMA {
14    pub fn new(period: usize) -> Self {
15        Self {
16            period,
17            window: VecDeque::with_capacity(period),
18            sum: 0.0,
19        }
20    }
21}
22
23impl From<usize> for SMA {
24    fn from(period: usize) -> Self {
25        Self::new(period)
26    }
27}
28
29impl Next<f64> for SMA {
30    type Output = f64;
31
32    fn next(&mut self, input: f64) -> Self::Output {
33        self.window.push_back(input);
34        self.sum += input;
35
36        if self.window.len() > self.period && let Some(oldest) = self.window.pop_front() {
37            self.sum -= oldest;
38        }
39
40        self.sum / self.window.len() as f64
41    }
42}
43
44/// Exponential Moving Average (EMA)
45#[derive(Debug, Clone)]
46pub struct EMA {
47    _period: usize,
48    alpha: f64,
49    current_ema: Option<f64>,
50}
51
52impl EMA {
53    pub fn new(period: usize) -> Self {
54        Self {
55            _period: period,
56            alpha: 2.0 / (period as f64 + 1.0),
57            current_ema: None,
58        }
59    }
60}
61
62impl From<usize> for EMA {
63    fn from(period: usize) -> Self {
64        Self::new(period)
65    }
66}
67
68impl Next<f64> for EMA {
69    type Output = f64;
70
71    fn next(&mut self, input: f64) -> Self::Output {
72        match self.current_ema {
73            Some(prev_ema) => {
74                let ema = self.alpha * input + (1.0 - self.alpha) * prev_ema;
75                self.current_ema = Some(ema);
76                ema
77            }
78            None => {
79                self.current_ema = Some(input);
80                input
81            }
82        }
83    }
84}
85
86/// Weighted Moving Average (WMA)
87#[derive(Debug, Clone)]
88pub struct WMA {
89    period: usize,
90    window: VecDeque<f64>,
91}
92
93impl WMA {
94    pub fn new(period: usize) -> Self {
95        Self {
96            period,
97            window: VecDeque::with_capacity(period),
98        }
99    }
100}
101
102impl From<usize> for WMA {
103    fn from(period: usize) -> Self {
104        Self::new(period)
105    }
106}
107
108impl Next<f64> for WMA {
109    type Output = f64;
110
111    fn next(&mut self, input: f64) -> Self::Output {
112        self.window.push_back(input);
113        if self.window.len() > self.period {
114            self.window.pop_front();
115        }
116
117        let mut weight_sum = 0.0;
118        let mut weighted_val_sum = 0.0;
119
120        for (i, &val) in self.window.iter().enumerate() {
121            let weight = (i + 1) as f64;
122            weighted_val_sum += val * weight;
123            weight_sum += weight;
124        }
125
126        if weight_sum == 0.0 {
127            0.0
128        } else {
129            weighted_val_sum / weight_sum
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::test_utils::{assert_indicator_parity, load_gold_standard};
138
139    #[test]
140    fn test_sma_gold_standard() {
141        let case = load_gold_standard("sma_5");
142        let sma = SMA::new(3); // The expected values in JSON are for SMA(3)
143        assert_indicator_parity(sma, &case.input, &case.expected);
144    }
145
146    #[test]
147    fn test_ema_basic() {
148        let mut ema = EMA::new(3);
149        assert_eq!(ema.next(10.0), 10.0);
150        approx::assert_relative_eq!(ema.next(12.0), 11.0); // alpha = 0.5. 0.5*12 + 0.5*10 = 11
151    }
152
153    #[test]
154    fn test_wma_basic() {
155        let mut wma = WMA::new(3);
156        assert_eq!(wma.next(1.0), 1.0);
157        approx::assert_relative_eq!(wma.next(2.0), 1.6666666666, epsilon = 1e-6); // (1*1 + 2*2)/3 = 5/3 = 1.666
158        approx::assert_relative_eq!(wma.next(3.0), 2.3333333333, epsilon = 1e-6); // (1*1 + 2*2 + 3*3)/6 = 14/6 = 2.333
159        approx::assert_relative_eq!(wma.next(4.0), 3.3333333333, epsilon = 1e-6); // (2*1 + 3*2 + 4*3)/6 = (2+6+12)/6 = 20/6 = 3.333
160    }
161}
162
163pub const SMA_METADATA: IndicatorMetadata = IndicatorMetadata {
164    name: "Simple Moving Average",
165    description: "The Simple Moving Average calculates the unweighted mean of the previous N data points.",
166    usage: "Use as the foundational smoothing module providing SMA, EMA, WMA, and SMMA implementations that power higher-level indicators across the library.",
167    keywords: &["moving-average", "smoothing", "classic", "ema"],
168    ehlers_summary: "The core smoothing algorithms — SMA, EMA, WMA — are the building blocks of nearly all technical indicators. EMA applies exponential decay weighting (alpha = 2/(n+1)), SMA applies uniform weighting over N bars, and WMA applies linearly increasing weights emphasizing more recent bars.",
169    params: &[ParamDef {
170        name: "period",
171        default: "14",
172        description: "Smoothing period",
173    }],
174    formula_source: "https://www.investopedia.com/terms/s/sma.asp",
175    formula_latex: r#"
176\[
177SMA = \frac{1}{n} \sum_{i=1}^{n} P_i
178\]
179"#,
180    gold_standard_file: "sma.json",
181    category: "Classic",
182};
183
184pub const EMA_METADATA: IndicatorMetadata = IndicatorMetadata {
185    name: "Exponential Moving Average",
186    description: "The Exponential Moving Average gives more weight to recent prices.",
187    usage: "Use as the foundational smoothing module providing SMA, EMA, WMA, and SMMA implementations that power higher-level indicators across the library.",
188    keywords: &["moving-average", "smoothing", "classic", "ema"],
189    ehlers_summary: "The core smoothing algorithms — SMA, EMA, WMA — are the building blocks of nearly all technical indicators. EMA applies exponential decay weighting (alpha = 2/(n+1)), SMA applies uniform weighting over N bars, and WMA applies linearly increasing weights emphasizing more recent bars.",
190    params: &[ParamDef {
191        name: "period",
192        default: "14",
193        description: "Smoothing period",
194    }],
195    formula_source: "https://www.investopedia.com/terms/e/ema.asp",
196    formula_latex: r#"
197\[
198EMA = P_t \times \alpha + EMA_{t-1} \times (1 - \alpha)
199\]
200"#,
201    gold_standard_file: "ema.json",
202    category: "Classic",
203};
204
205pub const WMA_METADATA: IndicatorMetadata = IndicatorMetadata {
206    name: "Weighted Moving Average",
207    description: "The Weighted Moving Average assigns linearly decreasing weights.",
208    usage: "Use as the foundational smoothing module providing SMA, EMA, WMA, and SMMA implementations that power higher-level indicators across the library.",
209    keywords: &["moving-average", "smoothing", "classic", "ema"],
210    ehlers_summary: "The core smoothing algorithms — SMA, EMA, WMA — are the building blocks of nearly all technical indicators. EMA applies exponential decay weighting (alpha = 2/(n+1)), SMA applies uniform weighting over N bars, and WMA applies linearly increasing weights emphasizing more recent bars.",
211    params: &[ParamDef {
212        name: "period",
213        default: "14",
214        description: "Smoothing period",
215    }],
216    formula_source: "https://www.investopedia.com/articles/technical/060401.asp",
217    formula_latex: r#"
218\[
219WMA = \frac{P_1 \times n + P_2 \times (n-1) + \dots}{n + (n-1) + \dots + 1}
220\]
221"#,
222    gold_standard_file: "wma.json",
223    category: "Classic",
224};