quantwave_core/indicators/
madh.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::hann::HannFilter;
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
11pub struct MADH {
12 filter1: HannFilter,
13 filter2: HannFilter,
14 _short_length: usize,
15 _dominant_cycle: usize,
16}
17
18impl MADH {
19 pub fn new(short_length: usize, dominant_cycle: usize) -> Self {
20 let long_length = short_length + dominant_cycle / 2;
21 Self {
22 filter1: HannFilter::new(short_length),
23 filter2: HannFilter::new(long_length),
24 _short_length: short_length,
25 _dominant_cycle: dominant_cycle,
26 }
27 }
28}
29
30impl Default for MADH {
31 fn default() -> Self {
32 Self::new(8, 27)
33 }
34}
35
36impl Next<f64> for MADH {
37 type Output = f64;
38
39 fn next(&mut self, input: f64) -> Self::Output {
40 let f1 = self.filter1.next(input);
41 let f2 = self.filter2.next(input);
42 if f2.abs() > 1e-10 {
43 100.0 * (f1 - f2) / f2
44 } else {
45 0.0
46 }
47 }
48}
49
50pub const MADH_METADATA: IndicatorMetadata = IndicatorMetadata {
51 name: "MADH",
52 description: "Moving Average Difference with Hann Windowing: 100 * (Hann(short) - Hann(long)) / Hann(long)",
53 params: &[
54 ParamDef {
55 name: "short_length",
56 default: "8",
57 description: "Short-term filter length",
58 },
59 ParamDef {
60 name: "dominant_cycle",
61 default: "27",
62 description: "Dominant cycle for calculating long length",
63 },
64 ],
65 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - NOVEMBER 2021.html",
66 formula_latex: r#"
67\[
68LongLength = \lfloor ShortLength + DominantCycle / 2 \rfloor
69\]
70\[
71Filt1 = HannWindow(Price, ShortLength)
72\]
73\[
74Filt2 = HannWindow(Price, LongLength)
75\]
76\[
77MADH = 100 \times \frac{Filt1 - Filt2}{Filt2}
78\]
79"#,
80 gold_standard_file: "madh.json",
81 category: "Ehlers DSP",
82};
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::traits::Next;
88 use proptest::prelude::*;
89 use std::f64::consts::PI;
90
91 #[test]
92 fn test_madh_basic() {
93 let mut madh = MADH::new(8, 27);
94 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
95 for input in inputs {
96 let res = madh.next(input);
97 assert!(!res.is_nan());
98 }
99 }
100
101 proptest! {
102 #[test]
103 fn test_madh_parity(
104 inputs in prop::collection::vec(1.0..100.0, 50..100),
105 ) {
106 let short = 8;
107 let dc = 27;
108 let long = short + dc / 2;
109 let mut madh = MADH::new(short, dc);
110 let streaming_results: Vec<f64> = inputs.iter().map(|&x| madh.next(x)).collect();
111
112 let mut batch_results = Vec::with_capacity(inputs.len());
114
115 let mut coeffs1 = Vec::new();
116 let mut sum1 = 0.0;
117 for count in 1..=short {
118 let c = 1.0 - (2.0 * PI * count as f64 / (short as f64 + 1.0)).cos();
119 coeffs1.push(c);
120 sum1 += c;
121 }
122
123 let mut coeffs2 = Vec::new();
124 let mut sum2 = 0.0;
125 for count in 1..=long {
126 let c = 1.0 - (2.0 * PI * count as f64 / (long as f64 + 1.0)).cos();
127 coeffs2.push(c);
128 sum2 += c;
129 }
130
131 for i in 0..inputs.len() {
132 let f1 = if i < short - 1 {
133 inputs[i]
134 } else {
135 let mut sum = 0.0;
136 for j in 0..short {
137 sum += coeffs1[j] * inputs[i - j];
138 }
139 sum / sum1
140 };
141
142 let f2 = if i < long - 1 {
143 inputs[i]
144 } else {
145 let mut sum = 0.0;
146 for j in 0..long {
147 sum += coeffs2[j] * inputs[i - j];
148 }
149 sum / sum2
150 };
151
152 let res = if f2.abs() > 1e-10 {
153 100.0 * (f1 - f2) / f2
154 } else {
155 0.0
156 };
157 batch_results.push(res);
158 }
159
160 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
161 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
162 }
163 }
164 }
165}