Skip to main content

quantwave_core/indicators/incremental/
dm.rs

1//! Native O(1) +DM / -DM — TA-Lib Wilder smoothing parity.
2
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
6struct PlusDmCore {
7    timeperiod: usize,
8    period_f: f64,
9    prev_high: Option<f64>,
10    prev_low: Option<f64>,
11    bar_index: usize,
12    sum: f64,
13    seeded: bool,
14}
15
16impl PlusDmCore {
17    fn new(timeperiod: usize) -> Self {
18        Self {
19            timeperiod,
20            period_f: timeperiod as f64,
21            prev_high: None,
22            prev_low: None,
23            bar_index: 0,
24            sum: 0.0,
25            seeded: false,
26        }
27    }
28
29    fn step(&mut self, high: f64, low: f64) -> Option<f64> {
30        let period = self.timeperiod;
31        if period < 1 {
32            return None;
33        }
34        if self.prev_high.is_none() {
35            self.prev_high = Some(high);
36            self.prev_low = Some(low);
37            self.bar_index = 1;
38            return None;
39        }
40        let (ph, pl) = match (self.prev_high, self.prev_low) {
41            (Some(ph), Some(pl)) => (ph, pl),
42            _ => {
43                self.prev_high = Some(high);
44                self.prev_low = Some(low);
45                self.bar_index = 1;
46                return None;
47            }
48        };
49        self.prev_high = Some(high);
50        self.prev_low = Some(low);
51        let i = self.bar_index;
52        self.bar_index += 1;
53
54        let up = high - ph;
55        let down = pl - low;
56        let pdm = if up > down && up > 0.0 { up } else { 0.0 };
57
58        if !self.seeded {
59            if i < period - 1 {
60                self.sum += pdm;
61                return None;
62            }
63            if i == period - 1 {
64                self.sum += pdm;
65                return Some(self.sum);
66            }
67            self.seeded = true;
68        }
69        self.sum = self.sum - self.sum / self.period_f + pdm;
70        Some(self.sum)
71    }
72}
73
74#[derive(Debug, Clone)]
75struct MinusDmCore {
76    timeperiod: usize,
77    period_f: f64,
78    prev_high: Option<f64>,
79    prev_low: Option<f64>,
80    bar_index: usize,
81    sum: f64,
82    seeded: bool,
83}
84
85impl MinusDmCore {
86    fn new(timeperiod: usize) -> Self {
87        Self {
88            timeperiod,
89            period_f: timeperiod as f64,
90            prev_high: None,
91            prev_low: None,
92            bar_index: 0,
93            sum: 0.0,
94            seeded: false,
95        }
96    }
97
98    fn step(&mut self, high: f64, low: f64) -> Option<f64> {
99        let period = self.timeperiod;
100        if period < 1 {
101            return None;
102        }
103        if self.prev_high.is_none() {
104            self.prev_high = Some(high);
105            self.prev_low = Some(low);
106            self.bar_index = 1;
107            return None;
108        }
109        let (ph, pl) = match (self.prev_high, self.prev_low) {
110            (Some(ph), Some(pl)) => (ph, pl),
111            _ => {
112                self.prev_high = Some(high);
113                self.prev_low = Some(low);
114                self.bar_index = 1;
115                return None;
116            }
117        };
118        self.prev_high = Some(high);
119        self.prev_low = Some(low);
120        let i = self.bar_index;
121        self.bar_index += 1;
122
123        let up = high - ph;
124        let down = pl - low;
125        let mdm = if down > up && down > 0.0 { down } else { 0.0 };
126
127        if !self.seeded {
128            if i < period - 1 {
129                self.sum += mdm;
130                return None;
131            }
132            if i == period - 1 {
133                self.sum += mdm;
134                return Some(self.sum);
135            }
136            self.seeded = true;
137        }
138        self.sum = self.sum - self.sum / self.period_f + mdm;
139        Some(self.sum)
140    }
141}
142
143/// Plus Directional Movement (+DM).
144#[derive(Debug, Clone)]
145#[allow(non_camel_case_types)]
146pub struct PLUS_DM {
147    pub timeperiod: usize,
148    core: PlusDmCore,
149}
150
151impl PLUS_DM {
152    pub fn new(timeperiod: usize) -> Self {
153        Self {
154            timeperiod,
155            core: PlusDmCore::new(timeperiod),
156        }
157    }
158}
159
160impl Next<(f64, f64)> for PLUS_DM {
161    type Output = f64;
162
163    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
164        self.core.step(high, low).unwrap_or(f64::NAN)
165    }
166}
167
168/// Minus Directional Movement (-DM).
169#[derive(Debug, Clone)]
170#[allow(non_camel_case_types)]
171pub struct MINUS_DM {
172    pub timeperiod: usize,
173    core: MinusDmCore,
174}
175
176impl MINUS_DM {
177    pub fn new(timeperiod: usize) -> Self {
178        Self {
179            timeperiod,
180            core: MinusDmCore::new(timeperiod),
181        }
182    }
183}
184
185impl Next<(f64, f64)> for MINUS_DM {
186    type Output = f64;
187
188    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
189        self.core.step(high, low).unwrap_or(f64::NAN)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use proptest::prelude::*;
197
198    proptest! {
199        #[test]
200        fn test_plus_dm_parity(
201            highs in prop::collection::vec(1.0..100.0, 1..100),
202            lows in prop::collection::vec(1.0..100.0, 1..100)
203        ) {
204            let len = highs.len().min(lows.len());
205            if len < 20 { return Ok(()); }
206            let mut high = Vec::with_capacity(len);
207            let mut low = Vec::with_capacity(len);
208            for i in 0..len {
209                let hi: f64 = highs[i];
210                let lo: f64 = lows[i];
211                high.push(hi.max(lo));
212                low.push(hi.min(lo));
213            }
214            let period = 14;
215            let mut pdm = PLUS_DM::new(period);
216            let streaming: Vec<f64> = (0..len).map(|i| pdm.next((high[i], low[i]))).collect();
217            let batch = talib_rs::momentum::plus_dm(&high, &low, period)
218                .unwrap_or_else(|_| vec![f64::NAN; len]);
219            for (s, b) in streaming.iter().zip(batch.iter()) {
220                if s.is_nan() { assert!(b.is_nan()); }
221                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
222            }
223        }
224    }
225}