wickra_core/indicators/
hilbert_dominant_cycle.rs1#![allow(clippy::manual_clamp)]
3
4use std::f64::consts::PI;
5
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Default)]
33pub struct HilbertDominantCycle {
34 smooth_buf: Vec<f64>,
36 detrender_buf: Vec<f64>,
38 q1_buf: Vec<f64>,
39 i1_buf: Vec<f64>,
40 prev_i2: f64,
42 prev_q2: f64,
43 prev_re: f64,
44 prev_im: f64,
45 prev_period: f64,
46 prev_smooth_period: f64,
47 count: usize,
48 last_value: Option<f64>,
49}
50
51impl HilbertDominantCycle {
52 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub const fn value(&self) -> Option<f64> {
59 self.last_value
60 }
61}
62
63impl Indicator for HilbertDominantCycle {
64 type Input = f64;
65 type Output = f64;
66
67 fn update(&mut self, input: f64) -> Option<f64> {
68 if !input.is_finite() {
69 return self.last_value;
70 }
71 self.count += 1;
72
73 Self::push_front(&mut self.smooth_buf, input, 7);
76 if self.smooth_buf.len() < 4 {
77 return None;
78 }
79 let smooth = (4.0 * self.smooth_buf[0]
80 + 3.0 * self.smooth_buf[1]
81 + 2.0 * self.smooth_buf[2]
82 + self.smooth_buf[3])
83 / 10.0;
84
85 let period = self.prev_period.max(6.0).min(50.0);
87 let adj = 0.075 * period + 0.54;
88
89 if self.smooth_buf.len() < 7 {
91 return None;
92 }
93
94 let s0 = smooth;
96 let s2 = self.smooth_buf[2];
97 let s4 = self.smooth_buf[4];
98 let s6 = self.smooth_buf[6];
99 let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
100 Self::push_front(&mut self.detrender_buf, detrender, 7);
101
102 if self.detrender_buf.len() < 7 {
103 return None;
104 }
105 let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
107 - 0.5769 * self.detrender_buf[4]
108 - 0.0962 * self.detrender_buf[6])
109 * adj;
110 let i1 = self.detrender_buf[3];
111
112 Self::push_front(&mut self.q1_buf, q1, 7);
113 Self::push_front(&mut self.i1_buf, i1, 7);
114 if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
115 return None;
116 }
117
118 let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
120 - 0.5769 * self.i1_buf[4]
121 - 0.0962 * self.i1_buf[6])
122 * adj;
123 let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
124 - 0.5769 * self.q1_buf[4]
125 - 0.0962 * self.q1_buf[6])
126 * adj;
127
128 let mut i2 = i1 - jq;
130 let mut q2 = q1 + ji;
131 i2 = 0.2 * i2 + 0.8 * self.prev_i2;
132 q2 = 0.2 * q2 + 0.8 * self.prev_q2;
133
134 let mut re = i2 * self.prev_i2 + q2 * self.prev_q2;
136 let mut im = i2 * self.prev_q2 - q2 * self.prev_i2;
137 re = 0.2 * re + 0.8 * self.prev_re;
138 im = 0.2 * im + 0.8 * self.prev_im;
139
140 self.prev_i2 = i2;
141 self.prev_q2 = q2;
142 self.prev_re = re;
143 self.prev_im = im;
144
145 let mut new_period = if im.abs() > f64::EPSILON && re.abs() > f64::EPSILON {
146 2.0 * PI / im.atan2(re)
147 } else {
148 self.prev_period
149 };
150 new_period = new_period.min(1.5 * self.prev_period);
152 new_period = new_period.max(0.67 * self.prev_period);
153 new_period = new_period.clamp(6.0, 50.0);
154
155 self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
157 self.prev_smooth_period = 0.33 * self.prev_period + 0.67 * self.prev_smooth_period;
159
160 if self.count < 50 {
161 return None;
162 }
163 self.last_value = Some(self.prev_smooth_period);
164 Some(self.prev_smooth_period)
165 }
166
167 fn reset(&mut self) {
168 self.smooth_buf.clear();
169 self.detrender_buf.clear();
170 self.q1_buf.clear();
171 self.i1_buf.clear();
172 self.prev_i2 = 0.0;
173 self.prev_q2 = 0.0;
174 self.prev_re = 0.0;
175 self.prev_im = 0.0;
176 self.prev_period = 0.0;
177 self.prev_smooth_period = 0.0;
178 self.count = 0;
179 self.last_value = None;
180 }
181
182 fn warmup_period(&self) -> usize {
183 50
184 }
185
186 fn is_ready(&self) -> bool {
187 self.last_value.is_some()
188 }
189
190 fn name(&self) -> &'static str {
191 "HilbertDominantCycle"
192 }
193}
194
195impl HilbertDominantCycle {
196 fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
198 buf.insert(0, v);
199 if buf.len() > cap {
200 buf.truncate(cap);
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::traits::BatchExt;
209
210 #[test]
211 fn accessors_and_metadata() {
212 let mut ht = HilbertDominantCycle::new();
213 assert_eq!(ht.warmup_period(), 50);
214 assert_eq!(ht.name(), "HilbertDominantCycle");
215 assert!(!ht.is_ready());
216 assert!(ht.value().is_none());
217 for i in 0..120 {
218 ht.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
219 }
220 assert!(ht.is_ready());
221 assert!(ht.value().is_some());
222 }
223
224 #[test]
225 fn output_within_clamp_band() {
226 let mut ht = HilbertDominantCycle::new();
227 let prices: Vec<f64> = (0..200)
228 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
229 .collect();
230 let out = ht.batch(&prices);
231 for v in out.iter().flatten() {
232 assert!((6.0..=50.0).contains(v), "period {v} outside [6, 50]");
233 }
234 }
235
236 #[test]
237 fn batch_equals_streaming() {
238 let prices: Vec<f64> = (0..200)
239 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
240 .collect();
241 let mut a = HilbertDominantCycle::new();
242 let mut b = HilbertDominantCycle::new();
243 let batch = a.batch(&prices);
244 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
245 assert_eq!(batch, streamed);
246 }
247
248 #[test]
249 fn ignores_non_finite_input() {
250 let mut ht = HilbertDominantCycle::new();
251 let prices: Vec<f64> = (0..120)
252 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
253 .collect();
254 ht.batch(&prices);
255 let before = ht.value();
256 assert!(before.is_some());
257 assert_eq!(ht.update(f64::NAN), before);
258 }
259
260 #[test]
261 fn reset_clears_state() {
262 let mut ht = HilbertDominantCycle::new();
263 let prices: Vec<f64> = (0..120)
264 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
265 .collect();
266 ht.batch(&prices);
267 assert!(ht.is_ready());
268 ht.reset();
269 assert!(!ht.is_ready());
270 assert!(ht.value().is_none());
271 }
272}