wickra_core/indicators/
decycler.rs1use std::f64::consts::PI;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
42pub struct Decycler {
43 period: usize,
44 alpha: f64,
45 prev_in_1: Option<f64>,
46 prev_in_2: Option<f64>,
47 prev_hp_1: f64,
48 prev_hp_2: f64,
49 last_value: Option<f64>,
50}
51
52impl Decycler {
53 pub fn new(period: usize) -> Result<Self> {
59 if period == 0 {
60 return Err(Error::PeriodZero);
61 }
62 if period > crate::error::MAX_PERIOD {
63 return Err(Error::InvalidPeriod {
64 message: crate::error::PERIOD_ABOVE_MAX,
65 });
66 }
67 let arg = 0.707 * 2.0 * PI / period as f64;
68 let c = arg.cos();
69 let alpha = (c + arg.sin() - 1.0) / c;
70 Ok(Self {
71 period,
72 alpha,
73 prev_in_1: None,
74 prev_in_2: None,
75 prev_hp_1: 0.0,
76 prev_hp_2: 0.0,
77 last_value: None,
78 })
79 }
80
81 pub const fn period(&self) -> usize {
83 self.period
84 }
85
86 pub const fn alpha(&self) -> f64 {
88 self.alpha
89 }
90
91 pub const fn value(&self) -> Option<f64> {
93 self.last_value
94 }
95
96 fn step_hp(&mut self, input: f64) -> f64 {
98 let (Some(x1), Some(x2)) = (self.prev_in_1, self.prev_in_2) else {
99 self.prev_hp_2 = self.prev_hp_1;
100 self.prev_hp_1 = 0.0;
101 return 0.0;
102 };
103 let one_minus_half_alpha = 1.0 - self.alpha / 2.0;
104 let one_minus_alpha = 1.0 - self.alpha;
105 let drv = one_minus_half_alpha * one_minus_half_alpha;
106 let term1 = drv * (input - 2.0 * x1 + x2);
107 let term2 = 2.0 * one_minus_alpha * self.prev_hp_1;
108 let term3 = one_minus_alpha * one_minus_alpha * self.prev_hp_2;
109 let hp = term1 + term2 - term3;
110 self.prev_hp_2 = self.prev_hp_1;
111 self.prev_hp_1 = hp;
112 hp
113 }
114}
115
116impl Indicator for Decycler {
117 type Input = f64;
118 type Output = f64;
119
120 #[inline]
121 fn update(&mut self, input: f64) -> Option<f64> {
122 if !input.is_finite() {
123 return None;
124 }
125 let hp = self.step_hp(input);
126 let v = input - hp;
127 self.prev_in_2 = self.prev_in_1;
128 self.prev_in_1 = Some(input);
129 self.last_value = Some(v);
130 Some(v)
131 }
132
133 fn reset(&mut self) {
134 self.prev_in_1 = None;
135 self.prev_in_2 = None;
136 self.prev_hp_1 = 0.0;
137 self.prev_hp_2 = 0.0;
138 self.last_value = None;
139 }
140
141 #[inline]
142 fn warmup_period(&self) -> usize {
143 1
144 }
145
146 #[inline]
147 fn is_ready(&self) -> bool {
148 self.last_value.is_some()
149 }
150
151 #[inline]
152 fn name(&self) -> &'static str {
153 "Decycler"
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::traits::BatchExt;
161 use approx::assert_relative_eq;
162
163 #[test]
164 fn new_rejects_zero_period() {
165 assert!(matches!(Decycler::new(0), Err(Error::PeriodZero)));
166 }
167
168 #[test]
169 fn accessors_and_metadata() {
170 let mut dc = Decycler::new(20).unwrap();
171 assert_eq!(dc.period(), 20);
172 assert_eq!(dc.warmup_period(), 1);
173 assert_eq!(dc.name(), "Decycler");
174 assert!(dc.alpha() > 0.0 && dc.alpha() < 1.0);
175 assert!(!dc.is_ready());
176 dc.update(100.0);
177 assert!(dc.is_ready());
178 assert!(dc.value().is_some());
179 }
180
181 #[test]
182 fn constant_series_passes_through() {
183 let mut dc = Decycler::new(20).unwrap();
186 let out = dc.batch(&[42.0_f64; 80]);
187 for x in out.iter().flatten() {
188 assert_relative_eq!(*x, 42.0, epsilon = 1e-9);
189 }
190 }
191
192 #[test]
193 fn batch_equals_streaming() {
194 let prices: Vec<f64> = (0..100)
195 .map(|i| 100.0 + (f64::from(i) * 0.15).sin() * 5.0)
196 .collect();
197 let mut a = Decycler::new(20).unwrap();
198 let mut b = Decycler::new(20).unwrap();
199 let batch = a.batch(&prices);
200 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
201 assert_eq!(batch, streamed);
202 }
203
204 #[test]
205 fn ignores_non_finite_input() {
206 let mut dc = Decycler::new(20).unwrap();
207 dc.batch(&(1..=30).map(f64::from).collect::<Vec<_>>());
208 let before = dc.value();
209 assert!(before.is_some());
210 assert_eq!(dc.update(f64::NAN), None);
211 assert_eq!(dc.update(f64::INFINITY), None);
212 }
213
214 #[test]
215 fn reset_clears_state() {
216 let mut dc = Decycler::new(20).unwrap();
217 dc.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
218 assert!(dc.is_ready());
219 dc.reset();
220 assert!(!dc.is_ready());
221 }
222}