wickra_core/indicators/
plus_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 PlusDi {
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 PlusDi {
48 pub fn new(period: usize) -> Result<Self> {
51 if period == 0 {
52 return Err(Error::PeriodZero);
53 }
54 Ok(Self {
55 period,
56 prev: None,
57 dm_seed: 0.0,
58 tr_seed: 0.0,
59 seed_count: 0,
60 dm_smooth: None,
61 tr_smooth: None,
62 })
63 }
64
65 pub const fn period(&self) -> usize {
67 self.period
68 }
69}
70
71impl Indicator for PlusDi {
72 type Input = Candle;
73 type Output = f64;
74
75 fn update(&mut self, candle: Candle) -> Option<f64> {
76 let Some(prev) = self.prev else {
77 self.prev = Some(candle);
78 return None;
79 };
80 self.prev = Some(candle);
81
82 let (plus_dm, _) = directional_movement(&prev, &candle);
83 let tr = candle.true_range(Some(prev.close));
84 let n = self.period as f64;
85
86 let (dm_v, tr_v) = if let (Some(d), Some(t)) = (self.dm_smooth, self.tr_smooth) {
87 let d_new = d - d / n + plus_dm;
88 let t_new = t - t / n + tr;
89 self.dm_smooth = Some(d_new);
90 self.tr_smooth = Some(t_new);
91 (d_new, t_new)
92 } else {
93 self.dm_seed += plus_dm;
94 self.tr_seed += tr;
95 self.seed_count += 1;
96 if self.seed_count < self.period {
97 return None;
98 }
99 self.dm_smooth = Some(self.dm_seed);
100 self.tr_smooth = Some(self.tr_seed);
101 (self.dm_seed, self.tr_seed)
102 };
103
104 let di = if tr_v == 0.0 {
105 0.0
106 } else {
107 100.0 * dm_v / tr_v
108 };
109 Some(di)
110 }
111
112 fn reset(&mut self) {
113 self.prev = None;
114 self.dm_seed = 0.0;
115 self.tr_seed = 0.0;
116 self.seed_count = 0;
117 self.dm_smooth = None;
118 self.tr_smooth = None;
119 }
120
121 fn warmup_period(&self) -> usize {
122 self.period
123 }
124
125 fn is_ready(&self) -> bool {
126 self.dm_smooth.is_some()
127 }
128
129 fn name(&self) -> &'static str {
130 "PLUS_DI"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 fn c(h: f64, l: f64, cl: f64) -> Candle {
141 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
142 }
143
144 #[test]
145 fn rejects_zero_period() {
146 assert!(matches!(PlusDi::new(0), Err(Error::PeriodZero)));
147 }
148
149 #[test]
150 fn accessors_report_config() {
151 let di = PlusDi::new(7).unwrap();
152 assert_eq!(di.period(), 7);
153 assert_eq!(di.name(), "PLUS_DI");
154 assert_eq!(di.warmup_period(), 7);
155 assert!(!di.is_ready());
156 }
157
158 #[test]
159 fn uptrend_drives_plus_di_high() {
160 let candles: Vec<Candle> = (0..12)
162 .map(|i| {
163 let base = 100.0 + f64::from(i) * 2.0;
164 c(base + 1.0, base - 0.5, base + 0.5)
165 })
166 .collect();
167 let mut di = PlusDi::new(3).unwrap();
168 let out: Vec<Option<f64>> = di.batch(&candles);
169 assert_eq!(out[0], None);
170 assert!(out[3].is_some());
172 let last = out.into_iter().flatten().last().unwrap();
173 assert!(last > 0.0 && last <= 100.0);
174 assert!(di.is_ready());
175 }
176
177 #[test]
178 fn flat_market_returns_zero() {
179 let candles: Vec<Candle> = (0..6).map(|_| c(50.0, 50.0, 50.0)).collect();
181 let mut di = PlusDi::new(3).unwrap();
182 let last = di.batch(&candles).into_iter().flatten().last().unwrap();
183 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
184 }
185
186 #[test]
187 fn reset_restores_initial_state() {
188 let candles: Vec<Candle> = (0..6)
189 .map(|i| {
190 let base = 100.0 + f64::from(i) * 2.0;
191 c(base + 1.0, base - 0.5, base + 0.5)
192 })
193 .collect();
194 let mut di = PlusDi::new(3).unwrap();
195 let _ = di.batch(&candles);
196 assert!(di.is_ready());
197 di.reset();
198 assert!(!di.is_ready());
199 assert_eq!(di.update(candles[0]), None);
200 }
201}