wickra_core/indicators/
spread_ar1_coefficient.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
56pub struct SpreadAr1Coefficient {
57 period: usize,
58 window: VecDeque<f64>,
59}
60
61impl SpreadAr1Coefficient {
62 pub fn new(period: usize) -> Result<Self> {
69 if period < 3 {
70 return Err(Error::InvalidPeriod {
71 message: "AR(1) spread coefficient needs period >= 3",
72 });
73 }
74 Ok(Self {
75 period,
76 window: VecDeque::with_capacity(period),
77 })
78 }
79
80 pub const fn period(&self) -> usize {
82 self.period
83 }
84}
85
86impl Indicator for SpreadAr1Coefficient {
87 type Input = (f64, f64);
88 type Output = f64;
89
90 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
91 let (a, b) = input;
92 if !a.is_finite() || !b.is_finite() {
93 return None;
94 }
95 if self.window.len() == self.period {
96 self.window.pop_front();
97 }
98 self.window.push_back(a - b);
99 if self.window.len() < self.period {
100 return None;
101 }
102 let spreads: Vec<f64> = self.window.iter().copied().collect();
104 let count = (spreads.len() - 1) as f64;
105 let mut sum_level = 0.0;
106 let mut sum_next = 0.0;
107 let mut sum_ll = 0.0;
108 let mut sum_ln = 0.0;
109 for pair in spreads.windows(2) {
110 let level = pair[0];
111 let next = pair[1];
112 sum_level += level;
113 sum_next += next;
114 sum_ll += level * level;
115 sum_ln += level * next;
116 }
117 let mean_level = sum_level / count;
118 let mean_next = sum_next / count;
119 let var_level = sum_ll / count - mean_level * mean_level;
120 if var_level <= 0.0 {
121 return Some(0.0);
123 }
124 let cov = sum_ln / count - mean_level * mean_next;
125 Some(cov / var_level)
126 }
127
128 fn reset(&mut self) {
129 self.window.clear();
130 }
131
132 fn warmup_period(&self) -> usize {
133 self.period
134 }
135
136 fn is_ready(&self) -> bool {
137 self.window.len() == self.period
138 }
139
140 fn name(&self) -> &'static str {
141 "SpreadAr1Coefficient"
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::traits::BatchExt;
149 use approx::assert_relative_eq;
150
151 #[test]
152 fn rejects_period_below_three() {
153 assert!(SpreadAr1Coefficient::new(2).is_err());
154 assert!(SpreadAr1Coefficient::new(3).is_ok());
155 }
156
157 #[test]
158 fn accessors_and_metadata() {
159 let ar1 = SpreadAr1Coefficient::new(30).unwrap();
160 assert_eq!(ar1.period(), 30);
161 assert_eq!(ar1.warmup_period(), 30);
162 assert_eq!(ar1.name(), "SpreadAr1Coefficient");
163 assert!(!ar1.is_ready());
164 }
165
166 #[test]
167 fn warmup_returns_none() {
168 let mut ar1 = SpreadAr1Coefficient::new(4).unwrap();
169 assert_eq!(ar1.update((1.0, 0.0)), None);
170 assert_eq!(ar1.update((2.0, 0.0)), None);
171 assert_eq!(ar1.update((3.0, 0.0)), None);
172 assert!(ar1.update((4.0, 0.0)).is_some());
173 assert!(ar1.is_ready());
174 }
175
176 #[test]
177 fn mean_reverting_spread_has_rho_below_one() {
178 let pairs: Vec<(f64, f64)> = (0..120)
180 .map(|t| {
181 let b = 100.0 + f64::from(t);
182 let a = b + 2.0 * (f64::from(t) * 0.9).sin();
183 (a, b)
184 })
185 .collect();
186 let last = SpreadAr1Coefficient::new(40)
187 .unwrap()
188 .batch(&pairs)
189 .into_iter()
190 .flatten()
191 .last()
192 .unwrap();
193 assert!(last > 0.0 && last < 1.0, "rho {last}");
194 }
195
196 #[test]
197 fn random_walk_spread_has_rho_near_one() {
198 let pairs: Vec<(f64, f64)> = (0..40)
201 .map(|t| (2.0 * f64::from(t), f64::from(t)))
202 .collect();
203 let last = SpreadAr1Coefficient::new(20)
204 .unwrap()
205 .batch(&pairs)
206 .into_iter()
207 .flatten()
208 .last()
209 .unwrap();
210 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
211 }
212
213 #[test]
214 fn flat_spread_returns_zero() {
215 let pairs: Vec<(f64, f64)> = (0..30)
217 .map(|t| (5.0 + f64::from(t), f64::from(t)))
218 .collect();
219 let last = SpreadAr1Coefficient::new(10)
220 .unwrap()
221 .batch(&pairs)
222 .into_iter()
223 .flatten()
224 .last()
225 .unwrap();
226 assert_eq!(last, 0.0);
227 }
228
229 #[test]
230 fn reset_clears_state() {
231 let mut ar1 = SpreadAr1Coefficient::new(5).unwrap();
232 for t in 0..10 {
233 ar1.update((f64::from(t) + (f64::from(t) * 0.7).sin(), f64::from(t)));
234 }
235 assert!(ar1.is_ready());
236 ar1.reset();
237 assert!(!ar1.is_ready());
238 assert_eq!(ar1.update((1.0, 0.0)), None);
239 }
240
241 #[test]
242 fn batch_equals_streaming() {
243 let pairs: Vec<(f64, f64)> = (0..80)
244 .map(|t| {
245 let b = 50.0 + 0.5 * f64::from(t);
246 (b + (f64::from(t) * 0.6).sin(), b)
247 })
248 .collect();
249 let batch = SpreadAr1Coefficient::new(25).unwrap().batch(&pairs);
250 let mut ar1 = SpreadAr1Coefficient::new(25).unwrap();
251 let streamed: Vec<_> = pairs.iter().map(|p| ar1.update(*p)).collect();
252 assert_eq!(batch, streamed);
253 }
254
255 #[test]
256 fn non_finite_input_returns_none() {
257 let mut ar1 = SpreadAr1Coefficient::new(4).unwrap();
258 assert_eq!(ar1.update((f64::NAN, 1.0)), None);
259 assert_eq!(ar1.update((1.0, f64::INFINITY)), None);
260 assert_eq!(ar1.update((1.0, 0.0)), None);
262 assert_eq!(ar1.update((2.0, 0.0)), None);
263 assert_eq!(ar1.update((3.0, 0.0)), None);
264 assert!(ar1.update((4.0, 0.0)).is_some());
265 }
266}