wickra_core/indicators/
dx.rs1use crate::error::{Error, Result};
4use crate::indicators::adx::directional_movement;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
36pub struct Dx {
37 period: usize,
38 prev: Option<Candle>,
39 plus_dm_seed: f64,
40 minus_dm_seed: f64,
41 tr_seed: f64,
42 seed_count: usize,
43 plus_dm_smooth: Option<f64>,
44 minus_dm_smooth: Option<f64>,
45 tr_smooth: Option<f64>,
46}
47
48impl Dx {
49 pub fn new(period: usize) -> Result<Self> {
52 if period == 0 {
53 return Err(Error::PeriodZero);
54 }
55 Ok(Self {
56 period,
57 prev: None,
58 plus_dm_seed: 0.0,
59 minus_dm_seed: 0.0,
60 tr_seed: 0.0,
61 seed_count: 0,
62 plus_dm_smooth: None,
63 minus_dm_smooth: None,
64 tr_smooth: None,
65 })
66 }
67
68 pub const fn period(&self) -> usize {
70 self.period
71 }
72}
73
74impl Indicator for Dx {
75 type Input = Candle;
76 type Output = f64;
77
78 fn update(&mut self, candle: Candle) -> Option<f64> {
79 let Some(prev) = self.prev else {
80 self.prev = Some(candle);
81 return None;
82 };
83 self.prev = Some(candle);
84
85 let (plus_dm, minus_dm) = directional_movement(&prev, &candle);
86 let tr = candle.true_range(Some(prev.close));
87 let n = self.period as f64;
88
89 let (plus_v, minus_v, tr_v) = if let (Some(p), Some(m), Some(t)) =
90 (self.plus_dm_smooth, self.minus_dm_smooth, self.tr_smooth)
91 {
92 let p_new = p - p / n + plus_dm;
93 let m_new = m - m / n + minus_dm;
94 let t_new = t - t / n + tr;
95 self.plus_dm_smooth = Some(p_new);
96 self.minus_dm_smooth = Some(m_new);
97 self.tr_smooth = Some(t_new);
98 (p_new, m_new, t_new)
99 } else {
100 self.plus_dm_seed += plus_dm;
101 self.minus_dm_seed += minus_dm;
102 self.tr_seed += tr;
103 self.seed_count += 1;
104 if self.seed_count < self.period {
105 return None;
106 }
107 self.plus_dm_smooth = Some(self.plus_dm_seed);
108 self.minus_dm_smooth = Some(self.minus_dm_seed);
109 self.tr_smooth = Some(self.tr_seed);
110 (self.plus_dm_seed, self.minus_dm_seed, self.tr_seed)
111 };
112
113 let (plus_di, minus_di) = if tr_v == 0.0 {
114 (0.0, 0.0)
115 } else {
116 (100.0 * plus_v / tr_v, 100.0 * minus_v / tr_v)
117 };
118 let di_sum = plus_di + minus_di;
119 let dx = if di_sum == 0.0 {
120 0.0
121 } else {
122 100.0 * (plus_di - minus_di).abs() / di_sum
123 };
124 Some(dx)
125 }
126
127 fn reset(&mut self) {
128 self.prev = None;
129 self.plus_dm_seed = 0.0;
130 self.minus_dm_seed = 0.0;
131 self.tr_seed = 0.0;
132 self.seed_count = 0;
133 self.plus_dm_smooth = None;
134 self.minus_dm_smooth = None;
135 self.tr_smooth = None;
136 }
137
138 fn warmup_period(&self) -> usize {
139 self.period
140 }
141
142 fn is_ready(&self) -> bool {
143 self.tr_smooth.is_some()
144 }
145
146 fn name(&self) -> &'static str {
147 "DX"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::BatchExt;
155 use approx::assert_relative_eq;
156
157 fn c(h: f64, l: f64, cl: f64) -> Candle {
158 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
159 }
160
161 #[test]
162 fn rejects_zero_period() {
163 assert!(matches!(Dx::new(0), Err(Error::PeriodZero)));
164 }
165
166 #[test]
167 fn accessors_report_config() {
168 let dx = Dx::new(7).unwrap();
169 assert_eq!(dx.period(), 7);
170 assert_eq!(dx.name(), "DX");
171 assert_eq!(dx.warmup_period(), 7);
172 assert!(!dx.is_ready());
173 }
174
175 #[test]
176 fn strong_trend_drives_dx_high() {
177 let candles: Vec<Candle> = (0..12)
179 .map(|i| {
180 let base = 100.0 + f64::from(i) * 2.0;
181 c(base + 1.0, base - 0.5, base + 0.5)
182 })
183 .collect();
184 let mut dx = Dx::new(3).unwrap();
185 let out: Vec<Option<f64>> = dx.batch(&candles);
186 assert_eq!(out[0], None);
187 assert!(out[3].is_some());
188 let last = out.into_iter().flatten().last().unwrap();
189 assert!(last > 50.0 && last <= 100.0);
190 assert!(dx.is_ready());
191 }
192
193 #[test]
194 fn flat_market_returns_zero() {
195 let candles: Vec<Candle> = (0..6).map(|_| c(50.0, 50.0, 50.0)).collect();
197 let mut dx = Dx::new(3).unwrap();
198 let last = dx.batch(&candles).into_iter().flatten().last().unwrap();
199 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
200 }
201
202 #[test]
203 fn balanced_directional_movement_is_low() {
204 let candles: Vec<Candle> = (0..30)
207 .map(|i| {
208 let base = if i % 2 == 0 { 100.0 } else { 101.0 };
209 c(base + 1.0, base - 1.0, base)
210 })
211 .collect();
212 let mut dx = Dx::new(5).unwrap();
213 let last = dx.batch(&candles).into_iter().flatten().last().unwrap();
214 assert!((0.0..=100.0).contains(&last));
215 }
216
217 #[test]
218 fn reset_restores_initial_state() {
219 let candles: Vec<Candle> = (0..6)
220 .map(|i| {
221 let base = 100.0 + f64::from(i) * 2.0;
222 c(base + 1.0, base - 0.5, base + 0.5)
223 })
224 .collect();
225 let mut dx = Dx::new(3).unwrap();
226 let _ = dx.batch(&candles);
227 assert!(dx.is_ready());
228 dx.reset();
229 assert!(!dx.is_ready());
230 assert_eq!(dx.update(candles[0]), None);
231 }
232}