wickra_core/indicators/
autocorrelation.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
42pub struct Autocorrelation {
43 period: usize,
44 lag: usize,
45 window: VecDeque<f64>,
46}
47
48impl Autocorrelation {
49 pub fn new(period: usize, lag: usize) -> Result<Self> {
54 if lag == 0 {
55 return Err(Error::InvalidPeriod {
56 message: "autocorrelation lag must be >= 1",
57 });
58 }
59 if period <= lag {
60 return Err(Error::InvalidPeriod {
61 message: "autocorrelation needs period > lag",
62 });
63 }
64 Ok(Self {
65 period,
66 lag,
67 window: VecDeque::with_capacity(period),
68 })
69 }
70
71 pub const fn period(&self) -> usize {
73 self.period
74 }
75
76 pub const fn lag(&self) -> usize {
78 self.lag
79 }
80}
81
82impl Indicator for Autocorrelation {
83 type Input = f64;
84 type Output = f64;
85
86 fn update(&mut self, value: f64) -> Option<f64> {
87 if !value.is_finite() {
88 return None;
89 }
90 if self.window.len() == self.period {
91 self.window.pop_front();
92 }
93 self.window.push_back(value);
94 if self.window.len() < self.period {
95 return None;
96 }
97 let n = self.period as f64;
102 let mean = self.window.iter().sum::<f64>() / n;
103 let mut denom = 0.0;
104 let mut numer = 0.0;
105 let (front, back) = self.window.as_slices();
107 let get = |i: usize| -> f64 {
108 if i < front.len() {
109 front[i]
110 } else {
111 back[i - front.len()]
112 }
113 };
114 for i in 0..self.period {
115 let d = get(i) - mean;
116 denom += d * d;
117 }
118 let lag = self.lag;
119 for i in 0..(self.period - lag) {
120 numer += (get(i) - mean) * (get(i + lag) - mean);
121 }
122 if denom == 0.0 {
123 return Some(0.0);
124 }
125 Some(numer / denom)
126 }
127
128 fn reset(&mut self) {
129 self.window.clear();
130 }
131
132 #[inline]
133 fn warmup_period(&self) -> usize {
134 self.period
135 }
136
137 #[inline]
138 fn is_ready(&self) -> bool {
139 self.window.len() == self.period
140 }
141
142 #[inline]
143 fn name(&self) -> &'static str {
144 "Autocorrelation"
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::traits::BatchExt;
152 use approx::assert_relative_eq;
153
154 #[test]
155 fn rejects_zero_lag() {
156 assert!(Autocorrelation::new(10, 0).is_err());
157 }
158
159 #[test]
160 fn rejects_lag_geq_period() {
161 assert!(Autocorrelation::new(5, 5).is_err());
162 assert!(Autocorrelation::new(5, 10).is_err());
163 }
164
165 #[test]
166 fn accessors_and_metadata() {
167 let a = Autocorrelation::new(14, 2).unwrap();
168 assert_eq!(a.period(), 14);
169 assert_eq!(a.lag(), 2);
170 assert_eq!(a.warmup_period(), 14);
171 assert_eq!(a.name(), "Autocorrelation");
172 }
173
174 #[test]
175 fn constant_series_yields_zero() {
176 let mut a = Autocorrelation::new(10, 1).unwrap();
177 for v in a.batch(&[42.0; 30]).into_iter().flatten() {
178 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
179 }
180 }
181
182 #[test]
183 fn alternating_series_lag_one_is_strongly_negative() {
184 let prices: Vec<f64> = (0..20)
186 .map(|i| if i % 2 == 0 { -1.0 } else { 1.0 })
187 .collect();
188 let mut a = Autocorrelation::new(10, 1).unwrap();
189 let last = a.batch(&prices).into_iter().flatten().last().unwrap();
190 assert!(
191 last < -0.5,
192 "alternating series should be strongly negative, got {last}"
193 );
194 }
195
196 #[test]
197 fn repeating_series_is_strongly_positive_at_period() {
198 let pattern = [1.0, 2.0, 3.0, 4.0];
200 let prices: Vec<f64> = (0..32).map(|i| pattern[i % 4]).collect();
201 let mut a = Autocorrelation::new(16, 4).unwrap();
202 let last = a.batch(&prices).into_iter().flatten().last().unwrap();
203 assert!(
204 last > 0.5,
205 "period-4 repeat should ACF(4) > 0.5, got {last}"
206 );
207 }
208
209 #[test]
210 fn reset_clears_state() {
211 let mut a = Autocorrelation::new(5, 1).unwrap();
212 a.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
213 assert!(a.is_ready());
214 a.reset();
215 assert!(!a.is_ready());
216 assert_eq!(a.update(1.0), None);
217 }
218
219 #[test]
220 fn batch_equals_streaming() {
221 let prices: Vec<f64> = (0..60).map(|i| (f64::from(i) * 0.3).sin()).collect();
222 let batch = Autocorrelation::new(14, 2).unwrap().batch(&prices);
223 let mut b = Autocorrelation::new(14, 2).unwrap();
224 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
225 assert_eq!(batch, streamed);
226 }
227}