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