wickra_core/indicators/
information_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
29pub struct InformationRatio {
30 period: usize,
31 window: VecDeque<f64>,
32 sum: f64,
33 sum_sq: f64,
34}
35
36impl InformationRatio {
37 pub fn new(period: usize) -> Result<Self> {
42 if period < 2 {
43 return Err(Error::InvalidPeriod {
44 message: "information ratio needs period >= 2",
45 });
46 }
47 Ok(Self {
48 period,
49 window: VecDeque::with_capacity(period),
50 sum: 0.0,
51 sum_sq: 0.0,
52 })
53 }
54
55 pub const fn period(&self) -> usize {
57 self.period
58 }
59}
60
61impl Indicator for InformationRatio {
62 type Input = (f64, f64);
63 type Output = f64;
64
65 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
66 let (a, b) = input;
67 if !a.is_finite() || !b.is_finite() {
68 return None;
69 }
70 let active = a - b;
71 if self.window.len() == self.period {
72 let old = self.window.pop_front().expect("non-empty");
73 self.sum -= old;
74 self.sum_sq -= old * old;
75 }
76 self.window.push_back(active);
77 self.sum += active;
78 self.sum_sq += active * active;
79 if self.window.len() < self.period {
80 return None;
81 }
82 let n = self.period as f64;
83 let mean = self.sum / n;
84 let var = ((self.sum_sq - n * mean * mean) / (n - 1.0)).max(0.0);
85 let te = var.sqrt();
86 if te == 0.0 {
87 return Some(0.0);
88 }
89 Some(mean / te)
90 }
91
92 fn reset(&mut self) {
93 self.window.clear();
94 self.sum = 0.0;
95 self.sum_sq = 0.0;
96 }
97
98 fn warmup_period(&self) -> usize {
99 self.period
100 }
101
102 fn is_ready(&self) -> bool {
103 self.window.len() == self.period
104 }
105
106 fn name(&self) -> &'static str {
107 "InformationRatio"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::traits::BatchExt;
115 use approx::assert_relative_eq;
116
117 #[test]
118 fn rejects_period_less_than_two() {
119 assert!(matches!(
120 InformationRatio::new(1),
121 Err(Error::InvalidPeriod { .. })
122 ));
123 }
124
125 #[test]
126 fn accessors_and_metadata() {
127 let i = InformationRatio::new(10).unwrap();
128 assert_eq!(i.period(), 10);
129 assert_eq!(i.name(), "InformationRatio");
130 assert_eq!(i.warmup_period(), 10);
131 }
132
133 #[test]
134 fn perfect_tracking_yields_zero() {
135 let mut i = InformationRatio::new(5).unwrap();
137 let inputs: Vec<(f64, f64)> = (0..5)
138 .map(|j| (f64::from(j) * 0.01, f64::from(j) * 0.01))
139 .collect();
140 let out = i.batch(&inputs);
141 assert_eq!(out[4], Some(0.0));
142 }
143
144 #[test]
145 fn reference_value() {
146 let mut i = InformationRatio::new(4).unwrap();
151 let inputs = vec![(0.02, 0.01), (0.04, 0.02), (0.06, 0.03), (0.08, 0.04)];
152 let out = i.batch(&inputs);
153 let expected = 0.025 / (0.000_166_666_666_666_666_67_f64).sqrt();
154 assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-9);
155 }
156
157 #[test]
158 fn ignores_non_finite_input() {
159 let mut i = InformationRatio::new(3).unwrap();
160 assert_eq!(i.update((f64::NAN, 0.01)), None);
161 assert_eq!(i.update((0.01, f64::INFINITY)), None);
162 }
163
164 #[test]
165 fn reset_clears_state() {
166 let mut i = InformationRatio::new(3).unwrap();
167 i.batch(&[(0.01, 0.005), (0.02, 0.01), (-0.01, -0.005)]);
168 assert!(i.is_ready());
169 i.reset();
170 assert!(!i.is_ready());
171 assert_eq!(i.update((0.01, 0.005)), None);
172 }
173
174 #[test]
175 fn batch_equals_streaming() {
176 let inputs: Vec<(f64, f64)> = (0..50)
177 .map(|j| {
178 let b = (f64::from(j) * 0.2).sin() * 0.01;
179 (b + 0.001, b)
180 })
181 .collect();
182 let batch = InformationRatio::new(10).unwrap().batch(&inputs);
183 let mut s = InformationRatio::new(10).unwrap();
184 let streamed: Vec<_> = inputs.iter().map(|x| s.update(*x)).collect();
185 assert_eq!(batch, streamed);
186 }
187}