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