wickra_core/indicators/
pairwise_beta.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
60pub struct PairwiseBeta {
61 period: usize,
62 prev: Option<(f64, f64)>,
63 window: VecDeque<(f64, f64)>,
64 sum_a: f64,
65 sum_b: f64,
66 sum_bb: f64,
67 sum_ab: f64,
68}
69
70impl PairwiseBeta {
71 pub fn new(period: usize) -> Result<Self> {
77 if period < 2 {
78 return Err(Error::InvalidPeriod {
79 message: "pairwise beta needs period >= 2",
80 });
81 }
82 Ok(Self {
83 period,
84 prev: None,
85 window: VecDeque::with_capacity(period),
86 sum_a: 0.0,
87 sum_b: 0.0,
88 sum_bb: 0.0,
89 sum_ab: 0.0,
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.sum_a -= oa;
102 self.sum_b -= ob;
103 self.sum_bb -= ob * ob;
104 self.sum_ab -= oa * ob;
105 }
106 self.window.push_back((ra, rb));
107 self.sum_a += ra;
108 self.sum_b += rb;
109 self.sum_bb += rb * rb;
110 self.sum_ab += ra * rb;
111 if self.window.len() < self.period {
112 return None;
113 }
114 let n = self.period as f64;
115 let mean_a = self.sum_a / n;
116 let mean_b = self.sum_b / n;
117 let var_b = (self.sum_bb / n - mean_b * mean_b).max(0.0);
118 let cov = self.sum_ab / n - mean_a * mean_b;
119 if var_b == 0.0 {
120 return Some(0.0);
122 }
123 Some(cov / var_b)
124 }
125}
126
127impl Indicator for PairwiseBeta {
128 type Input = (f64, f64);
130 type Output = f64;
131
132 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
133 let (a, b) = input;
134 if !(a > 0.0 && b > 0.0 && a.is_finite() && b.is_finite()) {
135 self.prev = None;
137 return None;
138 }
139 let Some((pa, pb)) = self.prev else {
140 self.prev = Some((a, b));
141 return None;
142 };
143 self.prev = Some((a, b));
144 let ra = (a / pa).ln();
145 let rb = (b / pb).ln();
146 self.push_return(ra, rb)
147 }
148
149 fn reset(&mut self) {
150 self.prev = None;
151 self.window.clear();
152 self.sum_a = 0.0;
153 self.sum_b = 0.0;
154 self.sum_bb = 0.0;
155 self.sum_ab = 0.0;
156 }
157
158 fn warmup_period(&self) -> usize {
159 self.period + 1
161 }
162
163 fn is_ready(&self) -> bool {
164 self.window.len() == self.period
165 }
166
167 fn name(&self) -> &'static str {
168 "PairwiseBeta"
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::traits::BatchExt;
176 use approx::assert_relative_eq;
177
178 #[test]
179 fn rejects_period_below_two() {
180 assert!(PairwiseBeta::new(0).is_err());
181 assert!(PairwiseBeta::new(1).is_err());
182 assert!(PairwiseBeta::new(2).is_ok());
183 }
184
185 #[test]
186 fn accessors_and_metadata() {
187 let b = PairwiseBeta::new(14).unwrap();
188 assert_eq!(b.period(), 14);
189 assert_eq!(b.warmup_period(), 15);
190 assert_eq!(b.name(), "PairwiseBeta");
191 }
192
193 #[test]
194 fn squared_price_gives_beta_two() {
195 let pairs: Vec<(f64, f64)> = (0..20)
197 .map(|i| {
198 let b = 100.0 + 10.0 * (f64::from(i) * 0.5).sin();
199 (b * b, b)
200 })
201 .collect();
202 let last = PairwiseBeta::new(5)
203 .unwrap()
204 .batch(&pairs)
205 .into_iter()
206 .flatten()
207 .last()
208 .unwrap();
209 assert_relative_eq!(last, 2.0, epsilon = 1e-9);
210 }
211
212 #[test]
213 fn inverse_price_gives_beta_minus_one() {
214 let pairs: Vec<(f64, f64)> = (0..20)
216 .map(|i| {
217 let b = 100.0 + 10.0 * (f64::from(i) * 0.5).sin();
218 (1.0 / b, b)
219 })
220 .collect();
221 let last = PairwiseBeta::new(5)
222 .unwrap()
223 .batch(&pairs)
224 .into_iter()
225 .flatten()
226 .last()
227 .unwrap();
228 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
229 }
230
231 #[test]
232 fn flat_benchmark_returns_zero() {
233 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (100.0 * 1.01_f64.powi(i), 7.0)).collect();
235 let last = PairwiseBeta::new(5)
236 .unwrap()
237 .batch(&pairs)
238 .into_iter()
239 .flatten()
240 .last()
241 .unwrap();
242 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
243 }
244
245 #[test]
246 fn bad_tick_breaks_return_chain() {
247 let mut b = PairwiseBeta::new(3).unwrap();
248 assert_eq!(b.update((100.0, 100.0)), None);
250 assert_eq!(b.update((101.0, 101.0)), None);
251 assert_eq!(b.update((0.0, 50.0)), None); assert!(!b.is_ready());
253 assert_eq!(b.update((f64::NAN, 50.0)), None);
255 assert!(!b.is_ready());
256 for i in 0..5 {
258 let p = 100.0 * 1.01_f64.powi(i);
259 b.update((p * p, p));
260 }
261 assert!(b.is_ready());
262 }
263
264 #[test]
265 fn reset_clears_state() {
266 let mut b = PairwiseBeta::new(3).unwrap();
267 for i in 0..6 {
268 let p = 100.0 * 1.01_f64.powi(i);
269 b.update((p * p, p));
270 }
271 assert!(b.is_ready());
272 b.reset();
273 assert!(!b.is_ready());
274 assert_eq!(b.update((100.0, 100.0)), None);
275 }
276
277 #[test]
278 fn batch_equals_streaming() {
279 let pairs: Vec<(f64, f64)> = (0..60)
280 .map(|i| {
281 let t = f64::from(i);
282 let b = 100.0 + 5.0 * t.sin();
283 let a = 100.0 + 3.0 * t.sin() + 0.5 * t.cos();
284 (a, b)
285 })
286 .collect();
287 let batch = PairwiseBeta::new(14).unwrap().batch(&pairs);
288 let mut b = PairwiseBeta::new(14).unwrap();
289 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
290 assert_eq!(batch, streamed);
291 }
292}