wickra_core/indicators/
ht_phasor.rs1#![allow(clippy::manual_clamp)]
3
4use std::f64::consts::PI;
5
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct HtPhasorOutput {
11 pub inphase: f64,
13 pub quadrature: f64,
15}
16
17#[derive(Debug, Clone, Default)]
41pub struct HtPhasor {
42 price_buf: Vec<f64>,
44 smooth_buf: Vec<f64>,
46 detrender_buf: Vec<f64>,
47 q1_buf: Vec<f64>,
48 i1_buf: Vec<f64>,
49 prev_i2: f64,
50 prev_q2: f64,
51 prev_re: f64,
52 prev_im: f64,
53 prev_period: f64,
54 ready: bool,
55}
56
57impl HtPhasor {
58 pub fn new() -> Self {
60 Self::default()
61 }
62
63 fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
64 buf.insert(0, v);
65 if buf.len() > cap {
66 buf.truncate(cap);
67 }
68 }
69}
70
71impl Indicator for HtPhasor {
72 type Input = f64;
73 type Output = HtPhasorOutput;
74
75 fn update(&mut self, input: f64) -> Option<HtPhasorOutput> {
76 if !input.is_finite() {
77 return None;
78 }
79
80 Self::push_front(&mut self.price_buf, input, 4);
81 if self.price_buf.len() < 4 {
82 return None;
83 }
84 let smooth = (4.0 * self.price_buf[0]
85 + 3.0 * self.price_buf[1]
86 + 2.0 * self.price_buf[2]
87 + self.price_buf[3])
88 / 10.0;
89 Self::push_front(&mut self.smooth_buf, smooth, 7);
90
91 let period = self.prev_period.max(6.0).min(50.0);
92 let adj = 0.075 * period + 0.54;
93
94 if self.smooth_buf.len() < 7 {
95 return None;
96 }
97 let s0 = smooth;
98 let s2 = self.smooth_buf[2];
99 let s4 = self.smooth_buf[4];
100 let s6 = self.smooth_buf[6];
101 let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
102 Self::push_front(&mut self.detrender_buf, detrender, 7);
103 if self.detrender_buf.len() < 7 {
104 return None;
105 }
106
107 let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
108 - 0.5769 * self.detrender_buf[4]
109 - 0.0962 * self.detrender_buf[6])
110 * adj;
111 let i1 = self.detrender_buf[3];
112
113 Self::push_front(&mut self.q1_buf, q1, 7);
114 Self::push_front(&mut self.i1_buf, i1, 7);
115 if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
116 return None;
117 }
118
119 let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
122 - 0.5769 * self.i1_buf[4]
123 - 0.0962 * self.i1_buf[6])
124 * adj;
125 let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
126 - 0.5769 * self.q1_buf[4]
127 - 0.0962 * self.q1_buf[6])
128 * adj;
129
130 let mut i2 = i1 - jq;
131 let mut q2 = q1 + ji;
132 i2 = 0.2 * i2 + 0.8 * self.prev_i2;
133 q2 = 0.2 * q2 + 0.8 * self.prev_q2;
134
135 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);
151 new_period = new_period.max(0.67 * self.prev_period);
152 new_period = new_period.clamp(6.0, 50.0);
153 self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
154
155 self.ready = true;
156 Some(HtPhasorOutput {
157 inphase: i1,
158 quadrature: q1,
159 })
160 }
161
162 fn reset(&mut self) {
163 self.price_buf.clear();
164 self.smooth_buf.clear();
165 self.detrender_buf.clear();
166 self.q1_buf.clear();
167 self.i1_buf.clear();
168 self.prev_i2 = 0.0;
169 self.prev_q2 = 0.0;
170 self.prev_re = 0.0;
171 self.prev_im = 0.0;
172 self.prev_period = 0.0;
173 self.ready = false;
174 }
175
176 #[inline]
177 fn warmup_period(&self) -> usize {
178 22
179 }
180
181 #[inline]
182 fn is_ready(&self) -> bool {
183 self.ready
184 }
185
186 #[inline]
187 fn name(&self) -> &'static str {
188 "HT_PHASOR"
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::traits::BatchExt;
196
197 fn sine_prices(n: usize) -> Vec<f64> {
198 (0..n)
199 .map(|i| 100.0 + (i as f64 * 0.4).sin() * 5.0)
200 .collect()
201 }
202
203 #[test]
204 fn accessors_and_metadata() {
205 let ht = HtPhasor::new();
206 assert_eq!(ht.warmup_period(), 22);
207 assert_eq!(ht.name(), "HT_PHASOR");
208 assert!(!ht.is_ready());
209 }
210
211 #[test]
212 fn emits_after_warmup_and_stays_finite() {
213 let mut ht = HtPhasor::new();
214 let out: Vec<Option<HtPhasorOutput>> = ht.batch(&sine_prices(120));
215 assert_eq!(out[0], None);
216 let first = out.iter().position(Option::is_some).expect("emits");
217 assert!(first <= 21, "first phasor at index {first}");
218 for o in out.into_iter().flatten() {
219 assert!(o.inphase.is_finite() && o.quadrature.is_finite());
220 }
221 assert!(ht.is_ready());
222 }
223
224 #[test]
225 fn ignores_non_finite_input() {
226 let mut ht = HtPhasor::new();
227 let _ = ht.batch(&sine_prices(120));
228 assert_eq!(ht.update(f64::NAN), None);
230 }
231
232 #[test]
233 fn batch_equals_streaming() {
234 let prices = sine_prices(150);
235 let mut a = HtPhasor::new();
236 let mut b = HtPhasor::new();
237 let batch = a.batch(&prices);
238 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
239 assert_eq!(batch, streamed);
240 }
241
242 #[test]
243 fn reset_clears_state() {
244 let mut ht = HtPhasor::new();
245 let _ = ht.batch(&sine_prices(120));
246 assert!(ht.is_ready());
247 ht.reset();
248 assert!(!ht.is_ready());
249 assert_eq!(ht.update(100.0), None);
250 }
251
252 use approx::assert_relative_eq;
253
254 #[test]
255 fn first_value_lands_exactly_at_warmup() {
256 let mut ht = HtPhasor::new();
260 let out = ht.batch(&sine_prices(60));
261 let warmup = ht.warmup_period();
262 assert_eq!(warmup, 22);
263 assert!(out[..warmup - 1].iter().all(Option::is_none));
264 assert!(out[warmup - 1].is_some());
265 }
266
267 #[test]
268 fn reset_replays_identically() {
269 let prices = sine_prices(150);
270 let fresh = HtPhasor::new().batch(&prices);
271 let mut ht = HtPhasor::new();
272 let first = ht.batch(&prices);
273 ht.reset();
274 let second = ht.batch(&prices);
275 assert_eq!(first, fresh);
276 assert_eq!(second, fresh);
277 }
278
279 #[test]
280 fn wma_of_raw_inputs_feeds_detrender_taps() {
281 let mut ht = HtPhasor::new();
282 for p in [10.0, 20.0, 30.0, 40.0] {
284 assert_eq!(ht.update(p), None);
285 }
286 assert_eq!(ht.smooth_buf, vec![30.0]);
287
288 let mut ht = HtPhasor::new();
293 let mut series = [0.0; 10];
294 series[7] = 10.0;
295 let _ = ht.batch(&series);
296 assert_eq!(ht.smooth_buf, vec![2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0]);
297 assert_eq!(ht.detrender_buf.len(), 1);
298 assert_relative_eq!(ht.detrender_buf[0], 2.475, epsilon = 1e-12);
299 }
300
301 #[test]
302 fn inphase_is_detrender_three_bars_back() {
303 let mut ht = HtPhasor::new();
306 let prices = sine_prices(60);
307 let _ = ht.batch(&prices[..59]);
308 let out = ht.update(prices[59]).unwrap();
309 assert_eq!(out.inphase.to_bits(), ht.detrender_buf[3].to_bits());
310 assert_eq!(out.quadrature.to_bits(), ht.q1_buf[0].to_bits());
311 }
312
313 #[test]
314 fn constant_input_stays_finite_and_period_clamps_to_six() {
315 let mut ht = HtPhasor::new();
318 let out = ht.batch(&[0.0; 200]);
319 assert!(out
320 .iter()
321 .flatten()
322 .all(|o| o.inphase.abs().to_bits() == 0 && o.quadrature.abs().to_bits() == 0));
323 assert_relative_eq!(ht.prev_period, 6.0, epsilon = 1e-9);
324 let mut ht = HtPhasor::new();
325 let out = ht.batch(&[100.0; 200]);
326 assert!(out
327 .iter()
328 .flatten()
329 .all(|o| o.inphase.is_finite() && o.quadrature.is_finite()));
330 assert_eq!(out.iter().flatten().count(), 200 - 21);
331 }
332}