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