quantwave_core/indicators/
autotune.rs1use crate::indicators::high_pass::HighPass;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4use crate::utils::RingBuffer as 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!(
196 at.dc_prev >= 8.0 && at.dc_prev <= 12.0,
197 "DC was {}",
198 at.dc_prev
199 );
200 }
201
202 proptest! {
203 #[test]
204 fn test_autotune_parity(
205 inputs in prop::collection::vec(90.0..110.0, 60..100),
206 ) {
207 let window = 20;
208 let bandwidth = 0.25;
209 let mut at_obj = AutoTuneFilter::new(window, bandwidth);
210 let streaming_results: Vec<f64> = inputs.iter().map(|&x| at_obj.next(x)).collect();
211
212 let mut hp = HighPass::new(window);
214 let mut filt_hist = VecDeque::with_capacity(2 * window);
215 let mut dc_prev = window as f64;
216 let mut price_prev = [0.0; 2];
217 let mut bp_hist = [0.0; 2];
218 let mut expected = Vec::with_capacity(inputs.len());
219
220 for (i, &input) in inputs.iter().enumerate() {
221 let filt = hp.next(input);
222 filt_hist.push_front(filt);
223 if filt_hist.len() > 2 * window {
224 filt_hist.pop_back();
225 }
226
227 if filt_hist.len() < 2 * window {
228 price_prev[1] = price_prev[0];
229 price_prev[0] = input;
230 expected.push(0.0);
231 continue;
232 }
233
234 let mut dc = dc_prev;
235 let mut min_corr = 1.0;
236 for lag in 1..=window {
237 let mut sx = 0.0; let mut sy = 0.0;
238 let mut sxx = 0.0; let mut sxy = 0.0; let mut syy = 0.0;
239 for j in 0..window {
240 let x = filt_hist[j];
241 let y = filt_hist[lag+j];
242 sx += x; sy += y;
243 sxx += x*x; sxy += x*y; syy += y*y;
244 }
245 let div1 = (window as f64) * sxx - sx*sx;
246 let div2 = (window as f64) * syy - sy*sy;
247 if div1 > 0.0 && div2 > 0.0 {
248 let corr = ((window as f64) * sxy - sx*sy) / (div1*div2).sqrt();
249 if corr < min_corr {
250 min_corr = corr;
251 dc = 2.0 * lag as f64;
252 }
253 }
254 }
255
256 if dc > dc_prev + 2.0 { dc = dc_prev + 2.0; }
257 if dc < dc_prev - 2.0 { dc = dc_prev - 2.0; }
258 if dc < 2.0 { dc = 2.0; }
259 dc_prev = dc;
260
261 let l1 = (2.0 * PI / dc).cos();
262 let g1 = (2.0 * PI * bandwidth / dc).cos();
263 let s1 = 1.0/g1 - (1.0/(g1*g1) - 1.0).max(0.0).sqrt();
264
265 let bp = 0.5 * (1.0 - s1) * (input - price_prev[1])
266 + l1 * (1.0 + s1) * bp_hist[0]
267 - s1 * bp_hist[1];
268
269 bp_hist[1] = bp_hist[0];
270 bp_hist[0] = bp;
271 price_prev[1] = price_prev[0];
272 price_prev[0] = input;
273 expected.push(bp);
274 }
275
276 for (s, e) in streaming_results.iter().zip(expected.iter()) {
277 approx::assert_relative_eq!(s, e, epsilon = 1e-10);
278 }
279 }
280 }
281}