use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct InformationRatio {
period: usize,
window: VecDeque<f64>,
sum: f64,
sum_sq: f64,
}
impl InformationRatio {
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "information ratio needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
sum: 0.0,
sum_sq: 0.0,
})
}
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for InformationRatio {
type Input = (f64, f64);
type Output = f64;
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
let active = a - b;
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
self.sum -= old;
self.sum_sq -= old * old;
}
self.window.push_back(active);
self.sum += active;
self.sum_sq += active * active;
if self.window.len() < self.period {
return None;
}
let n = self.period as f64;
let mean = self.sum / n;
let var = ((self.sum_sq - n * mean * mean) / (n - 1.0)).max(0.0);
let te = var.sqrt();
if te == 0.0 {
return Some(0.0);
}
Some(mean / te)
}
fn reset(&mut self) {
self.window.clear();
self.sum = 0.0;
self.sum_sq = 0.0;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"InformationRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
InformationRatio::new(1),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let i = InformationRatio::new(10).unwrap();
assert_eq!(i.period(), 10);
assert_eq!(i.name(), "InformationRatio");
assert_eq!(i.warmup_period(), 10);
}
#[test]
fn perfect_tracking_yields_zero() {
let mut i = InformationRatio::new(5).unwrap();
let inputs: Vec<(f64, f64)> = (0..5)
.map(|j| (f64::from(j) * 0.01, f64::from(j) * 0.01))
.collect();
let out = i.batch(&inputs);
assert_eq!(out[4], Some(0.0));
}
#[test]
fn reference_value() {
let mut i = InformationRatio::new(4).unwrap();
let inputs = vec![(0.02, 0.01), (0.04, 0.02), (0.06, 0.03), (0.08, 0.04)];
let out = i.batch(&inputs);
let expected = 0.025 / (0.000_166_666_666_666_666_67_f64).sqrt();
assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-9);
}
#[test]
fn ignores_non_finite_input() {
let mut i = InformationRatio::new(3).unwrap();
assert_eq!(i.update((f64::NAN, 0.01)), None);
assert_eq!(i.update((0.01, f64::INFINITY)), None);
}
#[test]
fn reset_clears_state() {
let mut i = InformationRatio::new(3).unwrap();
i.batch(&[(0.01, 0.005), (0.02, 0.01), (-0.01, -0.005)]);
assert!(i.is_ready());
i.reset();
assert!(!i.is_ready());
assert_eq!(i.update((0.01, 0.005)), None);
}
#[test]
fn batch_equals_streaming() {
let inputs: Vec<(f64, f64)> = (0..50)
.map(|j| {
let b = (f64::from(j) * 0.2).sin() * 0.01;
(b + 0.001, b)
})
.collect();
let batch = InformationRatio::new(10).unwrap().batch(&inputs);
let mut s = InformationRatio::new(10).unwrap();
let streamed: Vec<_> = inputs.iter().map(|x| s.update(*x)).collect();
assert_eq!(batch, streamed);
}
}