diff --git a/crates/wickra-core/src/indicators/beta.rs b/crates/wickra-core/src/indicators/beta.rs index 95763049..3d7be811 100644 --- a/crates/wickra-core/src/indicators/beta.rs +++ b/crates/wickra-core/src/indicators/beta.rs @@ -92,6 +92,9 @@ impl Indicator for Beta { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.window.len() == self.period { let (oa, ob) = self.window.pop_front().expect("non-empty"); self.sum_a -= oa; @@ -225,4 +228,15 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut b = Beta::new(3).unwrap(); + assert_eq!(b.update((f64::NAN, 1.0)), None); + assert_eq!(b.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(b.update((1.0, 2.0)), None); + assert_eq!(b.update((2.0, 5.0)), None); + assert!(b.update((3.0, 7.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/beta_neutral_spread.rs b/crates/wickra-core/src/indicators/beta_neutral_spread.rs index 52d8e882..8d43bd86 100644 --- a/crates/wickra-core/src/indicators/beta_neutral_spread.rs +++ b/crates/wickra-core/src/indicators/beta_neutral_spread.rs @@ -88,6 +88,9 @@ impl Indicator for BetaNeutralSpread { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.window.len() == self.period { let (oa, ob) = self.window.pop_front().expect("non-empty"); self.sum_a -= oa; @@ -244,4 +247,15 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| s.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut s = BetaNeutralSpread::new(3).unwrap(); + assert_eq!(s.update((f64::NAN, 1.0)), None); + assert_eq!(s.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(s.update((1.0, 2.0)), None); + assert_eq!(s.update((2.0, 5.0)), None); + assert!(s.update((3.0, 7.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/cointegration.rs b/crates/wickra-core/src/indicators/cointegration.rs index f307b0d8..dfee22b2 100644 --- a/crates/wickra-core/src/indicators/cointegration.rs +++ b/crates/wickra-core/src/indicators/cointegration.rs @@ -116,6 +116,9 @@ impl Indicator for Cointegration { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.window.len() == self.period { let (oa, ob) = self.window.pop_front().expect("non-empty"); self.sum_a -= oa; @@ -443,4 +446,16 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| c.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut c = Cointegration::new(4, 0).unwrap(); + assert_eq!(c.update((f64::NAN, 1.0)), None); + assert_eq!(c.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(c.update((1.0, 2.0)), None); + assert_eq!(c.update((2.0, 5.0)), None); + assert_eq!(c.update((3.0, 7.0)), None); + assert!(c.update((4.0, 11.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/distance_ssd.rs b/crates/wickra-core/src/indicators/distance_ssd.rs index f338e278..ac0db62e 100644 --- a/crates/wickra-core/src/indicators/distance_ssd.rs +++ b/crates/wickra-core/src/indicators/distance_ssd.rs @@ -76,6 +76,9 @@ impl Indicator for DistanceSsd { type Output = f64; fn update(&mut self, input: (f64, f64)) -> Option { + if !input.0.is_finite() || !input.1.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -232,4 +235,15 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| d.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut d = DistanceSsd::new(3).unwrap(); + assert_eq!(d.update((f64::NAN, 1.0)), None); + assert_eq!(d.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(d.update((1.0, 1.0)), None); + assert_eq!(d.update((2.0, 4.0)), None); + assert!(d.update((3.0, 9.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/granger_causality.rs b/crates/wickra-core/src/indicators/granger_causality.rs index 30c48517..27636198 100644 --- a/crates/wickra-core/src/indicators/granger_causality.rs +++ b/crates/wickra-core/src/indicators/granger_causality.rs @@ -96,6 +96,9 @@ impl Indicator for GrangerCausality { type Output = f64; fn update(&mut self, input: (f64, f64)) -> Option { + if !input.0.is_finite() || !input.1.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -332,4 +335,16 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| g.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut g = GrangerCausality::new(5, 1).unwrap(); + assert_eq!(g.update((f64::NAN, 1.0)), None); + assert_eq!(g.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + for t in 0..4 { + assert_eq!(g.update((f64::from(t), f64::from(t) * 0.5)), None); + } + assert!(g.update((4.0, 2.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/hasbrouck_information_share.rs b/crates/wickra-core/src/indicators/hasbrouck_information_share.rs index a74e2c54..a7ea3d1f 100644 --- a/crates/wickra-core/src/indicators/hasbrouck_information_share.rs +++ b/crates/wickra-core/src/indicators/hasbrouck_information_share.rs @@ -87,6 +87,9 @@ impl Indicator for HasbrouckInformationShare { fn update(&mut self, input: (f64, f64)) -> Option { let (x, y) = input; + if !x.is_finite() || !y.is_finite() { + return None; + } let Some((px, py)) = self.prev else { self.prev = Some((x, y)); return None; @@ -248,4 +251,15 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut h = HasbrouckInformationShare::new(2).unwrap(); + assert_eq!(h.update((f64::NAN, 1.0)), None); + assert_eq!(h.update((1.0, f64::INFINITY)), None); + // First finite tick seeds prev; two more returns fill the window. + assert_eq!(h.update((1.0, 1.0)), None); + assert_eq!(h.update((2.0, 3.0)), None); + assert!(h.update((3.0, 4.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/kendall_tau.rs b/crates/wickra-core/src/indicators/kendall_tau.rs index ccf4dc2f..6c9d8612 100644 --- a/crates/wickra-core/src/indicators/kendall_tau.rs +++ b/crates/wickra-core/src/indicators/kendall_tau.rs @@ -130,6 +130,9 @@ impl Indicator for KendallTau { type Output = f64; fn update(&mut self, input: (f64, f64)) -> Option { + if !input.0.is_finite() || !input.1.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -293,4 +296,14 @@ mod tests { let v = k.update((3.0, 3.0)).unwrap(); assert!((-1.0..=1.0).contains(&v), "got {v}"); } + + #[test] + fn non_finite_input_returns_none() { + let mut k = KendallTau::new(2).unwrap(); + assert_eq!(k.update((f64::NAN, 1.0)), None); + assert_eq!(k.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(k.update((1.0, 2.0)), None); + assert!(k.update((2.0, 5.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/lead_lag_cross_correlation.rs b/crates/wickra-core/src/indicators/lead_lag_cross_correlation.rs index dad02f84..2a303f5f 100644 --- a/crates/wickra-core/src/indicators/lead_lag_cross_correlation.rs +++ b/crates/wickra-core/src/indicators/lead_lag_cross_correlation.rs @@ -152,6 +152,9 @@ impl Indicator for LeadLagCrossCorrelation { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.a_buf.len() == self.len { self.a_buf.pop_front(); self.b_buf.pop_front(); @@ -321,4 +324,17 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| ll.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + // len = window + 2*max_lag = 2 + 2 = 4 finite ticks fill the buffers. + let mut ll = LeadLagCrossCorrelation::new(2, 1).unwrap(); + assert_eq!(ll.update((f64::NAN, 1.0)), None); + assert_eq!(ll.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(ll.update((1.0, 2.0)), None); + assert_eq!(ll.update((2.0, 1.0)), None); + assert_eq!(ll.update((3.0, 4.0)), None); + assert!(ll.update((4.0, 2.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/ou_half_life.rs b/crates/wickra-core/src/indicators/ou_half_life.rs index c4ccb10d..c9c40bc0 100644 --- a/crates/wickra-core/src/indicators/ou_half_life.rs +++ b/crates/wickra-core/src/indicators/ou_half_life.rs @@ -80,6 +80,9 @@ impl Indicator for OuHalfLife { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -242,4 +245,16 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| hl.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut hl = OuHalfLife::new(4).unwrap(); + assert_eq!(hl.update((f64::NAN, 1.0)), None); + assert_eq!(hl.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(hl.update((1.0, 0.0)), None); + assert_eq!(hl.update((2.0, 0.0)), None); + assert_eq!(hl.update((3.0, 0.0)), None); + assert!(hl.update((4.0, 0.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/pearson_correlation.rs b/crates/wickra-core/src/indicators/pearson_correlation.rs index bc0f7fe7..39a6d573 100644 --- a/crates/wickra-core/src/indicators/pearson_correlation.rs +++ b/crates/wickra-core/src/indicators/pearson_correlation.rs @@ -89,6 +89,9 @@ impl Indicator for PearsonCorrelation { fn update(&mut self, input: (f64, f64)) -> Option { let (x, y) = input; + if !x.is_finite() || !y.is_finite() { + return None; + } if self.window.len() == self.period { let (ox, oy) = self.window.pop_front().expect("non-empty"); self.sum_x -= ox; @@ -243,4 +246,15 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut p = PearsonCorrelation::new(3).unwrap(); + assert_eq!(p.update((f64::NAN, 1.0)), None); + assert_eq!(p.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(p.update((1.0, 2.0)), None); + assert_eq!(p.update((2.0, 5.0)), None); + assert!(p.update((3.0, 7.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/rolling_correlation.rs b/crates/wickra-core/src/indicators/rolling_correlation.rs index b6d53487..50e173f9 100644 --- a/crates/wickra-core/src/indicators/rolling_correlation.rs +++ b/crates/wickra-core/src/indicators/rolling_correlation.rs @@ -90,6 +90,9 @@ impl Indicator for RollingCorrelation { fn update(&mut self, input: (f64, f64)) -> Option { let (x, y) = input; + if !x.is_finite() || !y.is_finite() { + return None; + } let Some((px, py)) = self.prev else { // First level in each channel: store it, no return yet. self.prev = Some((x, y)); @@ -272,4 +275,15 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| rc.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut rc = RollingCorrelation::new(2).unwrap(); + assert_eq!(rc.update((f64::NAN, 1.0)), None); + assert_eq!(rc.update((1.0, f64::INFINITY)), None); + // First finite tick seeds prev; two more returns fill the window. + assert_eq!(rc.update((1.0, 1.0)), None); + assert_eq!(rc.update((2.0, 3.0)), None); + assert!(rc.update((3.0, 5.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/rolling_covariance.rs b/crates/wickra-core/src/indicators/rolling_covariance.rs index 42e67c96..80b8b5cc 100644 --- a/crates/wickra-core/src/indicators/rolling_covariance.rs +++ b/crates/wickra-core/src/indicators/rolling_covariance.rs @@ -86,6 +86,9 @@ impl Indicator for RollingCovariance { fn update(&mut self, input: (f64, f64)) -> Option { let (x, y) = input; + if !x.is_finite() || !y.is_finite() { + return None; + } let Some((px, py)) = self.prev else { self.prev = Some((x, y)); return None; @@ -240,4 +243,15 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| rc.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut rc = RollingCovariance::new(2).unwrap(); + assert_eq!(rc.update((f64::NAN, 1.0)), None); + assert_eq!(rc.update((1.0, f64::INFINITY)), None); + // First finite tick seeds prev; two more returns fill the window. + assert_eq!(rc.update((1.0, 1.0)), None); + assert_eq!(rc.update((2.0, 3.0)), None); + assert!(rc.update((3.0, 5.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/spearman_correlation.rs b/crates/wickra-core/src/indicators/spearman_correlation.rs index 4eee492c..688c391f 100644 --- a/crates/wickra-core/src/indicators/spearman_correlation.rs +++ b/crates/wickra-core/src/indicators/spearman_correlation.rs @@ -123,6 +123,9 @@ impl Indicator for SpearmanCorrelation { type Output = f64; fn update(&mut self, input: (f64, f64)) -> Option { + if !input.0.is_finite() || !input.1.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -311,4 +314,14 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut s = SpearmanCorrelation::new(2).unwrap(); + assert_eq!(s.update((f64::NAN, 1.0)), None); + assert_eq!(s.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(s.update((1.0, 2.0)), None); + assert!(s.update((2.0, 5.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/spread_ar1_coefficient.rs b/crates/wickra-core/src/indicators/spread_ar1_coefficient.rs index 3f2d89ea..fbfbfca2 100644 --- a/crates/wickra-core/src/indicators/spread_ar1_coefficient.rs +++ b/crates/wickra-core/src/indicators/spread_ar1_coefficient.rs @@ -89,6 +89,9 @@ impl Indicator for SpreadAr1Coefficient { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -248,4 +251,16 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| ar1.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut ar1 = SpreadAr1Coefficient::new(4).unwrap(); + assert_eq!(ar1.update((f64::NAN, 1.0)), None); + assert_eq!(ar1.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(ar1.update((1.0, 0.0)), None); + assert_eq!(ar1.update((2.0, 0.0)), None); + assert_eq!(ar1.update((3.0, 0.0)), None); + assert!(ar1.update((4.0, 0.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/spread_hurst.rs b/crates/wickra-core/src/indicators/spread_hurst.rs index 8182fe97..ef5784ac 100644 --- a/crates/wickra-core/src/indicators/spread_hurst.rs +++ b/crates/wickra-core/src/indicators/spread_hurst.rs @@ -84,6 +84,9 @@ impl Indicator for SpreadHurst { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -268,4 +271,16 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut h = SpreadHurst::new(8).unwrap(); + assert_eq!(h.update((f64::NAN, 1.0)), None); + assert_eq!(h.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + for t in 0..7 { + assert_eq!(h.update((f64::from(t), 0.0)), None); + } + assert!(h.update((7.0, 0.0)).is_some()); + } } diff --git a/crates/wickra-core/src/indicators/variance_ratio.rs b/crates/wickra-core/src/indicators/variance_ratio.rs index e4731c63..5be115ea 100644 --- a/crates/wickra-core/src/indicators/variance_ratio.rs +++ b/crates/wickra-core/src/indicators/variance_ratio.rs @@ -98,6 +98,9 @@ impl Indicator for VarianceRatio { fn update(&mut self, input: (f64, f64)) -> Option { let (a, b) = input; + if !a.is_finite() || !b.is_finite() { + return None; + } if self.window.len() == self.period { self.window.pop_front(); } @@ -264,4 +267,16 @@ mod tests { let streamed: Vec<_> = pairs.iter().map(|p| vr.update(*p)).collect(); assert_eq!(batch, streamed); } + + #[test] + fn non_finite_input_returns_none() { + let mut vr = VarianceRatio::new(4, 2).unwrap(); + assert_eq!(vr.update((f64::NAN, 1.0)), None); + assert_eq!(vr.update((1.0, f64::INFINITY)), None); + // The rejected ticks leave no trace: a fresh window still warms up. + assert_eq!(vr.update((1.0, 0.0)), None); + assert_eq!(vr.update((2.0, 0.0)), None); + assert_eq!(vr.update((3.0, 0.0)), None); + assert!(vr.update((4.0, 0.0)).is_some()); + } }