Skip to main content

quantwave_core/indicators/
dsma.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6/// Deviation Scaled Moving Average (DSMA)
7///
8/// Based on John Ehlers' "Deviation Scaled Moving Average" (2018).
9/// DSMA is an adaptive moving average that modifies the alpha term of an EMA
10/// based on the amplitude of an oscillator scaled in Standard Deviations from the mean.
11/// This allows it to adapt rapidly to price variations while maintaining heavy smoothing
12/// when variations are small.
13#[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        // price_history[0] is Close, [1] is Close[1], [2] is Close[2], [3] is Close[3]
57        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        // Zeros = Close - Close[2];
66        let zeros = self.price_history[0] - self.price_history[2];
67
68        // Filt = c1*(Zeros + Zeros[1]) / 2 + c2*Filt[1] + c3*Filt[2];
69        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        // Compute RMS (Standard Deviation from zero mean) over last Period bars
83        // The EL code uses a loop: For count = 0 to Period - 1 Begin RMS = RMS + Filt[count]*Filt[count]; End;
84        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        // Rescale Filt in terms of Standard Deviations
91        let scaled_filt = if rms != 0.0 { filt / rms } else { 0.0 };
92
93        // alpha1 = AbsValue(ScaledFilt)*5 / Period;
94        let mut alpha1 = scaled_filt.abs() * 5.0 / self.period as f64;
95        if alpha1 > 1.0 {
96            alpha1 = 1.0;
97        }
98
99        // DSMA = alpha1*Close + (1 - alpha1)*DSMA[1];
100        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    params: &[ParamDef {
111        name: "period",
112        default: "40",
113        description: "Critical period for smoothing and RMS calculation",
114    }],
115    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/DEVIATION%20SCALED%20MOVING%20AVERAGE.pdf",
116    formula_latex: r#"
117\[
118Zeros = Close - Close_{t-2}
119\]
120\[
121Filt = c_1 \frac{Zeros + Zeros_{t-1}}{2} + c_2 Filt_{t-1} + c_3 Filt_{t-2}
122\]
123\[
124RMS = \sqrt{\frac{1}{P} \sum_{i=0}^{P-1} Filt_{t-i}^2}
125\]
126\[
127\alpha = \min\left(1.0, \left| \frac{Filt}{RMS} \right| \frac{5}{P}\right)
128\]
129\[
130DSMA = \alpha \cdot Close + (1 - \alpha) \cdot DSMA_{t-1}
131\]
132"#,
133    gold_standard_file: "dsma.json",
134    category: "Ehlers DSP",
135};
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::traits::Next;
141    use proptest::prelude::*;
142
143    #[test]
144    fn test_dsma_basic() {
145        let mut dsma = DSMA::new(40);
146        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
147        for input in inputs {
148            let res = dsma.next(input);
149            assert!(!res.is_nan());
150        }
151    }
152
153    proptest! {
154        #[test]
155        fn test_dsma_parity(
156            inputs in prop::collection::vec(1.0..100.0, 100..200),
157        ) {
158            let period = 40;
159            let mut dsma = DSMA::new(period);
160            let streaming_results: Vec<f64> = inputs.iter().map(|&x| dsma.next(x)).collect();
161
162            // Batch implementation
163            let mut batch_results = Vec::with_capacity(inputs.len());
164            let period_f = period as f64;
165            let a1 = (-1.414 * PI / (0.5 * period_f)).exp();
166            let c2 = 2.0 * a1 * (1.414 * PI / (0.5 * period_f)).cos();
167            let c3 = -a1 * a1;
168            let c1 = 1.0 - c2 - c3;
169
170            let mut price_hist = vec![0.0; inputs.len() + 4];
171            let mut zeros_hist = vec![0.0; inputs.len() + 4];
172            let mut filt_hist = vec![0.0; inputs.len() + 4];
173            let mut dsma_prev = 0.0;
174
175            for (i, &input) in inputs.iter().enumerate() {
176                let bar = i + 1;
177                let idx = i + 2; // Offset for historical access
178                price_hist[idx] = input;
179
180                if bar == 1 {
181                    dsma_prev = input;
182                    batch_results.push(input);
183                    continue;
184                }
185
186                let zeros = price_hist[idx] - price_hist[idx-2];
187                zeros_hist[idx] = zeros;
188
189                let filt = c1 * (zeros + zeros_hist[idx-1]) / 2.0
190                    + c2 * filt_hist[idx-1]
191                    + c3 * filt_hist[idx-2];
192                filt_hist[idx] = filt;
193
194                let mut sum_sq = 0.0;
195                for j in 0..period {
196                    if idx >= j {
197                        let f = filt_hist[idx-j];
198                        sum_sq += f * f;
199                    }
200                }
201                let rms = (sum_sq / period_f).sqrt();
202
203                let scaled_filt = if rms != 0.0 { filt / rms } else { 0.0 };
204                let mut alpha1 = scaled_filt.abs() * 5.0 / period_f;
205                if alpha1 > 1.0 { alpha1 = 1.0; }
206
207                let dsma = alpha1 * input + (1.0 - alpha1) * dsma_prev;
208                dsma_prev = dsma;
209                batch_results.push(dsma);
210            }
211
212            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
213                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
214            }
215        }
216    }
217}