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