wickra_core/indicators/
super_smoother.rs1#![allow(clippy::doc_markdown)]
3
4use std::f64::consts::PI;
5
6use crate::error::{Error, Result};
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
40pub struct SuperSmoother {
41 period: usize,
42 c1: f64,
43 c2: f64,
44 c3: f64,
45 prev_input: Option<f64>,
46 prev_output_1: Option<f64>,
47 prev_output_2: Option<f64>,
48 count: usize,
49}
50
51impl SuperSmoother {
52 pub fn new(period: usize) -> Result<Self> {
58 if period == 0 {
59 return Err(Error::PeriodZero);
60 }
61 let arg = std::f64::consts::SQRT_2 * PI / period as f64;
62 let a1 = (-arg).exp();
63 let b1 = 2.0 * a1 * arg.cos();
64 let c2 = b1;
65 let c3 = -a1 * a1;
66 let c1 = 1.0 - c2 - c3;
67 Ok(Self {
68 period,
69 c1,
70 c2,
71 c3,
72 prev_input: None,
73 prev_output_1: None,
74 prev_output_2: None,
75 count: 0,
76 })
77 }
78
79 pub const fn period(&self) -> usize {
81 self.period
82 }
83
84 pub const fn coefficients(&self) -> (f64, f64, f64) {
86 (self.c1, self.c2, self.c3)
87 }
88
89 pub const fn value(&self) -> Option<f64> {
91 self.prev_output_1
92 }
93}
94
95impl Indicator for SuperSmoother {
96 type Input = f64;
97 type Output = f64;
98
99 fn update(&mut self, input: f64) -> Option<f64> {
100 if !input.is_finite() {
101 return self.prev_output_1;
102 }
103 self.count += 1;
104 let output = match (self.prev_input, self.prev_output_1, self.prev_output_2) {
105 (Some(p_in), Some(y1), Some(y2)) => {
106 let avg = 0.5 * (input + p_in);
107 self.c1 * avg + self.c2 * y1 + self.c3 * y2
108 }
109 _ => input,
110 };
111 self.prev_output_2 = self.prev_output_1;
112 self.prev_output_1 = Some(output);
113 self.prev_input = Some(input);
114 Some(output)
115 }
116
117 fn reset(&mut self) {
118 self.prev_input = None;
119 self.prev_output_1 = None;
120 self.prev_output_2 = None;
121 self.count = 0;
122 }
123
124 fn warmup_period(&self) -> usize {
125 1
126 }
127
128 fn is_ready(&self) -> bool {
129 self.prev_output_1.is_some()
130 }
131
132 fn name(&self) -> &'static str {
133 "SuperSmoother"
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::traits::BatchExt;
141 use approx::assert_relative_eq;
142
143 #[test]
144 fn new_rejects_zero_period() {
145 assert!(matches!(SuperSmoother::new(0), Err(Error::PeriodZero)));
146 }
147
148 #[test]
149 fn accessors_and_metadata() {
150 let mut ss = SuperSmoother::new(10).unwrap();
151 assert_eq!(ss.period(), 10);
152 assert_eq!(ss.name(), "SuperSmoother");
153 assert_eq!(ss.warmup_period(), 1);
154 let (c1, c2, c3) = ss.coefficients();
155 assert_relative_eq!(c1 + c2 + c3, 1.0, epsilon = 1e-12);
157 assert!(ss.value().is_none());
158 ss.update(42.0);
159 assert!(ss.value().is_some());
160 assert!(ss.is_ready());
161 }
162
163 #[test]
164 fn first_output_equals_input_then_filters() {
165 let mut ss = SuperSmoother::new(10).unwrap();
166 assert_eq!(ss.update(100.0), Some(100.0));
168 assert_eq!(ss.update(101.0), Some(101.0));
169 let third = ss.update(102.0).unwrap();
170 assert!((third - 102.0).abs() < 5.0);
173 }
174
175 #[test]
176 fn constant_series_converges_to_constant() {
177 let mut ss = SuperSmoother::new(20).unwrap();
180 let out = ss.batch(&[50.0_f64; 200]);
181 for x in out.iter().skip(50).flatten() {
182 assert_relative_eq!(*x, 50.0, epsilon = 1e-9);
183 }
184 }
185
186 #[test]
187 fn batch_equals_streaming() {
188 let prices: Vec<f64> = (0..120)
189 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
190 .collect();
191 let mut a = SuperSmoother::new(15).unwrap();
192 let mut b = SuperSmoother::new(15).unwrap();
193 let batch = a.batch(&prices);
194 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
195 assert_eq!(batch, streamed);
196 }
197
198 #[test]
199 fn ignores_non_finite_input() {
200 let mut ss = SuperSmoother::new(10).unwrap();
201 ss.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
202 let before = ss.value();
203 assert!(before.is_some());
204 assert_eq!(ss.update(f64::NAN), before);
205 assert_eq!(ss.update(f64::INFINITY), before);
206 }
207
208 #[test]
209 fn reset_clears_state() {
210 let mut ss = SuperSmoother::new(10).unwrap();
211 ss.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
212 assert!(ss.is_ready());
213 ss.reset();
214 assert!(!ss.is_ready());
215 assert_eq!(ss.update(50.0), Some(50.0));
216 }
217}