Skip to main content

quantwave_core/indicators/
dmh.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6/// The DMH: An Improved Directional Movement Indicator
7///
8/// Based on John Ehlers' article "The DMH: An Improved Directional Movement Indicator" (TASC Dec 2021).
9/// This indicator modernizes Wilder's Directional Movement by applying Hann windowing to an EMA
10/// of the directional difference, significantly reducing lag and noise compared to the classic ADX/DMI.
11#[derive(Debug, Clone)]
12pub struct DMH {
13    length: usize,
14    sf: f64,
15    ema: f64,
16    ema_history: VecDeque<f64>,
17    prev_high: Option<f64>,
18    prev_low: Option<f64>,
19    hann_coeffs: Vec<f64>,
20    sum_coeffs: f64,
21    count: usize,
22}
23
24impl DMH {
25    pub fn new(length: usize) -> Self {
26        let sf = 1.0 / (length as f64);
27
28        // Pre-calculate Hann coefficients
29        let mut hann_coeffs = Vec::with_capacity(length);
30        let mut sum_coeffs = 0.0;
31        let length_plus_1 = (length + 1) as f64;
32
33        for i in 1..=length {
34            let coef = 1.0 - (2.0 * PI * (i as f64) / length_plus_1).cos();
35            hann_coeffs.push(coef);
36            sum_coeffs += coef;
37        }
38
39        Self {
40            length,
41            sf,
42            ema: 0.0,
43            ema_history: VecDeque::with_capacity(length),
44            prev_high: None,
45            prev_low: None,
46            hann_coeffs,
47            sum_coeffs,
48            count: 0,
49        }
50    }
51}
52
53impl Next<(f64, f64)> for DMH {
54    type Output = f64;
55
56    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
57        self.count += 1;
58
59        let (plus_dm, minus_dm) = match (self.prev_high, self.prev_low) {
60            (Some(ph), Some(pl)) => {
61                let upper_move = high - ph;
62                let lower_move = pl - low;
63
64                let mut p_dm = 0.0;
65                let mut m_dm = 0.0;
66
67                if upper_move > lower_move && upper_move > 0.0 {
68                    p_dm = upper_move;
69                } else if lower_move > upper_move && lower_move > 0.0 {
70                    m_dm = lower_move;
71                }
72                (p_dm, m_dm)
73            }
74            _ => (0.0, 0.0),
75        };
76
77        self.prev_high = Some(high);
78        self.prev_low = Some(low);
79
80        // Wilder's EMA (Smoothing Factor = 1/Length)
81        let diff = plus_dm - minus_dm;
82        if self.count == 1 {
83            self.ema = diff;
84        } else {
85            self.ema = self.sf * diff + (1.0 - self.sf) * self.ema;
86        }
87
88        self.ema_history.push_front(self.ema);
89        if self.ema_history.len() > self.length {
90            self.ema_history.pop_back();
91        }
92
93        // Apply Hann Windowed FIR filter
94        if self.ema_history.len() < self.length {
95            // During startup, we can still calculate but we'll use a partial window
96            let mut dm_sum = 0.0;
97            let mut partial_sum_coeffs = 0.0;
98            for (i, &val) in self.ema_history.iter().enumerate() {
99                let coef = self.hann_coeffs[i];
100                dm_sum += coef * val;
101                partial_sum_coeffs += coef;
102            }
103            if partial_sum_coeffs != 0.0 {
104                dm_sum / partial_sum_coeffs
105            } else {
106                0.0
107            }
108        } else {
109            let mut dm_sum = 0.0;
110            for (i, &val) in self.ema_history.iter().enumerate() {
111                dm_sum += self.hann_coeffs[i] * val;
112            }
113            if self.sum_coeffs != 0.0 {
114                dm_sum / self.sum_coeffs
115            } else {
116                0.0
117            }
118        }
119    }
120}
121
122pub const DMH_METADATA: IndicatorMetadata = IndicatorMetadata {
123    name: "DMH",
124    description: "An improved Directional Movement indicator using Hann windowing for smoother signals and reduced lag.",
125    usage: "Use as a momentum oscillator with high-pass filtering to isolate cyclical momentum while removing the trend bias that corrupts standard momentum indicators.",
126    keywords: &["momentum", "oscillator", "ehlers", "high-pass", "dsp"],
127    ehlers_summary: "Ehlers constructs the DMH by applying a high-pass filter to the momentum calculation, removing the low-frequency trend component that causes conventional momentum to drift. The result is a zero-centered momentum oscillator that oscillates cleanly around the cycle midpoint.",
128    params: &[ParamDef {
129        name: "length",
130        default: "14",
131        description: "Smoothing period",
132    }],
133    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/implemented/TRADERS%E2%80%99%20TIPS%20-%20DECEMBER%202021.html",
134    formula_latex: r#"
135\[
136\text{PlusDM} = \text{High} - \text{High}_{t-1} \text{ if } > (\text{Low}_{t-1} - \text{Low}) \text{ and } > 0, \text{ else } 0
137\]
138\[
139\text{MinusDM} = \text{Low}_{t-1} - \text{Low} \text{ if } > (\text{High} - \text{High}_{t-1}) \text{ and } > 0, \text{ else } 0
140\]
141\[
142\text{EMA} = \frac{1}{L}(\text{PlusDM} - \text{MinusDM}) + (1 - \frac{1}{L})\text{EMA}_{t-1}
143\]
144\[
145\text{DMH} = \frac{\sum_{i=1}^{L} w_i \text{EMA}_{t-i+1}}{\sum_{i=1}^{L} w_i}, \text{ where } w_i = 1 - \cos\left(\frac{2\pi i}{L+1}\right)
146\]
147"#,
148    gold_standard_file: "dmh.json",
149    category: "Ehlers DSP",
150};
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::traits::Next;
156    use proptest::prelude::*;
157
158    #[test]
159    fn test_dmh_basic() {
160        let mut dmh = DMH::new(14);
161        let inputs = vec![
162            (10.0, 9.0),
163            (11.0, 10.0),
164            (12.0, 11.0),
165            (13.0, 12.0),
166            (12.0, 11.0),
167            (11.0, 10.0),
168        ];
169        for input in inputs {
170            let res = dmh.next(input);
171            assert!(!res.is_nan());
172        }
173    }
174
175    proptest! {
176        #[test]
177        fn test_dmh_parity(
178            highs in prop::collection::vec(10.0..20.0, 50..100),
179            lows in prop::collection::vec(5.0..15.0, 50..100),
180        ) {
181            let len = highs.len().min(lows.len());
182            let inputs: Vec<(f64, f64)> = (0..len).map(|i| {
183                let h: f64 = highs[i];
184                let l: f64 = lows[i];
185                (h.max(l), h.min(l))
186            }).collect();
187
188            let length = 14;
189            let mut dmh = DMH::new(length);
190            let streaming_results: Vec<f64> = inputs.iter().map(|&val| dmh.next(val)).collect();
191
192            // Reference implementation
193            let mut ema = 0.0;
194            let mut ema_hist = Vec::new();
195            let mut batch_results = Vec::with_capacity(len);
196
197            let sf = 1.0 / length as f64;
198            let mut hann_coeffs = Vec::new();
199            for i in 1..=length {
200                let c = 1.0 - (2.0 * PI * i as f64 / (length + 1) as f64).cos();
201                hann_coeffs.push(c);
202            }
203
204            for i in 0..len {
205                let (plus_dm, minus_dm) = if i == 0 {
206                    (0.0, 0.0)
207                } else {
208                    let um = inputs[i].0 - inputs[i-1].0;
209                    let lm = inputs[i-1].1 - inputs[i].1;
210                    let mut p = 0.0;
211                    let mut m = 0.0;
212                    if um > lm && um > 0.0 { p = um; }
213                    else if lm > um && lm > 0.0 { m = lm; }
214                    (p, m)
215                };
216
217                let diff = plus_dm - minus_dm;
218                if i == 0 {
219                    ema = diff;
220                } else {
221                    ema = sf * diff + (1.0 - sf) * ema;
222                }
223
224                ema_hist.push(ema);
225
226                let mut dm_sum = 0.0;
227                let mut cur_sum_coeffs = 0.0;
228
229                let start = if i + 1 > length { i + 1 - length } else { 0 };
230                let window = &ema_hist[start..i+1];
231
232                for (j, &val) in window.iter().rev().enumerate() {
233                    let c = hann_coeffs[j];
234                    dm_sum += c * val;
235                    cur_sum_coeffs += c;
236                }
237
238                batch_results.push(dm_sum / cur_sum_coeffs);
239            }
240
241            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
242                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
243            }
244        }
245    }
246}