quantwave_core/indicators/
ultimate_smoother.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5#[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 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}