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