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 params: &[ParamDef {
126 name: "length",
127 default: "14",
128 description: "Smoothing period",
129 }],
130 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/implemented/TRADERS%E2%80%99%20TIPS%20-%20DECEMBER%202021.html",
131 formula_latex: r#"
132\[
133\text{PlusDM} = \text{High} - \text{High}_{t-1} \text{ if } > (\text{Low}_{t-1} - \text{Low}) \text{ and } > 0, \text{ else } 0
134\]
135\[
136\text{MinusDM} = \text{Low}_{t-1} - \text{Low} \text{ if } > (\text{High} - \text{High}_{t-1}) \text{ and } > 0, \text{ else } 0
137\]
138\[
139\text{EMA} = \frac{1}{L}(\text{PlusDM} - \text{MinusDM}) + (1 - \frac{1}{L})\text{EMA}_{t-1}
140\]
141\[
142\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)
143\]
144"#,
145 gold_standard_file: "dmh.json",
146 category: "Ehlers DSP",
147};
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::traits::Next;
153 use proptest::prelude::*;
154
155 #[test]
156 fn test_dmh_basic() {
157 let mut dmh = DMH::new(14);
158 let inputs = vec![
159 (10.0, 9.0),
160 (11.0, 10.0),
161 (12.0, 11.0),
162 (13.0, 12.0),
163 (12.0, 11.0),
164 (11.0, 10.0),
165 ];
166 for input in inputs {
167 let res = dmh.next(input);
168 assert!(!res.is_nan());
169 }
170 }
171
172 proptest! {
173 #[test]
174 fn test_dmh_parity(
175 highs in prop::collection::vec(10.0..20.0, 50..100),
176 lows in prop::collection::vec(5.0..15.0, 50..100),
177 ) {
178 let len = highs.len().min(lows.len());
179 let inputs: Vec<(f64, f64)> = (0..len).map(|i| {
180 let h: f64 = highs[i];
181 let l: f64 = lows[i];
182 (h.max(l), h.min(l))
183 }).collect();
184
185 let length = 14;
186 let mut dmh = DMH::new(length);
187 let streaming_results: Vec<f64> = inputs.iter().map(|&val| dmh.next(val)).collect();
188
189 let mut ema = 0.0;
191 let mut ema_hist = Vec::new();
192 let mut batch_results = Vec::with_capacity(len);
193
194 let sf = 1.0 / length as f64;
195 let mut hann_coeffs = Vec::new();
196 for i in 1..=length {
197 let c = 1.0 - (2.0 * PI * i as f64 / (length + 1) as f64).cos();
198 hann_coeffs.push(c);
199 }
200
201 for i in 0..len {
202 let (plus_dm, minus_dm) = if i == 0 {
203 (0.0, 0.0)
204 } else {
205 let um = inputs[i].0 - inputs[i-1].0;
206 let lm = inputs[i-1].1 - inputs[i].1;
207 let mut p = 0.0;
208 let mut m = 0.0;
209 if um > lm && um > 0.0 { p = um; }
210 else if lm > um && lm > 0.0 { m = lm; }
211 (p, m)
212 };
213
214 let diff = plus_dm - minus_dm;
215 if i == 0 {
216 ema = diff;
217 } else {
218 ema = sf * diff + (1.0 - sf) * ema;
219 }
220
221 ema_hist.push(ema);
222
223 let mut dm_sum = 0.0;
224 let mut cur_sum_coeffs = 0.0;
225
226 let start = if i + 1 > length { i + 1 - length } else { 0 };
227 let window = &ema_hist[start..i+1];
228
229 for (j, &val) in window.iter().rev().enumerate() {
230 let c = hann_coeffs[j];
231 dm_sum += c * val;
232 cur_sum_coeffs += c;
233 }
234
235 batch_results.push(dm_sum / cur_sum_coeffs);
236 }
237
238 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
239 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
240 }
241 }
242 }
243}