Skip to main content

quantwave_core/indicators/
super_smoother.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5/// SuperSmoother Filter
6///
7/// Based on John Ehlers' "The Ultimate Smoother"
8/// A second-order IIR filter with a maximally flat Butterworth response.
9/// It provides superior smoothing compared to a first-order EMA with equivalent lag.
10#[derive(Debug, Clone)]
11pub struct SuperSmoother {
12    c1: f64,
13    c2: f64,
14    c3: f64,
15    price_prev: f64,
16    ss_history: [f64; 2],
17    count: usize,
18}
19
20impl SuperSmoother {
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;
27        Self {
28            c1,
29            c2,
30            c3,
31            price_prev: 0.0,
32            ss_history: [0.0; 2],
33            count: 0,
34        }
35    }
36}
37
38impl Next<f64> for SuperSmoother {
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            self.c1 * (input + self.price_prev) / 2.0
47                + self.c2 * self.ss_history[0]
48                + self.c3 * self.ss_history[1]
49        };
50
51        self.ss_history[1] = self.ss_history[0];
52        self.ss_history[0] = res;
53        self.price_prev = input;
54        res
55    }
56}
57
58pub const SUPER_SMOOTHER_METADATA: IndicatorMetadata = IndicatorMetadata {
59    name: "SuperSmoother",
60    description: "A second-order IIR filter with a maximally flat Butterworth response for superior smoothing with minimal lag.",
61    params: &[ParamDef {
62        name: "period",
63        default: "20",
64        description: "Critical period (wavelength)",
65    }],
66    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/UltimateSmoother.pdf",
67    formula_latex: r#"
68\[
69a_1 = \exp\left(-\frac{1.414\pi}{Period}\right)
70\]
71\[
72c_2 = 2a_1 \cos\left(\frac{1.414\pi}{Period}\right)
73\]
74\[
75c_3 = -a_1^2
76\]
77\[
78c_1 = 1 - c_2 - c_3
79\]
80\[
81SS = c_1 \frac{Price + Price_{t-1}}{2} + c_2 SS_{t-1} + c_3 SS_{t-2}
82\]
83"#,
84    gold_standard_file: "super_smoother.json",
85    category: "Ehlers DSP",
86};
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::traits::Next;
92    use proptest::prelude::*;
93
94    #[test]
95    fn test_super_smoother_basic() {
96        let mut ss = SuperSmoother::new(20);
97        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
98        for input in inputs {
99            let res = ss.next(input);
100            println!("Input: {}, Output: {}", input, res);
101            assert!(!res.is_nan());
102        }
103    }
104
105    proptest! {
106        #[test]
107        fn test_super_smoother_parity(
108            inputs in prop::collection::vec(1.0..100.0, 10..100),
109        ) {
110            let period = 20;
111            let mut ss = SuperSmoother::new(period);
112            let streaming_results: Vec<f64> = inputs.iter().map(|&x| ss.next(x)).collect();
113
114            // Batch implementation
115            let mut batch_results = Vec::with_capacity(inputs.len());
116            let period_f = period as f64;
117            let a1 = (-1.414 * PI / period_f).exp();
118            let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
119            let c3 = -a1 * a1;
120            let c1 = 1.0 - c2 - c3;
121
122            let mut ss_hist = [0.0; 2];
123            let mut price_prev = 0.0;
124
125            for (i, &input) in inputs.iter().enumerate() {
126                let bar = i + 1;
127                let res = if bar < 4 {
128                    input
129                } else {
130                    c1 * (input + price_prev) / 2.0 + c2 * ss_hist[0] + c3 * ss_hist[1]
131                };
132                ss_hist[1] = ss_hist[0];
133                ss_hist[0] = res;
134                price_prev = input;
135                batch_results.push(res);
136            }
137
138            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
139                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
140            }
141        }
142    }
143}