wickra_core/indicators/
alpha.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
33pub struct Alpha {
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 Alpha {
44 pub fn new(period: usize, risk_free: f64) -> Result<Self> {
49 if period < 2 {
50 return Err(Error::InvalidPeriod {
51 message: "alpha 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 Alpha {
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(mean_a - self.risk_free);
107 }
108 let cov_ab = (self.sum_ab / n) - mean_a * mean_b;
109 let beta = cov_ab / var_b;
110 Some(mean_a - (self.risk_free + beta * (mean_b - self.risk_free)))
111 }
112
113 fn reset(&mut self) {
114 self.window.clear();
115 self.sum_a = 0.0;
116 self.sum_b = 0.0;
117 self.sum_bb = 0.0;
118 self.sum_ab = 0.0;
119 }
120
121 fn warmup_period(&self) -> usize {
122 self.period
123 }
124
125 fn is_ready(&self) -> bool {
126 self.window.len() == self.period
127 }
128
129 fn name(&self) -> &'static str {
130 "Alpha"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 #[test]
141 fn rejects_period_less_than_two() {
142 assert!(matches!(
143 Alpha::new(1, 0.0),
144 Err(Error::InvalidPeriod { .. })
145 ));
146 }
147
148 #[test]
149 fn accessors_and_metadata() {
150 let a = Alpha::new(20, 0.001).unwrap();
151 assert_eq!(a.period(), 20);
152 assert_relative_eq!(a.risk_free(), 0.001, epsilon = 1e-12);
153 assert_eq!(a.name(), "Alpha");
154 assert_eq!(a.warmup_period(), 20);
155 }
156
157 #[test]
158 fn capm_perfect_fit_yields_zero_alpha() {
159 let mut a = Alpha::new(20, 0.0).unwrap();
162 let inputs: Vec<(f64, f64)> = (1..=20)
163 .map(|i| (2.0 * f64::from(i) * 0.01, f64::from(i) * 0.01))
164 .collect();
165 let out = a.batch(&inputs);
166 assert_relative_eq!(out[19].unwrap(), 0.0, epsilon = 1e-12);
167 }
168
169 #[test]
170 fn constant_alpha_offset_recovered() {
171 let mut a = Alpha::new(20, 0.0).unwrap();
174 let inputs: Vec<(f64, f64)> = (1..=20)
175 .map(|i| (f64::from(i) * 0.01 + 0.005, f64::from(i) * 0.01))
176 .collect();
177 let out = a.batch(&inputs);
178 assert_relative_eq!(out[19].unwrap(), 0.005, epsilon = 1e-9);
179 }
180
181 #[test]
182 fn flat_benchmark_falls_back_to_excess_return() {
183 let mut a = Alpha::new(4, 0.001).unwrap();
185 let out = a.batch(&[(0.01, 0.0), (0.02, 0.0), (-0.01, 0.0), (0.04, 0.0)]);
186 let mean = (0.01 + 0.02 - 0.01 + 0.04) / 4.0;
187 assert_relative_eq!(out[3].unwrap(), mean - 0.001, epsilon = 1e-12);
188 }
189
190 #[test]
191 fn ignores_non_finite_input() {
192 let mut a = Alpha::new(3, 0.0).unwrap();
193 assert_eq!(a.update((f64::NAN, 0.0)), None);
194 assert_eq!(a.update((0.0, f64::INFINITY)), None);
195 }
196
197 #[test]
198 fn reset_clears_state() {
199 let mut a = Alpha::new(3, 0.0).unwrap();
200 a.batch(&[(0.01, 0.005), (0.02, 0.01), (-0.01, -0.005)]);
201 assert!(a.is_ready());
202 a.reset();
203 assert!(!a.is_ready());
204 assert_eq!(a.update((0.01, 0.005)), None);
205 }
206
207 #[test]
208 fn batch_equals_streaming() {
209 let inputs: Vec<(f64, f64)> = (0..50)
210 .map(|i| {
211 let b = (f64::from(i) * 0.2).sin() * 0.01;
212 (1.5 * b + 0.002, b)
213 })
214 .collect();
215 let batch = Alpha::new(10, 0.0).unwrap().batch(&inputs);
216 let mut s = Alpha::new(10, 0.0).unwrap();
217 let streamed: Vec<_> = inputs.iter().map(|x| s.update(*x)).collect();
218 assert_eq!(batch, streamed);
219 }
220}