Skip to main content

quantwave_core/indicators/
ehlers_autocorrelation.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::ultimate_smoother::UltimateSmoother;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6/// Ehlers Autocorrelation
7///
8/// Based on John Ehlers' "Drunkard’s Walk: Theory And Measurement By Autocorrelation" (2025).
9/// It computes the Pearson correlation of smoothed price data with its own lagged versions.
10/// This is typically displayed as a heatmap to identify cycles and trends.
11#[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), // Default smoother period from paper
25            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>; // Correlation for each lag from 0 to num_lags-1
41
42    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            // Batch implementation
149            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}