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 smooth_buf: Vec<f64>,
43 detrender_buf: Vec<f64>,
44 q1_buf: Vec<f64>,
45 i1_buf: Vec<f64>,
46 prev_i2: f64,
47 prev_q2: f64,
48 prev_re: f64,
49 prev_im: f64,
50 prev_period: f64,
51 ready: bool,
52}
53
54impl HtPhasor {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
61 buf.insert(0, v);
62 if buf.len() > cap {
63 buf.truncate(cap);
64 }
65 }
66}
67
68impl Indicator for HtPhasor {
69 type Input = f64;
70 type Output = HtPhasorOutput;
71
72 fn update(&mut self, input: f64) -> Option<HtPhasorOutput> {
73 if !input.is_finite() {
74 return None;
75 }
76
77 Self::push_front(&mut self.smooth_buf, input, 7);
78 if self.smooth_buf.len() < 7 {
79 return None;
80 }
81 let smooth = (4.0 * self.smooth_buf[0]
82 + 3.0 * self.smooth_buf[1]
83 + 2.0 * self.smooth_buf[2]
84 + self.smooth_buf[3])
85 / 10.0;
86
87 let period = self.prev_period.max(6.0).min(50.0);
88 let adj = 0.075 * period + 0.54;
89
90 let s0 = smooth;
91 let s2 = self.smooth_buf[2];
92 let s4 = self.smooth_buf[4];
93 let s6 = self.smooth_buf[6];
94 let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
95 Self::push_front(&mut self.detrender_buf, detrender, 7);
96 if self.detrender_buf.len() < 7 {
97 return None;
98 }
99
100 let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
101 - 0.5769 * self.detrender_buf[4]
102 - 0.0962 * self.detrender_buf[6])
103 * adj;
104 let i1 = self.detrender_buf[3];
105
106 Self::push_front(&mut self.q1_buf, q1, 7);
107 Self::push_front(&mut self.i1_buf, i1, 7);
108 if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
109 return None;
110 }
111
112 let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
115 - 0.5769 * self.i1_buf[4]
116 - 0.0962 * self.i1_buf[6])
117 * adj;
118 let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
119 - 0.5769 * self.q1_buf[4]
120 - 0.0962 * self.q1_buf[6])
121 * adj;
122
123 let mut i2 = i1 - jq;
124 let mut q2 = q1 + ji;
125 i2 = 0.2 * i2 + 0.8 * self.prev_i2;
126 q2 = 0.2 * q2 + 0.8 * self.prev_q2;
127
128 let mut re = i2 * self.prev_i2 + q2 * self.prev_q2;
129 let mut im = i2 * self.prev_q2 - q2 * self.prev_i2;
130 re = 0.2 * re + 0.8 * self.prev_re;
131 im = 0.2 * im + 0.8 * self.prev_im;
132
133 self.prev_i2 = i2;
134 self.prev_q2 = q2;
135 self.prev_re = re;
136 self.prev_im = im;
137
138 let mut new_period = if im.abs() > f64::EPSILON && re.abs() > f64::EPSILON {
139 2.0 * PI / im.atan2(re)
140 } else {
141 self.prev_period
142 };
143 new_period = new_period.min(1.5 * self.prev_period);
144 new_period = new_period.max(0.67 * self.prev_period);
145 new_period = new_period.clamp(6.0, 50.0);
146 self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
147
148 self.ready = true;
149 Some(HtPhasorOutput {
150 inphase: i1,
151 quadrature: q1,
152 })
153 }
154
155 fn reset(&mut self) {
156 self.smooth_buf.clear();
157 self.detrender_buf.clear();
158 self.q1_buf.clear();
159 self.i1_buf.clear();
160 self.prev_i2 = 0.0;
161 self.prev_q2 = 0.0;
162 self.prev_re = 0.0;
163 self.prev_im = 0.0;
164 self.prev_period = 0.0;
165 self.ready = false;
166 }
167
168 fn warmup_period(&self) -> usize {
169 19
170 }
171
172 fn is_ready(&self) -> bool {
173 self.ready
174 }
175
176 fn name(&self) -> &'static str {
177 "HT_PHASOR"
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::traits::BatchExt;
185
186 fn sine_prices(n: usize) -> Vec<f64> {
187 (0..n)
188 .map(|i| 100.0 + (i as f64 * 0.4).sin() * 5.0)
189 .collect()
190 }
191
192 #[test]
193 fn accessors_and_metadata() {
194 let ht = HtPhasor::new();
195 assert_eq!(ht.warmup_period(), 19);
196 assert_eq!(ht.name(), "HT_PHASOR");
197 assert!(!ht.is_ready());
198 }
199
200 #[test]
201 fn emits_after_warmup_and_stays_finite() {
202 let mut ht = HtPhasor::new();
203 let out: Vec<Option<HtPhasorOutput>> = ht.batch(&sine_prices(120));
204 assert_eq!(out[0], None);
205 let first = out.iter().position(Option::is_some).expect("emits");
206 assert!(first <= 19, "first phasor at index {first}");
207 for o in out.into_iter().flatten() {
208 assert!(o.inphase.is_finite() && o.quadrature.is_finite());
209 }
210 assert!(ht.is_ready());
211 }
212
213 #[test]
214 fn ignores_non_finite_input() {
215 let mut ht = HtPhasor::new();
216 let _ = ht.batch(&sine_prices(120));
217 assert_eq!(ht.update(f64::NAN), None);
219 }
220
221 #[test]
222 fn batch_equals_streaming() {
223 let prices = sine_prices(150);
224 let mut a = HtPhasor::new();
225 let mut b = HtPhasor::new();
226 let batch = a.batch(&prices);
227 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
228 assert_eq!(batch, streamed);
229 }
230
231 #[test]
232 fn reset_clears_state() {
233 let mut ht = HtPhasor::new();
234 let _ = ht.batch(&sine_prices(120));
235 assert!(ht.is_ready());
236 ht.reset();
237 assert!(!ht.is_ready());
238 assert_eq!(ht.update(100.0), None);
239 }
240}