wickra_core/indicators/
treynor_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
33pub struct TreynorRatio {
34 period: usize,
35 risk_free: f64,
36 window: VecDeque<(f64, f64)>,
37 sum_a: f64,
38 sum_b: f64,
39 sum_bb: f64,
40 sum_ab: f64,
41}
42
43impl TreynorRatio {
44 pub fn new(period: usize, risk_free: f64) -> Result<Self> {
49 if period < 2 {
50 return Err(Error::InvalidPeriod {
51 message: "treynor ratio needs period >= 2",
52 });
53 }
54 Ok(Self {
55 period,
56 risk_free,
57 window: VecDeque::with_capacity(period),
58 sum_a: 0.0,
59 sum_b: 0.0,
60 sum_bb: 0.0,
61 sum_ab: 0.0,
62 })
63 }
64
65 pub const fn period(&self) -> usize {
67 self.period
68 }
69
70 pub const fn risk_free(&self) -> f64 {
72 self.risk_free
73 }
74}
75
76impl Indicator for TreynorRatio {
77 type Input = (f64, f64);
78 type Output = f64;
79
80 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
81 let (a, b) = input;
82 if !a.is_finite() || !b.is_finite() {
83 return None;
84 }
85 if self.window.len() == self.period {
86 let (oa, ob) = self.window.pop_front().expect("non-empty");
87 self.sum_a -= oa;
88 self.sum_b -= ob;
89 self.sum_bb -= ob * ob;
90 self.sum_ab -= oa * ob;
91 }
92 self.window.push_back((a, b));
93 self.sum_a += a;
94 self.sum_b += b;
95 self.sum_bb += b * b;
96 self.sum_ab += a * b;
97 if self.window.len() < self.period {
98 return None;
99 }
100 let n = self.period as f64;
101 let mean_a = self.sum_a / n;
102 let mean_b = self.sum_b / n;
103 let var_b = (self.sum_bb / n) - mean_b * mean_b;
104 if var_b <= 0.0 {
105 return Some(0.0);
106 }
107 let cov_ab = (self.sum_ab / n) - mean_a * mean_b;
108 let beta = cov_ab / var_b;
109 if beta == 0.0 {
110 return Some(0.0);
111 }
112 Some((mean_a - self.risk_free) / beta)
113 }
114
115 fn reset(&mut self) {
116 self.window.clear();
117 self.sum_a = 0.0;
118 self.sum_b = 0.0;
119 self.sum_bb = 0.0;
120 self.sum_ab = 0.0;
121 }
122
123 fn warmup_period(&self) -> usize {
124 self.period
125 }
126
127 fn is_ready(&self) -> bool {
128 self.window.len() == self.period
129 }
130
131 fn name(&self) -> &'static str {
132 "TreynorRatio"
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::traits::BatchExt;
140 use approx::assert_relative_eq;
141
142 #[test]
143 fn rejects_period_less_than_two() {
144 assert!(matches!(
145 TreynorRatio::new(1, 0.0),
146 Err(Error::InvalidPeriod { .. })
147 ));
148 }
149
150 #[test]
151 fn accessors_and_metadata() {
152 let t = TreynorRatio::new(20, 0.001).unwrap();
153 assert_eq!(t.period(), 20);
154 assert_relative_eq!(t.risk_free(), 0.001, epsilon = 1e-12);
155 assert_eq!(t.name(), "TreynorRatio");
156 assert_eq!(t.warmup_period(), 20);
157 }
158
159 #[test]
160 fn reference_beta_two_payoff() {
161 let mut t = TreynorRatio::new(20, 0.0).unwrap();
164 let inputs: Vec<(f64, f64)> = (1..=20)
165 .map(|i| (2.0 * f64::from(i) * 0.01, f64::from(i) * 0.01))
166 .collect();
167 let out = t.batch(&inputs);
168 let last = out[19].unwrap();
169 let expected = inputs.iter().map(|(_, b)| *b).sum::<f64>() / 20.0;
170 assert_relative_eq!(last, expected, epsilon = 1e-9);
171 }
172
173 #[test]
174 fn flat_benchmark_yields_zero() {
175 let mut t = TreynorRatio::new(4, 0.0).unwrap();
177 let out = t.batch(&[(0.01, 0.0), (0.02, 0.0), (-0.01, 0.0), (0.03, 0.0)]);
178 assert_eq!(out[3], Some(0.0));
179 }
180
181 #[test]
182 fn ignores_non_finite_input() {
183 let mut t = TreynorRatio::new(3, 0.0).unwrap();
184 assert_eq!(t.update((f64::NAN, 0.0)), None);
185 assert_eq!(t.update((0.0, f64::INFINITY)), None);
186 }
187
188 #[test]
189 fn reset_clears_state() {
190 let mut t = TreynorRatio::new(3, 0.0).unwrap();
191 t.batch(&[(0.01, 0.005), (0.02, 0.01), (-0.01, -0.005)]);
192 assert!(t.is_ready());
193 t.reset();
194 assert!(!t.is_ready());
195 assert_eq!(t.update((0.01, 0.005)), None);
196 }
197
198 #[test]
199 fn batch_equals_streaming() {
200 let inputs: Vec<(f64, f64)> = (0..50)
201 .map(|i| {
202 let b = (f64::from(i) * 0.2).sin() * 0.01;
203 (1.5 * b + 0.001, b)
204 })
205 .collect();
206 let batch = TreynorRatio::new(10, 0.0).unwrap().batch(&inputs);
207 let mut s = TreynorRatio::new(10, 0.0).unwrap();
208 let streamed: Vec<_> = inputs.iter().map(|x| s.update(*x)).collect();
209 assert_eq!(batch, streamed);
210 }
211
212 #[test]
213 fn zero_beta_returns_zero() {
214 let mut t = TreynorRatio::new(4, 0.0).unwrap();
217 let pairs: [(f64, f64); 4] = [(0.01, 0.005), (0.01, -0.002), (0.01, 0.001), (0.01, 0.003)];
218 let mut last = None;
219 for p in pairs {
220 last = t.update(p);
221 }
222 assert_eq!(last, Some(0.0));
223 }
224}