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    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            // Batch implementation
166            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; // Offset for historical access
181                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}