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, Copy, PartialEq)]
11pub struct CointegrationOutput {
12 pub hedge_ratio: f64,
14 pub spread: f64,
16 pub adf_stat: f64,
21}
22
23#[derive(Debug, Clone)]
63pub struct Cointegration {
64 period: usize,
65 adf_lags: usize,
66 window: VecDeque<(f64, f64)>,
67 moments: ShiftedPairMoments,
68}
69
70impl Cointegration {
71 pub fn new(period: usize, adf_lags: usize) -> Result<Self> {
82 let min_period = 2 * adf_lags + 4;
83 if period < min_period {
84 return Err(Error::InvalidPeriod {
85 message: "cointegration needs period >= 2*adf_lags + 4",
86 });
87 }
88 Ok(Self {
89 period,
90 adf_lags,
91 window: VecDeque::with_capacity(period),
92 moments: ShiftedPairMoments::new(),
93 })
94 }
95
96 pub const fn period(&self) -> usize {
98 self.period
99 }
100
101 pub const fn adf_lags(&self) -> usize {
103 self.adf_lags
104 }
105}
106
107impl Indicator for Cointegration {
108 type Input = (f64, f64);
110 type Output = CointegrationOutput;
111
112 fn update(&mut self, input: (f64, f64)) -> Option<CointegrationOutput> {
113 let (a, b) = input;
114 if !a.is_finite() || !b.is_finite() {
115 return None;
116 }
117 if self.window.len() == self.period {
118 let (oa, ob) = self.window.pop_front().expect("non-empty");
119 self.moments.evict(oa, ob);
120 }
121 self.window.push_back((a, b));
122 self.moments.push(a, b);
123 if self.moments.needs_reseed(self.period) {
124 self.moments.reseed(self.window.iter().copied());
125 }
126 if self.window.len() < self.period {
127 return None;
128 }
129 let mean_a = self.moments.mean_a(self.period);
130 let mean_b = self.moments.mean_b(self.period);
131 let var_b = self.moments.var_b(self.period);
132 let (hedge_ratio, intercept) = if var_b == 0.0 {
133 (0.0, mean_a)
135 } else {
136 let cov = self.moments.cov(self.period);
137 let beta = cov / var_b;
138 (beta, mean_a - beta * mean_b)
139 };
140 let spreads: Vec<f64> = self
142 .window
143 .iter()
144 .map(|&(ai, bi)| ai - (intercept + hedge_ratio * bi))
145 .collect();
146 let spread = *spreads.last().expect("window is full");
147 let adf_stat = adf_no_constant(&spreads, self.adf_lags);
148 Some(CointegrationOutput {
149 hedge_ratio,
150 spread,
151 adf_stat,
152 })
153 }
154
155 fn reset(&mut self) {
156 self.window.clear();
157 self.moments.reset();
158 }
159
160 #[inline]
161 fn warmup_period(&self) -> usize {
162 self.period
163 }
164
165 #[inline]
166 fn is_ready(&self) -> bool {
167 self.window.len() == self.period
168 }
169
170 #[inline]
171 fn name(&self) -> &'static str {
172 "Cointegration"
173 }
174}
175
176fn solve(mut mat: Vec<Vec<f64>>, mut rhs: Vec<f64>) -> Option<Vec<f64>> {
181 let dim = rhs.len();
182 for col in 0..dim {
183 let pivot = mat[col][col];
184 if pivot.abs() < 1e-12 {
185 return None;
186 }
187 let pivot_row = mat[col].clone();
188 for row in (col + 1)..dim {
189 let factor = mat[row][col] / pivot;
190 for (cell, &above) in mat[row].iter_mut().zip(&pivot_row).skip(col) {
191 *cell -= factor * above;
192 }
193 rhs[row] -= factor * rhs[col];
194 }
195 }
196 let mut sol = vec![0.0; dim];
197 for row in (0..dim).rev() {
198 let known: f64 = mat[row]
199 .iter()
200 .zip(&sol)
201 .skip(row + 1)
202 .map(|(coeff, value)| coeff * value)
203 .sum();
204 sol[row] = (rhs[row] - known) / mat[row][row];
205 }
206 Some(sol)
207}
208
209fn adf_no_constant(series: &[f64], lags: usize) -> f64 {
216 let len = series.len();
217 let num_reg = lags + 1; let first = lags + 1; if len <= first {
220 return 0.0;
221 }
222 let num_obs = len - first;
223 if num_obs <= num_reg {
224 return 0.0; }
226 let regressors = |idx: usize| -> Vec<f64> {
227 let mut row = vec![0.0; num_reg];
228 row[0] = series[idx - 1];
229 for lag in 1..=lags {
230 row[lag] = series[idx - lag] - series[idx - lag - 1];
231 }
232 row
233 };
234 let mut xtx = vec![vec![0.0; num_reg]; num_reg];
235 let mut xty = vec![0.0; num_reg];
236 for idx in first..len {
237 let diff = series[idx] - series[idx - 1];
238 let row = regressors(idx);
239 for (ri, &left) in row.iter().enumerate() {
240 xty[ri] += left * diff;
241 for (ci, &right) in row.iter().enumerate() {
242 xtx[ri][ci] += left * right;
243 }
244 }
245 }
246 let Some(theta) = solve(xtx.clone(), xty) else {
247 return 0.0;
248 };
249 let rho = theta[0];
250 let mut rss = 0.0;
251 for idx in first..len {
252 let diff = series[idx] - series[idx - 1];
253 let pred: f64 = regressors(idx)
254 .iter()
255 .zip(&theta)
256 .map(|(coeff, value)| coeff * value)
257 .sum();
258 let resid = diff - pred;
259 rss += resid * resid;
260 }
261 let dof = (num_obs - num_reg) as f64;
262 let sigma2 = rss / dof;
263 let mut unit = vec![0.0; num_reg];
266 unit[0] = 1.0;
267 let inverse = solve(xtx, unit).expect("xtx is non-singular: the coefficient solve succeeded");
268 let var_rho = sigma2 * inverse[0];
269 if var_rho <= 0.0 {
270 return 0.0;
271 }
272 rho / var_rho.sqrt()
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::traits::BatchExt;
279 use approx::assert_relative_eq;
280
281 #[test]
282 fn rejects_too_small_period() {
283 assert!(Cointegration::new(3, 0).is_err()); assert!(Cointegration::new(4, 0).is_ok());
286 assert!(Cointegration::new(5, 1).is_err()); assert!(Cointegration::new(6, 1).is_ok());
288 }
289
290 #[test]
291 fn accessors_and_metadata() {
292 let c = Cointegration::new(30, 2).unwrap();
293 assert_eq!(c.period(), 30);
294 assert_eq!(c.adf_lags(), 2);
295 assert_eq!(c.warmup_period(), 30);
296 assert_eq!(c.name(), "Cointegration");
297 }
298
299 #[test]
300 fn adf_guards_and_degenerate_spread() {
301 assert_eq!(adf_no_constant(&[1.0], 1), 0.0);
303 assert_eq!(adf_no_constant(&[1.0, 2.0, 3.0], 1), 0.0);
305 let geom: Vec<f64> = (0..8).map(|t| 0.5_f64.powi(t)).collect();
308 assert_eq!(adf_no_constant(&geom, 0), 0.0);
309 }
310
311 #[test]
312 fn recovers_hedge_ratio() {
313 let pairs: Vec<(f64, f64)> = (0..60)
315 .map(|t| {
316 let b = 100.0 + f64::from(t);
317 let a = 2.0 * b + 5.0 + 0.4 * (f64::from(t) * 0.9).sin();
318 (a, b)
319 })
320 .collect();
321 let out = Cointegration::new(30, 1)
322 .unwrap()
323 .batch(&pairs)
324 .into_iter()
325 .flatten()
326 .last()
327 .unwrap();
328 assert!(
329 (out.hedge_ratio - 2.0).abs() < 0.1,
330 "beta {}",
331 out.hedge_ratio
332 );
333 }
334
335 #[test]
336 fn stationary_spread_is_strongly_negative() {
337 let pairs: Vec<(f64, f64)> = (0..80)
339 .map(|t| {
340 let b = 50.0 + 0.5 * f64::from(t);
341 let a = 2.0 * b + 1.0 + 0.5 * (f64::from(t) * 0.6).sin();
342 (a, b)
343 })
344 .collect();
345 let out = Cointegration::new(40, 1)
346 .unwrap()
347 .batch(&pairs)
348 .into_iter()
349 .flatten()
350 .last()
351 .unwrap();
352 assert!(out.adf_stat < -2.0, "adf {}", out.adf_stat);
353 }
354
355 #[test]
356 fn perfect_cointegration_has_zero_spread_and_defined_ratio() {
357 let pairs: Vec<(f64, f64)> = (0..40)
359 .map(|t| {
360 let b = 100.0 + f64::from(t);
361 (2.0 * b + 5.0, b)
362 })
363 .collect();
364 let out = Cointegration::new(20, 1)
365 .unwrap()
366 .batch(&pairs)
367 .into_iter()
368 .flatten()
369 .last()
370 .unwrap();
371 assert_relative_eq!(out.hedge_ratio, 2.0, epsilon = 1e-9);
372 assert_relative_eq!(out.spread, 0.0, epsilon = 1e-6);
373 assert_relative_eq!(out.adf_stat, 0.0, epsilon = 1e-12);
374 }
375
376 #[test]
377 fn flat_b_falls_back_to_level() {
378 let pairs: Vec<(f64, f64)> = (0..20)
380 .map(|t| (10.0 + 0.3 * (f64::from(t) * 0.5).sin(), 7.0))
381 .collect();
382 let out = Cointegration::new(10, 0)
383 .unwrap()
384 .batch(&pairs)
385 .into_iter()
386 .flatten()
387 .last()
388 .unwrap();
389 assert_relative_eq!(out.hedge_ratio, 0.0, epsilon = 1e-12);
390 }
391
392 #[test]
393 fn plain_dickey_fuller_lags_zero() {
394 let pairs: Vec<(f64, f64)> = (0..40)
396 .map(|t| {
397 let b = 20.0 + 0.4 * f64::from(t);
398 let a = 1.5 * b + 0.6 * (f64::from(t) * 0.7).sin();
399 (a, b)
400 })
401 .collect();
402 let out = Cointegration::new(20, 0)
403 .unwrap()
404 .batch(&pairs)
405 .into_iter()
406 .flatten()
407 .last()
408 .unwrap();
409 assert!((out.hedge_ratio - 1.5).abs() < 0.1);
410 assert!(out.adf_stat < 0.0);
411 }
412
413 #[test]
414 fn reset_clears_state() {
415 let mut c = Cointegration::new(10, 1).unwrap();
416 for t in 0..20 {
417 let b = 100.0 + f64::from(t);
418 c.update((2.0 * b + (f64::from(t) * 0.5).sin(), b));
419 }
420 assert!(c.is_ready());
421 c.reset();
422 assert!(!c.is_ready());
423 assert_eq!(c.update((1.0, 1.0)), None);
424 }
425
426 #[test]
427 fn batch_equals_streaming() {
428 let pairs: Vec<(f64, f64)> = (0..80)
429 .map(|t| {
430 let b = 30.0 + 0.7 * f64::from(t);
431 let a = 1.8 * b + 2.0 + 0.5 * (f64::from(t) * 0.4).sin();
432 (a, b)
433 })
434 .collect();
435 let batch = Cointegration::new(25, 2).unwrap().batch(&pairs);
436 let mut c = Cointegration::new(25, 2).unwrap();
437 let streamed: Vec<_> = pairs.iter().map(|p| c.update(*p)).collect();
438 assert_eq!(batch, streamed);
439 }
440
441 #[test]
442 fn non_finite_input_returns_none() {
443 let mut c = Cointegration::new(4, 0).unwrap();
444 assert_eq!(c.update((f64::NAN, 1.0)), None);
445 assert_eq!(c.update((1.0, f64::INFINITY)), None);
446 assert_eq!(c.update((1.0, 2.0)), None);
448 assert_eq!(c.update((2.0, 5.0)), None);
449 assert_eq!(c.update((3.0, 7.0)), None);
450 assert!(c.update((4.0, 11.0)).is_some());
451 }
452}