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