use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct Rvi {
period: usize,
raw: VecDeque<(f64, f64)>,
window: VecDeque<(f64, f64)>,
sum_num: f64,
sum_den: f64,
current: Option<f64>,
}
impl Rvi {
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
if period > crate::error::MAX_PERIOD {
return Err(Error::InvalidPeriod {
message: crate::error::PERIOD_ABOVE_MAX,
});
}
Ok(Self {
period,
raw: VecDeque::with_capacity(4),
window: VecDeque::with_capacity(period),
sum_num: 0.0,
sum_den: 0.0,
current: None,
})
}
pub const fn period(&self) -> usize {
self.period
}
pub const fn value(&self) -> Option<f64> {
self.current
}
}
impl Indicator for Rvi {
type Input = Candle;
type Output = f64;
#[inline]
fn update(&mut self, candle: Candle) -> Option<f64> {
if self.raw.len() == 4 {
self.raw.pop_front();
}
self.raw
.push_back((candle.close - candle.open, candle.high - candle.low));
if self.raw.len() < 4 {
return None;
}
let (n3, d3) = self.raw[0];
let (n2, d2) = self.raw[1];
let (n1, d1) = self.raw[2];
let (n0, d0) = self.raw[3];
let num = (n0 + 2.0 * n1 + 2.0 * n2 + n3) / 6.0;
let den = (d0 + 2.0 * d1 + 2.0 * d2 + d3) / 6.0;
if self.window.len() == self.period {
let (old_n, old_d) = self.window.pop_front().expect("window is non-empty");
self.sum_num -= old_n;
self.sum_den -= old_d;
}
self.window.push_back((num, den));
self.sum_num += num;
self.sum_den += den;
if self.window.len() < self.period {
return None;
}
if self.sum_den <= 0.0 {
return self.current;
}
let value = self.sum_num / self.sum_den;
self.current = Some(value);
Some(value)
}
fn reset(&mut self) {
self.raw.clear();
self.window.clear();
self.sum_num = 0.0;
self.sum_den = 0.0;
self.current = None;
}
#[inline]
fn warmup_period(&self) -> usize {
self.period + 3
}
#[inline]
fn is_ready(&self) -> bool {
self.current.is_some()
}
#[inline]
fn name(&self) -> &'static str {
"RVI"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_zero_period() {
assert!(matches!(Rvi::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let mut r = Rvi::new(10).unwrap();
assert_eq!(r.period(), 10);
assert_eq!(r.warmup_period(), 13);
assert_eq!(r.name(), "RVI");
assert_eq!(r.value(), None);
for i in 0..13 {
r.update(candle(10.0, 11.0, 9.0, 10.5, i));
}
assert!(r.value().is_some());
}
#[test]
fn reference_value_period_2() {
let mut r = Rvi::new(2).unwrap();
for i in 0..4 {
assert_eq!(r.update(candle(10.0, 11.0, 9.0, 10.5, i)), None);
}
let v = r.update(candle(10.5, 11.5, 10.0, 11.5, 4)).unwrap();
assert_relative_eq!(v, 6.5 / 23.5, epsilon = 1e-12);
}
#[test]
fn warmup_emits_first_value_at_period_plus_three() {
let mut r = Rvi::new(3).unwrap();
for i in 0..5 {
assert_eq!(r.update(candle(10.0, 11.0, 9.0, 10.5, i)), None);
}
assert!(r.update(candle(10.5, 11.5, 10.0, 11.0, 5)).is_some());
}
#[test]
fn pure_uptrend_is_positive() {
let mut r = Rvi::new(5).unwrap();
for i in 0..10 {
let o = 10.0 + f64::from(i);
let c = o + 0.5;
r.update(candle(o, c + 0.2, o - 0.2, c, i64::from(i)));
}
let v = r.value().unwrap();
assert!(v > 0.0, "uptrend RVI should be positive: {v}");
}
#[test]
fn zero_range_window_holds_value() {
let mut r = Rvi::new(3).unwrap();
for i in 0..5 {
r.update(candle(10.0, 10.0, 10.0, 10.0, i));
}
assert_eq!(r.update(candle(10.0, 10.0, 10.0, 10.0, 5)), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40_i64)
.map(|i| {
let o = 100.0 + (i as f64 * 0.3).sin() * 5.0;
let c = o + (i as f64 * 0.1).cos();
candle(o, o.max(c) + 0.5, o.min(c) - 0.5, c, i)
})
.collect();
let batch = Rvi::new(10).unwrap().batch(&candles);
let mut b = Rvi::new(10).unwrap();
let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn reset_clears_state() {
let mut r = Rvi::new(5).unwrap();
for i in 0..10 {
r.update(candle(10.0, 11.0, 9.0, 10.5, i));
}
assert!(r.is_ready());
r.reset();
assert!(!r.is_ready());
assert_eq!(r.update(candle(10.0, 11.0, 9.0, 10.5, 0)), None);
}
fn wave(len: i64) -> Vec<Candle> {
(0..len)
.map(|i| {
let step = f64::from(i32::try_from(i).unwrap());
let o = 100.0 + (step * 0.37).sin() * 6.0;
let cl = o + (step * 0.11).cos() * 1.5;
candle(o, o.max(cl) + 0.4, o.min(cl) - 0.4, cl, i)
})
.collect()
}
#[test]
fn rejects_period_above_max() {
let too_big = crate::error::MAX_PERIOD + 1;
assert!(matches!(
Rvi::new(too_big),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn warmup_is_exact_for_several_periods() {
let candles = wave(40);
for period in [1_usize, 2, 5, 10] {
let mut r = Rvi::new(period).unwrap();
let warmup = r.warmup_period();
let out = r.batch(&candles);
assert!(out[..warmup - 1].iter().all(Option::is_none));
assert!(out[warmup - 1..].iter().all(Option::is_some));
}
}
#[test]
fn reference_value_period_1_is_one_weighted_bar() {
let mut r = Rvi::new(1).unwrap();
assert_eq!(r.update(candle(10.0, 11.5, 9.5, 11.0, 0)), None);
assert_eq!(r.update(candle(11.0, 11.5, 10.5, 10.5, 1)), None);
assert_eq!(r.update(candle(10.0, 13.0, 9.0, 12.0, 2)), None);
let v = r.update(candle(12.0, 14.0, 11.0, 12.5, 3)).unwrap();
assert_relative_eq!(v, 0.3, epsilon = 1e-12);
}
#[test]
fn downtrend_is_negative() {
let mut r = Rvi::new(3).unwrap();
let mut last = None;
for i in 0..10_i32 {
let o = 50.0 - f64::from(i);
let cl = o - 0.5;
last = r.update(candle(o, o + 0.2, cl - 0.2, cl, i64::from(i)));
}
assert!(last.unwrap() < 0.0);
}
#[test]
fn flat_stretch_after_movement_holds_previous_value() {
let mut r = Rvi::new(1).unwrap();
for i in 0..4 {
r.update(candle(10.0, 11.0, 9.0, 10.5, i));
}
let before = r.value().unwrap();
assert_relative_eq!(before, 0.25, epsilon = 1e-12);
for i in 4..10 {
let v = r.update(candle(10.0, 10.0, 10.0, 10.0, i)).unwrap();
assert_eq!(v.to_bits(), before.to_bits());
}
assert!(r.is_ready());
}
#[test]
fn reset_reproduces_a_fresh_run() {
let candles = wave(50);
let mut r = Rvi::new(10).unwrap();
let first = r.batch(&candles);
r.reset();
assert_eq!(r.value(), None);
let second = r.batch(&candles);
assert_eq!(first, second);
assert_eq!(second, Rvi::new(10).unwrap().batch(&candles));
}
#[test]
fn batch_nan_into_matches_streaming_bits() {
let candles = wave(50);
let mut streaming = Rvi::new(4).unwrap();
let expected: Vec<u64> = candles
.iter()
.map(|c| streaming.update(*c).unwrap_or(f64::NAN).to_bits())
.collect();
let mut out = vec![0.0; candles.len()];
Rvi::new(4).unwrap().batch_nan_into(&candles, &mut out);
let got: Vec<u64> = out.iter().map(|v| v.to_bits()).collect();
assert_eq!(got, expected);
}
}