wickra_core/indicators/
beta.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)]
53pub struct Beta {
54 period: usize,
55 window: VecDeque<(f64, f64)>,
56 moments: ShiftedPairMoments,
57}
58
59impl Beta {
60 pub fn new(period: usize) -> Result<Self> {
65 if period < 2 {
66 return Err(Error::InvalidPeriod {
67 message: "beta needs period >= 2",
68 });
69 }
70 if period > crate::error::MAX_PERIOD {
71 return Err(Error::InvalidPeriod {
72 message: crate::error::PERIOD_ABOVE_MAX,
73 });
74 }
75 Ok(Self {
76 period,
77 window: VecDeque::with_capacity(period),
78 moments: ShiftedPairMoments::new(),
79 })
80 }
81
82 pub const fn period(&self) -> usize {
84 self.period
85 }
86}
87
88impl Indicator for Beta {
89 type Input = (f64, f64);
91 type Output = f64;
92
93 #[inline]
94 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
95 let (a, b) = input;
96 if !a.is_finite() || !b.is_finite() {
97 return None;
98 }
99 if self.window.len() == self.period {
100 let (oa, ob) = self.window.pop_front().expect("non-empty");
101 self.moments.evict(oa, ob);
102 }
103 self.window.push_back((a, b));
104 self.moments.push(a, b);
105 if self.moments.needs_reseed(self.period) {
106 self.moments.reseed(self.window.iter().copied());
107 }
108 if self.window.len() < self.period {
109 return None;
110 }
111 let var_b = self.moments.var_b(self.period);
112 let cov = self.moments.cov(self.period);
113 if var_b == 0.0 {
114 return Some(0.0);
116 }
117 Some(cov / var_b)
118 }
119
120 fn reset(&mut self) {
121 self.window.clear();
122 self.moments.reset();
123 }
124
125 #[inline]
126 fn warmup_period(&self) -> usize {
127 self.period
128 }
129
130 #[inline]
131 fn is_ready(&self) -> bool {
132 self.window.len() == self.period
133 }
134
135 #[inline]
136 fn name(&self) -> &'static str {
137 "Beta"
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::traits::BatchExt;
145 use approx::assert_relative_eq;
146
147 #[test]
148 fn rejects_period_below_two() {
149 assert!(Beta::new(0).is_err());
150 assert!(Beta::new(1).is_err());
151 assert!(Beta::new(2).is_ok());
152 }
153
154 #[test]
155 fn accessors_and_metadata() {
156 let b = Beta::new(14).unwrap();
157 assert_eq!(b.period(), 14);
158 assert_eq!(b.warmup_period(), 14);
159 assert_eq!(b.name(), "Beta");
160 }
161
162 #[test]
163 fn perfect_two_to_one_relationship() {
164 let pairs: Vec<(f64, f64)> = (0..10)
165 .map(|i| (2.0 * f64::from(i), f64::from(i)))
166 .collect();
167 let last = Beta::new(5)
168 .unwrap()
169 .batch(&pairs)
170 .into_iter()
171 .flatten()
172 .last()
173 .unwrap();
174 assert_relative_eq!(last, 2.0, epsilon = 1e-9);
175 }
176
177 #[test]
178 fn perfect_negative_one() {
179 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (-f64::from(i), f64::from(i))).collect();
180 let last = Beta::new(5)
181 .unwrap()
182 .batch(&pairs)
183 .into_iter()
184 .flatten()
185 .last()
186 .unwrap();
187 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
188 }
189
190 #[test]
191 fn constant_benchmark_yields_zero() {
192 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
193 let last = Beta::new(5)
194 .unwrap()
195 .batch(&pairs)
196 .into_iter()
197 .flatten()
198 .last()
199 .unwrap();
200 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
201 }
202
203 #[test]
204 fn reset_clears_state() {
205 let mut b = Beta::new(5).unwrap();
206 b.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
207 assert!(b.is_ready());
208 b.reset();
209 assert!(!b.is_ready());
210 assert_eq!(b.update((1.0, 1.0)), None);
211 }
212
213 #[test]
214 fn batch_equals_streaming() {
215 let pairs: Vec<(f64, f64)> = (0..60)
216 .map(|i| {
217 let t = f64::from(i);
218 (t.sin() * 2.0 + 0.3 * t.cos(), t.sin())
219 })
220 .collect();
221 let batch = Beta::new(14).unwrap().batch(&pairs);
222 let mut b = Beta::new(14).unwrap();
223 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
224 assert_eq!(batch, streamed);
225 }
226
227 #[test]
228 fn non_finite_input_returns_none() {
229 let mut b = Beta::new(3).unwrap();
230 assert_eq!(b.update((f64::NAN, 1.0)), None);
231 assert_eq!(b.update((1.0, f64::INFINITY)), None);
232 assert_eq!(b.update((1.0, 2.0)), None);
234 assert_eq!(b.update((2.0, 5.0)), None);
235 assert!(b.update((3.0, 7.0)).is_some());
236 }
237}