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 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 "Autocorrelation"
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_zero_lag() {
153 assert!(Autocorrelation::new(10, 0).is_err());
154 }
155
156 #[test]
157 fn rejects_lag_geq_period() {
158 assert!(Autocorrelation::new(5, 5).is_err());
159 assert!(Autocorrelation::new(5, 10).is_err());
160 }
161
162 #[test]
163 fn accessors_and_metadata() {
164 let a = Autocorrelation::new(14, 2).unwrap();
165 assert_eq!(a.period(), 14);
166 assert_eq!(a.lag(), 2);
167 assert_eq!(a.warmup_period(), 14);
168 assert_eq!(a.name(), "Autocorrelation");
169 }
170
171 #[test]
172 fn constant_series_yields_zero() {
173 let mut a = Autocorrelation::new(10, 1).unwrap();
174 for v in a.batch(&[42.0; 30]).into_iter().flatten() {
175 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
176 }
177 }
178
179 #[test]
180 fn alternating_series_lag_one_is_strongly_negative() {
181 let prices: Vec<f64> = (0..20)
183 .map(|i| if i % 2 == 0 { -1.0 } else { 1.0 })
184 .collect();
185 let mut a = Autocorrelation::new(10, 1).unwrap();
186 let last = a.batch(&prices).into_iter().flatten().last().unwrap();
187 assert!(
188 last < -0.5,
189 "alternating series should be strongly negative, got {last}"
190 );
191 }
192
193 #[test]
194 fn repeating_series_is_strongly_positive_at_period() {
195 let pattern = [1.0, 2.0, 3.0, 4.0];
197 let prices: Vec<f64> = (0..32).map(|i| pattern[i % 4]).collect();
198 let mut a = Autocorrelation::new(16, 4).unwrap();
199 let last = a.batch(&prices).into_iter().flatten().last().unwrap();
200 assert!(
201 last > 0.5,
202 "period-4 repeat should ACF(4) > 0.5, got {last}"
203 );
204 }
205
206 #[test]
207 fn reset_clears_state() {
208 let mut a = Autocorrelation::new(5, 1).unwrap();
209 a.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
210 assert!(a.is_ready());
211 a.reset();
212 assert!(!a.is_ready());
213 assert_eq!(a.update(1.0), None);
214 }
215
216 #[test]
217 fn batch_equals_streaming() {
218 let prices: Vec<f64> = (0..60).map(|i| (f64::from(i) * 0.3).sin()).collect();
219 let batch = Autocorrelation::new(14, 2).unwrap().batch(&prices);
220 let mut b = Autocorrelation::new(14, 2).unwrap();
221 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
222 assert_eq!(batch, streamed);
223 }
224}