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