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    params: &[ParamDef {
64        name: "period",
65        default: "20",
66        description: "Critical period (wavelength)",
67    }],
68    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/UltimateSmoother.pdf",
69    formula_latex: r#"
70\[
71a_1 = \exp\left(-\frac{1.414\pi}{Period}\right)
72\]
73\[
74c_2 = 2a_1 \cos\left(\frac{1.414\pi}{Period}\right)
75\]
76\[
77c_3 = -a_1^2
78\]
79\[
80c_1 = (1 + c_2 - c_3) / 4
81\]
82\[
83US = (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}
84\]
85"#,
86    gold_standard_file: "ultimate_smoother.json",
87    category: "Ehlers DSP",
88};
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::traits::Next;
94    use proptest::prelude::*;
95
96    #[test]
97    fn test_ultimate_smoother_basic() {
98        let mut us = UltimateSmoother::new(20);
99        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
100        for input in inputs {
101            let res = us.next(input);
102            println!("Input: {}, Output: {}", input, res);
103            assert!(!res.is_nan());
104        }
105    }
106
107    proptest! {
108        #[test]
109        fn test_ultimate_smoother_parity(
110            inputs in prop::collection::vec(1.0..100.0, 10..100),
111        ) {
112            let period = 20;
113            let mut us = UltimateSmoother::new(period);
114            let streaming_results: Vec<f64> = inputs.iter().map(|&x| us.next(x)).collect();
115
116            // Batch implementation
117            let mut batch_results = Vec::with_capacity(inputs.len());
118            let period_f = period as f64;
119            let a1 = (-1.414 * PI / period_f).exp();
120            let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
121            let c3 = -a1 * a1;
122            let c1 = (1.0 + c2 - c3) / 4.0;
123
124            let mut us_hist = [0.0; 2];
125            let mut price_hist = [0.0; 2];
126
127            for (i, &input) in inputs.iter().enumerate() {
128                let bar = i + 1;
129                let res = if bar < 4 {
130                    input
131                } else {
132                    (1.0 - c1) * input + (2.0 * c1 - c2) * price_hist[0] - (c1 + c3) * price_hist[1] + c2 * us_hist[0] + c3 * us_hist[1]
133                };
134                us_hist[1] = us_hist[0];
135                us_hist[0] = res;
136                price_hist[1] = price_hist[0];
137                price_hist[0] = input;
138                batch_results.push(res);
139            }
140
141            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
142                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
143            }
144        }
145    }
146}