use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::indicators::ultimate_smoother::UltimateSmoother;
use crate::traits::Next;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct EhlersAutocorrelation {
length: usize,
num_lags: usize,
smoother: UltimateSmoother,
filt_history: VecDeque<f64>,
}
impl EhlersAutocorrelation {
pub fn new(length: usize, num_lags: usize) -> Self {
Self {
length,
num_lags,
smoother: UltimateSmoother::new(20), filt_history: VecDeque::from(vec![0.0; length + num_lags]),
}
}
pub fn with_smoother_period(length: usize, num_lags: usize, smoother_period: usize) -> Self {
Self {
length,
num_lags,
smoother: UltimateSmoother::new(smoother_period),
filt_history: VecDeque::from(vec![0.0; length + num_lags]),
}
}
}
impl Next<f64> for EhlersAutocorrelation {
type Output = Vec<f64>;
fn next(&mut self, input: f64) -> Self::Output {
let filt = self.smoother.next(input);
self.filt_history.push_front(filt);
self.filt_history.pop_back();
let mut results = Vec::with_capacity(self.num_lags);
let len_f = self.length as f64;
for lag in 0..self.num_lags {
let mut sx = 0.0;
let mut sy = 0.0;
let mut sxx = 0.0;
let mut sxy = 0.0;
let mut syy = 0.0;
for j in 0..self.length {
let x = self.filt_history[j];
let y = self.filt_history[lag + j];
sx += x;
sy += y;
sxx += x * x;
sxy += x * y;
syy += y * y;
}
let denom_x = len_f * sxx - sx * sx;
let denom_y = len_f * syy - sy * sy;
let corr = if denom_x > 0.0 && denom_y > 0.0 {
(len_f * sxy - sx * sy) / (denom_x * denom_y).sqrt()
} else if lag == 0 {
1.0
} else {
0.0
};
results.push(corr);
}
results
}
}
pub const EHLERS_AUTOCORRELATION_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Ehlers Autocorrelation",
description: "Computes Pearson correlation of smoothed price with its lags to identify market structure.",
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.",
keywords: &["cycle", "spectral", "ehlers", "dsp", "dominant-cycle"],
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.",
params: &[
ParamDef {
name: "length",
default: "20",
description: "Correlation window length",
},
ParamDef {
name: "num_lags",
default: "100",
description: "Number of lags to compute",
},
],
formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - FEBRUARY 2025.html",
formula_latex: r#"
\[
\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)}}
\]
"#,
gold_standard_file: "ehlers_autocorrelation.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use crate::test_utils::{load_gold_standard_vec, assert_indicator_parity_vec};
use proptest::prelude::*;
#[test]
fn test_ehlers_autocorrelation_gold_standard() {
let case = load_gold_standard_vec("ehlers_autocorrelation");
let ac = EhlersAutocorrelation::new(20, 10);
assert_indicator_parity_vec(ac, &case.input, &case.expected);
}
#[test]
fn test_ehlers_autocorrelation_basic() {
let mut ac = EhlersAutocorrelation::new(20, 10);
let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
for input in inputs {
let res = ac.next(input);
assert_eq!(res.len(), 10);
approx::assert_relative_eq!(res[0], 1.0, epsilon = 1e-10);
}
}
proptest! {
#[test]
fn test_ehlers_autocorrelation_parity(
inputs in prop::collection::vec(1.0..100.0, 50..100),
) {
let length = 20;
let num_lags = 10;
let mut ac = EhlersAutocorrelation::new(length, num_lags);
let streaming_results: Vec<Vec<f64>> = inputs.iter().map(|&x| ac.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
let mut smoother = UltimateSmoother::new(20);
let filtered: Vec<f64> = inputs.iter().map(|&x| smoother.next(x)).collect();
for i in 0..inputs.len() {
let mut bar_results = Vec::with_capacity(num_lags);
for lag in 0..num_lags {
let mut sx = 0.0;
let mut sy = 0.0;
let mut sxx = 0.0;
let mut sxy = 0.0;
let mut syy = 0.0;
for j in 0..length {
let idx_x = i as i32 - j as i32;
let idx_y = i as i32 - (lag + j) as i32;
let x = if idx_x >= 0 { filtered[idx_x as usize] } else { 0.0 };
let y = if idx_y >= 0 { filtered[idx_y as usize] } else { 0.0 };
sx += x;
sy += y;
sxx += x * x;
sxy += x * y;
syy += y * y;
}
let len_f = length as f64;
let denom_x = len_f * sxx - sx * sx;
let denom_y = len_f * syy - sy * sy;
let corr = if denom_x > 0.0 && denom_y > 0.0 {
(len_f * sxy - sx * sy) / (denom_x * denom_y).sqrt()
} else if lag == 0 {
1.0
} else {
0.0
};
bar_results.push(corr);
}
batch_results.push(bar_results);
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
for (sv, bv) in s.iter().zip(b.iter()) {
approx::assert_relative_eq!(sv, bv, epsilon = 1e-10);
}
}
}
}
}