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