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