1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
45pub struct Rvi {
46 period: usize,
47 raw: VecDeque<(f64, f64)>,
49 window: VecDeque<(f64, f64)>,
51 sum_num: f64,
52 sum_den: f64,
53 current: Option<f64>,
54}
55
56impl Rvi {
57 pub fn new(period: usize) -> Result<Self> {
60 if period == 0 {
61 return Err(Error::PeriodZero);
62 }
63 if period > crate::error::MAX_PERIOD {
64 return Err(Error::InvalidPeriod {
65 message: crate::error::PERIOD_ABOVE_MAX,
66 });
67 }
68 Ok(Self {
69 period,
70 raw: VecDeque::with_capacity(4),
71 window: VecDeque::with_capacity(period),
72 sum_num: 0.0,
73 sum_den: 0.0,
74 current: None,
75 })
76 }
77
78 pub const fn period(&self) -> usize {
80 self.period
81 }
82
83 pub const fn value(&self) -> Option<f64> {
85 self.current
86 }
87}
88
89impl Indicator for Rvi {
90 type Input = Candle;
91 type Output = f64;
92
93 #[inline]
94 fn update(&mut self, candle: Candle) -> Option<f64> {
95 if self.raw.len() == 4 {
96 self.raw.pop_front();
97 }
98 self.raw
99 .push_back((candle.close - candle.open, candle.high - candle.low));
100 if self.raw.len() < 4 {
101 return None;
102 }
103 let (n3, d3) = self.raw[0];
105 let (n2, d2) = self.raw[1];
106 let (n1, d1) = self.raw[2];
107 let (n0, d0) = self.raw[3];
108 let num = (n0 + 2.0 * n1 + 2.0 * n2 + n3) / 6.0;
109 let den = (d0 + 2.0 * d1 + 2.0 * d2 + d3) / 6.0;
110 if self.window.len() == self.period {
111 let (old_n, old_d) = self.window.pop_front().expect("window is non-empty");
112 self.sum_num -= old_n;
113 self.sum_den -= old_d;
114 }
115 self.window.push_back((num, den));
116 self.sum_num += num;
117 self.sum_den += den;
118 if self.window.len() < self.period {
119 return None;
120 }
121 if self.sum_den <= 0.0 {
122 return self.current;
125 }
126 let value = self.sum_num / self.sum_den;
127 self.current = Some(value);
128 Some(value)
129 }
130
131 fn reset(&mut self) {
132 self.raw.clear();
133 self.window.clear();
134 self.sum_num = 0.0;
135 self.sum_den = 0.0;
136 self.current = None;
137 }
138
139 #[inline]
140 fn warmup_period(&self) -> usize {
141 self.period + 3
142 }
143
144 #[inline]
145 fn is_ready(&self) -> bool {
146 self.current.is_some()
147 }
148
149 #[inline]
150 fn name(&self) -> &'static str {
151 "RVI"
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::traits::BatchExt;
159 use approx::assert_relative_eq;
160
161 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
162 Candle::new(open, high, low, close, 1.0, ts).unwrap()
163 }
164
165 #[test]
166 fn rejects_zero_period() {
167 assert!(matches!(Rvi::new(0), Err(Error::PeriodZero)));
168 }
169
170 #[test]
171 fn accessors_and_metadata() {
172 let mut r = Rvi::new(10).unwrap();
173 assert_eq!(r.period(), 10);
174 assert_eq!(r.warmup_period(), 13);
175 assert_eq!(r.name(), "RVI");
176 assert_eq!(r.value(), None);
177 for i in 0..13 {
178 r.update(candle(10.0, 11.0, 9.0, 10.5, i));
179 }
180 assert!(r.value().is_some());
181 }
182
183 #[test]
184 fn reference_value_period_2() {
185 let mut r = Rvi::new(2).unwrap();
191 for i in 0..4 {
192 assert_eq!(r.update(candle(10.0, 11.0, 9.0, 10.5, i)), None);
193 }
194 let v = r.update(candle(10.5, 11.5, 10.0, 11.5, 4)).unwrap();
195 assert_relative_eq!(v, 6.5 / 23.5, epsilon = 1e-12);
196 }
197
198 #[test]
199 fn warmup_emits_first_value_at_period_plus_three() {
200 let mut r = Rvi::new(3).unwrap();
201 for i in 0..5 {
202 assert_eq!(r.update(candle(10.0, 11.0, 9.0, 10.5, i)), None);
203 }
204 assert!(r.update(candle(10.5, 11.5, 10.0, 11.0, 5)).is_some());
205 }
206
207 #[test]
208 fn pure_uptrend_is_positive() {
209 let mut r = Rvi::new(5).unwrap();
211 for i in 0..10 {
212 let o = 10.0 + f64::from(i);
213 let c = o + 0.5;
214 r.update(candle(o, c + 0.2, o - 0.2, c, i64::from(i)));
215 }
216 let v = r.value().unwrap();
217 assert!(v > 0.0, "uptrend RVI should be positive: {v}");
218 }
219
220 #[test]
221 fn zero_range_window_holds_value() {
222 let mut r = Rvi::new(3).unwrap();
225 for i in 0..5 {
226 r.update(candle(10.0, 10.0, 10.0, 10.0, i));
227 }
228 assert_eq!(r.update(candle(10.0, 10.0, 10.0, 10.0, 5)), None);
229 }
230
231 #[test]
232 fn batch_equals_streaming() {
233 let candles: Vec<Candle> = (0..40_i64)
234 .map(|i| {
235 let o = 100.0 + (i as f64 * 0.3).sin() * 5.0;
236 let c = o + (i as f64 * 0.1).cos();
237 candle(o, o.max(c) + 0.5, o.min(c) - 0.5, c, i)
238 })
239 .collect();
240 let batch = Rvi::new(10).unwrap().batch(&candles);
241 let mut b = Rvi::new(10).unwrap();
242 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
243 assert_eq!(batch, streamed);
244 }
245
246 #[test]
247 fn reset_clears_state() {
248 let mut r = Rvi::new(5).unwrap();
249 for i in 0..10 {
250 r.update(candle(10.0, 11.0, 9.0, 10.5, i));
251 }
252 assert!(r.is_ready());
253 r.reset();
254 assert!(!r.is_ready());
255 assert_eq!(r.update(candle(10.0, 11.0, 9.0, 10.5, 0)), None);
256 }
257
258 fn wave(len: i64) -> Vec<Candle> {
259 (0..len)
260 .map(|i| {
261 let step = f64::from(i32::try_from(i).unwrap());
262 let o = 100.0 + (step * 0.37).sin() * 6.0;
263 let cl = o + (step * 0.11).cos() * 1.5;
264 candle(o, o.max(cl) + 0.4, o.min(cl) - 0.4, cl, i)
265 })
266 .collect()
267 }
268
269 #[test]
270 fn rejects_period_above_max() {
271 let too_big = crate::error::MAX_PERIOD + 1;
272 assert!(matches!(
273 Rvi::new(too_big),
274 Err(Error::InvalidPeriod { .. })
275 ));
276 }
277
278 #[test]
279 fn warmup_is_exact_for_several_periods() {
280 let candles = wave(40);
281 for period in [1_usize, 2, 5, 10] {
282 let mut r = Rvi::new(period).unwrap();
283 let warmup = r.warmup_period();
284 let out = r.batch(&candles);
285 assert!(out[..warmup - 1].iter().all(Option::is_none));
286 assert!(out[warmup - 1..].iter().all(Option::is_some));
287 }
288 }
289
290 #[test]
291 fn reference_value_period_1_is_one_weighted_bar() {
292 let mut r = Rvi::new(1).unwrap();
298 assert_eq!(r.update(candle(10.0, 11.5, 9.5, 11.0, 0)), None);
299 assert_eq!(r.update(candle(11.0, 11.5, 10.5, 10.5, 1)), None);
300 assert_eq!(r.update(candle(10.0, 13.0, 9.0, 12.0, 2)), None);
301 let v = r.update(candle(12.0, 14.0, 11.0, 12.5, 3)).unwrap();
302 assert_relative_eq!(v, 0.3, epsilon = 1e-12);
303 }
304
305 #[test]
306 fn downtrend_is_negative() {
307 let mut r = Rvi::new(3).unwrap();
308 let mut last = None;
309 for i in 0..10_i32 {
310 let o = 50.0 - f64::from(i);
311 let cl = o - 0.5;
312 last = r.update(candle(o, o + 0.2, cl - 0.2, cl, i64::from(i)));
313 }
314 assert!(last.unwrap() < 0.0);
315 }
316
317 #[test]
318 fn flat_stretch_after_movement_holds_previous_value() {
319 let mut r = Rvi::new(1).unwrap();
322 for i in 0..4 {
323 r.update(candle(10.0, 11.0, 9.0, 10.5, i));
324 }
325 let before = r.value().unwrap();
326 assert_relative_eq!(before, 0.25, epsilon = 1e-12);
327 for i in 4..10 {
328 let v = r.update(candle(10.0, 10.0, 10.0, 10.0, i)).unwrap();
329 assert_eq!(v.to_bits(), before.to_bits());
330 }
331 assert!(r.is_ready());
332 }
333
334 #[test]
335 fn reset_reproduces_a_fresh_run() {
336 let candles = wave(50);
337 let mut r = Rvi::new(10).unwrap();
338 let first = r.batch(&candles);
339 r.reset();
340 assert_eq!(r.value(), None);
341 let second = r.batch(&candles);
342 assert_eq!(first, second);
343 assert_eq!(second, Rvi::new(10).unwrap().batch(&candles));
344 }
345
346 #[test]
347 fn batch_nan_into_matches_streaming_bits() {
348 let candles = wave(50);
349 let mut streaming = Rvi::new(4).unwrap();
350 let expected: Vec<u64> = candles
351 .iter()
352 .map(|c| streaming.update(*c).unwrap_or(f64::NAN).to_bits())
353 .collect();
354 let mut out = vec![0.0; candles.len()];
355 Rvi::new(4).unwrap().batch_nan_into(&candles, &mut out);
356 let got: Vec<u64> = out.iter().map(|v| v.to_bits()).collect();
357 assert_eq!(got, expected);
358 }
359}