quantwave_core/indicators/
ehlers_autocorrelation.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::ultimate_smoother::UltimateSmoother;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6#[derive(Debug, Clone)]
12pub struct EhlersAutocorrelation {
13 length: usize,
14 num_lags: usize,
15 smoother: UltimateSmoother,
16 filt_history: VecDeque<f64>,
17}
18
19impl EhlersAutocorrelation {
20 pub fn new(length: usize, num_lags: usize) -> Self {
21 Self {
22 length,
23 num_lags,
24 smoother: UltimateSmoother::new(20), filt_history: VecDeque::from(vec![0.0; length + num_lags]),
26 }
27 }
28
29 pub fn with_smoother_period(length: usize, num_lags: usize, smoother_period: usize) -> Self {
30 Self {
31 length,
32 num_lags,
33 smoother: UltimateSmoother::new(smoother_period),
34 filt_history: VecDeque::from(vec![0.0; length + num_lags]),
35 }
36 }
37}
38
39impl Next<f64> for EhlersAutocorrelation {
40 type Output = Vec<f64>; fn next(&mut self, input: f64) -> Self::Output {
43 let filt = self.smoother.next(input);
44 self.filt_history.push_front(filt);
45 self.filt_history.pop_back();
46
47 let mut results = Vec::with_capacity(self.num_lags);
48 let len_f = self.length as f64;
49
50 for lag in 0..self.num_lags {
51 let mut sx = 0.0;
52 let mut sy = 0.0;
53 let mut sxx = 0.0;
54 let mut sxy = 0.0;
55 let mut syy = 0.0;
56
57 for j in 0..self.length {
58 let x = self.filt_history[j];
59 let y = self.filt_history[lag + j];
60 sx += x;
61 sy += y;
62 sxx += x * x;
63 sxy += x * y;
64 syy += y * y;
65 }
66
67 let denom_x = len_f * sxx - sx * sx;
68 let denom_y = len_f * syy - sy * sy;
69
70 let corr = if denom_x > 0.0 && denom_y > 0.0 {
71 (len_f * sxy - sx * sy) / (denom_x * denom_y).sqrt()
72 } else if lag == 0 {
73 1.0
74 } else {
75 0.0
76 };
77
78 results.push(corr);
79 }
80
81 results
82 }
83}
84
85pub const EHLERS_AUTOCORRELATION_METADATA: IndicatorMetadata = IndicatorMetadata {
86 name: "Ehlers Autocorrelation",
87 description: "Computes Pearson correlation of smoothed price with its lags to identify market structure.",
88 usage: "Use to generate an autocorrelation periodogram showing which cycle periods are currently dominant. Visualise as a heatmap to track cycle period shifts over time.",
89 keywords: &["cycle", "spectral", "ehlers", "dsp", "dominant-cycle"],
90 ehlers_summary: "Ehlers introduces autocorrelation-based cycle measurement in Cycle Analytics for Traders (2013) as a more robust alternative to DFT. By computing autocorrelation of Roofing-filtered price at each lag, then applying a spectral DFT to the lag series, he obtains a periodogram insensitive to amplitude variations.",
91 params: &[
92 ParamDef {
93 name: "length",
94 default: "20",
95 description: "Correlation window length",
96 },
97 ParamDef {
98 name: "num_lags",
99 default: "100",
100 description: "Number of lags to compute",
101 },
102 ],
103 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - FEBRUARY 2025.html",
104 formula_latex: r#"
105\[
106\rho(lag) = \frac{N \sum X Y - \sum X \sum Y}{\sqrt{(N \sum X^2 - (\sum X)^2)(N \sum Y^2 - (\sum Y)^2)}}
107\]
108"#,
109 gold_standard_file: "ehlers_autocorrelation.json",
110 category: "Ehlers DSP",
111};
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::traits::Next;
117 use crate::test_utils::{load_gold_standard_vec, assert_indicator_parity_vec};
118 use proptest::prelude::*;
119
120 #[test]
121 fn test_ehlers_autocorrelation_gold_standard() {
122 let case = load_gold_standard_vec("ehlers_autocorrelation");
123 let ac = EhlersAutocorrelation::new(20, 10);
124 assert_indicator_parity_vec(ac, &case.input, &case.expected);
125 }
126
127 #[test]
128 fn test_ehlers_autocorrelation_basic() {
129 let mut ac = EhlersAutocorrelation::new(20, 10);
130 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
131 for input in inputs {
132 let res = ac.next(input);
133 assert_eq!(res.len(), 10);
134 approx::assert_relative_eq!(res[0], 1.0, epsilon = 1e-10);
135 }
136 }
137
138 proptest! {
139 #[test]
140 fn test_ehlers_autocorrelation_parity(
141 inputs in prop::collection::vec(1.0..100.0, 50..100),
142 ) {
143 let length = 20;
144 let num_lags = 10;
145 let mut ac = EhlersAutocorrelation::new(length, num_lags);
146 let streaming_results: Vec<Vec<f64>> = inputs.iter().map(|&x| ac.next(x)).collect();
147
148 let mut batch_results = Vec::with_capacity(inputs.len());
150 let mut smoother = UltimateSmoother::new(20);
151 let filtered: Vec<f64> = inputs.iter().map(|&x| smoother.next(x)).collect();
152
153 for i in 0..inputs.len() {
154 let mut bar_results = Vec::with_capacity(num_lags);
155 for lag in 0..num_lags {
156 let mut sx = 0.0;
157 let mut sy = 0.0;
158 let mut sxx = 0.0;
159 let mut sxy = 0.0;
160 let mut syy = 0.0;
161
162 for j in 0..length {
163 let idx_x = i as i32 - j as i32;
164 let idx_y = i as i32 - (lag + j) as i32;
165
166 let x = if idx_x >= 0 { filtered[idx_x as usize] } else { 0.0 };
167 let y = if idx_y >= 0 { filtered[idx_y as usize] } else { 0.0 };
168
169 sx += x;
170 sy += y;
171 sxx += x * x;
172 sxy += x * y;
173 syy += y * y;
174 }
175
176 let len_f = length as f64;
177 let denom_x = len_f * sxx - sx * sx;
178 let denom_y = len_f * syy - sy * sy;
179
180 let corr = if denom_x > 0.0 && denom_y > 0.0 {
181 (len_f * sxy - sx * sy) / (denom_x * denom_y).sqrt()
182 } else if lag == 0 {
183 1.0
184 } else {
185 0.0
186 };
187 bar_results.push(corr);
188 }
189 batch_results.push(bar_results);
190 }
191
192 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
193 for (sv, bv) in s.iter().zip(b.iter()) {
194 approx::assert_relative_eq!(sv, bv, epsilon = 1e-10);
195 }
196 }
197 }
198 }
199}