1#![allow(clippy::manual_clamp)]
3
4use std::f64::consts::PI;
5
6use crate::indicators::dc_phasor::{self, MAX_DC_PERIOD};
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Default)]
35pub struct HtDcPhase {
36 price_buf: Vec<f64>,
38 smooth_buf: Vec<f64>,
40 detrender_buf: Vec<f64>,
41 q1_buf: Vec<f64>,
42 i1_buf: Vec<f64>,
43 smooth_price: Vec<f64>,
46 prev_i2: f64,
47 prev_q2: f64,
48 prev_re: f64,
49 prev_im: f64,
50 prev_period: f64,
51 prev_smooth_period: f64,
52 count: usize,
53 last_value: Option<f64>,
54}
55
56impl HtDcPhase {
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub const fn value(&self) -> Option<f64> {
64 self.last_value
65 }
66
67 fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
68 buf.insert(0, v);
69 if buf.len() > cap {
70 buf.truncate(cap);
71 }
72 }
73}
74
75impl Indicator for HtDcPhase {
76 type Input = f64;
77 type Output = f64;
78
79 fn update(&mut self, input: f64) -> Option<f64> {
80 if !input.is_finite() {
81 return None;
82 }
83 self.count += 1;
84
85 Self::push_front(&mut self.price_buf, input, 4);
86 if self.price_buf.len() < 4 {
87 return None;
88 }
89 let smooth = (4.0 * self.price_buf[0]
90 + 3.0 * self.price_buf[1]
91 + 2.0 * self.price_buf[2]
92 + self.price_buf[3])
93 / 10.0;
94 Self::push_front(&mut self.smooth_buf, smooth, 7);
95 Self::push_front(&mut self.smooth_price, smooth, MAX_DC_PERIOD);
96
97 let period = self.prev_period.max(6.0).min(50.0);
98 let adj = 0.075 * period + 0.54;
99
100 if self.smooth_buf.len() < 7 {
101 return None;
102 }
103 let s0 = smooth;
104 let s2 = self.smooth_buf[2];
105 let s4 = self.smooth_buf[4];
106 let s6 = self.smooth_buf[6];
107 let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
108 Self::push_front(&mut self.detrender_buf, detrender, 7);
109 if self.detrender_buf.len() < 7 {
110 return None;
111 }
112
113 let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
114 - 0.5769 * self.detrender_buf[4]
115 - 0.0962 * self.detrender_buf[6])
116 * adj;
117 let i1 = self.detrender_buf[3];
118
119 Self::push_front(&mut self.q1_buf, q1, 7);
120 Self::push_front(&mut self.i1_buf, i1, 7);
121 if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
122 return None;
123 }
124
125 let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
126 - 0.5769 * self.i1_buf[4]
127 - 0.0962 * self.i1_buf[6])
128 * adj;
129 let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
130 - 0.5769 * self.q1_buf[4]
131 - 0.0962 * self.q1_buf[6])
132 * adj;
133
134 let mut i2 = i1 - jq;
135 let mut q2 = q1 + ji;
136 i2 = 0.2 * i2 + 0.8 * self.prev_i2;
137 q2 = 0.2 * q2 + 0.8 * self.prev_q2;
138
139 let mut re = i2 * self.prev_i2 + q2 * self.prev_q2;
140 let mut im = i2 * self.prev_q2 - q2 * self.prev_i2;
141 re = 0.2 * re + 0.8 * self.prev_re;
142 im = 0.2 * im + 0.8 * self.prev_im;
143
144 self.prev_i2 = i2;
145 self.prev_q2 = q2;
146 self.prev_re = re;
147 self.prev_im = im;
148
149 let mut new_period = if im.abs() > f64::EPSILON && re.abs() > f64::EPSILON {
150 2.0 * PI / im.atan2(re)
151 } else {
152 self.prev_period
153 };
154 new_period = new_period.min(1.5 * self.prev_period);
155 new_period = new_period.max(0.67 * self.prev_period);
156 new_period = new_period.clamp(6.0, 50.0);
157 self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
158 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
164 let smooth_period = self.prev_smooth_period;
167 let dc_period = (smooth_period + 0.5) as usize;
168 let dc_period = dc_period.clamp(1, self.smooth_price.len());
169 let mut real_part = 0.0;
170 let mut imag_part = 0.0;
171 for (&(sin, cos), &sp) in dc_phasor::phasor(dc_period).iter().zip(&self.smooth_price) {
172 real_part += sin * sp;
173 imag_part += cos * sp;
174 }
175
176 let dc_phase = compute_dc_phase(real_part, imag_part, smooth_period);
177
178 self.last_value = Some(dc_phase);
179 Some(dc_phase)
180 }
181
182 fn reset(&mut self) {
183 self.price_buf.clear();
184 self.smooth_buf.clear();
185 self.detrender_buf.clear();
186 self.q1_buf.clear();
187 self.i1_buf.clear();
188 self.smooth_price.clear();
189 self.prev_i2 = 0.0;
190 self.prev_q2 = 0.0;
191 self.prev_re = 0.0;
192 self.prev_im = 0.0;
193 self.prev_period = 0.0;
194 self.prev_smooth_period = 0.0;
195 self.count = 0;
196 self.last_value = None;
197 }
198
199 #[inline]
200 fn warmup_period(&self) -> usize {
201 50
202 }
203
204 #[inline]
205 fn is_ready(&self) -> bool {
206 self.last_value.is_some()
207 }
208
209 #[inline]
210 fn name(&self) -> &'static str {
211 "HT_DCPHASE"
212 }
213}
214
215fn compute_dc_phase(real_part: f64, imag_part: f64, smooth_period: f64) -> f64 {
222 let mut dc_phase = if imag_part.abs() > 0.001 {
223 (real_part / imag_part).atan().to_degrees()
224 } else if real_part < 0.0 {
225 -90.0
226 } else {
227 90.0
228 };
229 dc_phase += 90.0;
230 dc_phase += 360.0 / smooth_period;
232 if imag_part < 0.0 {
233 dc_phase += 180.0;
234 }
235 if dc_phase > 315.0 {
236 dc_phase -= 360.0;
237 }
238 dc_phase
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::traits::BatchExt;
245
246 fn sine_prices(n: usize) -> Vec<f64> {
247 (0..n)
248 .map(|i| 100.0 + (i as f64 * 0.4).sin() * 5.0)
249 .collect()
250 }
251
252 #[test]
253 fn accessors_and_metadata() {
254 let ht = HtDcPhase::new();
255 assert_eq!(ht.warmup_period(), 50);
256 assert_eq!(ht.name(), "HT_DCPHASE");
257 assert!(!ht.is_ready());
258 }
259
260 #[test]
261 fn near_zero_imaginary_collapses_to_signed_ninety() {
262 let pos = compute_dc_phase(1.0, 0.0, 20.0);
266 let neg = compute_dc_phase(-1.0, 0.0, 20.0);
267 assert!((pos - 198.0).abs() < 1e-9);
268 assert!((neg - 18.0).abs() < 1e-9);
269 let mid = compute_dc_phase(1.0, 1.0, 20.0);
271 assert!((mid - 153.0).abs() < 1e-9);
272 }
273
274 #[test]
275 fn emits_after_warmup_within_phase_band() {
276 let mut ht = HtDcPhase::new();
277 let out: Vec<Option<f64>> = ht.batch(&sine_prices(200));
278 assert_eq!(out[0], None);
279 assert!(ht.is_ready());
280 for v in out.into_iter().flatten() {
281 assert!(v.is_finite(), "phase must be finite");
282 assert!((-360.0..=360.0).contains(&v), "phase {v} outside band");
283 }
284 }
285
286 #[test]
287 fn ignores_non_finite_input() {
288 let mut ht = HtDcPhase::new();
289 let _ = ht.batch(&sine_prices(120));
290 let before = ht.value();
291 assert_eq!(ht.update(f64::NAN), None);
292 assert_eq!(ht.value(), before);
294 }
295
296 #[test]
297 fn batch_equals_streaming() {
298 let prices = sine_prices(200);
299 let mut a = HtDcPhase::new();
300 let mut b = HtDcPhase::new();
301 let batch = a.batch(&prices);
302 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
303 assert_eq!(batch, streamed);
304 }
305
306 #[test]
307 fn reset_clears_state() {
308 let mut ht = HtDcPhase::new();
309 let _ = ht.batch(&sine_prices(120));
310 assert!(ht.is_ready());
311 ht.reset();
312 assert!(!ht.is_ready());
313 assert_eq!(ht.update(100.0), None);
314 }
315
316 use crate::traits::BatchNanExt;
317 use approx::assert_relative_eq;
318
319 #[test]
320 fn first_value_lands_exactly_at_warmup() {
321 let mut ht = HtDcPhase::new();
322 let out = ht.batch(&sine_prices(120));
323 let warmup = ht.warmup_period();
324 assert!(out[..warmup - 1].iter().all(Option::is_none));
325 assert!(out[warmup - 1].is_some());
326 }
327
328 #[test]
329 fn reset_replays_identically() {
330 let prices = sine_prices(150);
331 let fresh = HtDcPhase::new().batch(&prices);
332 let mut ht = HtDcPhase::new();
333 let first = ht.batch(&prices);
334 ht.reset();
335 let second = ht.batch(&prices);
336 assert_eq!(first, fresh);
337 assert_eq!(second, fresh);
338 }
339
340 #[test]
341 fn batch_nan_paths_match_streaming_bitwise() {
342 let prices = sine_prices(150);
343 let mut out = vec![0.0; prices.len()];
344 HtDcPhase::new().batch_nan_into(&prices, &mut out);
345 let nan = HtDcPhase::new().batch_nan(&prices);
346 let fast = HtDcPhase::new().batch_fast(&prices);
347 let mut stream = HtDcPhase::new();
348 let expected: Vec<u64> = prices
349 .iter()
350 .map(|&p| stream.update(p).unwrap_or(f64::NAN).to_bits())
351 .collect();
352 assert!(out.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
353 assert!(nan.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
354 assert!(fast.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
355 }
356
357 #[test]
358 fn wma_of_raw_inputs_feeds_detrender_taps() {
359 let mut ht = HtDcPhase::new();
360 for p in [10.0, 20.0, 30.0, 40.0] {
362 assert_eq!(ht.update(p), None);
363 }
364 assert_eq!(ht.smooth_buf, vec![30.0]);
365 assert_eq!(ht.smooth_price, vec![30.0]);
366
367 let mut ht = HtDcPhase::new();
372 let mut series = [0.0; 10];
373 series[7] = 10.0;
374 let _ = ht.batch(&series);
375 assert_eq!(ht.smooth_buf, vec![2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0]);
376 assert_eq!(ht.detrender_buf.len(), 1);
377 assert_relative_eq!(ht.detrender_buf[0], 2.475, epsilon = 1e-12);
378 }
379
380 #[test]
381 fn dc_phase_quadrant_and_wrap_hand_computed() {
382 assert_relative_eq!(compute_dc_phase(1.0, -1.0, 20.0), 243.0, epsilon = 1e-9);
385 assert_relative_eq!(compute_dc_phase(-1.0, -1.0, 20.0), -27.0, epsilon = 1e-9);
388 assert_relative_eq!(compute_dc_phase(0.0, 0.0, 6.0), 240.0, epsilon = 1e-12);
390 }
391
392 #[test]
393 fn zero_series_phase_converges_to_degenerate_value() {
394 let mut ht = HtDcPhase::new();
397 let out = ht.batch(&[0.0; 400]);
398 assert!(out.iter().flatten().all(|v| v.is_finite()));
399 assert_relative_eq!(ht.value().unwrap(), 240.0, epsilon = 1e-6);
400 let mut ht = HtDcPhase::new();
402 let out = ht.batch(&[100.0; 400]);
403 assert_eq!(
404 out.iter().flatten().filter(|v| v.is_finite()).count(),
405 400 - 49
406 );
407 }
408}