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    params: &[
89        ParamDef {
90            name: "length",
91            default: "20",
92            description: "Correlation window length",
93        },
94        ParamDef {
95            name: "num_lags",
96            default: "100",
97            description: "Number of lags to compute",
98        },
99    ],
100    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - FEBRUARY 2025.html",
101    formula_latex: r#"
102\[
103\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)}}
104\]
105"#,
106    gold_standard_file: "ehlers_autocorrelation.json",
107    category: "Ehlers DSP",
108};
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::traits::Next;
114    use crate::test_utils::{load_gold_standard_vec, assert_indicator_parity_vec};
115    use proptest::prelude::*;
116
117    #[test]
118    fn test_ehlers_autocorrelation_gold_standard() {
119        let case = load_gold_standard_vec("ehlers_autocorrelation");
120        let ac = EhlersAutocorrelation::new(20, 10);
121        assert_indicator_parity_vec(ac, &case.input, &case.expected);
122    }
123
124    #[test]
125    fn test_ehlers_autocorrelation_basic() {
126        let mut ac = EhlersAutocorrelation::new(20, 10);
127        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
128        for input in inputs {
129            let res = ac.next(input);
130            assert_eq!(res.len(), 10);
131            approx::assert_relative_eq!(res[0], 1.0, epsilon = 1e-10);
132        }
133    }
134
135    proptest! {
136        #[test]
137        fn test_ehlers_autocorrelation_parity(
138            inputs in prop::collection::vec(1.0..100.0, 50..100),
139        ) {
140            let length = 20;
141            let num_lags = 10;
142            let mut ac = EhlersAutocorrelation::new(length, num_lags);
143            let streaming_results: Vec<Vec<f64>> = inputs.iter().map(|&x| ac.next(x)).collect();
144
145            // Batch implementation
146            let mut batch_results = Vec::with_capacity(inputs.len());
147            let mut smoother = UltimateSmoother::new(20);
148            let filtered: Vec<f64> = inputs.iter().map(|&x| smoother.next(x)).collect();
149            
150            for i in 0..inputs.len() {
151                let mut bar_results = Vec::with_capacity(num_lags);
152                for lag in 0..num_lags {
153                    let mut sx = 0.0;
154                    let mut sy = 0.0;
155                    let mut sxx = 0.0;
156                    let mut sxy = 0.0;
157                    let mut syy = 0.0;
158                    
159                    for j in 0..length {
160                        let idx_x = i as i32 - j as i32;
161                        let idx_y = i as i32 - (lag + j) as i32;
162                        
163                        let x = if idx_x >= 0 { filtered[idx_x as usize] } else { 0.0 };
164                        let y = if idx_y >= 0 { filtered[idx_y as usize] } else { 0.0 };
165                        
166                        sx += x;
167                        sy += y;
168                        sxx += x * x;
169                        sxy += x * y;
170                        syy += y * y;
171                    }
172                    
173                    let len_f = length as f64;
174                    let denom_x = len_f * sxx - sx * sx;
175                    let denom_y = len_f * syy - sy * sy;
176                    
177                    let corr = if denom_x > 0.0 && denom_y > 0.0 {
178                        (len_f * sxy - sx * sy) / (denom_x * denom_y).sqrt()
179                    } else if lag == 0 {
180                        1.0
181                    } else {
182                        0.0
183                    };
184                    bar_results.push(corr);
185                }
186                batch_results.push(bar_results);
187            }
188
189            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
190                for (sv, bv) in s.iter().zip(b.iter()) {
191                    approx::assert_relative_eq!(sv, bv, epsilon = 1e-10);
192                }
193            }
194        }
195    }
196}