quantwave_core/indicators/
smoothing.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4use serde::{Deserialize, Serialize};
5
6#[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 if self.sum.is_nan() || self.sum.is_infinite() {
55 self.sum = self.window.iter().sum();
56 }
57
58 self.sum / self.window.len() as f64
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct EMA {
65 _period: usize,
66 alpha: f64,
67 current_ema: Option<f64>,
68}
69
70impl EMA {
71 pub fn new(period: usize) -> Self {
72 Self {
73 _period: period,
74 alpha: 2.0 / (period as f64 + 1.0),
75 current_ema: None,
76 }
77 }
78}
79
80impl From<usize> for EMA {
81 fn from(period: usize) -> Self {
82 Self::new(period)
83 }
84}
85
86impl Next<f64> for EMA {
87 type Output = f64;
88
89 fn next(&mut self, input: f64) -> Self::Output {
90 match self.current_ema {
91 Some(prev_ema) => {
92 let ema = self.alpha * input + (1.0 - self.alpha) * prev_ema;
93 self.current_ema = Some(ema);
94 ema
95 }
96 None => {
97 self.current_ema = Some(input);
98 input
99 }
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct WMA {
107 period: usize,
108 window: VecDeque<f64>,
109}
110
111impl WMA {
112 pub fn new(period: usize) -> Self {
113 Self {
114 period,
115 window: VecDeque::with_capacity(period),
116 }
117 }
118}
119
120impl From<usize> for WMA {
121 fn from(period: usize) -> Self {
122 Self::new(period)
123 }
124}
125
126impl Next<f64> for WMA {
127 type Output = f64;
128
129 fn next(&mut self, input: f64) -> Self::Output {
130 self.window.push_back(input);
131 if self.window.len() > self.period {
132 self.window.pop_front();
133 }
134
135 let mut weight_sum = 0.0;
136 let mut weighted_val_sum = 0.0;
137
138 for (i, &val) in self.window.iter().enumerate() {
139 let weight = (i + 1) as f64;
140 weighted_val_sum += val * weight;
141 weight_sum += weight;
142 }
143
144 if weight_sum == 0.0 {
145 0.0
146 } else {
147 weighted_val_sum / weight_sum
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::test_utils::{assert_indicator_parity, load_gold_standard};
156
157 #[test]
158 fn test_sma_gold_standard() {
159 let case = load_gold_standard("sma_5");
160 let sma = SMA::new(3); assert_indicator_parity(sma, &case.input, &case.expected);
162 }
163
164 #[test]
170 fn test_sma_self_heals_after_nan_ages_out_of_window() {
171 let mut sma = SMA::new(3);
172 assert_eq!(sma.next(1.0), 1.0);
173 assert_eq!(sma.next(2.0), 1.5);
174 assert!(sma.next(f64::NAN).is_nan());
176 assert!(sma.next(4.0).is_nan());
178 assert!(sma.next(5.0).is_nan());
180 let recovered = sma.next(6.0);
183 assert!(
184 !recovered.is_nan(),
185 "SMA should self-heal once the NaN ages out of the window, got {recovered}"
186 );
187 approx::assert_relative_eq!(recovered, 5.0);
188 approx::assert_relative_eq!(sma.next(9.0), 20.0 / 3.0);
190 }
191
192 #[test]
193 fn test_ema_basic() {
194 let mut ema = EMA::new(3);
195 assert_eq!(ema.next(10.0), 10.0);
196 approx::assert_relative_eq!(ema.next(12.0), 11.0); }
198
199 #[test]
200 fn test_wma_basic() {
201 let mut wma = WMA::new(3);
202 assert_eq!(wma.next(1.0), 1.0);
203 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); }
207}
208
209pub const SMA_METADATA: IndicatorMetadata = IndicatorMetadata {
210 name: "Simple Moving Average",
211 description: "The Simple Moving Average calculates the unweighted mean of the previous N data points.",
212 usage: "Use as the foundational smoothing module providing SMA, EMA, WMA, and SMMA implementations that power higher-level indicators across the library.",
213 keywords: &["moving-average", "smoothing", "classic", "ema"],
214 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.",
215 params: &[ParamDef {
216 name: "period",
217 default: "14",
218 description: "Smoothing period",
219 }],
220 formula_source: "https://www.investopedia.com/terms/s/sma.asp",
221 formula_latex: r#"
222\[
223SMA = \frac{1}{n} \sum_{i=1}^{n} P_i
224\]
225"#,
226 gold_standard_file: "sma.json",
227 category: "Classic",
228};
229
230pub const EMA_METADATA: IndicatorMetadata = IndicatorMetadata {
231 name: "Exponential Moving Average",
232 description: "The Exponential Moving Average gives more weight to recent prices.",
233 usage: "Use as the foundational smoothing module providing SMA, EMA, WMA, and SMMA implementations that power higher-level indicators across the library.",
234 keywords: &["moving-average", "smoothing", "classic", "ema"],
235 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.",
236 params: &[ParamDef {
237 name: "period",
238 default: "14",
239 description: "Smoothing period",
240 }],
241 formula_source: "https://www.investopedia.com/terms/e/ema.asp",
242 formula_latex: r#"
243\[
244EMA = P_t \times \alpha + EMA_{t-1} \times (1 - \alpha)
245\]
246"#,
247 gold_standard_file: "ema.json",
248 category: "Classic",
249};
250
251pub const WMA_METADATA: IndicatorMetadata = IndicatorMetadata {
252 name: "Weighted Moving Average",
253 description: "The Weighted Moving Average assigns linearly decreasing weights.",
254 usage: "Use as the foundational smoothing module providing SMA, EMA, WMA, and SMMA implementations that power higher-level indicators across the library.",
255 keywords: &["moving-average", "smoothing", "classic", "ema"],
256 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.",
257 params: &[ParamDef {
258 name: "period",
259 default: "14",
260 description: "Smoothing period",
261 }],
262 formula_source: "https://www.investopedia.com/articles/technical/060401.asp",
263 formula_latex: r#"
264\[
265WMA = \frac{P_1 \times n + P_2 \times (n-1) + \dots}{n + (n-1) + \dots + 1}
266\]
267"#,
268 gold_standard_file: "wma.json",
269 category: "Classic",
270};