Skip to main content

quantwave_core/indicators/incremental/
dmi.rs

1//! Native O(1) DMI family — TA-Lib Wilder smoothing parity (ADX, ADXR, DX, +DI, -DI).
2
3use crate::traits::Next;
4
5/// Shared Wilder-smoothed TR / +DM / -DM state.
6#[derive(Debug, Clone)]
7struct DmiCore {
8    timeperiod: usize,
9    period_f: f64,
10    prev_high: Option<f64>,
11    prev_low: Option<f64>,
12    prev_close: Option<f64>,
13    bar_index: usize,
14    sum_tr: f64,
15    sum_pdm: f64,
16    sum_mdm: f64,
17    seeded: bool,
18}
19
20impl DmiCore {
21    fn new(timeperiod: usize) -> Self {
22        Self {
23            timeperiod,
24            period_f: timeperiod as f64,
25            prev_high: None,
26            prev_low: None,
27            prev_close: None,
28            bar_index: 0,
29            sum_tr: 0.0,
30            sum_pdm: 0.0,
31            sum_mdm: 0.0,
32            seeded: false,
33        }
34    }
35
36    #[inline]
37    fn dm_components(&self, high: f64, low: f64) -> (f64, f64, f64) {
38        let (ph, pl, pc) = match (self.prev_high, self.prev_low, self.prev_close) {
39            (Some(ph), Some(pl), Some(pc)) => (ph, pl, pc),
40            _ => return (0.0, 0.0, 0.0),
41        };
42        let hl = high - low;
43        let hc = (high - pc).abs();
44        let lc = (low - pc).abs();
45        let tr = hl.max(hc).max(lc);
46        let up = high - ph;
47        let down = pl - low;
48        let pdm = if up > down && up > 0.0 { up } else { 0.0 };
49        let mdm = if down > up && down > 0.0 { down } else { 0.0 };
50        (tr, pdm, mdm)
51    }
52
53    /// Advance one bar; returns `Some((pdi, mdi, dx))` once DI values are defined.
54    fn step(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64)> {
55        let period = self.timeperiod;
56        if period < 1 {
57            return None;
58        }
59
60        if self.prev_high.is_none() {
61            self.prev_high = Some(high);
62            self.prev_low = Some(low);
63            self.prev_close = Some(close);
64            self.bar_index = 1;
65            return None;
66        }
67
68        let (tr, pdm, mdm) = self.dm_components(high, low);
69        self.prev_high = Some(high);
70        self.prev_low = Some(low);
71        self.prev_close = Some(close);
72        let i = self.bar_index;
73        self.bar_index += 1;
74
75        if !self.seeded {
76            if i < period {
77                self.sum_tr += tr;
78                self.sum_pdm += pdm;
79                self.sum_mdm += mdm;
80                return None;
81            }
82            self.seeded = true;
83        }
84        self.sum_tr = self.sum_tr - self.sum_tr / self.period_f + tr;
85        self.sum_pdm = self.sum_pdm - self.sum_pdm / self.period_f + pdm;
86        self.sum_mdm = self.sum_mdm - self.sum_mdm / self.period_f + mdm;
87
88        if self.sum_tr <= 0.0 {
89            return None;
90        }
91        let pdi = 100.0 * self.sum_pdm / self.sum_tr;
92        let mdi = 100.0 * self.sum_mdm / self.sum_tr;
93        let sum_di = pdi + mdi;
94        let dx = if sum_di > 0.0 {
95            100.0 * (pdi - mdi).abs() / sum_di
96        } else {
97            0.0
98        };
99        Some((pdi, mdi, dx))
100    }
101}
102
103/// Plus Directional Indicator (+DI).
104#[derive(Debug, Clone)]
105#[allow(non_camel_case_types)]
106pub struct PLUS_DI {
107    pub timeperiod: usize,
108    core: DmiCore,
109}
110
111impl PLUS_DI {
112    pub fn new(timeperiod: usize) -> Self {
113        Self {
114            timeperiod,
115            core: DmiCore::new(timeperiod),
116        }
117    }
118}
119
120impl Next<(f64, f64, f64)> for PLUS_DI {
121    type Output = f64;
122
123    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
124        match self.core.step(high, low, close) {
125            Some((pdi, _, _)) => pdi,
126            None => f64::NAN,
127        }
128    }
129}
130
131/// Minus Directional Indicator (-DI).
132#[derive(Debug, Clone)]
133#[allow(non_camel_case_types)]
134pub struct MINUS_DI {
135    pub timeperiod: usize,
136    core: DmiCore,
137}
138
139impl MINUS_DI {
140    pub fn new(timeperiod: usize) -> Self {
141        Self {
142            timeperiod,
143            core: DmiCore::new(timeperiod),
144        }
145    }
146}
147
148impl Next<(f64, f64, f64)> for MINUS_DI {
149    type Output = f64;
150
151    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
152        match self.core.step(high, low, close) {
153            Some((_, mdi, _)) => mdi,
154            None => f64::NAN,
155        }
156    }
157}
158
159/// Directional Movement Index (DX).
160#[derive(Debug, Clone)]
161#[allow(non_camel_case_types)]
162pub struct DX {
163    pub timeperiod: usize,
164    core: DmiCore,
165}
166
167impl DX {
168    pub fn new(timeperiod: usize) -> Self {
169        Self {
170            timeperiod,
171            core: DmiCore::new(timeperiod),
172        }
173    }
174}
175
176impl Next<(f64, f64, f64)> for DX {
177    type Output = f64;
178
179    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
180        match self.core.step(high, low, close) {
181            Some((_, _, dx)) => dx,
182            None => f64::NAN,
183        }
184    }
185}
186
187/// Average Directional Index (ADX).
188#[derive(Debug, Clone)]
189#[allow(non_camel_case_types)]
190pub struct ADX {
191    pub timeperiod: usize,
192    core: DmiCore,
193    dx_values: Vec<f64>,
194    adx: f64,
195    adx_ready: bool,
196}
197
198impl ADX {
199    pub fn new(timeperiod: usize) -> Self {
200        Self {
201            timeperiod,
202            core: DmiCore::new(timeperiod),
203            dx_values: Vec::new(),
204            adx: 0.0,
205            adx_ready: false,
206        }
207    }
208}
209
210impl Next<(f64, f64, f64)> for ADX {
211    type Output = f64;
212
213    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
214        let period = self.timeperiod;
215        if period < 2 {
216            return f64::NAN;
217        }
218
219        let Some((_, _, dx)) = self.core.step(high, low, close) else {
220            return f64::NAN;
221        };
222
223        let adx_start = 2 * period - 1;
224        let bar = self.core.bar_index.saturating_sub(1);
225
226        if bar < period {
227            return f64::NAN;
228        }
229
230        if bar < adx_start {
231            self.dx_values.push(dx);
232            return f64::NAN;
233        }
234
235        if bar == adx_start {
236            self.dx_values.push(dx);
237            let seed: f64 = self.dx_values.iter().sum::<f64>() / period as f64;
238            self.adx = seed;
239            self.adx_ready = true;
240            return seed;
241        }
242
243        if self.adx_ready {
244            self.adx = (self.adx * (period as f64 - 1.0) + dx) / period as f64;
245            return self.adx;
246        }
247
248        f64::NAN
249    }
250}
251
252/// Average Directional Movement Index Rating (ADXR).
253#[derive(Debug, Clone)]
254#[allow(non_camel_case_types)]
255pub struct ADXR {
256    pub timeperiod: usize,
257    adx: ADX,
258    adx_history: Vec<f64>,
259}
260
261impl ADXR {
262    pub fn new(timeperiod: usize) -> Self {
263        Self {
264            timeperiod,
265            adx: ADX::new(timeperiod),
266            adx_history: Vec::new(),
267        }
268    }
269}
270
271impl Next<(f64, f64, f64)> for ADXR {
272    type Output = f64;
273
274    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
275        let period = self.timeperiod;
276        let adx_val = self.adx.next((high, low, close));
277        self.adx_history.push(adx_val);
278
279        let adxr_lookback = 3 * period - 2;
280        let bar = self.adx_history.len().saturating_sub(1);
281        if bar < adxr_lookback {
282            return f64::NAN;
283        }
284        if adx_val.is_nan() {
285            return f64::NAN;
286        }
287        let past_idx = bar + 1 - period;
288        let past = self.adx_history[past_idx];
289        if past.is_nan() {
290            return f64::NAN;
291        }
292        (adx_val + past) / 2.0
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use proptest::prelude::*;
300
301    fn hlc(len: usize, h: &[f64], l: &[f64], c: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
302        let mut high = Vec::with_capacity(len);
303        let mut low = Vec::with_capacity(len);
304        let mut close = Vec::with_capacity(len);
305        for i in 0..len {
306            let val_h = h[i];
307            let val_l = l[i];
308            let val_c = c[i];
309            high.push(val_h.max(val_l).max(val_c));
310            low.push(val_h.min(val_l).min(val_c));
311            close.push(val_c);
312        }
313        (high, low, close)
314    }
315
316    proptest! {
317        #[test]
318        fn test_adx_parity(
319            h in prop::collection::vec(1.0..100.0, 1..100),
320            l in prop::collection::vec(1.0..100.0, 1..100),
321            c in prop::collection::vec(1.0..100.0, 1..100)
322        ) {
323            let len = h.len().min(l.len()).min(c.len());
324            if len < 30 { return Ok(()); }
325            let (high, low, close) = hlc(len, &h, &l, &c);
326            let period = 14;
327            let mut adx = ADX::new(period);
328            let streaming: Vec<f64> = (0..len)
329                .map(|i| adx.next((high[i], low[i], close[i])))
330                .collect();
331            let batch = talib_rs::momentum::adx(&high, &low, &close, period)
332                .unwrap_or_else(|_| vec![f64::NAN; len]);
333            for (s, b) in streaming.iter().zip(batch.iter()) {
334                if s.is_nan() { assert!(b.is_nan()); }
335                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
336            }
337        }
338
339        #[test]
340        fn test_dx_parity(
341            h in prop::collection::vec(1.0..100.0, 1..100),
342            l in prop::collection::vec(1.0..100.0, 1..100),
343            c in prop::collection::vec(1.0..100.0, 1..100)
344        ) {
345            let len = h.len().min(l.len()).min(c.len());
346            if len < 20 { return Ok(()); }
347            let (high, low, close) = hlc(len, &h, &l, &c);
348            let period = 14;
349            let mut dx = DX::new(period);
350            let streaming: Vec<f64> = (0..len)
351                .map(|i| dx.next((high[i], low[i], close[i])))
352                .collect();
353            let batch = talib_rs::momentum::dx(&high, &low, &close, period)
354                .unwrap_or_else(|_| vec![f64::NAN; len]);
355            for (s, b) in streaming.iter().zip(batch.iter()) {
356                if s.is_nan() { assert!(b.is_nan()); }
357                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
358            }
359        }
360
361        #[test]
362        fn test_plus_di_parity(
363            h in prop::collection::vec(1.0..100.0, 1..100),
364            l in prop::collection::vec(1.0..100.0, 1..100),
365            c in prop::collection::vec(1.0..100.0, 1..100)
366        ) {
367            let len = h.len().min(l.len()).min(c.len());
368            if len < 20 { return Ok(()); }
369            let (high, low, close) = hlc(len, &h, &l, &c);
370            let period = 14;
371            let mut pdi = PLUS_DI::new(period);
372            let streaming: Vec<f64> = (0..len)
373                .map(|i| pdi.next((high[i], low[i], close[i])))
374                .collect();
375            let batch = talib_rs::momentum::plus_di(&high, &low, &close, period)
376                .unwrap_or_else(|_| vec![f64::NAN; len]);
377            for (s, b) in streaming.iter().zip(batch.iter()) {
378                if s.is_nan() { assert!(b.is_nan()); }
379                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
380            }
381        }
382    }
383}