wickra_core/indicators/
rwi.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct RwiOutput {
12 pub high: f64,
14 pub low: f64,
16}
17
18#[derive(Debug, Clone)]
60pub struct Rwi {
61 period: usize,
62 candles: VecDeque<Candle>,
64 trs: VecDeque<f64>,
67 scratch: Vec<f64>,
70 last: Option<RwiOutput>,
71}
72
73impl Rwi {
74 pub fn new(period: usize) -> Result<Self> {
82 if period == 0 {
83 return Err(Error::PeriodZero);
84 }
85 if period > crate::error::MAX_PERIOD {
86 return Err(Error::InvalidPeriod {
87 message: crate::error::PERIOD_ABOVE_MAX,
88 });
89 }
90 if period < 2 {
91 return Err(Error::InvalidPeriod {
92 message: "RWI requires period >= 2",
93 });
94 }
95 Ok(Self {
96 period,
97 candles: VecDeque::with_capacity(period),
98 trs: VecDeque::with_capacity(period),
99 scratch: Vec::with_capacity(period),
100 last: None,
101 })
102 }
103
104 pub const fn period(&self) -> usize {
106 self.period
107 }
108
109 pub const fn value(&self) -> Option<RwiOutput> {
111 self.last
112 }
113}
114
115impl Indicator for Rwi {
116 type Input = Candle;
117 type Output = RwiOutput;
118
119 fn update(&mut self, candle: Candle) -> Option<RwiOutput> {
120 let tr = if let Some(prev) = self.candles.back() {
123 candle.true_range(Some(prev.close))
124 } else {
125 candle.high - candle.low
126 };
127
128 if self.candles.len() == self.period {
129 self.candles.pop_front();
130 }
131 self.candles.push_back(candle);
132
133 if self.candles.len() >= 2 {
137 if self.trs.len() == self.period - 1 {
138 self.trs.pop_front();
139 }
140 self.trs.push_back(tr);
141 }
142
143 if self.candles.len() < self.period {
145 return None;
146 }
147
148 let candles = &self.candles;
151 self.scratch.clear();
152 self.scratch.extend(self.trs.iter().copied());
153 let trs = &self.scratch;
154 let n = candles.len(); let last_high = candles[n - 1].high;
156 let last_low = candles[n - 1].low;
157
158 let mut rwi_high = 0.0_f64;
159 let mut rwi_low = 0.0_f64;
160 for i in 2..=self.period {
168 let tr_start = n - i;
172 let tr_end = n - 1;
173 let count = tr_end - tr_start;
174 let atr_i: f64 = trs[tr_start..tr_end].iter().sum::<f64>() / (count as f64);
175 let denom = atr_i * (i as f64).sqrt();
176 if denom == 0.0 {
177 continue;
178 }
179 let old_low = candles[n - i].low;
180 let old_high = candles[n - i].high;
181 let h = (last_high - old_low) / denom;
182 let l = (old_high - last_low) / denom;
183 if h > rwi_high {
184 rwi_high = h;
185 }
186 if l > rwi_low {
187 rwi_low = l;
188 }
189 }
190
191 let out = RwiOutput {
192 high: rwi_high,
193 low: rwi_low,
194 };
195 self.last = Some(out);
196 Some(out)
197 }
198
199 fn reset(&mut self) {
200 self.candles.clear();
201 self.trs.clear();
202 self.scratch.clear();
203 self.last = None;
204 }
205
206 #[inline]
207 fn warmup_period(&self) -> usize {
208 self.period
210 }
211
212 #[inline]
213 fn is_ready(&self) -> bool {
214 self.last.is_some()
215 }
216
217 #[inline]
218 fn name(&self) -> &'static str {
219 "RWI"
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::traits::BatchExt;
227
228 fn candle(h: f64, l: f64, c: f64, ts: i64) -> Candle {
229 Candle::new(c, h, l, c, 1.0, ts).unwrap()
230 }
231
232 #[test]
233 fn rejects_zero_period() {
234 assert!(matches!(Rwi::new(0), Err(Error::PeriodZero)));
235 }
236
237 #[test]
238 fn rejects_period_one() {
239 assert!(matches!(Rwi::new(1), Err(Error::InvalidPeriod { .. })));
240 }
241
242 #[test]
243 fn accessors_and_metadata() {
244 let mut r = Rwi::new(14).unwrap();
245 assert_eq!(r.period(), 14);
246 assert_eq!(r.warmup_period(), 14);
247 assert_eq!(r.name(), "RWI");
248 assert!(r.value().is_none());
249 for i in 0..30_i64 {
250 let p = 100.0 + (i as f64);
251 r.update(candle(p + 1.0, p - 1.0, p, i));
252 }
253 assert!(r.value().is_some());
254 }
255
256 #[test]
257 fn first_emission_at_warmup_period() {
258 let candles: Vec<Candle> = (0..40_i64)
259 .map(|i| {
260 let p = 100.0 + ((i as f64) * 0.3).sin() * 5.0;
261 candle(p + 1.0, p - 1.0, p, i)
262 })
263 .collect();
264 let mut r = Rwi::new(5).unwrap();
265 let out = r.batch(&candles);
266 for v in out.iter().take(4) {
267 assert!(v.is_none());
268 }
269 assert!(out[4].is_some());
270 }
271
272 #[test]
273 fn constant_series_yields_zero_outputs() {
274 let candles: Vec<Candle> = (0..30_i64).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
277 let mut r = Rwi::new(5).unwrap();
278 let last = r.batch(&candles).into_iter().flatten().last().unwrap();
279 assert_eq!(last.high, 0.0);
280 assert_eq!(last.low, 0.0);
281 }
282
283 #[test]
284 fn pure_uptrend_high_dominates_low() {
285 let candles: Vec<Candle> = (0..40_i64)
287 .map(|i| {
288 let base = 100.0 + (i as f64) * 2.0;
289 candle(base + 1.0, base - 0.5, base + 0.5, i)
290 })
291 .collect();
292 let mut r = Rwi::new(14).unwrap();
293 let last = r.batch(&candles).into_iter().flatten().last().unwrap();
294 assert!(
295 last.high > last.low,
296 "RWI_High {} should exceed RWI_Low {}",
297 last.high,
298 last.low
299 );
300 assert!(
301 last.high > 1.0,
302 "strong uptrend should exceed 1, got {}",
303 last.high
304 );
305 }
306
307 #[test]
308 fn pure_downtrend_low_dominates_high() {
309 let candles: Vec<Candle> = (0..40_i64)
310 .rev()
311 .map(|i| {
312 let base = 100.0 + (i as f64) * 2.0;
313 candle(base + 0.5, base - 1.0, base - 0.5, 40 - i)
314 })
315 .collect();
316 let mut r = Rwi::new(14).unwrap();
317 let last = r.batch(&candles).into_iter().flatten().last().unwrap();
318 assert!(last.low > last.high);
319 assert!(last.low > 1.0);
320 }
321
322 #[test]
323 fn outputs_non_negative() {
324 let candles: Vec<Candle> = (0..120_i64)
325 .map(|i| {
326 let p = 100.0 + ((i as f64) * 0.25).sin() * 6.0;
327 candle(p + 1.5, p - 1.5, p, i)
328 })
329 .collect();
330 let mut r = Rwi::new(10).unwrap();
331 for v in r.batch(&candles).into_iter().flatten() {
332 assert!(v.high >= 0.0 && v.low >= 0.0);
333 assert!(v.high.is_finite() && v.low.is_finite());
334 }
335 }
336
337 #[test]
338 fn batch_equals_streaming() {
339 let candles: Vec<Candle> = (0..80_i64)
340 .map(|i| {
341 let p = 100.0 + ((i as f64) * 0.3).sin() * 5.0;
342 candle(p + 1.0, p - 1.0, p, i)
343 })
344 .collect();
345 let mut a = Rwi::new(7).unwrap();
346 let mut b = Rwi::new(7).unwrap();
347 assert_eq!(
348 a.batch(&candles),
349 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
350 );
351 }
352
353 #[test]
354 fn reset_clears_state() {
355 let candles: Vec<Candle> = (0..30_i64).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
356 let mut r = Rwi::new(5).unwrap();
357 r.batch(&candles);
358 assert!(r.is_ready());
359 r.reset();
360 assert!(!r.is_ready());
361 assert_eq!(r.update(candles[0]), None);
362 }
363}