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 self.window.len() == self.period {
93 self.window.pop_front();
94 }
95 self.window.push_back(a - b);
96 if self.window.len() < self.period {
97 return None;
98 }
99 let spreads: Vec<f64> = self.window.iter().copied().collect();
101 let count = (spreads.len() - 1) as f64;
102 let mut sum_level = 0.0;
103 let mut sum_next = 0.0;
104 let mut sum_ll = 0.0;
105 let mut sum_ln = 0.0;
106 for pair in spreads.windows(2) {
107 let level = pair[0];
108 let next = pair[1];
109 sum_level += level;
110 sum_next += next;
111 sum_ll += level * level;
112 sum_ln += level * next;
113 }
114 let mean_level = sum_level / count;
115 let mean_next = sum_next / count;
116 let var_level = sum_ll / count - mean_level * mean_level;
117 if var_level <= 0.0 {
118 return Some(0.0);
120 }
121 let cov = sum_ln / count - mean_level * mean_next;
122 Some(cov / var_level)
123 }
124
125 fn reset(&mut self) {
126 self.window.clear();
127 }
128
129 fn warmup_period(&self) -> usize {
130 self.period
131 }
132
133 fn is_ready(&self) -> bool {
134 self.window.len() == self.period
135 }
136
137 fn name(&self) -> &'static str {
138 "SpreadAr1Coefficient"
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::traits::BatchExt;
146 use approx::assert_relative_eq;
147
148 #[test]
149 fn rejects_period_below_three() {
150 assert!(SpreadAr1Coefficient::new(2).is_err());
151 assert!(SpreadAr1Coefficient::new(3).is_ok());
152 }
153
154 #[test]
155 fn accessors_and_metadata() {
156 let ar1 = SpreadAr1Coefficient::new(30).unwrap();
157 assert_eq!(ar1.period(), 30);
158 assert_eq!(ar1.warmup_period(), 30);
159 assert_eq!(ar1.name(), "SpreadAr1Coefficient");
160 assert!(!ar1.is_ready());
161 }
162
163 #[test]
164 fn warmup_returns_none() {
165 let mut ar1 = SpreadAr1Coefficient::new(4).unwrap();
166 assert_eq!(ar1.update((1.0, 0.0)), None);
167 assert_eq!(ar1.update((2.0, 0.0)), None);
168 assert_eq!(ar1.update((3.0, 0.0)), None);
169 assert!(ar1.update((4.0, 0.0)).is_some());
170 assert!(ar1.is_ready());
171 }
172
173 #[test]
174 fn mean_reverting_spread_has_rho_below_one() {
175 let pairs: Vec<(f64, f64)> = (0..120)
177 .map(|t| {
178 let b = 100.0 + f64::from(t);
179 let a = b + 2.0 * (f64::from(t) * 0.9).sin();
180 (a, b)
181 })
182 .collect();
183 let last = SpreadAr1Coefficient::new(40)
184 .unwrap()
185 .batch(&pairs)
186 .into_iter()
187 .flatten()
188 .last()
189 .unwrap();
190 assert!(last > 0.0 && last < 1.0, "rho {last}");
191 }
192
193 #[test]
194 fn random_walk_spread_has_rho_near_one() {
195 let pairs: Vec<(f64, f64)> = (0..40)
198 .map(|t| (2.0 * f64::from(t), f64::from(t)))
199 .collect();
200 let last = SpreadAr1Coefficient::new(20)
201 .unwrap()
202 .batch(&pairs)
203 .into_iter()
204 .flatten()
205 .last()
206 .unwrap();
207 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
208 }
209
210 #[test]
211 fn flat_spread_returns_zero() {
212 let pairs: Vec<(f64, f64)> = (0..30)
214 .map(|t| (5.0 + f64::from(t), f64::from(t)))
215 .collect();
216 let last = SpreadAr1Coefficient::new(10)
217 .unwrap()
218 .batch(&pairs)
219 .into_iter()
220 .flatten()
221 .last()
222 .unwrap();
223 assert_eq!(last, 0.0);
224 }
225
226 #[test]
227 fn reset_clears_state() {
228 let mut ar1 = SpreadAr1Coefficient::new(5).unwrap();
229 for t in 0..10 {
230 ar1.update((f64::from(t) + (f64::from(t) * 0.7).sin(), f64::from(t)));
231 }
232 assert!(ar1.is_ready());
233 ar1.reset();
234 assert!(!ar1.is_ready());
235 assert_eq!(ar1.update((1.0, 0.0)), None);
236 }
237
238 #[test]
239 fn batch_equals_streaming() {
240 let pairs: Vec<(f64, f64)> = (0..80)
241 .map(|t| {
242 let b = 50.0 + 0.5 * f64::from(t);
243 (b + (f64::from(t) * 0.6).sin(), b)
244 })
245 .collect();
246 let batch = SpreadAr1Coefficient::new(25).unwrap().batch(&pairs);
247 let mut ar1 = SpreadAr1Coefficient::new(25).unwrap();
248 let streamed: Vec<_> = pairs.iter().map(|p| ar1.update(*p)).collect();
249 assert_eq!(batch, streamed);
250 }
251}