Skip to main content

quantwave_core/indicators/
harrington_adx.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4
5/// Harrington ADX Oscillator
6///
7/// Based on Neil Jon Harrington's article "Revisualizing The ADX Oscillator" (TASC December 2024).
8/// This indicator revisualizes the standard ADX by giving it a sign based on the DMI direction.
9/// It uses a histogram-like display where the sign depends on (Smoothed DMI+ - Smoothed DMI-).
10#[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        // Wilder's Smoothing (EMA with alpha = 1/length)
76        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            // Reference implementation
207            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}