Skip to main content

quantwave_core/indicators/
ultimate_smoother.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5/// UltimateSmoother Filter
6///
7/// Based on John Ehlers' "The Ultimate Smoother"
8/// It conceptually has zero lag in the Pass Band and has minimum lag in the transition band.
9/// It is constructed by subtracting the High Pass response from the input data (cancellation).
10#[derive(Debug, Clone)]
11pub struct UltimateSmoother {
12    c1: f64,
13    c2: f64,
14    c3: f64,
15    price_history: [f64; 2],
16    us_history: [f64; 2],
17    count: usize,
18}
19
20impl UltimateSmoother {
21    pub fn new(period: usize) -> Self {
22        let period_f = period as f64;
23        let a1 = (-1.414 * PI / period_f).exp();
24        let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
25        let c3 = -a1 * a1;
26        let c1 = (1.0 + c2 - c3) / 4.0;
27        Self {
28            c1,
29            c2,
30            c3,
31            price_history: [0.0; 2],
32            us_history: [0.0; 2],
33            count: 0,
34        }
35    }
36}
37
38impl Next<f64> for UltimateSmoother {
39    type Output = f64;
40
41    fn next(&mut self, input: f64) -> Self::Output {
42        self.count += 1;
43        let res = if self.count < 4 {
44            input
45        } else {
46            (1.0 - self.c1) * input + (2.0 * self.c1 - self.c2) * self.price_history[0]
47                - (self.c1 + self.c3) * self.price_history[1]
48                + self.c2 * self.us_history[0]
49                + self.c3 * self.us_history[1]
50        };
51
52        self.us_history[1] = self.us_history[0];
53        self.us_history[0] = res;
54        self.price_history[1] = self.price_history[0];
55        self.price_history[0] = input;
56        res
57    }
58}
59
60pub const ULTIMATE_SMOOTHER_METADATA: IndicatorMetadata = IndicatorMetadata {
61    name: "UltimateSmoother",
62    description: "An Ehlers filter with zero lag in the Pass Band, constructed by subtracting High Pass response from the input data.",
63    usage: "Use when you need near-zero phase lag smoothing with very low ripple. It is Ehlers preferred smoother for applications where timing precision is critical.",
64    keywords: &["filter", "smoothing", "ehlers", "dsp", "zero-lag"],
65    ehlers_summary: "Ehlers designs the Ultimate Smoother in Cycle Analytics for Traders to minimize both lag and ripple simultaneously. It achieves near-zero phase shift across the passband while providing excellent attenuation of high-frequency noise, making it his preferred general-purpose smoother for cycle-sensitive applications.",
66    params: &[ParamDef {
67        name: "period",
68        default: "20",
69        description: "Critical period (wavelength)",
70    }],
71    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/UltimateSmoother.pdf",
72    formula_latex: r#"
73\[
74a_1 = \exp\left(-\frac{1.414\pi}{Period}\right)
75\]
76\[
77c_2 = 2a_1 \cos\left(\frac{1.414\pi}{Period}\right)
78\]
79\[
80c_3 = -a_1^2
81\]
82\[
83c_1 = (1 + c_2 - c_3) / 4
84\]
85\[
86US = (1 - c_1) Price + (2c_1 - c_2) Price_{t-1} - (c_1 + c_3) Price_{t-2} + c_2 US_{t-1} + c_3 US_{t-2}
87\]
88"#,
89    gold_standard_file: "ultimate_smoother.json",
90    category: "Ehlers DSP",
91};
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::traits::Next;
97    use proptest::prelude::*;
98
99    #[test]
100    fn test_ultimate_smoother_basic() {
101        let mut us = UltimateSmoother::new(20);
102        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
103        for input in inputs {
104            let res = us.next(input);
105            println!("Input: {}, Output: {}", input, res);
106            assert!(!res.is_nan());
107        }
108    }
109
110    proptest! {
111        #[test]
112        fn test_ultimate_smoother_parity(
113            inputs in prop::collection::vec(1.0..100.0, 10..100),
114        ) {
115            let period = 20;
116            let mut us = UltimateSmoother::new(period);
117            let streaming_results: Vec<f64> = inputs.iter().map(|&x| us.next(x)).collect();
118
119            // Batch implementation
120            let mut batch_results = Vec::with_capacity(inputs.len());
121            let period_f = period as f64;
122            let a1 = (-1.414 * PI / period_f).exp();
123            let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
124            let c3 = -a1 * a1;
125            let c1 = (1.0 + c2 - c3) / 4.0;
126
127            let mut us_hist = [0.0; 2];
128            let mut price_hist = [0.0; 2];
129
130            for (i, &input) in inputs.iter().enumerate() {
131                let bar = i + 1;
132                let res = if bar < 4 {
133                    input
134                } else {
135                    (1.0 - c1) * input + (2.0 * c1 - c2) * price_hist[0] - (c1 + c3) * price_hist[1] + c2 * us_hist[0] + c3 * us_hist[1]
136                };
137                us_hist[1] = us_hist[0];
138                us_hist[0] = res;
139                price_hist[1] = price_hist[0];
140                price_hist[0] = input;
141                batch_results.push(res);
142            }
143
144            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
145                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
146            }
147        }
148    }
149}