quantwave_core/indicators/
smoothing.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[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#[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#[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); 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); }
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); approx::assert_relative_eq!(wma.next(3.0), 2.3333333333, epsilon = 1e-6); approx::assert_relative_eq!(wma.next(4.0), 3.3333333333, epsilon = 1e-6); }
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};