use crate::utils::RingBuffer;
#[derive(Debug, Clone)]
pub struct RollingVariance {
period: usize,
buf: RingBuffer<f64>,
origin: f64,
sum_d: f64,
sum_d2: f64,
since_refresh: usize,
}
impl RollingVariance {
pub fn new(period: usize) -> Self {
Self {
period,
buf: RingBuffer::with_capacity(period.max(1)),
origin: 0.0,
sum_d: 0.0,
sum_d2: 0.0,
since_refresh: 0,
}
}
#[inline]
pub fn len(&self) -> usize {
self.buf.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
pub fn values(&self) -> Vec<f64> {
self.buf.iter().cloned().collect()
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &f64> {
self.buf.iter()
}
pub fn push(&mut self, v: f64) -> bool {
if self.period == 0 {
return false;
}
if self.buf.len() >= self.period
&& let Some(old) = self.buf.pop_front()
{
let d = old - self.origin;
self.sum_d -= d;
self.sum_d2 -= d * d;
}
self.buf.push_back(v);
let d = v - self.origin;
self.sum_d += d;
self.sum_d2 += d * d;
self.since_refresh += 1;
if self.buf.len() < self.period || self.since_refresh >= self.period {
self.refresh();
}
self.buf.len() >= self.period
}
fn refresh(&mut self) {
self.since_refresh = 0;
let n = self.buf.len();
if n == 0 {
self.origin = 0.0;
self.sum_d = 0.0;
self.sum_d2 = 0.0;
return;
}
let mut total = 0.0;
for &v in self.buf.iter() {
total += v;
}
let k = total / n as f64;
let mut sum_d = 0.0;
let mut sum_d2 = 0.0;
for &v in self.buf.iter() {
let d = v - k;
sum_d += d;
sum_d2 += d * d;
}
self.origin = k;
self.sum_d = sum_d;
self.sum_d2 = sum_d2;
}
#[inline]
pub fn sum(&self) -> f64 {
self.sum_d + self.origin * self.buf.len() as f64
}
#[inline]
pub fn mean(&self) -> f64 {
let n = self.buf.len();
if n == 0 {
return f64::NAN;
}
self.origin + self.sum_d / n as f64
}
#[inline]
pub fn variance(&self) -> f64 {
let n = self.buf.len();
if n == 0 {
return f64::NAN;
}
let n_f = n as f64;
((self.sum_d2 - self.sum_d * self.sum_d / n_f) / n_f).max(0.0)
}
#[inline]
pub fn stddev(&self) -> f64 {
self.variance().sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{log_price_series, reference_stddev};
fn one_pass_stddev(w: &[f64]) -> f64 {
let n = w.len() as f64;
let sum: f64 = w.iter().sum();
let sum_sq: f64 = w.iter().map(|&x| x * x).sum();
let mean = sum / n;
(sum_sq / n - mean * mean).max(0.0).sqrt()
}
#[test]
fn stable_on_log_price_where_one_pass_fails() {
let period = 20;
let data = log_price_series(600);
let mut acc = RollingVariance::new(period);
let mut worst_new: f64 = 0.0;
let mut worst_old: f64 = 0.0;
for (i, &x) in data.iter().enumerate() {
let full = acc.push(x);
if !full {
continue;
}
let window = &data[i + 1 - period..=i];
let reference = reference_stddev(window);
assert!(reference > 0.0);
let rel_new = ((acc.stddev() - reference) / reference).abs();
let rel_old = ((one_pass_stddev(window) - reference) / reference).abs();
worst_new = worst_new.max(rel_new);
worst_old = worst_old.max(rel_old);
}
println!("worst rel err: one-pass {worst_old:e}, stable {worst_new:e}");
assert!(
worst_old > 1e-7,
"one-pass should fail the 1e-7 gate on log-price, got {worst_old:e}"
);
assert!(
worst_new < 1e-12,
"stable accumulator rel err {worst_new:e} (one-pass was {worst_old:e})"
);
}
#[test]
fn no_drift_over_long_stream() {
let period = 30;
let data = log_price_series(50_000);
let mut acc = RollingVariance::new(period);
let mut worst: f64 = 0.0;
for (i, &x) in data.iter().enumerate() {
if !acc.push(x) {
continue;
}
if i % 97 != 0 {
continue;
}
let reference = reference_stddev(&data[i + 1 - period..=i]);
if reference > 0.0 {
worst = worst.max(((acc.stddev() - reference) / reference).abs());
}
}
assert!(worst < 1e-12, "drift {worst:e} after 50k bars");
}
#[test]
fn matches_reference_on_raw_prices() {
let period = 14;
let data: Vec<f64> = (0..500)
.map(|i| 2980.0 + (i as f64 * 0.31).sin() * 40.0)
.collect();
let mut acc = RollingVariance::new(period);
for (i, &x) in data.iter().enumerate() {
if !acc.push(x) {
continue;
}
let reference = reference_stddev(&data[i + 1 - period..=i]);
approx::assert_relative_eq!(acc.stddev(), reference, max_relative = 1e-12);
}
}
#[test]
fn constant_window_is_exactly_zero() {
let mut acc = RollingVariance::new(10);
for _ in 0..50 {
acc.push(1234.5678);
}
assert_eq!(acc.stddev(), 0.0);
assert_eq!(acc.variance(), 0.0);
}
#[test]
fn sum_and_mean_track_the_window() {
let mut acc = RollingVariance::new(4);
for x in [1.0, 2.0, 3.0, 4.0, 5.0] {
acc.push(x);
}
approx::assert_relative_eq!(acc.sum(), 14.0, max_relative = 1e-12);
approx::assert_relative_eq!(acc.mean(), 3.5, max_relative = 1e-12);
assert_eq!(acc.len(), 4);
assert_eq!(acc.values(), vec![2.0, 3.0, 4.0, 5.0]);
}
#[test]
fn refresh_cadence_is_deterministic() {
let data = log_price_series(1_000);
let mut a = RollingVariance::new(20);
let mut b = RollingVariance::new(20);
for &x in &data {
a.push(x);
b.push(x);
assert_eq!(a.stddev().to_bits(), b.stddev().to_bits());
}
}
}