quantwave_core/indicators/
dsma.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as 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: &[
112 "moving-average",
113 "adaptive",
114 "ehlers",
115 "dsp",
116 "dominant-cycle",
117 ],
118 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.",
119 params: &[ParamDef {
120 name: "period",
121 default: "40",
122 description: "Critical period for smoothing and RMS calculation",
123 }],
124 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/DEVIATION%20SCALED%20MOVING%20AVERAGE.pdf",
125 formula_latex: r#"
126\[
127Zeros = Close - Close_{t-2}
128\]
129\[
130Filt = c_1 \frac{Zeros + Zeros_{t-1}}{2} + c_2 Filt_{t-1} + c_3 Filt_{t-2}
131\]
132\[
133RMS = \sqrt{\frac{1}{P} \sum_{i=0}^{P-1} Filt_{t-i}^2}
134\]
135\[
136\alpha = \min\left(1.0, \left| \frac{Filt}{RMS} \right| \frac{5}{P}\right)
137\]
138\[
139DSMA = \alpha \cdot Close + (1 - \alpha) \cdot DSMA_{t-1}
140\]
141"#,
142 gold_standard_file: "dsma.json",
143 category: "Ehlers DSP",
144};
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::traits::Next;
150 use proptest::prelude::*;
151
152 #[test]
153 fn test_dsma_basic() {
154 let mut dsma = DSMA::new(40);
155 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
156 for input in inputs {
157 let res = dsma.next(input);
158 assert!(!res.is_nan());
159 }
160 }
161
162 proptest! {
163 #[test]
164 fn test_dsma_parity(
165 inputs in prop::collection::vec(1.0..100.0, 100..200),
166 ) {
167 let period = 40;
168 let mut dsma = DSMA::new(period);
169 let streaming_results: Vec<f64> = inputs.iter().map(|&x| dsma.next(x)).collect();
170
171 let mut batch_results = Vec::with_capacity(inputs.len());
173 let period_f = period as f64;
174 let a1 = (-1.414 * PI / (0.5 * period_f)).exp();
175 let c2 = 2.0 * a1 * (1.414 * PI / (0.5 * period_f)).cos();
176 let c3 = -a1 * a1;
177 let c1 = 1.0 - c2 - c3;
178
179 let mut price_hist = vec![0.0; inputs.len() + 4];
180 let mut zeros_hist = vec![0.0; inputs.len() + 4];
181 let mut filt_hist = vec![0.0; inputs.len() + 4];
182 let mut dsma_prev = 0.0;
183
184 for (i, &input) in inputs.iter().enumerate() {
185 let bar = i + 1;
186 let idx = i + 2; price_hist[idx] = input;
188
189 if bar == 1 {
190 dsma_prev = input;
191 batch_results.push(input);
192 continue;
193 }
194
195 let zeros = price_hist[idx] - price_hist[idx-2];
196 zeros_hist[idx] = zeros;
197
198 let filt = c1 * (zeros + zeros_hist[idx-1]) / 2.0
199 + c2 * filt_hist[idx-1]
200 + c3 * filt_hist[idx-2];
201 filt_hist[idx] = filt;
202
203 let mut sum_sq = 0.0;
204 for j in 0..period {
205 if idx >= j {
206 let f = filt_hist[idx-j];
207 sum_sq += f * f;
208 }
209 }
210 let rms = (sum_sq / period_f).sqrt();
211
212 let scaled_filt = if rms != 0.0 { filt / rms } else { 0.0 };
213 let mut alpha1 = scaled_filt.abs() * 5.0 / period_f;
214 if alpha1 > 1.0 { alpha1 = 1.0; }
215
216 let dsma = alpha1 * input + (1.0 - alpha1) * dsma_prev;
217 dsma_prev = dsma;
218 batch_results.push(dsma);
219 }
220
221 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
222 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
223 }
224 }
225 }
226}