wickra_core/indicators/
ht_dcphase.rs1#![allow(clippy::manual_clamp)]
3
4use std::f64::consts::PI;
5
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Default)]
34pub struct HtDcPhase {
35 smooth_buf: Vec<f64>,
36 detrender_buf: Vec<f64>,
37 q1_buf: Vec<f64>,
38 i1_buf: Vec<f64>,
39 smooth_price: Vec<f64>,
42 prev_i2: f64,
43 prev_q2: f64,
44 prev_re: f64,
45 prev_im: f64,
46 prev_period: f64,
47 prev_smooth_period: f64,
48 count: usize,
49 last_value: Option<f64>,
50}
51
52impl HtDcPhase {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub const fn value(&self) -> Option<f64> {
60 self.last_value
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 HtDcPhase {
72 type Input = f64;
73 type Output = f64;
74
75 fn update(&mut self, input: f64) -> Option<f64> {
76 if !input.is_finite() {
77 return self.last_value;
78 }
79 self.count += 1;
80
81 Self::push_front(&mut self.smooth_buf, input, 7);
82 if self.smooth_buf.len() < 7 {
83 return None;
84 }
85 let smooth = (4.0 * self.smooth_buf[0]
86 + 3.0 * self.smooth_buf[1]
87 + 2.0 * self.smooth_buf[2]
88 + self.smooth_buf[3])
89 / 10.0;
90 Self::push_front(&mut self.smooth_price, smooth, 50);
91
92 let period = self.prev_period.max(6.0).min(50.0);
93 let adj = 0.075 * period + 0.54;
94
95 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 if self.detrender_buf.len() < 7 {
102 return None;
103 }
104
105 let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
106 - 0.5769 * self.detrender_buf[4]
107 - 0.0962 * self.detrender_buf[6])
108 * adj;
109 let i1 = self.detrender_buf[3];
110
111 Self::push_front(&mut self.q1_buf, q1, 7);
112 Self::push_front(&mut self.i1_buf, i1, 7);
113 if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
114 return None;
115 }
116
117 let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
118 - 0.5769 * self.i1_buf[4]
119 - 0.0962 * self.i1_buf[6])
120 * adj;
121 let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
122 - 0.5769 * self.q1_buf[4]
123 - 0.0962 * self.q1_buf[6])
124 * adj;
125
126 let mut i2 = i1 - jq;
127 let mut q2 = q1 + ji;
128 i2 = 0.2 * i2 + 0.8 * self.prev_i2;
129 q2 = 0.2 * q2 + 0.8 * self.prev_q2;
130
131 let mut re = i2 * self.prev_i2 + q2 * self.prev_q2;
132 let mut im = i2 * self.prev_q2 - q2 * self.prev_i2;
133 re = 0.2 * re + 0.8 * self.prev_re;
134 im = 0.2 * im + 0.8 * self.prev_im;
135
136 self.prev_i2 = i2;
137 self.prev_q2 = q2;
138 self.prev_re = re;
139 self.prev_im = im;
140
141 let mut new_period = if im.abs() > f64::EPSILON && re.abs() > f64::EPSILON {
142 2.0 * PI / im.atan2(re)
143 } else {
144 self.prev_period
145 };
146 new_period = new_period.min(1.5 * self.prev_period);
147 new_period = new_period.max(0.67 * self.prev_period);
148 new_period = new_period.clamp(6.0, 50.0);
149 self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
150 self.prev_smooth_period = 0.33 * self.prev_period + 0.67 * self.prev_smooth_period;
151
152 if self.count < 50 {
153 return None;
154 }
155
156 let smooth_period = self.prev_smooth_period;
159 let dc_period = (smooth_period + 0.5) as usize;
160 let dc_period = dc_period.clamp(1, self.smooth_price.len());
161 let mut real_part = 0.0;
162 let mut imag_part = 0.0;
163 for i in 0..dc_period {
164 let angle = (i as f64) * 2.0 * PI / (dc_period as f64);
165 let sp = self.smooth_price[i];
166 real_part += angle.sin() * sp;
167 imag_part += angle.cos() * sp;
168 }
169
170 let dc_phase = compute_dc_phase(real_part, imag_part, smooth_period);
171
172 self.last_value = Some(dc_phase);
173 Some(dc_phase)
174 }
175
176 fn reset(&mut self) {
177 self.smooth_buf.clear();
178 self.detrender_buf.clear();
179 self.q1_buf.clear();
180 self.i1_buf.clear();
181 self.smooth_price.clear();
182 self.prev_i2 = 0.0;
183 self.prev_q2 = 0.0;
184 self.prev_re = 0.0;
185 self.prev_im = 0.0;
186 self.prev_period = 0.0;
187 self.prev_smooth_period = 0.0;
188 self.count = 0;
189 self.last_value = None;
190 }
191
192 fn warmup_period(&self) -> usize {
193 50
194 }
195
196 fn is_ready(&self) -> bool {
197 self.last_value.is_some()
198 }
199
200 fn name(&self) -> &'static str {
201 "HT_DCPHASE"
202 }
203}
204
205fn compute_dc_phase(real_part: f64, imag_part: f64, smooth_period: f64) -> f64 {
212 let mut dc_phase = if imag_part.abs() > 0.001 {
213 (real_part / imag_part).atan().to_degrees()
214 } else if real_part < 0.0 {
215 -90.0
216 } else {
217 90.0
218 };
219 dc_phase += 90.0;
220 dc_phase += 360.0 / smooth_period;
222 if imag_part < 0.0 {
223 dc_phase += 180.0;
224 }
225 if dc_phase > 315.0 {
226 dc_phase -= 360.0;
227 }
228 dc_phase
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::traits::BatchExt;
235
236 fn sine_prices(n: usize) -> Vec<f64> {
237 (0..n)
238 .map(|i| 100.0 + (i as f64 * 0.4).sin() * 5.0)
239 .collect()
240 }
241
242 #[test]
243 fn accessors_and_metadata() {
244 let ht = HtDcPhase::new();
245 assert_eq!(ht.warmup_period(), 50);
246 assert_eq!(ht.name(), "HT_DCPHASE");
247 assert!(!ht.is_ready());
248 }
249
250 #[test]
251 fn near_zero_imaginary_collapses_to_signed_ninety() {
252 let pos = compute_dc_phase(1.0, 0.0, 20.0);
256 let neg = compute_dc_phase(-1.0, 0.0, 20.0);
257 assert!((pos - 198.0).abs() < 1e-9);
258 assert!((neg - 18.0).abs() < 1e-9);
259 let mid = compute_dc_phase(1.0, 1.0, 20.0);
261 assert!((mid - 153.0).abs() < 1e-9);
262 }
263
264 #[test]
265 fn emits_after_warmup_within_phase_band() {
266 let mut ht = HtDcPhase::new();
267 let out: Vec<Option<f64>> = ht.batch(&sine_prices(200));
268 assert_eq!(out[0], None);
269 assert!(ht.is_ready());
270 for v in out.into_iter().flatten() {
271 assert!(v.is_finite(), "phase must be finite");
272 assert!((-360.0..=360.0).contains(&v), "phase {v} outside band");
273 }
274 }
275
276 #[test]
277 fn ignores_non_finite_input() {
278 let mut ht = HtDcPhase::new();
279 let _ = ht.batch(&sine_prices(120));
280 let before = ht.value();
281 assert_eq!(ht.update(f64::NAN), before);
282 }
283
284 #[test]
285 fn batch_equals_streaming() {
286 let prices = sine_prices(200);
287 let mut a = HtDcPhase::new();
288 let mut b = HtDcPhase::new();
289 let batch = a.batch(&prices);
290 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
291 assert_eq!(batch, streamed);
292 }
293
294 #[test]
295 fn reset_clears_state() {
296 let mut ht = HtDcPhase::new();
297 let _ = ht.batch(&sine_prices(120));
298 assert!(ht.is_ready());
299 ht.reset();
300 assert!(!ht.is_ready());
301 assert_eq!(ht.update(100.0), None);
302 }
303}