Skip to main content

quantwave_core/indicators/
smoothing.rs

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