quantwave_core/indicators/
super_smoother.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5#[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 usage: "Use as a drop-in replacement for any moving average when maximum smoothing with minimal lag is needed. Ideal as a pre-filter before oscillators to eliminate high-frequency noise.",
62 keywords: &["filter", "smoothing", "ehlers", "dsp", "low-pass"],
63 ehlers_summary: "Ehlers describes the SuperSmoother as a two-pole Butterworth filter achieving the same smoothing as a longer SMA with far less lag. It uses a critically-damped design to eliminate Gibbs phenomenon overshoot while retaining cycle information. — Cybernetic Analysis for Stocks and Futures, 2004",
64 params: &[ParamDef {
65 name: "period",
66 default: "20",
67 description: "Critical period (wavelength)",
68 }],
69 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/UltimateSmoother.pdf",
70 formula_latex: r#"
71\[
72a_1 = \exp\left(-\frac{1.414\pi}{Period}\right)
73\]
74\[
75c_2 = 2a_1 \cos\left(\frac{1.414\pi}{Period}\right)
76\]
77\[
78c_3 = -a_1^2
79\]
80\[
81c_1 = 1 - c_2 - c_3
82\]
83\[
84SS = c_1 \frac{Price + Price_{t-1}}{2} + c_2 SS_{t-1} + c_3 SS_{t-2}
85\]
86"#,
87 gold_standard_file: "super_smoother.json",
88 category: "Ehlers DSP",
89};
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::traits::Next;
95 use proptest::prelude::*;
96
97 #[test]
98 fn test_super_smoother_basic() {
99 let mut ss = SuperSmoother::new(20);
100 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
101 for input in inputs {
102 let res = ss.next(input);
103 println!("Input: {}, Output: {}", input, res);
104 assert!(!res.is_nan());
105 }
106 }
107
108 proptest! {
109 #[test]
110 fn test_super_smoother_parity(
111 inputs in prop::collection::vec(1.0..100.0, 10..100),
112 ) {
113 let period = 20;
114 let mut ss = SuperSmoother::new(period);
115 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ss.next(x)).collect();
116
117 let mut batch_results = Vec::with_capacity(inputs.len());
119 let period_f = period as f64;
120 let a1 = (-1.414 * PI / period_f).exp();
121 let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
122 let c3 = -a1 * a1;
123 let c1 = 1.0 - c2 - c3;
124
125 let mut ss_hist = [0.0; 2];
126 let mut price_prev = 0.0;
127
128 for (i, &input) in inputs.iter().enumerate() {
129 let bar = i + 1;
130 let res = if bar < 4 {
131 input
132 } else {
133 c1 * (input + price_prev) / 2.0 + c2 * ss_hist[0] + c3 * ss_hist[1]
134 };
135 ss_hist[1] = ss_hist[0];
136 ss_hist[0] = res;
137 price_prev = 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}