quantwave_core/indicators/
autotune.rs1use crate::indicators::high_pass::HighPass;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4use std::collections::VecDeque;
5use std::f64::consts::PI;
6
7#[derive(Debug, Clone)]
13pub struct AutoTuneFilter {
14 window: usize,
15 bandwidth: f64,
16 highpass: HighPass,
17 filt_history: VecDeque<f64>,
18 dc_prev: f64,
19 price_prev: [f64; 2],
20 bp_history: [f64; 2],
21 count: usize,
22}
23
24impl AutoTuneFilter {
25 pub fn new(window: usize, bandwidth: f64) -> Self {
26 Self {
27 window,
28 bandwidth,
29 highpass: HighPass::new(window),
30 filt_history: VecDeque::with_capacity(2 * window),
31 dc_prev: window as f64, price_prev: [0.0; 2],
33 bp_history: [0.0; 2],
34 count: 0,
35 }
36 }
37}
38
39impl Next<f64> for AutoTuneFilter {
40 type Output = f64;
41
42 fn next(&mut self, input: f64) -> Self::Output {
43 self.count += 1;
44 let filt = self.highpass.next(input);
45 self.filt_history.push_front(filt);
46 if self.filt_history.len() > 2 * self.window {
47 self.filt_history.pop_back();
48 }
49
50 if self.filt_history.len() < 2 * self.window {
51 self.price_prev[1] = self.price_prev[0];
52 self.price_prev[0] = input;
53 return 0.0;
54 }
55
56 let mut dc = self.dc_prev;
57 let mut min_corr = 1.0;
58 let window_f = self.window as f64;
59
60 for lag in 1..=self.window {
62 let mut sx = 0.0;
63 let mut sy = 0.0;
64 let mut sxx = 0.0;
65 let mut sxy = 0.0;
66 let mut syy = 0.0;
67
68 for j in 0..self.window {
69 let x = self.filt_history[j];
70 let y = self.filt_history[lag + j];
71 sx += x;
72 sy += y;
73 sxx += x * x;
74 sxy += x * y;
75 syy += y * y;
76 }
77
78 let div1 = window_f * sxx - sx * sx;
79 let div2 = window_f * syy - sy * sy;
80
81 if div1 > 0.0 && div2 > 0.0 {
82 let corr = (window_f * sxy - sx * sy) / (div1 * div2).sqrt();
83 if corr < min_corr {
84 min_corr = corr;
85 dc = 2.0 * lag as f64;
86 }
87 }
88 }
89
90 if dc > self.dc_prev + 2.0 {
92 dc = self.dc_prev + 2.0;
93 }
94 if dc < self.dc_prev - 2.0 {
95 dc = self.dc_prev - 2.0;
96 }
97 if dc < 2.0 {
98 dc = 2.0;
99 }
100 self.dc_prev = dc;
101
102 let l1 = (2.0 * PI / dc).cos();
104 let g1 = (2.0 * PI * self.bandwidth / dc).cos();
105 let s1 = if g1.abs() > 1e-10 {
107 let gamma_inv = 1.0 / g1;
108 gamma_inv - (gamma_inv * gamma_inv - 1.0).max(0.0).sqrt()
109 } else {
110 1.0
111 };
112
113 let bp = 0.5 * (1.0 - s1) * (input - self.price_prev[1])
114 + l1 * (1.0 + s1) * self.bp_history[0]
115 - s1 * self.bp_history[1];
116
117 self.bp_history[1] = self.bp_history[0];
118 self.bp_history[0] = bp;
119 self.price_prev[1] = self.price_prev[0];
120 self.price_prev[0] = input;
121
122 bp
123 }
124}
125
126pub const AUTOTUNE_FILTER_METADATA: IndicatorMetadata = IndicatorMetadata {
127 name: "AutoTune Filter",
128 description: "An adaptive BandPass filter that dynamically tunes itself to the market's dominant cycle.",
129 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.",
130 keywords: &["adaptive", "filter", "cycle", "ehlers", "dsp", "autotune"],
131 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.",
132 params: &[
133 ParamDef {
134 name: "window",
135 default: "20",
136 description: "Window length for autocorrelation and HighPass filter",
137 },
138 ParamDef {
139 name: "bandwidth",
140 default: "0.25",
141 description: "Bandwidth of the tuned BandPass filter",
142 },
143 ],
144 formula_source: "references/Ehlers Papers/The AutoTune Filter.pdf",
145 formula_latex: r#"
146\[
147R(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)}}
148\]
149\[
150DC = 2 \times \text{argmin}_{lag} R(lag)
151\]
152\[
153BP = \text{BandPass}(Price, DC, BW)
154\]
155"#,
156 gold_standard_file: "autotune_filter.json",
157 category: "Ehlers DSP",
158};
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::traits::Next;
164 use proptest::prelude::*;
165
166 #[test]
167 fn test_autotune_basic() {
168 let mut at = AutoTuneFilter::new(20, 0.25);
169 for _ in 0..40 {
171 at.next(100.0);
172 }
173 for i in 0..100 {
174 let val = at.next(100.0 + (i as f64 * 2.0 * PI / 20.0).sin());
176 assert!(!val.is_nan());
177 }
178 }
179
180 #[test]
181 fn test_autotune_dc_tracking() {
182 let window = 20;
183 let mut at = AutoTuneFilter::new(window, 0.25);
184
185 let period = 10.0;
187 for i in 0..100 {
188 let _ = at.next(100.0 + (i as f64 * 2.0 * PI / period).sin());
189 }
190
191 assert!(at.dc_prev >= 8.0 && at.dc_prev <= 12.0, "DC was {}", at.dc_prev);
196 }
197
198 proptest! {
199 #[test]
200 fn test_autotune_parity(
201 inputs in prop::collection::vec(90.0..110.0, 60..100),
202 ) {
203 let window = 20;
204 let bandwidth = 0.25;
205 let mut at_obj = AutoTuneFilter::new(window, bandwidth);
206 let streaming_results: Vec<f64> = inputs.iter().map(|&x| at_obj.next(x)).collect();
207
208 let mut hp = HighPass::new(window);
210 let mut filt_hist = VecDeque::new();
211 let mut dc_prev = window as f64;
212 let mut price_prev = [0.0; 2];
213 let mut bp_hist = [0.0; 2];
214 let mut expected = Vec::with_capacity(inputs.len());
215
216 for (i, &input) in inputs.iter().enumerate() {
217 let filt = hp.next(input);
218 filt_hist.push_front(filt);
219 if filt_hist.len() > 2 * window {
220 filt_hist.pop_back();
221 }
222
223 if filt_hist.len() < 2 * window {
224 price_prev[1] = price_prev[0];
225 price_prev[0] = input;
226 expected.push(0.0);
227 continue;
228 }
229
230 let mut dc = dc_prev;
231 let mut min_corr = 1.0;
232 for lag in 1..=window {
233 let mut sx = 0.0; let mut sy = 0.0;
234 let mut sxx = 0.0; let mut sxy = 0.0; let mut syy = 0.0;
235 for j in 0..window {
236 let x = filt_hist[j];
237 let y = filt_hist[lag+j];
238 sx += x; sy += y;
239 sxx += x*x; sxy += x*y; syy += y*y;
240 }
241 let div1 = (window as f64) * sxx - sx*sx;
242 let div2 = (window as f64) * syy - sy*sy;
243 if div1 > 0.0 && div2 > 0.0 {
244 let corr = ((window as f64) * sxy - sx*sy) / (div1*div2).sqrt();
245 if corr < min_corr {
246 min_corr = corr;
247 dc = 2.0 * lag as f64;
248 }
249 }
250 }
251
252 if dc > dc_prev + 2.0 { dc = dc_prev + 2.0; }
253 if dc < dc_prev - 2.0 { dc = dc_prev - 2.0; }
254 if dc < 2.0 { dc = 2.0; }
255 dc_prev = dc;
256
257 let l1 = (2.0 * PI / dc).cos();
258 let g1 = (2.0 * PI * bandwidth / dc).cos();
259 let s1 = 1.0/g1 - (1.0/(g1*g1) - 1.0).max(0.0).sqrt();
260
261 let bp = 0.5 * (1.0 - s1) * (input - price_prev[1])
262 + l1 * (1.0 + s1) * bp_hist[0]
263 - s1 * bp_hist[1];
264
265 bp_hist[1] = bp_hist[0];
266 bp_hist[0] = bp;
267 price_prev[1] = price_prev[0];
268 price_prev[0] = input;
269 expected.push(bp);
270 }
271
272 for (s, e) in streaming_results.iter().zip(expected.iter()) {
273 approx::assert_relative_eq!(s, e, epsilon = 1e-10);
274 }
275 }
276 }
277}