1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedPairMoments;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
47pub struct RollingCovariance {
48 period: usize,
49 prev: Option<(f64, f64)>,
50 window: VecDeque<(f64, f64)>,
51 moments: ShiftedPairMoments,
52}
53
54impl RollingCovariance {
55 pub fn new(period: usize) -> Result<Self> {
61 if period < 2 {
62 return Err(Error::InvalidPeriod {
63 message: "rolling covariance needs period >= 2",
64 });
65 }
66 if period > crate::error::MAX_PERIOD {
67 return Err(Error::InvalidPeriod {
68 message: crate::error::PERIOD_ABOVE_MAX,
69 });
70 }
71 Ok(Self {
72 period,
73 prev: None,
74 window: VecDeque::with_capacity(period),
75 moments: ShiftedPairMoments::new(),
76 })
77 }
78
79 pub const fn period(&self) -> usize {
81 self.period
82 }
83}
84
85impl Indicator for RollingCovariance {
86 type Input = (f64, f64);
87 type Output = f64;
88
89 #[inline]
90 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
91 let (x, y) = input;
92 if !x.is_finite() || !y.is_finite() {
93 return None;
94 }
95 let Some((px, py)) = self.prev else {
96 self.prev = Some((x, y));
97 return None;
98 };
99 self.prev = Some((x, y));
100 let (rx, ry) = (x - px, y - py);
101 if self.window.len() == self.period {
102 let (ox, oy) = self.window.pop_front().expect("non-empty");
103 self.moments.evict(ox, oy);
104 }
105 self.window.push_back((rx, ry));
106 self.moments.push(rx, ry);
107 if self.moments.needs_reseed(self.period) {
108 self.moments.reseed(self.window.iter().copied());
109 }
110 if self.window.len() < self.period {
111 return None;
112 }
113 Some(self.moments.cov(self.period))
114 }
115
116 fn reset(&mut self) {
117 self.prev = None;
118 self.window.clear();
119 self.moments.reset();
120 }
121
122 #[inline]
123 fn warmup_period(&self) -> usize {
124 self.period + 1
125 }
126
127 #[inline]
128 fn is_ready(&self) -> bool {
129 self.window.len() == self.period
130 }
131
132 #[inline]
133 fn name(&self) -> &'static str {
134 "RollingCovariance"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::BatchExt;
142 use approx::assert_relative_eq;
143
144 #[test]
145 fn rejects_period_below_two() {
146 assert!(RollingCovariance::new(0).is_err());
147 assert!(RollingCovariance::new(1).is_err());
148 assert!(RollingCovariance::new(2).is_ok());
149 }
150
151 #[test]
152 fn accessors_and_metadata() {
153 let rc = RollingCovariance::new(14).unwrap();
154 assert_eq!(rc.period(), 14);
155 assert_eq!(rc.warmup_period(), 15);
156 assert_eq!(rc.name(), "RollingCovariance");
157 assert!(!rc.is_ready());
158 }
159
160 #[test]
161 fn warmup_needs_period_plus_one() {
162 let mut rc = RollingCovariance::new(3).unwrap();
163 assert_eq!(rc.update((1.0, 1.0)), None);
164 assert_eq!(rc.update((2.0, 3.0)), None);
165 assert_eq!(rc.update((3.0, 5.0)), None);
166 assert!(rc.update((4.0, 7.0)).is_some());
167 assert!(rc.is_ready());
168 }
169
170 #[test]
171 fn hand_computed_value() {
172 let pairs = [
176 (0.0, 0.0),
177 (1.0, 2.0),
178 (3.0, 6.0),
179 (6.0, 12.0),
180 (10.0, 20.0),
181 ];
182 let last = RollingCovariance::new(3)
183 .unwrap()
184 .batch(&pairs)
185 .into_iter()
186 .flatten()
187 .last()
188 .unwrap();
189 assert_relative_eq!(last, 4.0 / 3.0, epsilon = 1e-9);
190 }
191
192 #[test]
193 fn opposing_returns_give_negative_covariance() {
194 let pairs: Vec<(f64, f64)> = (0..30)
195 .map(|i| {
196 let x = (f64::from(i) * 0.4).sin() * 10.0;
197 (x, -x)
198 })
199 .collect();
200 let last = RollingCovariance::new(10)
201 .unwrap()
202 .batch(&pairs)
203 .into_iter()
204 .flatten()
205 .last()
206 .unwrap();
207 assert!(last < 0.0, "cov {last}");
208 }
209
210 #[test]
211 fn flat_channel_gives_zero() {
212 let pairs: Vec<(f64, f64)> = (0..20).map(|i| (f64::from(i), 7.0)).collect();
213 let last = RollingCovariance::new(6)
214 .unwrap()
215 .batch(&pairs)
216 .into_iter()
217 .flatten()
218 .last()
219 .unwrap();
220 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
221 }
222
223 #[test]
224 fn reset_clears_state() {
225 let mut rc = RollingCovariance::new(4).unwrap();
226 rc.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 1.0), (4.0, 9.0), (5.0, 2.0)]);
227 assert!(rc.is_ready());
228 rc.reset();
229 assert!(!rc.is_ready());
230 assert_eq!(rc.update((1.0, 1.0)), None);
231 }
232
233 #[test]
234 fn batch_equals_streaming() {
235 let pairs: Vec<(f64, f64)> = (0..60)
236 .map(|i| {
237 let t = f64::from(i);
238 (t.sin() * 4.0, (t * 0.5).cos() * 2.0)
239 })
240 .collect();
241 let batch = RollingCovariance::new(12).unwrap().batch(&pairs);
242 let mut rc = RollingCovariance::new(12).unwrap();
243 let streamed: Vec<_> = pairs.iter().map(|p| rc.update(*p)).collect();
244 assert_eq!(batch, streamed);
245 }
246
247 #[test]
248 fn non_finite_input_returns_none() {
249 let mut rc = RollingCovariance::new(2).unwrap();
250 assert_eq!(rc.update((f64::NAN, 1.0)), None);
251 assert_eq!(rc.update((1.0, f64::INFINITY)), None);
252 assert_eq!(rc.update((1.0, 1.0)), None);
254 assert_eq!(rc.update((2.0, 3.0)), None);
255 assert!(rc.update((3.0, 5.0)).is_some());
256 }
257
258 #[test]
264 fn matches_a_two_pass_reference_over_a_long_stream() {
265 const PERIOD: usize = 20;
266 let series: Vec<(f64, f64)> = (0..2000)
267 .map(|i| {
268 let t = f64::from(i);
269 (
270 1e5 * (1.0 + 0.05 * (t * 0.11).sin()),
271 1e5 * (1.0 + 0.04 * (t * 0.07).cos()),
272 )
273 })
274 .collect();
275
276 let mut ind = RollingCovariance::new(PERIOD).unwrap();
277 let (mut dx, mut dy): (Vec<f64>, Vec<f64>) = (Vec::new(), Vec::new());
278 let mut prev: Option<(f64, f64)> = None;
279 let mut compared = 0_usize;
280 for &(x, y) in &series {
281 let got = ind.update((x, y));
282 if let Some((px, py)) = prev {
283 dx.push(x - px);
284 dy.push(y - py);
285 }
286 prev = Some((x, y));
287 let Some(cov) = got else { continue };
288 let k = dx.len();
289 let (xs, ys) = (&dx[k - PERIOD..], &dy[k - PERIOD..]);
290 let n = PERIOD as f64;
291 let mean_x = xs.iter().sum::<f64>() / n;
292 let mean_y = ys.iter().sum::<f64>() / n;
293 let want = xs
294 .iter()
295 .zip(ys)
296 .map(|(u, v)| (u - mean_x) * (v - mean_y))
297 .sum::<f64>()
298 / n;
299 compared += 1;
300 assert_relative_eq!(cov, want, max_relative = 1e-10);
301 }
302 assert_eq!(compared, series.len() - ind.warmup_period() + 1);
303 }
304}