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