1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::ema::Ema;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10const MAX_EXPONENT: f64 = 88.0;
13
14#[derive(Debug, Clone)]
59pub struct DemandIndex {
60 period: usize,
61 buy: Ema,
62 sell: Ema,
63 prev: Option<Candle>,
64 volumes: VecDeque<f64>,
65 volume_sum: f64,
66 ranges: VecDeque<f64>,
67 range_sum: f64,
68 last: f64,
69 ready: bool,
70}
71
72impl DemandIndex {
73 pub fn new(period: usize) -> Result<Self> {
78 if period == 0 {
79 return Err(Error::PeriodZero);
80 }
81 if period > crate::error::MAX_PERIOD {
82 return Err(Error::InvalidPeriod {
83 message: crate::error::PERIOD_ABOVE_MAX,
84 });
85 }
86 let alpha = 2.0 / (period as f64 + 1.0);
87 Ok(Self {
88 period,
89 buy: Ema::with_alpha(alpha)?,
90 sell: Ema::with_alpha(alpha)?,
91 prev: None,
92 volumes: VecDeque::with_capacity(period),
93 volume_sum: 0.0,
94 ranges: VecDeque::with_capacity(period),
95 range_sum: 0.0,
96 last: 0.0,
97 ready: false,
98 })
99 }
100
101 pub const fn period(&self) -> usize {
103 self.period
104 }
105
106 fn push(window: &mut VecDeque<f64>, sum: &mut f64, len: usize, x: f64) {
108 if window.len() == len {
109 *sum -= window.pop_front().expect("window is non-empty");
110 }
111 window.push_back(x);
112 *sum += x;
113 }
114}
115
116fn weighted_close(c: &Candle) -> f64 {
118 (c.high + c.low + 2.0 * c.close) * 0.25
119}
120
121impl Indicator for DemandIndex {
122 type Input = Candle;
123 type Output = f64;
124
125 #[inline]
126 fn update(&mut self, candle: Candle) -> Option<f64> {
127 let period = self.period;
128 Self::push(
129 &mut self.volumes,
130 &mut self.volume_sum,
131 period,
132 candle.volume,
133 );
134 let prev = self.prev.replace(candle)?;
135 let two_bar_range = candle.high.max(prev.high) - candle.low.min(prev.low);
136 Self::push(&mut self.ranges, &mut self.range_sum, period, two_bar_range);
137 if self.ranges.len() < period {
138 return None;
139 }
140 let n = period as f64;
141 let avg_range = self.range_sum / n;
142 let avg_volume = self.volume_sum / n;
143 let wc = weighted_close(&candle);
144 let wc_prev = weighted_close(&prev);
145 if wc != 0.0 && wc_prev != 0.0 && avg_range != 0.0 && avg_volume != 0.0 {
146 let ratio = (wc - wc_prev) / wc.min(wc_prev);
147 let vol = candle.volume / avg_volume;
148 let exponent = ((3.0 * wc / avg_range) * ratio.abs()).min(MAX_EXPONENT);
149 let damped = vol / exponent.exp();
150 let (bp, sp) = if ratio > 0.0 {
151 (vol, damped)
152 } else {
153 (damped, vol)
154 };
155 let b = self.buy.update(bp).unwrap_or(bp);
156 let s = self.sell.update(sp).unwrap_or(sp);
157 self.last = if b > s {
158 100.0 * (1.0 - s / b)
159 } else if b < s {
160 -100.0 * (1.0 - b / s)
161 } else {
162 0.0
163 };
164 }
165 self.ready = true;
166 Some(self.last)
167 }
168
169 fn reset(&mut self) {
170 self.buy.reset();
171 self.sell.reset();
172 self.prev = None;
173 self.volumes.clear();
174 self.volume_sum = 0.0;
175 self.ranges.clear();
176 self.range_sum = 0.0;
177 self.last = 0.0;
178 self.ready = false;
179 }
180
181 #[inline]
182 fn warmup_period(&self) -> usize {
183 self.period + 1
185 }
186
187 #[inline]
188 fn is_ready(&self) -> bool {
189 self.ready
190 }
191
192 #[inline]
193 fn name(&self) -> &'static str {
194 "DemandIndex"
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::traits::BatchExt;
202 use approx::assert_relative_eq;
203
204 fn c(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
205 Candle::new(open, high, low, close, volume, ts).unwrap()
206 }
207
208 #[test]
209 fn rejects_zero_period() {
210 assert!(matches!(DemandIndex::new(0), Err(Error::PeriodZero)));
211 }
212
213 #[test]
214 fn accessors_and_metadata() {
215 let di = DemandIndex::new(10).unwrap();
216 assert_eq!(di.period(), 10);
217 assert_eq!(di.name(), "DemandIndex");
218 assert_eq!(di.warmup_period(), 11);
219 }
220
221 #[test]
222 fn constant_series_yields_zero() {
223 let candles: Vec<Candle> = (0..40)
225 .map(|i| c(10.0, 10.0, 10.0, 10.0, 100.0, i))
226 .collect();
227 let mut di = DemandIndex::new(5).unwrap();
228 for v in di.batch(&candles).into_iter().flatten() {
229 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
230 }
231 }
232
233 #[test]
234 fn rising_series_yields_positive_signal() {
235 let candles: Vec<Candle> = (0..40)
238 .map(|i| {
239 let f = i as f64;
240 c(100.0 + f, 101.0 + f, 99.0 + f, 100.5 + f, 100.0, i)
241 })
242 .collect();
243 let mut di = DemandIndex::new(5).unwrap();
244 let out = di.batch(&candles);
245 let last = out.iter().filter_map(|x| *x).next_back().unwrap();
246 assert!(last > 0.0, "rising series must yield positive DI");
247 }
248
249 #[test]
250 fn falling_series_yields_negative_signal() {
251 let candles: Vec<Candle> = (0..40)
252 .map(|i| {
253 let f = i as f64;
254 c(200.0 - f, 201.0 - f, 199.0 - f, 199.5 - f, 100.0, i)
255 })
256 .collect();
257 let mut di = DemandIndex::new(5).unwrap();
258 let out = di.batch(&candles);
259 let last = out.iter().filter_map(|x| *x).next_back().unwrap();
260 assert!(last < 0.0, "falling series must yield negative DI");
261 }
262
263 #[test]
264 fn zero_weighted_close_contributes_no_signal() {
265 let mut di = DemandIndex::new(3).unwrap();
268 di.update(c(0.0, 0.0, 0.0, 0.0, 100.0, 0));
269 di.update(c(0.0, 1.0, 0.0, 1.0, 100.0, 1));
271 di.update(c(1.0, 2.0, 1.0, 2.0, 100.0, 2));
273 let v = di.update(c(2.0, 3.0, 2.0, 3.0, 100.0, 3));
275 assert!(v.is_some());
276 assert!(v.unwrap().is_finite());
277 }
278
279 #[test]
280 fn batch_equals_streaming() {
281 let candles: Vec<Candle> = (0..100i64)
282 .map(|i| {
283 let f = i as f64;
284 let mid = 100.0 + (f * 0.2).sin() * 5.0;
285 c(
286 mid,
287 mid + 1.5,
288 mid - 1.5,
289 mid + 0.3,
290 80.0 + (i % 5) as f64,
291 i,
292 )
293 })
294 .collect();
295 let mut a = DemandIndex::new(10).unwrap();
296 let mut b = DemandIndex::new(10).unwrap();
297 assert_eq!(
298 a.batch(&candles),
299 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
300 );
301 }
302
303 #[test]
304 fn reset_clears_state() {
305 let candles: Vec<Candle> = (0..40)
306 .map(|i| {
307 let f = i as f64;
308 c(100.0 + f, 101.0 + f, 99.0 + f, 100.5 + f, 100.0, i)
309 })
310 .collect();
311 let mut di = DemandIndex::new(5).unwrap();
312 di.batch(&candles);
313 assert!(di.is_ready());
314 di.reset();
315 assert!(!di.is_ready());
316 assert_eq!(di.update(candles[0]), None);
317 }
318
319 fn wavy_series() -> Vec<Candle> {
320 (0..60i64)
321 .map(|i| {
322 let f = i as f64;
323 let mid = 100.0 + (f * 0.37).sin() * 4.0 + f * 0.05;
324 c(
325 mid,
326 mid + 1.0 + (f * 0.11).cos().abs(),
327 mid - 1.2,
328 mid + (f * 0.5).sin() * 0.8,
329 50.0 + (i % 7) as f64 * 10.0,
330 i,
331 )
332 })
333 .collect()
334 }
335
336 #[test]
337 fn rejects_period_above_maximum() {
338 assert!(matches!(
339 DemandIndex::new(crate::error::MAX_PERIOD + 1),
340 Err(Error::InvalidPeriod { .. })
341 ));
342 }
343
344 #[test]
345 fn first_value_lands_exactly_at_warmup_minus_one() {
346 let candles = wavy_series();
347 for period in [1usize, 3, 7] {
348 let mut di = DemandIndex::new(period).unwrap();
349 let out = di.batch(&candles);
350 let warm = di.warmup_period();
351 assert_eq!(warm, period + 1);
352 assert!(out[..warm - 1].iter().all(Option::is_none));
353 assert!(out[warm - 1].is_some());
354 }
355 }
356
357 #[test]
358 fn not_ready_before_warmup_completes() {
359 let candles = wavy_series();
360 let mut di = DemandIndex::new(4).unwrap();
361 for candle in &candles[..4] {
362 assert_eq!(di.update(*candle), None);
363 assert!(!di.is_ready());
364 }
365 assert!(di.update(candles[4]).is_some());
366 assert!(di.is_ready());
367 }
368
369 #[test]
370 fn reset_replays_identically_to_fresh_instance() {
371 let candles = wavy_series();
372 let mut di = DemandIndex::new(5).unwrap();
373 let first = di.batch(&candles);
374 di.reset();
375 let second = di.batch(&candles);
376 let fresh = DemandIndex::new(5).unwrap().batch(&candles);
377 assert_eq!(first, second);
378 assert_eq!(second, fresh);
379 }
380
381 #[test]
382 fn batch_nan_into_is_bit_identical_to_streaming() {
383 let candles = wavy_series();
384 let mut batch_di = DemandIndex::new(6).unwrap();
385 let mut out = vec![0.0; candles.len()];
386 batch_di.batch_nan_into(&candles, &mut out);
387 let mut stream_di = DemandIndex::new(6).unwrap();
388 let streamed: Vec<f64> = candles
389 .iter()
390 .map(|x| stream_di.update(*x).unwrap_or(f64::NAN))
391 .collect();
392 assert!(out
393 .iter()
394 .zip(&streamed)
395 .all(|(a, b)| a.to_bits() == b.to_bits()));
396 }
397
398 #[test]
399 fn hand_computed_period_one_up_then_down() {
400 let mut di = DemandIndex::new(1).unwrap();
402 assert_eq!(di.update(c(10.0, 11.0, 9.0, 10.0, 100.0, 0)), None);
404 let up = di.update(c(10.0, 12.0, 10.0, 11.5, 200.0, 1)).unwrap();
411 assert_relative_eq!(up, 100.0 * (1.0 - (-1.40625f64).exp()), epsilon = 1e-12);
412 assert_relative_eq!(up, 75.493_946_075_447_41, epsilon = 1e-9);
413 let down = di.update(c(11.0, 11.5, 9.5, 10.0, 50.0, 2)).unwrap();
420 assert_relative_eq!(down, -100.0 * (1.0 - (-1.2f64).exp()), epsilon = 1e-9);
421 assert_relative_eq!(down, -69.880_578_808_779_77, epsilon = 1e-9);
422 }
423
424 #[test]
425 fn equal_pressure_returns_exact_zero() {
426 let mut di = DemandIndex::new(1).unwrap();
430 di.update(c(10.0, 11.0, 9.0, 10.0, 100.0, 0));
431 let up = di.update(c(10.0, 12.0, 10.0, 11.5, 200.0, 1)).unwrap();
432 assert!(up > 0.0);
433 let flat = di.update(c(10.0, 12.0, 10.0, 11.5, 200.0, 2)).unwrap();
434 assert_eq!(flat.to_bits(), 0.0f64.to_bits());
435 let mut di5 = DemandIndex::new(5).unwrap();
438 let candle = c(10.0, 12.0, 8.0, 10.0, 100.0, 0);
439 let out = di5.batch(&[candle; 12]);
440 assert!(out
441 .iter()
442 .flatten()
443 .all(|v| v.to_bits() == 0.0f64.to_bits()));
444 }
445
446 #[test]
447 fn zero_volume_repeats_previous_reading() {
448 let mut di = DemandIndex::new(1).unwrap();
450 di.update(c(10.0, 11.0, 9.0, 10.0, 100.0, 0));
451 let up = di.update(c(10.0, 12.0, 10.0, 11.5, 200.0, 1)).unwrap();
452 let held = di.update(c(11.0, 11.5, 9.5, 10.0, 0.0, 2)).unwrap();
453 assert_eq!(held.to_bits(), up.to_bits());
454 let mut quiet = DemandIndex::new(2).unwrap();
456 let candles: Vec<Candle> = (0..6)
457 .map(|i| {
458 c(
459 10.0 + i as f64,
460 11.0 + i as f64,
461 9.0 + i as f64,
462 10.5 + i as f64,
463 0.0,
464 i,
465 )
466 })
467 .collect();
468 assert!(quiet
469 .batch(&candles)
470 .iter()
471 .flatten()
472 .all(|v| v.to_bits() == 0.0f64.to_bits()));
473 }
474
475 #[test]
476 fn exponent_is_capped_at_max_exponent() {
477 let mut di = DemandIndex::new(1).unwrap();
482 di.update(c(1.0, 1.0, 1.0, 1.0, 100.0, 0));
483 let v = di
484 .update(c(1000.0, 1000.0, 1000.0, 1000.0, 100.0, 1))
485 .unwrap();
486 let sell = di.sell.value().unwrap();
487 assert_eq!(sell.to_bits(), (1.0 / MAX_EXPONENT.exp()).to_bits());
488 assert!(sell > 0.0);
489 assert_eq!(di.buy.value(), Some(1.0));
490 assert_relative_eq!(v, 100.0, epsilon = 1e-12);
491 }
492}