quantwave_core/indicators/
dmh.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6#[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 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 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 if self.ema_history.len() < self.length {
95 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 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}