use crate::indicators::high_pass::HighPass;
use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::collections::VecDeque;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct AutoTuneFilter {
window: usize,
bandwidth: f64,
highpass: HighPass,
filt_history: VecDeque<f64>,
dc_prev: f64,
price_prev: [f64; 2],
bp_history: [f64; 2],
count: usize,
}
impl AutoTuneFilter {
pub fn new(window: usize, bandwidth: f64) -> Self {
Self {
window,
bandwidth,
highpass: HighPass::new(window),
filt_history: VecDeque::with_capacity(2 * window),
dc_prev: window as f64, price_prev: [0.0; 2],
bp_history: [0.0; 2],
count: 0,
}
}
}
impl Next<f64> for AutoTuneFilter {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
self.count += 1;
let filt = self.highpass.next(input);
self.filt_history.push_front(filt);
if self.filt_history.len() > 2 * self.window {
self.filt_history.pop_back();
}
if self.filt_history.len() < 2 * self.window {
self.price_prev[1] = self.price_prev[0];
self.price_prev[0] = input;
return 0.0;
}
let mut dc = self.dc_prev;
let mut min_corr = 1.0;
let window_f = self.window as f64;
for lag in 1..=self.window {
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.window {
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 div1 = window_f * sxx - sx * sx;
let div2 = window_f * syy - sy * sy;
if div1 > 0.0 && div2 > 0.0 {
let corr = (window_f * sxy - sx * sy) / (div1 * div2).sqrt();
if corr < min_corr {
min_corr = corr;
dc = 2.0 * lag as f64;
}
}
}
if dc > self.dc_prev + 2.0 {
dc = self.dc_prev + 2.0;
}
if dc < self.dc_prev - 2.0 {
dc = self.dc_prev - 2.0;
}
if dc < 2.0 {
dc = 2.0;
}
self.dc_prev = dc;
let l1 = (2.0 * PI / dc).cos();
let g1 = (2.0 * PI * self.bandwidth / dc).cos();
let s1 = if g1.abs() > 1e-10 {
let gamma_inv = 1.0 / g1;
gamma_inv - (gamma_inv * gamma_inv - 1.0).max(0.0).sqrt()
} else {
1.0
};
let bp = 0.5 * (1.0 - s1) * (input - self.price_prev[1])
+ l1 * (1.0 + s1) * self.bp_history[0]
- s1 * self.bp_history[1];
self.bp_history[1] = self.bp_history[0];
self.bp_history[0] = bp;
self.price_prev[1] = self.price_prev[0];
self.price_prev[0] = input;
bp
}
}
pub const AUTOTUNE_FILTER_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "AutoTune Filter",
description: "An adaptive BandPass filter that dynamically tunes itself to the market's dominant cycle.",
usage: "Use to isolate the cyclical component of price while automatically adapting to changes in cycle length. Zero crossings of the output or its rate of change can be used as trading signals.",
keywords: &["adaptive", "filter", "cycle", "ehlers", "dsp", "autotune"],
ehlers_summary: "The AutoTune filter provides a bridge between the time domain and frequency domain by using a rolling autocorrelation function to measure the Dominant Cycle in real time. By dynamically tuning a Bandpass filter to twice the lag at which autocorrelation is minimized, it maintains consistent performance and avoids the destructive phase shifts typical of fixed-tuned filters.",
params: &[
ParamDef {
name: "window",
default: "20",
description: "Window length for autocorrelation and HighPass filter",
},
ParamDef {
name: "bandwidth",
default: "0.25",
description: "Bandwidth of the tuned BandPass filter",
},
],
formula_source: "references/Ehlers Papers/The AutoTune Filter.pdf",
formula_latex: r#"
\[
R(lag) = \frac{n \sum X_i Y_i - \sum X_i \sum Y_i}{\sqrt{(n \sum X_i^2 - (\sum X_i)^2)(n \sum Y_i^2 - (\sum Y_i)^2)}}
\]
\[
DC = 2 \times \text{argmin}_{lag} R(lag)
\]
\[
BP = \text{BandPass}(Price, DC, BW)
\]
"#,
gold_standard_file: "autotune_filter.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_autotune_basic() {
let mut at = AutoTuneFilter::new(20, 0.25);
for _ in 0..40 {
at.next(100.0);
}
for i in 0..100 {
let val = at.next(100.0 + (i as f64 * 2.0 * PI / 20.0).sin());
assert!(!val.is_nan());
}
}
#[test]
fn test_autotune_dc_tracking() {
let window = 20;
let mut at = AutoTuneFilter::new(window, 0.25);
let period = 10.0;
for i in 0..100 {
let _ = at.next(100.0 + (i as f64 * 2.0 * PI / period).sin());
}
assert!(at.dc_prev >= 8.0 && at.dc_prev <= 12.0, "DC was {}", at.dc_prev);
}
proptest! {
#[test]
fn test_autotune_parity(
inputs in prop::collection::vec(90.0..110.0, 60..100),
) {
let window = 20;
let bandwidth = 0.25;
let mut at_obj = AutoTuneFilter::new(window, bandwidth);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| at_obj.next(x)).collect();
let mut hp = HighPass::new(window);
let mut filt_hist = VecDeque::new();
let mut dc_prev = window as f64;
let mut price_prev = [0.0; 2];
let mut bp_hist = [0.0; 2];
let mut expected = Vec::with_capacity(inputs.len());
for (i, &input) in inputs.iter().enumerate() {
let filt = hp.next(input);
filt_hist.push_front(filt);
if filt_hist.len() > 2 * window {
filt_hist.pop_back();
}
if filt_hist.len() < 2 * window {
price_prev[1] = price_prev[0];
price_prev[0] = input;
expected.push(0.0);
continue;
}
let mut dc = dc_prev;
let mut min_corr = 1.0;
for lag in 1..=window {
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..window {
let x = filt_hist[j];
let y = filt_hist[lag+j];
sx += x; sy += y;
sxx += x*x; sxy += x*y; syy += y*y;
}
let div1 = (window as f64) * sxx - sx*sx;
let div2 = (window as f64) * syy - sy*sy;
if div1 > 0.0 && div2 > 0.0 {
let corr = ((window as f64) * sxy - sx*sy) / (div1*div2).sqrt();
if corr < min_corr {
min_corr = corr;
dc = 2.0 * lag as f64;
}
}
}
if dc > dc_prev + 2.0 { dc = dc_prev + 2.0; }
if dc < dc_prev - 2.0 { dc = dc_prev - 2.0; }
if dc < 2.0 { dc = 2.0; }
dc_prev = dc;
let l1 = (2.0 * PI / dc).cos();
let g1 = (2.0 * PI * bandwidth / dc).cos();
let s1 = 1.0/g1 - (1.0/(g1*g1) - 1.0).max(0.0).sqrt();
let bp = 0.5 * (1.0 - s1) * (input - price_prev[1])
+ l1 * (1.0 + s1) * bp_hist[0]
- s1 * bp_hist[1];
bp_hist[1] = bp_hist[0];
bp_hist[0] = bp;
price_prev[1] = price_prev[0];
price_prev[0] = input;
expected.push(bp);
}
for (s, e) in streaming_results.iter().zip(expected.iter()) {
approx::assert_relative_eq!(s, e, epsilon = 1e-10);
}
}
}
}