wickra_core/indicators/
alpha.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedPairMoments;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
34pub struct Alpha {
35 period: usize,
36 risk_free: f64,
37 window: VecDeque<(f64, f64)>,
38 moments: ShiftedPairMoments,
39}
40
41impl Alpha {
42 pub fn new(period: usize, risk_free: f64) -> Result<Self> {
47 if period < 2 {
48 return Err(Error::InvalidPeriod {
49 message: "alpha needs period >= 2",
50 });
51 }
52 if period > crate::error::MAX_PERIOD {
53 return Err(Error::InvalidPeriod {
54 message: crate::error::PERIOD_ABOVE_MAX,
55 });
56 }
57 Ok(Self {
58 period,
59 risk_free,
60 window: VecDeque::with_capacity(period),
61 moments: ShiftedPairMoments::new(),
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 #[inline]
81 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
82 let (a, b) = input;
83 if !a.is_finite() || !b.is_finite() {
84 return None;
85 }
86 if self.window.len() == self.period {
87 let (oa, ob) = self.window.pop_front().expect("non-empty");
88 self.moments.evict(oa, ob);
89 }
90 self.window.push_back((a, b));
91 self.moments.push(a, b);
92 if self.moments.needs_reseed(self.period) {
93 self.moments.reseed(self.window.iter().copied());
94 }
95 if self.window.len() < self.period {
96 return None;
97 }
98 let mean_a = self.moments.mean_a(self.period);
99 let mean_b = self.moments.mean_b(self.period);
100 let var_b = self.moments.var_b(self.period);
101 if var_b <= 0.0 {
102 return Some(mean_a - self.risk_free);
104 }
105 let cov_ab = self.moments.cov(self.period);
106 let beta = cov_ab / var_b;
107 Some(mean_a - (self.risk_free + beta * (mean_b - self.risk_free)))
108 }
109
110 fn reset(&mut self) {
111 self.window.clear();
112 self.moments.reset();
113 }
114
115 #[inline]
116 fn warmup_period(&self) -> usize {
117 self.period
118 }
119
120 #[inline]
121 fn is_ready(&self) -> bool {
122 self.window.len() == self.period
123 }
124
125 #[inline]
126 fn name(&self) -> &'static str {
127 "Alpha"
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::traits::BatchExt;
135 use approx::assert_relative_eq;
136
137 #[test]
138 fn rejects_period_less_than_two() {
139 assert!(matches!(
140 Alpha::new(1, 0.0),
141 Err(Error::InvalidPeriod { .. })
142 ));
143 }
144
145 #[test]
146 fn accessors_and_metadata() {
147 let a = Alpha::new(20, 0.001).unwrap();
148 assert_eq!(a.period(), 20);
149 assert_relative_eq!(a.risk_free(), 0.001, epsilon = 1e-12);
150 assert_eq!(a.name(), "Alpha");
151 assert_eq!(a.warmup_period(), 20);
152 }
153
154 #[test]
155 fn capm_perfect_fit_yields_zero_alpha() {
156 let mut a = Alpha::new(20, 0.0).unwrap();
159 let inputs: Vec<(f64, f64)> = (1..=20)
160 .map(|i| (2.0 * f64::from(i) * 0.01, f64::from(i) * 0.01))
161 .collect();
162 let out = a.batch(&inputs);
163 assert_relative_eq!(out[19].unwrap(), 0.0, epsilon = 1e-12);
164 }
165
166 #[test]
167 fn constant_alpha_offset_recovered() {
168 let mut a = Alpha::new(20, 0.0).unwrap();
171 let inputs: Vec<(f64, f64)> = (1..=20)
172 .map(|i| (f64::from(i) * 0.01 + 0.005, f64::from(i) * 0.01))
173 .collect();
174 let out = a.batch(&inputs);
175 assert_relative_eq!(out[19].unwrap(), 0.005, epsilon = 1e-9);
176 }
177
178 #[test]
179 fn flat_benchmark_falls_back_to_excess_return() {
180 let mut a = Alpha::new(4, 0.001).unwrap();
182 let out = a.batch(&[(0.01, 0.0), (0.02, 0.0), (-0.01, 0.0), (0.04, 0.0)]);
183 let mean = (0.01 + 0.02 - 0.01 + 0.04) / 4.0;
184 assert_relative_eq!(out[3].unwrap(), mean - 0.001, epsilon = 1e-12);
185 }
186
187 #[test]
188 fn ignores_non_finite_input() {
189 let mut a = Alpha::new(3, 0.0).unwrap();
190 assert_eq!(a.update((f64::NAN, 0.0)), None);
191 assert_eq!(a.update((0.0, f64::INFINITY)), None);
192 }
193
194 #[test]
195 fn reset_clears_state() {
196 let mut a = Alpha::new(3, 0.0).unwrap();
197 a.batch(&[(0.01, 0.005), (0.02, 0.01), (-0.01, -0.005)]);
198 assert!(a.is_ready());
199 a.reset();
200 assert!(!a.is_ready());
201 assert_eq!(a.update((0.01, 0.005)), None);
202 }
203
204 #[test]
205 fn batch_equals_streaming() {
206 let inputs: Vec<(f64, f64)> = (0..50)
207 .map(|i| {
208 let b = (f64::from(i) * 0.2).sin() * 0.01;
209 (1.5 * b + 0.002, b)
210 })
211 .collect();
212 let batch = Alpha::new(10, 0.0).unwrap().batch(&inputs);
213 let mut s = Alpha::new(10, 0.0).unwrap();
214 let streamed: Vec<_> = inputs.iter().map(|x| s.update(*x)).collect();
215 assert_eq!(batch, streamed);
216 }
217}