use alloc::collections::VecDeque;
use crate::statistics::EwmaVarF64;
#[derive(Debug, Clone)]
pub struct VarianceRatioF64 {
q: usize,
short_var: EwmaVarF64,
long_var: EwmaVarF64,
buffer: VecDeque<f64>,
prev: f64,
count: u64,
}
#[derive(Debug, Clone)]
pub struct VarianceRatioF64Builder {
q: Option<usize>,
alpha: Option<f64>,
}
impl VarianceRatioF64 {
#[inline]
#[must_use]
pub fn builder() -> VarianceRatioF64Builder {
VarianceRatioF64Builder {
q: None,
alpha: None,
}
}
#[inline]
pub fn update(&mut self, value: f64) -> Result<(), crate::DataError> {
check_finite!(value);
self.buffer.push_back(value);
self.count += 1;
if self.count >= 2 {
let ret1 = value - self.prev;
let _ = self.short_var.update(ret1)?;
}
if self.buffer.len() > self.q + 1 {
self.buffer.pop_front();
}
if self.buffer.len() == self.q + 1 {
let ret_q = value - self.buffer[0];
let _ = self.long_var.update(ret_q)?;
}
self.prev = value;
Ok(())
}
#[inline]
#[must_use]
pub fn variance_ratio(&self) -> Option<f64> {
let var1 = self.short_var.variance()?;
let var_q = self.long_var.variance()?;
if var1 <= 0.0 {
return None;
}
Some(var_q / (self.q as f64 * var1))
}
#[inline]
#[must_use]
pub fn is_mean_reverting(&self) -> bool {
self.variance_ratio().is_some_and(|vr| vr < 1.0)
}
#[inline]
#[must_use]
pub fn is_trending(&self) -> bool {
self.variance_ratio().is_some_and(|vr| vr > 1.0)
}
#[inline]
#[must_use]
pub fn q(&self) -> usize {
self.q
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.short_var.is_primed() && self.long_var.is_primed()
}
pub fn reset(&mut self) {
self.short_var.reset();
self.long_var.reset();
self.buffer.clear();
self.prev = 0.0;
self.count = 0;
}
}
impl VarianceRatioF64Builder {
#[inline]
#[must_use]
pub fn q(mut self, q: usize) -> Self {
self.q = Some(q);
self
}
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = Some(alpha);
self
}
pub fn build(self) -> Result<VarianceRatioF64, crate::ConfigError> {
let q = self.q.ok_or(crate::ConfigError::Missing("q"))?;
if q < 2 {
return Err(crate::ConfigError::Invalid("q must be >= 2"));
}
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
let short_var = EwmaVarF64::builder().alpha(alpha).build()?;
let long_var = EwmaVarF64::builder().alpha(alpha).build()?;
let mut buffer = VecDeque::new();
buffer.reserve_exact(q + 1);
Ok(VarianceRatioF64 {
q,
short_var,
long_var,
buffer,
prev: 0.0,
count: 0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trending_series_vr_above_1() {
let mut vr = VarianceRatioF64::builder()
.q(5)
.alpha(0.05)
.build()
.unwrap();
let mut price = 100.0;
for i in 0..500 {
price += (i as f64).mul_add(0.001, 1.0);
vr.update(price).unwrap();
}
assert!(vr.is_primed());
let ratio = vr.variance_ratio().unwrap();
assert!(
ratio > 0.8, "VR should be > 0.8 for trending, got {ratio}"
);
}
#[test]
fn mean_reverting_series_vr_below_1() {
let mut vr = VarianceRatioF64::builder()
.q(5)
.alpha(0.05)
.build()
.unwrap();
for i in 0..500 {
let val = 100.0 + if i % 2 == 0 { 5.0 } else { -5.0 };
vr.update(val).unwrap();
}
assert!(vr.is_primed());
let ratio = vr.variance_ratio().unwrap();
assert!(
ratio < 1.0,
"VR should be < 1.0 for mean-reverting, got {ratio}"
);
assert!(vr.is_mean_reverting());
}
#[test]
fn not_primed_early() {
let mut vr = VarianceRatioF64::builder()
.q(5)
.alpha(0.05)
.build()
.unwrap();
vr.update(100.0).unwrap();
assert!(!vr.is_primed());
assert!(vr.variance_ratio().is_none());
}
#[test]
fn q_must_be_at_least_2() {
let result = VarianceRatioF64::builder().q(1).alpha(0.05).build();
assert!(result.is_err());
}
#[test]
fn reset_clears_state() {
let mut vr = VarianceRatioF64::builder()
.q(5)
.alpha(0.05)
.build()
.unwrap();
for i in 0..100 {
vr.update(i as f64).unwrap();
}
vr.reset();
assert_eq!(vr.count(), 0);
assert!(!vr.is_primed());
}
#[test]
fn nan_rejected() {
let mut vr = VarianceRatioF64::builder()
.q(5)
.alpha(0.05)
.build()
.unwrap();
assert!(vr.update(f64::NAN).is_err());
}
#[test]
fn inf_rejected() {
let mut vr = VarianceRatioF64::builder()
.q(5)
.alpha(0.05)
.build()
.unwrap();
assert!(vr.update(f64::INFINITY).is_err());
}
}