1#![allow(clippy::doc_markdown)]
3
4use std::collections::VecDeque;
5use std::f64::consts::TAU;
6
7use crate::error::{Error, Result};
8use crate::indicators::roofing_filter::RoofingFilter;
9use crate::traits::Indicator;
10
11const AVG_LENGTH: usize = 3;
13
14const TABLE_LIMIT: usize = 1 << 16;
17
18#[derive(Debug, Clone)]
59pub struct AutocorrelationPeriodogram {
60 min_period: usize,
61 max_period: usize,
62 roof: RoofingFilter,
63 buffer: VecDeque<f64>,
64 r: Vec<f64>,
65 max_pwr: f64,
66 last: Option<f64>,
67 trig: Option<Box<[(f64, f64)]>>,
70 corr: Vec<f64>,
72}
73
74impl AutocorrelationPeriodogram {
75 pub fn new(min_period: usize, max_period: usize) -> Result<Self> {
84 if min_period == 0 || max_period == 0 {
85 return Err(Error::PeriodZero);
86 }
87 if min_period < AVG_LENGTH + 1 || max_period <= min_period {
88 return Err(Error::InvalidPeriod {
89 message: "autocorrelation periodogram needs AvgLength < min_period < max_period",
90 });
91 }
92 Ok(Self {
93 min_period,
94 max_period,
95 roof: RoofingFilter::new(10, max_period)?,
96 buffer: VecDeque::with_capacity(max_period + AVG_LENGTH),
97 r: vec![0.0; max_period + 1],
98 max_pwr: 0.0,
99 last: None,
100 trig: trig_table(min_period, max_period),
101 corr: vec![0.0; max_period + 1],
102 })
103 }
104
105 pub const fn periods(&self) -> (usize, usize) {
107 (self.min_period, self.max_period)
108 }
109
110 pub const fn value(&self) -> Option<f64> {
112 self.last
113 }
114
115 fn correlation(&self, lag: usize) -> f64 {
118 let len = self.buffer.len();
119 let filt = |k: usize| self.buffer[len - 1 - k];
120 let m = AVG_LENGTH as f64;
121 let (mut sx, mut sy, mut sxx, mut syy, mut sxy) = (0.0, 0.0, 0.0, 0.0, 0.0);
122 for count in 0..AVG_LENGTH {
123 let x = filt(count);
124 let y = filt(lag + count);
125 sx += x;
126 sy += y;
127 sxx += x * x;
128 syy += y * y;
129 sxy += x * y;
130 }
131 let denom = (m * sxx - sx * sx) * (m * syy - sy * sy);
132 if denom > 0.0 {
133 (m * sxy - sx * sy) / denom.sqrt()
134 } else {
135 0.0
136 }
137 }
138}
139
140fn trig_term(n: usize, period: usize) -> (f64, f64) {
142 let angle = TAU * n as f64 / period as f64;
143 (angle.cos(), angle.sin())
144}
145
146fn trig_table(min_period: usize, max_period: usize) -> Option<Box<[(f64, f64)]>> {
150 let pairs = (max_period + 1 - min_period) * (max_period + 1 - AVG_LENGTH);
151 (pairs <= TABLE_LIMIT).then(|| {
152 (min_period..=max_period)
153 .flat_map(|period| (AVG_LENGTH..=max_period).map(move |n| trig_term(n, period)))
154 .collect()
155 })
156}
157
158impl Indicator for AutocorrelationPeriodogram {
159 type Input = f64;
160 type Output = f64;
161
162 fn update(&mut self, price: f64) -> Option<f64> {
163 if !price.is_finite() {
164 return None;
165 }
166 let filt = self.roof.update(price)?;
167 if self.buffer.len() == self.max_period + AVG_LENGTH {
168 self.buffer.pop_front();
169 }
170 self.buffer.push_back(filt);
171 if self.buffer.len() < self.max_period + AVG_LENGTH {
172 return None;
173 }
174
175 let mut corr = std::mem::take(&mut self.corr);
177 for (lag, c) in corr.iter_mut().enumerate() {
178 *c = self.correlation(lag);
179 }
180
181 self.max_pwr *= 0.995;
183 let lags = self.max_period + 1 - AVG_LENGTH;
184 for (row, period) in (self.min_period..=self.max_period).enumerate() {
185 let mut cosine = 0.0;
186 let mut sine = 0.0;
187 if let Some(table) = &self.trig {
188 let weights = &table[row * lags..(row + 1) * lags];
189 for (&cn, &(cos, sin)) in corr[AVG_LENGTH..].iter().zip(weights) {
190 cosine += cn * cos;
191 sine += cn * sin;
192 }
193 } else {
194 for (n, &cn) in corr.iter().enumerate().skip(AVG_LENGTH) {
195 let (cos, sin) = trig_term(n, period);
196 cosine += cn * cos;
197 sine += cn * sin;
198 }
199 }
200 let power = cosine * cosine + sine * sine;
201 self.r[period] = 0.2 * power + 0.8 * self.r[period];
202 if self.r[period] > self.max_pwr {
203 self.max_pwr = self.r[period];
204 }
205 }
206
207 let mut spx = 0.0;
209 let mut sp = 0.0;
210 for period in self.min_period..=self.max_period {
211 let pwr = if self.max_pwr > 0.0 {
212 self.r[period] / self.max_pwr
213 } else {
214 0.0
215 };
216 if pwr >= 0.5 {
217 spx += period as f64 * pwr;
218 sp += pwr;
219 }
220 }
221 let dominant = if sp > 0.0 {
222 (spx / sp).clamp(self.min_period as f64, self.max_period as f64)
223 } else {
224 self.min_period as f64
225 };
226 self.corr = corr;
227 self.last = Some(dominant);
228 Some(dominant)
229 }
230
231 fn reset(&mut self) {
232 self.roof.reset();
233 self.buffer.clear();
234 self.r.iter_mut().for_each(|x| *x = 0.0);
235 self.max_pwr = 0.0;
236 self.last = None;
237 }
238
239 #[inline]
240 fn warmup_period(&self) -> usize {
241 self.max_period + AVG_LENGTH
242 }
243
244 #[inline]
245 fn is_ready(&self) -> bool {
246 self.last.is_some()
247 }
248
249 #[inline]
250 fn name(&self) -> &'static str {
251 "AutocorrelationPeriodogram"
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::traits::BatchExt;
259
260 #[test]
261 fn the_trig_table_gives_the_bits_of_the_terms_computed_in_place() {
262 let prices: Vec<f64> = (0..600)
263 .map(|i| {
264 let t = f64::from(i);
265 100.0 + (TAU * t / 23.0).sin() * 5.0 + (t * 0.37).cos()
266 })
267 .collect();
268 let mut table = AutocorrelationPeriodogram::new(10, 48).unwrap();
269 let mut in_place = table.clone();
270 assert!(table.trig.is_some());
271 in_place.trig = None;
272 for &price in &prices {
273 let (a, b) = (table.update(price), in_place.update(price));
274 assert_eq!(a.map(f64::to_bits), b.map(f64::to_bits));
275 }
276 assert!(AutocorrelationPeriodogram::new(10, 300)
278 .unwrap()
279 .trig
280 .is_none());
281 }
282
283 #[test]
284 fn rejects_invalid_periods() {
285 assert!(matches!(
286 AutocorrelationPeriodogram::new(0, 48),
287 Err(Error::PeriodZero)
288 ));
289 assert!(matches!(
290 AutocorrelationPeriodogram::new(3, 48),
291 Err(Error::InvalidPeriod { .. })
292 ));
293 assert!(matches!(
294 AutocorrelationPeriodogram::new(48, 10),
295 Err(Error::InvalidPeriod { .. })
296 ));
297 }
298
299 #[test]
300 fn accessors_and_metadata() {
301 let p = AutocorrelationPeriodogram::new(10, 48).unwrap();
302 assert_eq!(p.periods(), (10, 48));
303 assert_eq!(p.warmup_period(), 51);
304 assert_eq!(p.name(), "AutocorrelationPeriodogram");
305 assert!(!p.is_ready());
306 assert_eq!(p.value(), None);
307 }
308
309 #[test]
310 fn first_emission_at_warmup_period() {
311 let mut p = AutocorrelationPeriodogram::new(8, 20).unwrap();
312 let xs: Vec<f64> = (0..40)
313 .map(|i| 100.0 + (TAU * f64::from(i) / 12.0).sin() * 5.0)
314 .collect();
315 let out = p.batch(&xs);
316 let warmup = p.warmup_period(); assert_eq!(warmup, 23);
318 for v in out.iter().take(warmup - 1) {
319 assert!(v.is_none());
320 }
321 assert!(out[warmup - 1].is_some());
322 }
323
324 #[test]
325 fn output_within_period_band() {
326 let mut p = AutocorrelationPeriodogram::new(10, 48).unwrap();
327 let xs: Vec<f64> = (0..400)
328 .map(|i| 100.0 + (TAU * f64::from(i) / 20.0).sin() * 5.0)
329 .collect();
330 for v in p.batch(&xs).into_iter().flatten() {
331 assert!((10.0..=48.0).contains(&v), "cycle out of band: {v}");
332 }
333 }
334
335 #[test]
336 fn detects_injected_cycle() {
337 let mut p = AutocorrelationPeriodogram::new(10, 48).unwrap();
339 let xs: Vec<f64> = (0..600)
340 .map(|i| 100.0 + (TAU * f64::from(i) / 20.0).sin() * 5.0)
341 .collect();
342 let last = p.batch(&xs).into_iter().flatten().last().unwrap();
343 assert!(
344 (last - 20.0).abs() < 6.0,
345 "expected ~20-bar cycle, got {last}"
346 );
347 }
348
349 #[test]
350 fn ignores_non_finite() {
351 let mut p = AutocorrelationPeriodogram::new(10, 48).unwrap();
352 p.batch(
353 &(0..80)
354 .map(|i| 100.0 + (TAU * f64::from(i) / 20.0).sin() * 5.0)
355 .collect::<Vec<_>>(),
356 );
357 let before = p.value();
358 assert_eq!(p.update(f64::NAN), None);
359 assert_eq!(p.value(), before);
361 }
362
363 #[test]
364 fn reset_clears_state() {
365 let mut p = AutocorrelationPeriodogram::new(10, 48).unwrap();
366 p.batch(
367 &(0..120)
368 .map(|i| 100.0 + (TAU * f64::from(i) / 20.0).sin() * 5.0)
369 .collect::<Vec<_>>(),
370 );
371 assert!(p.is_ready());
372 p.reset();
373 assert!(!p.is_ready());
374 assert_eq!(p.value(), None);
375 }
376
377 #[test]
378 fn batch_equals_streaming() {
379 let xs: Vec<f64> = (0..200)
380 .map(|i| 100.0 + (TAU * f64::from(i) / 20.0).sin() * 5.0)
381 .collect();
382 let batch = AutocorrelationPeriodogram::new(10, 48).unwrap().batch(&xs);
383 let mut b = AutocorrelationPeriodogram::new(10, 48).unwrap();
384 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
385 assert_eq!(batch, streamed);
386 }
387
388 #[test]
389 fn flat_input_falls_back_to_min_period() {
390 let flat = [100.0_f64; 200];
394 let last = AutocorrelationPeriodogram::new(10, 48)
395 .unwrap()
396 .batch(&flat)
397 .into_iter()
398 .flatten()
399 .last()
400 .unwrap();
401 assert_eq!(last, 10.0);
402 }
403}