quantwave_core/indicators/
harrington_adx.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
11pub struct HarringtonADXOscillator {
12 adx_length: usize,
13 adx_smooth_length: usize,
14 sf: f64,
15 tr_ema: f64,
16 pdm_ema: f64,
17 mdm_ema: f64,
18 adx_ema: f64,
19 p_sma: SMA,
20 m_sma: SMA,
21 prev_h: f64,
22 prev_l: f64,
23 prev_c: f64,
24 count: usize,
25}
26
27impl HarringtonADXOscillator {
28 pub fn new(adx_length: usize, adx_smooth_length: usize) -> Self {
29 Self {
30 adx_length,
31 adx_smooth_length,
32 sf: 1.0 / (adx_length as f64),
33 tr_ema: 0.0,
34 pdm_ema: 0.0,
35 mdm_ema: 0.0,
36 adx_ema: 0.0,
37 p_sma: SMA::new(adx_smooth_length),
38 m_sma: SMA::new(adx_smooth_length),
39 prev_h: 0.0,
40 prev_l: 0.0,
41 prev_c: 0.0,
42 count: 0,
43 }
44 }
45}
46
47impl Next<(f64, f64, f64)> for HarringtonADXOscillator {
48 type Output = f64;
49
50 fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
51 self.count += 1;
52
53 if self.count == 1 {
54 self.prev_h = high;
55 self.prev_l = low;
56 self.prev_c = close;
57 self.tr_ema = high - low;
58 return 0.0;
59 }
60
61 let tr = (high - low)
62 .max((high - self.prev_c).abs())
63 .max((low - self.prev_c).abs());
64 let up_move = high - self.prev_h;
65 let down_move = self.prev_l - low;
66
67 let (pdm, mdm) = if up_move > down_move && up_move > 0.0 {
68 (up_move, 0.0)
69 } else if down_move > up_move && down_move > 0.0 {
70 (0.0, down_move)
71 } else {
72 (0.0, 0.0)
73 };
74
75 self.tr_ema = self.sf * tr + (1.0 - self.sf) * self.tr_ema;
77 self.pdm_ema = self.sf * pdm + (1.0 - self.sf) * self.pdm_ema;
78 self.mdm_ema = self.sf * mdm + (1.0 - self.sf) * self.mdm_ema;
79
80 let (pdi, mdi) = if self.tr_ema > 0.0 {
81 (
82 100.0 * self.pdm_ema / self.tr_ema,
83 100.0 * self.mdm_ema / self.tr_ema,
84 )
85 } else {
86 (0.0, 0.0)
87 };
88
89 let dx = if (pdi + mdi) > 0.0 {
90 100.0 * (pdi - mdi).abs() / (pdi + mdi)
91 } else {
92 0.0
93 };
94
95 self.adx_ema = self.sf * dx + (1.0 - self.sf) * self.adx_ema;
96
97 let smoothed_plus = self.p_sma.next(pdi);
98 let smoothed_minus = self.m_sma.next(mdi);
99 let net_dmi = smoothed_plus - smoothed_minus;
100
101 self.prev_h = high;
102 self.prev_l = low;
103 self.prev_c = close;
104
105 if net_dmi >= 0.0 {
106 self.adx_ema
107 } else {
108 -self.adx_ema
109 }
110 }
111}
112
113pub const HARRINGTON_ADX_METADATA: IndicatorMetadata = IndicatorMetadata {
114 name: "Harrington ADX Oscillator",
115 description: "An oscillator variant of the ADX where the sign reflects trend direction determined by DMI+ and DMI-.",
116 usage: "The oscillator is positive when DMI+ > DMI- and negative when DMI- > DMI+. The magnitude represents trend strength (ADX). Thresholds at 15 and 40 are often used to identify trend initiation and overextended states.",
117 keywords: &["adx", "dmi", "oscillator", "wilder", "momentum"],
118 ehlers_summary: "While originally created by Wilder, this revisualization by Harrington transforms the unipolar ADX into a bipolar oscillator. This allows for simultaneous identification of trend strength and direction in a single histogram display, simplifying the interpretation of complex directional movement data.",
119 params: &[
120 ParamDef {
121 name: "adx_length",
122 default: "10",
123 description: "Wilder's ADX period",
124 },
125 ParamDef {
126 name: "adx_smooth_length",
127 default: "1",
128 description: "SMA period for DMI components smoothing",
129 },
130 ],
131 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20DECEMBER%202024.html",
132 formula_latex: r#"
133\[
134TR = \max(H-L, |H-C_{t-1}|, |L-C_{t-1}|)
135\]
136\[
137+DM = (H-H_{t-1} > L_{t-1}-L) \text{ and } (H-H_{t-1} > 0) ? H-H_{t-1} : 0
138\]
139\[
140-DM = (L_{t-1}-L > H-H_{t-1}) \text{ and } (L_{t-1}-L > 0) ? L_{t-1}-L : 0
141\]
142\[
143+DI = 100 \cdot \frac{EMA(+DM, 1/L)}{EMA(TR, 1/L)}
144\]
145\[
146-DI = 100 \cdot \frac{EMA(-DM, 1/L)}{EMA(TR, 1/L)}
147\]
148\[
149DX = 100 \cdot \frac{|+DI - -DI|}{+DI + -DI}
150\]
151\[
152ADX = EMA(DX, 1/L)
153\]
154\[
155Result = (SMA(+DI, S) \ge SMA(-DI, S)) ? ADX : -ADX
156\]
157"#,
158 gold_standard_file: "harrington_adx.json",
159 category: "Wilder",
160};
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::traits::Next;
166 use proptest::prelude::*;
167
168 #[test]
169 fn test_harrington_adx_basic() {
170 let mut hadx = HarringtonADXOscillator::new(10, 1);
171 let inputs = vec![
172 (10.0, 9.0, 9.5),
173 (11.0, 10.0, 10.5),
174 (12.0, 11.0, 11.5),
175 (11.0, 10.0, 10.5),
176 (10.0, 9.0, 9.5),
177 ];
178 for input in inputs {
179 let res = hadx.next(input);
180 assert!(!res.is_nan());
181 }
182 }
183
184 proptest! {
185 #[test]
186 fn test_harrington_adx_parity(
187 highs in prop::collection::vec(100.0..110.0, 50..100),
188 lows in prop::collection::vec(90.0..100.0, 50..100),
189 closes in prop::collection::vec(90.0..110.0, 50..100),
190 ) {
191 let len = highs.len().min(lows.len()).min(closes.len());
192 let mut inputs = Vec::with_capacity(len);
193 for i in 0..len {
194 let h: f64 = highs[i];
195 let l: f64 = lows[i];
196 let c_val: f64 = closes[i];
197 let c: f64 = c_val.max(l).min(h);
198 inputs.push((h, l, c));
199 }
200
201 let adx_len = 10;
202 let smooth_len = 1;
203 let mut hadx = HarringtonADXOscillator::new(adx_len, smooth_len);
204 let streaming_results: Vec<f64> = inputs.iter().map(|&x| hadx.next(x)).collect();
205
206 let mut tr_ema = 0.0;
208 let mut pdm_ema = 0.0;
209 let mut mdm_ema = 0.0;
210 let mut adx_ema = 0.0;
211 let mut p_sma = SMA::new(smooth_len);
212 let mut m_sma = SMA::new(smooth_len);
213 let mut prev_h = 0.0;
214 let mut prev_l = 0.0;
215 let mut prev_c = 0.0;
216 let sf = 1.0 / adx_len as f64;
217 let mut batch_results = Vec::with_capacity(len);
218
219 for i in 0..len {
220 let (h, l, c) = inputs[i];
221 if i == 0 {
222 prev_h = h;
223 prev_l = l;
224 prev_c = c;
225 tr_ema = h - l;
226 batch_results.push(0.0);
227 continue;
228 }
229
230 let tr = (h - l).max((h - prev_c).abs()).max((l - prev_c).abs());
231 let up = h - prev_h;
232 let down = prev_l - l;
233 let (pdm, mdm) = if up > down && up > 0.0 { (up, 0.0) } else if down > up && down > 0.0 { (0.0, down) } else { (0.0, 0.0) };
234
235 tr_ema = sf * tr + (1.0 - sf) * tr_ema;
236 pdm_ema = sf * pdm + (1.0 - sf) * pdm_ema;
237 mdm_ema = sf * mdm + (1.0 - sf) * mdm_ema;
238
239 let (pdi, mdi) = if tr_ema > 0.0 { (100.0 * pdm_ema / tr_ema, 100.0 * mdm_ema / tr_ema) } else { (0.0, 0.0) };
240 let dx = if (pdi + mdi) > 0.0 { 100.0 * (pdi - mdi).abs() / (pdi + mdi) } else { 0.0 };
241 adx_ema = sf * dx + (1.0 - sf) * adx_ema;
242
243 let sp = p_sma.next(pdi);
244 let sm = m_sma.next(mdi);
245 let res = if sp >= sm { adx_ema } else { -adx_ema };
246 batch_results.push(res);
247
248 prev_h = h;
249 prev_l = l;
250 prev_c = c;
251 }
252
253 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
254 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
255 }
256 }
257 }
258}