quantwave_core/indicators/
dsma.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6#[derive(Debug, Clone)]
14pub struct DSMA {
15 period: usize,
16 c1: f64,
17 c2: f64,
18 c3: f64,
19 price_history: VecDeque<f64>,
20 zeros_history: [f64; 2],
21 filt_history: [f64; 2],
22 filt_window: VecDeque<f64>,
23 dsma_prev: f64,
24 count: usize,
25}
26
27impl DSMA {
28 pub fn new(period: usize) -> Self {
29 let period_f = period as f64;
30 let a1 = (-1.414 * PI / (0.5 * period_f)).exp();
31 let c2 = 2.0 * a1 * (1.414 * PI / (0.5 * period_f)).cos();
32 let c3 = -a1 * a1;
33 let c1 = 1.0 - c2 - c3;
34
35 Self {
36 period,
37 c1,
38 c2,
39 c3,
40 price_history: VecDeque::from(vec![0.0; 4]),
41 zeros_history: [0.0; 2],
42 filt_history: [0.0; 2],
43 filt_window: VecDeque::from(vec![0.0; period]),
44 dsma_prev: 0.0,
45 count: 0,
46 }
47 }
48}
49
50impl Next<f64> for DSMA {
51 type Output = f64;
52
53 fn next(&mut self, input: f64) -> Self::Output {
54 self.count += 1;
55
56 self.price_history.push_front(input);
58 self.price_history.pop_back();
59
60 if self.count == 1 {
61 self.dsma_prev = input;
62 return input;
63 }
64
65 let zeros = self.price_history[0] - self.price_history[2];
67
68 let filt = self.c1 * (zeros + self.zeros_history[0]) / 2.0
70 + self.c2 * self.filt_history[0]
71 + self.c3 * self.filt_history[1];
72
73 self.zeros_history[1] = self.zeros_history[0];
74 self.zeros_history[0] = zeros;
75
76 self.filt_history[1] = self.filt_history[0];
77 self.filt_history[0] = filt;
78
79 self.filt_window.push_front(filt);
80 self.filt_window.pop_back();
81
82 let mut sum_sq = 0.0;
85 for &f in &self.filt_window {
86 sum_sq += f * f;
87 }
88 let rms = (sum_sq / self.period as f64).sqrt();
89
90 let scaled_filt = if rms != 0.0 { filt / rms } else { 0.0 };
92
93 let mut alpha1 = scaled_filt.abs() * 5.0 / self.period as f64;
95 if alpha1 > 1.0 {
96 alpha1 = 1.0;
97 }
98
99 let dsma = alpha1 * input + (1.0 - alpha1) * self.dsma_prev;
101 self.dsma_prev = dsma;
102
103 dsma
104 }
105}
106
107pub const DSMA_METADATA: IndicatorMetadata = IndicatorMetadata {
108 name: "DSMA",
109 description: "Deviation Scaled Moving Average adapts to price variations using standard deviation scaled oscillators.",
110 usage: "Use as a highly adaptive moving average that tracks price closely during trends and large moves but provides heavy filtering during consolidation. Ideal for trend-following entries and trailing stops.",
111 keywords: &["moving-average", "adaptive", "ehlers", "dsp", "dominant-cycle"],
112 ehlers_summary: "In 'The Deviation-Scaled Moving Average' (2018), Ehlers introduces an adaptive EMA where the alpha (smoothing factor) is dynamically adjusted based on a deviation-scaled oscillator. By scaling the SuperSmoother-filtered momentum by its RMS, the indicator becomes reactive to significant price deviations while remaining smooth during low-volatility periods.",
113 params: &[ParamDef {
114 name: "period",
115 default: "40",
116 description: "Critical period for smoothing and RMS calculation",
117 }],
118 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/DEVIATION%20SCALED%20MOVING%20AVERAGE.pdf",
119 formula_latex: r#"
120\[
121Zeros = Close - Close_{t-2}
122\]
123\[
124Filt = c_1 \frac{Zeros + Zeros_{t-1}}{2} + c_2 Filt_{t-1} + c_3 Filt_{t-2}
125\]
126\[
127RMS = \sqrt{\frac{1}{P} \sum_{i=0}^{P-1} Filt_{t-i}^2}
128\]
129\[
130\alpha = \min\left(1.0, \left| \frac{Filt}{RMS} \right| \frac{5}{P}\right)
131\]
132\[
133DSMA = \alpha \cdot Close + (1 - \alpha) \cdot DSMA_{t-1}
134\]
135"#,
136 gold_standard_file: "dsma.json",
137 category: "Ehlers DSP",
138};
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::traits::Next;
144 use proptest::prelude::*;
145
146 #[test]
147 fn test_dsma_basic() {
148 let mut dsma = DSMA::new(40);
149 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
150 for input in inputs {
151 let res = dsma.next(input);
152 assert!(!res.is_nan());
153 }
154 }
155
156 proptest! {
157 #[test]
158 fn test_dsma_parity(
159 inputs in prop::collection::vec(1.0..100.0, 100..200),
160 ) {
161 let period = 40;
162 let mut dsma = DSMA::new(period);
163 let streaming_results: Vec<f64> = inputs.iter().map(|&x| dsma.next(x)).collect();
164
165 let mut batch_results = Vec::with_capacity(inputs.len());
167 let period_f = period as f64;
168 let a1 = (-1.414 * PI / (0.5 * period_f)).exp();
169 let c2 = 2.0 * a1 * (1.414 * PI / (0.5 * period_f)).cos();
170 let c3 = -a1 * a1;
171 let c1 = 1.0 - c2 - c3;
172
173 let mut price_hist = vec![0.0; inputs.len() + 4];
174 let mut zeros_hist = vec![0.0; inputs.len() + 4];
175 let mut filt_hist = vec![0.0; inputs.len() + 4];
176 let mut dsma_prev = 0.0;
177
178 for (i, &input) in inputs.iter().enumerate() {
179 let bar = i + 1;
180 let idx = i + 2; price_hist[idx] = input;
182
183 if bar == 1 {
184 dsma_prev = input;
185 batch_results.push(input);
186 continue;
187 }
188
189 let zeros = price_hist[idx] - price_hist[idx-2];
190 zeros_hist[idx] = zeros;
191
192 let filt = c1 * (zeros + zeros_hist[idx-1]) / 2.0
193 + c2 * filt_hist[idx-1]
194 + c3 * filt_hist[idx-2];
195 filt_hist[idx] = filt;
196
197 let mut sum_sq = 0.0;
198 for j in 0..period {
199 if idx >= j {
200 let f = filt_hist[idx-j];
201 sum_sq += f * f;
202 }
203 }
204 let rms = (sum_sq / period_f).sqrt();
205
206 let scaled_filt = if rms != 0.0 { filt / rms } else { 0.0 };
207 let mut alpha1 = scaled_filt.abs() * 5.0 / period_f;
208 if alpha1 > 1.0 { alpha1 = 1.0; }
209
210 let dsma = alpha1 * input + (1.0 - alpha1) * dsma_prev;
211 dsma_prev = dsma;
212 batch_results.push(dsma);
213 }
214
215 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
216 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
217 }
218 }
219 }
220}