wickra_core/indicators/
demand_index.rs1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
46pub struct DemandIndex {
47 period: usize,
48 ema: Ema,
49 prev_close: Option<f64>,
50}
51
52impl DemandIndex {
53 pub fn new(period: usize) -> Result<Self> {
58 if period == 0 {
59 return Err(Error::PeriodZero);
60 }
61 if period > crate::error::MAX_PERIOD {
62 return Err(Error::InvalidPeriod {
63 message: crate::error::PERIOD_ABOVE_MAX,
64 });
65 }
66 Ok(Self {
67 period,
68 ema: Ema::new(period)?,
69 prev_close: None,
70 })
71 }
72
73 pub const fn period(&self) -> usize {
75 self.period
76 }
77}
78
79impl Indicator for DemandIndex {
80 type Input = Candle;
81 type Output = f64;
82
83 #[inline]
84 fn update(&mut self, candle: Candle) -> Option<f64> {
85 let Some(prev) = self.prev_close else {
86 self.prev_close = Some(candle.close);
87 return None;
88 };
89 let pressure = if prev == 0.0 {
90 0.0
92 } else {
93 let ret = (candle.close - prev) / prev;
94 let range_norm = (candle.high - candle.low) / prev;
95 candle.volume * ret * (1.0 + range_norm)
96 };
97 self.prev_close = Some(candle.close);
98 self.ema.update(pressure)
99 }
100
101 fn reset(&mut self) {
102 self.ema.reset();
103 self.prev_close = None;
104 }
105
106 #[inline]
107 fn warmup_period(&self) -> usize {
108 self.period + 1
111 }
112
113 #[inline]
114 fn is_ready(&self) -> bool {
115 self.ema.is_ready()
116 }
117
118 #[inline]
119 fn name(&self) -> &'static str {
120 "DemandIndex"
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::traits::BatchExt;
128 use approx::assert_relative_eq;
129
130 fn c(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
131 Candle::new(open, high, low, close, volume, ts).unwrap()
132 }
133
134 #[test]
135 fn rejects_zero_period() {
136 assert!(matches!(DemandIndex::new(0), Err(Error::PeriodZero)));
137 }
138
139 #[test]
140 fn accessors_and_metadata() {
141 let di = DemandIndex::new(10).unwrap();
142 assert_eq!(di.period(), 10);
143 assert_eq!(di.name(), "DemandIndex");
144 assert_eq!(di.warmup_period(), 11);
145 }
146
147 #[test]
148 fn constant_series_yields_zero() {
149 let candles: Vec<Candle> = (0..40)
151 .map(|i| c(10.0, 10.0, 10.0, 10.0, 100.0, i))
152 .collect();
153 let mut di = DemandIndex::new(5).unwrap();
154 for v in di.batch(&candles).into_iter().flatten() {
155 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
156 }
157 }
158
159 #[test]
160 fn rising_series_yields_positive_signal() {
161 let candles: Vec<Candle> = (0..40)
164 .map(|i| {
165 let f = i as f64;
166 c(100.0 + f, 101.0 + f, 99.0 + f, 100.5 + f, 100.0, i)
167 })
168 .collect();
169 let mut di = DemandIndex::new(5).unwrap();
170 let out = di.batch(&candles);
171 let last = out.iter().filter_map(|x| *x).next_back().unwrap();
172 assert!(
173 last > 0.0,
174 "rising series must yield positive DI, got {last}"
175 );
176 }
177
178 #[test]
179 fn falling_series_yields_negative_signal() {
180 let candles: Vec<Candle> = (0..40)
181 .map(|i| {
182 let f = i as f64;
183 c(200.0 - f, 201.0 - f, 199.0 - f, 199.5 - f, 100.0, i)
184 })
185 .collect();
186 let mut di = DemandIndex::new(5).unwrap();
187 let out = di.batch(&candles);
188 let last = out.iter().filter_map(|x| *x).next_back().unwrap();
189 assert!(
190 last < 0.0,
191 "falling series must yield negative DI, got {last}"
192 );
193 }
194
195 #[test]
196 fn zero_prev_close_contributes_no_signal() {
197 let mut di = DemandIndex::new(3).unwrap();
200 di.update(c(0.0, 0.0, 0.0, 0.0, 100.0, 0));
201 di.update(c(0.0, 1.0, 0.0, 1.0, 100.0, 1));
203 di.update(c(1.0, 2.0, 1.0, 2.0, 100.0, 2));
205 let v = di.update(c(2.0, 3.0, 2.0, 3.0, 100.0, 3));
208 assert!(v.is_some());
209 assert!(v.unwrap().is_finite());
210 }
211
212 #[test]
213 fn batch_equals_streaming() {
214 let candles: Vec<Candle> = (0..100i64)
215 .map(|i| {
216 let f = i as f64;
217 let mid = 100.0 + (f * 0.2).sin() * 5.0;
218 c(
219 mid,
220 mid + 1.5,
221 mid - 1.5,
222 mid + 0.3,
223 80.0 + (i % 5) as f64,
224 i,
225 )
226 })
227 .collect();
228 let mut a = DemandIndex::new(10).unwrap();
229 let mut b = DemandIndex::new(10).unwrap();
230 assert_eq!(
231 a.batch(&candles),
232 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
233 );
234 }
235
236 #[test]
237 fn reset_clears_state() {
238 let candles: Vec<Candle> = (0..40)
239 .map(|i| {
240 let f = i as f64;
241 c(100.0 + f, 101.0 + f, 99.0 + f, 100.5 + f, 100.0, i)
242 })
243 .collect();
244 let mut di = DemandIndex::new(5).unwrap();
245 di.batch(&candles);
246 assert!(di.is_ready());
247 di.reset();
248 assert!(!di.is_ready());
249 assert_eq!(di.update(candles[0]), None);
250 }
251}