wickra_core/indicators/
pairwise_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)]
61pub struct PairwiseBeta {
62 period: usize,
63 prev: Option<(f64, f64)>,
64 window: VecDeque<(f64, f64)>,
65 moments: ShiftedPairMoments,
66}
67
68impl PairwiseBeta {
69 pub fn new(period: usize) -> Result<Self> {
75 if period < 2 {
76 return Err(Error::InvalidPeriod {
77 message: "pairwise beta needs period >= 2",
78 });
79 }
80 if period > crate::error::MAX_PERIOD {
81 return Err(Error::InvalidPeriod {
82 message: crate::error::PERIOD_ABOVE_MAX,
83 });
84 }
85 Ok(Self {
86 period,
87 prev: None,
88 window: VecDeque::with_capacity(period),
89 moments: ShiftedPairMoments::new(),
90 })
91 }
92
93 pub const fn period(&self) -> usize {
95 self.period
96 }
97
98 fn push_return(&mut self, ra: f64, rb: f64) -> Option<f64> {
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((ra, rb));
104 self.moments.push(ra, rb);
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
121impl Indicator for PairwiseBeta {
122 type Input = (f64, f64);
124 type Output = f64;
125
126 #[inline]
127 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
128 let (a, b) = input;
129 if !(a > 0.0 && b > 0.0 && a.is_finite() && b.is_finite()) {
130 return None;
136 }
137 let Some((pa, pb)) = self.prev else {
138 self.prev = Some((a, b));
139 return None;
140 };
141 self.prev = Some((a, b));
142 let ra = (a / pa).ln();
143 let rb = (b / pb).ln();
144 self.push_return(ra, rb)
145 }
146
147 fn reset(&mut self) {
148 self.prev = None;
149 self.window.clear();
150 self.moments.reset();
151 }
152
153 #[inline]
154 fn warmup_period(&self) -> usize {
155 self.period + 1
157 }
158
159 #[inline]
160 fn is_ready(&self) -> bool {
161 self.window.len() == self.period
162 }
163
164 #[inline]
165 fn name(&self) -> &'static str {
166 "PairwiseBeta"
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::traits::BatchExt;
174 use approx::assert_relative_eq;
175
176 #[test]
177 fn rejects_period_below_two() {
178 assert!(PairwiseBeta::new(0).is_err());
179 assert!(PairwiseBeta::new(1).is_err());
180 assert!(PairwiseBeta::new(2).is_ok());
181 }
182
183 #[test]
184 fn accessors_and_metadata() {
185 let b = PairwiseBeta::new(14).unwrap();
186 assert_eq!(b.period(), 14);
187 assert_eq!(b.warmup_period(), 15);
188 assert_eq!(b.name(), "PairwiseBeta");
189 }
190
191 #[test]
192 fn squared_price_gives_beta_two() {
193 let pairs: Vec<(f64, f64)> = (0..20)
195 .map(|i| {
196 let b = 100.0 + 10.0 * (f64::from(i) * 0.5).sin();
197 (b * b, b)
198 })
199 .collect();
200 let last = PairwiseBeta::new(5)
201 .unwrap()
202 .batch(&pairs)
203 .into_iter()
204 .flatten()
205 .last()
206 .unwrap();
207 assert_relative_eq!(last, 2.0, epsilon = 1e-9);
208 }
209
210 #[test]
211 fn inverse_price_gives_beta_minus_one() {
212 let pairs: Vec<(f64, f64)> = (0..20)
214 .map(|i| {
215 let b = 100.0 + 10.0 * (f64::from(i) * 0.5).sin();
216 (1.0 / b, b)
217 })
218 .collect();
219 let last = PairwiseBeta::new(5)
220 .unwrap()
221 .batch(&pairs)
222 .into_iter()
223 .flatten()
224 .last()
225 .unwrap();
226 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
227 }
228
229 #[test]
230 fn flat_benchmark_returns_zero() {
231 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (100.0 * 1.01_f64.powi(i), 7.0)).collect();
233 let last = PairwiseBeta::new(5)
234 .unwrap()
235 .batch(&pairs)
236 .into_iter()
237 .flatten()
238 .last()
239 .unwrap();
240 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
241 }
242
243 #[test]
244 fn bad_tick_breaks_return_chain() {
245 let mut b = PairwiseBeta::new(3).unwrap();
246 assert_eq!(b.update((100.0, 100.0)), None);
248 assert_eq!(b.update((101.0, 101.0)), None);
249 assert_eq!(b.update((0.0, 50.0)), None); assert!(!b.is_ready());
251 assert_eq!(b.update((f64::NAN, 50.0)), None);
253 assert!(!b.is_ready());
254 for i in 0..5 {
256 let p = 100.0 * 1.01_f64.powi(i);
257 b.update((p * p, p));
258 }
259 assert!(b.is_ready());
260 }
261
262 #[test]
263 fn reset_clears_state() {
264 let mut b = PairwiseBeta::new(3).unwrap();
265 for i in 0..6 {
266 let p = 100.0 * 1.01_f64.powi(i);
267 b.update((p * p, p));
268 }
269 assert!(b.is_ready());
270 b.reset();
271 assert!(!b.is_ready());
272 assert_eq!(b.update((100.0, 100.0)), None);
273 }
274
275 #[test]
276 fn batch_equals_streaming() {
277 let pairs: Vec<(f64, f64)> = (0..60)
278 .map(|i| {
279 let t = f64::from(i);
280 let b = 100.0 + 5.0 * t.sin();
281 let a = 100.0 + 3.0 * t.sin() + 0.5 * t.cos();
282 (a, b)
283 })
284 .collect();
285 let batch = PairwiseBeta::new(14).unwrap().batch(&pairs);
286 let mut b = PairwiseBeta::new(14).unwrap();
287 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
288 assert_eq!(batch, streamed);
289 }
290}