Skip to main content

quantwave_core/indicators/
smoothing.rs

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