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 Ok(Self {
53 period,
54 prev: None,
55 seed: 0.0,
56 seed_count: 0,
57 smooth: None,
58 })
59 }
60
61 pub const fn period(&self) -> usize {
63 self.period
64 }
65}
66
67impl Indicator for MinusDm {
68 type Input = Candle;
69 type Output = f64;
70
71 fn update(&mut self, candle: Candle) -> Option<f64> {
72 let Some(prev) = self.prev else {
73 self.prev = Some(candle);
74 return None;
75 };
76 self.prev = Some(candle);
77
78 let (_, minus_dm) = directional_movement(&prev, &candle);
79 let n = self.period as f64;
80
81 if let Some(s) = self.smooth {
82 let s_new = s - s / n + minus_dm;
83 self.smooth = Some(s_new);
84 return Some(s_new);
85 }
86
87 self.seed += minus_dm;
88 self.seed_count += 1;
89 if self.seed_count < self.period {
90 return None;
91 }
92 self.smooth = Some(self.seed);
93 Some(self.seed)
94 }
95
96 fn reset(&mut self) {
97 self.prev = None;
98 self.seed = 0.0;
99 self.seed_count = 0;
100 self.smooth = None;
101 }
102
103 fn warmup_period(&self) -> usize {
104 self.period
105 }
106
107 fn is_ready(&self) -> bool {
108 self.smooth.is_some()
109 }
110
111 fn name(&self) -> &'static str {
112 "MINUS_DM"
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::traits::BatchExt;
120 use approx::assert_relative_eq;
121
122 fn c(h: f64, l: f64, cl: f64) -> Candle {
124 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
125 }
126
127 #[test]
128 fn rejects_zero_period() {
129 assert!(matches!(MinusDm::new(0), Err(Error::PeriodZero)));
130 }
131
132 #[test]
133 fn accessors_report_config() {
134 let dm = MinusDm::new(7).unwrap();
135 assert_eq!(dm.period(), 7);
136 assert_eq!(dm.name(), "MINUS_DM");
137 assert_eq!(dm.warmup_period(), 7);
138 assert!(!dm.is_ready());
139 }
140
141 #[test]
142 fn seeds_then_smooths_a_constant_minus_dm() {
143 let candles: Vec<Candle> = (0..5)
146 .map(|i| {
147 c(
148 20.0 - 0.5 * f64::from(i),
149 18.0 - f64::from(i),
150 19.0 - f64::from(i),
151 )
152 })
153 .collect();
154 let mut dm = MinusDm::new(3).unwrap();
155 let out: Vec<Option<f64>> = dm.batch(&candles);
156 assert_eq!(out[0], None);
157 assert_eq!(out[1], None);
158 assert_eq!(out[2], None);
159 assert_relative_eq!(out[3].unwrap(), 3.0, epsilon = 1e-12);
161 assert_relative_eq!(out[4].unwrap(), 3.0, epsilon = 1e-12);
163 assert!(dm.is_ready());
164 }
165
166 #[test]
167 fn up_moves_contribute_zero() {
168 let candles: Vec<Candle> = (0..6)
170 .map(|i| c(20.0 + f64::from(i), 5.0 + f64::from(i), 12.0 + f64::from(i)))
171 .collect();
172 let mut dm = MinusDm::new(3).unwrap();
173 let last = dm.batch(&candles).into_iter().flatten().last().unwrap();
174 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
175 }
176
177 #[test]
178 fn reset_restores_initial_state() {
179 let candles: Vec<Candle> = (0..5)
180 .map(|i| {
181 c(
182 20.0 - 0.5 * f64::from(i),
183 18.0 - f64::from(i),
184 19.0 - f64::from(i),
185 )
186 })
187 .collect();
188 let mut dm = MinusDm::new(3).unwrap();
189 let _ = dm.batch(&candles);
190 assert!(dm.is_ready());
191 dm.reset();
192 assert!(!dm.is_ready());
193 assert_eq!(dm.update(candles[0]), None);
194 }
195}