use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::collections::VecDeque;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct DSMA {
period: usize,
c1: f64,
c2: f64,
c3: f64,
price_history: VecDeque<f64>,
zeros_history: [f64; 2],
filt_history: [f64; 2],
filt_window: VecDeque<f64>,
dsma_prev: f64,
count: usize,
}
impl DSMA {
pub fn new(period: usize) -> Self {
let period_f = period as f64;
let a1 = (-1.414 * PI / (0.5 * period_f)).exp();
let c2 = 2.0 * a1 * (1.414 * PI / (0.5 * period_f)).cos();
let c3 = -a1 * a1;
let c1 = 1.0 - c2 - c3;
Self {
period,
c1,
c2,
c3,
price_history: VecDeque::from(vec![0.0; 4]),
zeros_history: [0.0; 2],
filt_history: [0.0; 2],
filt_window: VecDeque::from(vec![0.0; period]),
dsma_prev: 0.0,
count: 0,
}
}
}
impl Next<f64> for DSMA {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
self.count += 1;
self.price_history.push_front(input);
self.price_history.pop_back();
if self.count == 1 {
self.dsma_prev = input;
return input;
}
let zeros = self.price_history[0] - self.price_history[2];
let filt = self.c1 * (zeros + self.zeros_history[0]) / 2.0
+ self.c2 * self.filt_history[0]
+ self.c3 * self.filt_history[1];
self.zeros_history[1] = self.zeros_history[0];
self.zeros_history[0] = zeros;
self.filt_history[1] = self.filt_history[0];
self.filt_history[0] = filt;
self.filt_window.push_front(filt);
self.filt_window.pop_back();
let mut sum_sq = 0.0;
for &f in &self.filt_window {
sum_sq += f * f;
}
let rms = (sum_sq / self.period as f64).sqrt();
let scaled_filt = if rms != 0.0 { filt / rms } else { 0.0 };
let mut alpha1 = scaled_filt.abs() * 5.0 / self.period as f64;
if alpha1 > 1.0 {
alpha1 = 1.0;
}
let dsma = alpha1 * input + (1.0 - alpha1) * self.dsma_prev;
self.dsma_prev = dsma;
dsma
}
}
pub const DSMA_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "DSMA",
description: "Deviation Scaled Moving Average adapts to price variations using standard deviation scaled oscillators.",
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.",
keywords: &["moving-average", "adaptive", "ehlers", "dsp", "dominant-cycle"],
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.",
params: &[ParamDef {
name: "period",
default: "40",
description: "Critical period for smoothing and RMS calculation",
}],
formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/DEVIATION%20SCALED%20MOVING%20AVERAGE.pdf",
formula_latex: r#"
\[
Zeros = Close - Close_{t-2}
\]
\[
Filt = c_1 \frac{Zeros + Zeros_{t-1}}{2} + c_2 Filt_{t-1} + c_3 Filt_{t-2}
\]
\[
RMS = \sqrt{\frac{1}{P} \sum_{i=0}^{P-1} Filt_{t-i}^2}
\]
\[
\alpha = \min\left(1.0, \left| \frac{Filt}{RMS} \right| \frac{5}{P}\right)
\]
\[
DSMA = \alpha \cdot Close + (1 - \alpha) \cdot DSMA_{t-1}
\]
"#,
gold_standard_file: "dsma.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_dsma_basic() {
let mut dsma = DSMA::new(40);
let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
for input in inputs {
let res = dsma.next(input);
assert!(!res.is_nan());
}
}
proptest! {
#[test]
fn test_dsma_parity(
inputs in prop::collection::vec(1.0..100.0, 100..200),
) {
let period = 40;
let mut dsma = DSMA::new(period);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| dsma.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
let period_f = period as f64;
let a1 = (-1.414 * PI / (0.5 * period_f)).exp();
let c2 = 2.0 * a1 * (1.414 * PI / (0.5 * period_f)).cos();
let c3 = -a1 * a1;
let c1 = 1.0 - c2 - c3;
let mut price_hist = vec![0.0; inputs.len() + 4];
let mut zeros_hist = vec![0.0; inputs.len() + 4];
let mut filt_hist = vec![0.0; inputs.len() + 4];
let mut dsma_prev = 0.0;
for (i, &input) in inputs.iter().enumerate() {
let bar = i + 1;
let idx = i + 2; price_hist[idx] = input;
if bar == 1 {
dsma_prev = input;
batch_results.push(input);
continue;
}
let zeros = price_hist[idx] - price_hist[idx-2];
zeros_hist[idx] = zeros;
let filt = c1 * (zeros + zeros_hist[idx-1]) / 2.0
+ c2 * filt_hist[idx-1]
+ c3 * filt_hist[idx-2];
filt_hist[idx] = filt;
let mut sum_sq = 0.0;
for j in 0..period {
if idx >= j {
let f = filt_hist[idx-j];
sum_sq += f * f;
}
}
let rms = (sum_sq / period_f).sqrt();
let scaled_filt = if rms != 0.0 { filt / rms } else { 0.0 };
let mut alpha1 = scaled_filt.abs() * 5.0 / period_f;
if alpha1 > 1.0 { alpha1 = 1.0; }
let dsma = alpha1 * input + (1.0 - alpha1) * dsma_prev;
dsma_prev = dsma;
batch_results.push(dsma);
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-10);
}
}
}
}