Skip to main content

quantwave_core/indicators/incremental/
hilbert_ta.rs

1//! Native O(1)-per-bar Hilbert Transform indicators — TA-Lib parity.
2//!
3//! Faithful incremental port of `talib_rs::cycle` and `talib_rs::overlap::ht_trendline`.
4
5use crate::traits::Next;
6
7const RAD2DEG: f64 = 180.0 / std::f64::consts::PI;
8const DEG2RAD: f64 = std::f64::consts::PI / 180.0;
9const CONST_DEG2RAD_BY360: f64 = 2.0 * std::f64::consts::PI;
10const A: f64 = 0.0962;
11const B: f64 = 0.5769;
12const SMOOTH_PRICE_SIZE: usize = 50;
13
14#[derive(Debug, Clone, Default)]
15struct HilbertVars {
16    odd: [f64; 3],
17    even: [f64; 3],
18    prev_odd: f64,
19    prev_even: f64,
20    prev_input_odd: f64,
21    prev_input_even: f64,
22}
23
24#[inline(always)]
25fn do_hilbert_even(vars: &mut HilbertVars, input: f64, hilbert_idx: usize, adj: f64) -> f64 {
26    let t = A * input;
27    let mut result = -vars.even[hilbert_idx];
28    vars.even[hilbert_idx] = t;
29    result += t;
30    result -= vars.prev_even;
31    vars.prev_even = B * vars.prev_input_even;
32    result += vars.prev_even;
33    vars.prev_input_even = input;
34    result * adj
35}
36
37#[inline(always)]
38fn do_hilbert_odd(vars: &mut HilbertVars, input: f64, hilbert_idx: usize, adj: f64) -> f64 {
39    let t = A * input;
40    let mut result = -vars.odd[hilbert_idx];
41    vars.odd[hilbert_idx] = t;
42    result += t;
43    result -= vars.prev_odd;
44    vars.prev_odd = B * vars.prev_input_odd;
45    result += vars.prev_odd;
46    vars.prev_input_odd = input;
47    result * adj
48}
49
50#[derive(Debug, Clone)]
51struct HtWma {
52    period_wma_sub: f64,
53    period_wma_sum: f64,
54    trailing_wma_value: f64,
55    prices: Vec<f64>,
56    trailing_idx: usize,
57}
58
59impl HtWma {
60    fn from_first_three(p0: f64, p1: f64, p2: f64) -> Self {
61        Self {
62            period_wma_sub: p0 + p1 + p2,
63            period_wma_sum: p0 + p1 * 2.0 + p2 * 3.0,
64            trailing_wma_value: 0.0,
65            prices: vec![p0, p1, p2],
66            trailing_idx: 0,
67        }
68    }
69
70    fn next(&mut self, new_price: f64) -> f64 {
71        self.prices.push(new_price);
72        self.period_wma_sub += new_price;
73        self.period_wma_sub -= self.trailing_wma_value;
74        self.period_wma_sum += new_price * 4.0;
75        self.trailing_wma_value = self.prices[self.trailing_idx];
76        self.trailing_idx += 1;
77        let smoothed = self.period_wma_sum * 0.1;
78        self.period_wma_sum -= self.period_wma_sub;
79        smoothed
80    }
81}
82
83#[derive(Debug, Clone)]
84struct HilbertPeriodState {
85    hilbert_idx: usize,
86    detrender_vars: HilbertVars,
87    q1_vars: HilbertVars,
88    ji_vars: HilbertVars,
89    jq_vars: HilbertVars,
90    period: f64,
91    smooth_period: f64,
92    prev_i2: f64,
93    prev_q2: f64,
94    re: f64,
95    im: f64,
96    i1_for_odd_prev2: f64,
97    i1_for_odd_prev3: f64,
98    i1_for_even_prev2: f64,
99    i1_for_even_prev3: f64,
100}
101
102impl Default for HilbertPeriodState {
103    fn default() -> Self {
104        Self {
105            hilbert_idx: 0,
106            detrender_vars: HilbertVars::default(),
107            q1_vars: HilbertVars::default(),
108            ji_vars: HilbertVars::default(),
109            jq_vars: HilbertVars::default(),
110            period: 0.0,
111            smooth_period: 0.0,
112            prev_i2: 0.0,
113            prev_q2: 0.0,
114            re: 0.0,
115            im: 0.0,
116            i1_for_odd_prev2: 0.0,
117            i1_for_odd_prev3: 0.0,
118            i1_for_even_prev2: 0.0,
119            i1_for_even_prev3: 0.0,
120        }
121    }
122}
123
124impl HilbertPeriodState {
125    fn adjust_period(&mut self) {
126        let temp_real = self.period;
127        if self.im != 0.0 && self.re != 0.0 {
128            self.period = 360.0 / ((self.im / self.re).atan() * RAD2DEG);
129        }
130        let mut temp_real2 = 1.5 * temp_real;
131        if self.period > temp_real2 {
132            self.period = temp_real2;
133        }
134        temp_real2 = 0.67 * temp_real;
135        if self.period < temp_real2 {
136            self.period = temp_real2;
137        }
138        if self.period < 6.0 {
139            self.period = 6.0;
140        } else if self.period > 50.0 {
141            self.period = 50.0;
142        }
143        self.period = 0.2 * self.period + 0.8 * temp_real;
144        self.smooth_period = 0.33 * self.period + 0.67 * self.smooth_period;
145    }
146
147    fn step_hilbert(&mut self, today: usize, smoothed: f64, adj: f64) -> (f64, f64, f64, f64) {
148        let (detrender, q1, i2, q2);
149        if today % 2 == 0 {
150            detrender = do_hilbert_even(&mut self.detrender_vars, smoothed, self.hilbert_idx, adj);
151            q1 = do_hilbert_even(&mut self.q1_vars, detrender, self.hilbert_idx, adj);
152            let ji = do_hilbert_even(
153                &mut self.ji_vars,
154                self.i1_for_even_prev3,
155                self.hilbert_idx,
156                adj,
157            );
158            let jq = do_hilbert_even(&mut self.jq_vars, q1, self.hilbert_idx, adj);
159            self.hilbert_idx += 1;
160            if self.hilbert_idx == 3 {
161                self.hilbert_idx = 0;
162            }
163            q2 = 0.2 * (q1 + ji) + 0.8 * self.prev_q2;
164            i2 = 0.2 * (self.i1_for_even_prev3 - jq) + 0.8 * self.prev_i2;
165            self.i1_for_odd_prev3 = self.i1_for_odd_prev2;
166            self.i1_for_odd_prev2 = detrender;
167        } else {
168            detrender = do_hilbert_odd(&mut self.detrender_vars, smoothed, self.hilbert_idx, adj);
169            q1 = do_hilbert_odd(&mut self.q1_vars, detrender, self.hilbert_idx, adj);
170            let ji = do_hilbert_odd(
171                &mut self.ji_vars,
172                self.i1_for_odd_prev3,
173                self.hilbert_idx,
174                adj,
175            );
176            let jq = do_hilbert_odd(&mut self.jq_vars, q1, self.hilbert_idx, adj);
177            q2 = 0.2 * (q1 + ji) + 0.8 * self.prev_q2;
178            i2 = 0.2 * (self.i1_for_odd_prev3 - jq) + 0.8 * self.prev_i2;
179            self.i1_for_even_prev3 = self.i1_for_even_prev2;
180            self.i1_for_even_prev2 = detrender;
181        }
182        self.re = 0.2 * (i2 * self.prev_i2 + q2 * self.prev_q2) + 0.8 * self.re;
183        self.im = 0.2 * (i2 * self.prev_q2 - q2 * self.prev_i2) + 0.8 * self.im;
184        self.prev_q2 = q2;
185        self.prev_i2 = i2;
186        (detrender, q1, i2, q2)
187    }
188}
189
190#[derive(Debug, Clone)]
191struct HtEngine32 {
192    prices: Vec<f64>,
193    wma: Option<HtWma>,
194    warmup_left: u8,
195    hs: HilbertPeriodState,
196}
197
198impl HtEngine32 {
199    const LOOKBACK: usize = 32;
200    const WARMUP: u8 = 9;
201
202    fn new() -> Self {
203        Self {
204            prices: Vec::new(),
205            wma: None,
206            warmup_left: Self::WARMUP,
207            hs: HilbertPeriodState::default(),
208        }
209    }
210
211    fn push(&mut self, price: f64) {
212        self.prices.push(price);
213    }
214
215    fn today(&self) -> usize {
216        self.prices.len().saturating_sub(1)
217    }
218
219    fn step_wma(&mut self) -> Option<f64> {
220        let n = self.prices.len();
221        if n < 3 {
222            return None;
223        }
224        if self.wma.is_none() {
225            self.wma = Some(HtWma::from_first_three(
226                self.prices[0],
227                self.prices[1],
228                self.prices[2],
229            ));
230            return None;
231        }
232        let wma = self.wma.as_mut().unwrap();
233        let price = *self.prices.last().unwrap_or(&0.0);
234        if self.warmup_left > 0 {
235            self.warmup_left -= 1;
236            let _ = wma.next(price);
237            return None;
238        }
239        Some(wma.next(price))
240    }
241}
242
243/// HT_DCPERIOD — lookback 32
244#[derive(Debug, Clone)]
245#[allow(non_camel_case_types)]
246pub struct HT_DCPERIOD {
247    eng: HtEngine32,
248}
249
250impl Default for HT_DCPERIOD {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256impl HT_DCPERIOD {
257    pub fn new() -> Self {
258        Self {
259            eng: HtEngine32::new(),
260        }
261    }
262}
263
264impl Next<f64> for HT_DCPERIOD {
265    type Output = f64;
266
267    fn next(&mut self, input: f64) -> Self::Output {
268        self.eng.push(input);
269        let Some(smoothed) = self.eng.step_wma() else {
270            return f64::NAN;
271        };
272        let today = self.eng.today();
273        let adj = 0.075 * self.eng.hs.period + 0.54;
274        self.eng.hs.step_hilbert(today, smoothed, adj);
275        self.eng.hs.adjust_period();
276        if today >= HtEngine32::LOOKBACK {
277            self.eng.hs.smooth_period
278        } else {
279            f64::NAN
280        }
281    }
282}
283
284/// HT_PHASOR — lookback 32
285#[derive(Debug, Clone)]
286#[allow(non_camel_case_types)]
287pub struct HT_PHASOR {
288    eng: HtEngine32,
289}
290
291impl Default for HT_PHASOR {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297impl HT_PHASOR {
298    pub fn new() -> Self {
299        Self {
300            eng: HtEngine32::new(),
301        }
302    }
303}
304
305impl Next<f64> for HT_PHASOR {
306    type Output = (f64, f64);
307
308    fn next(&mut self, input: f64) -> Self::Output {
309        self.eng.push(input);
310        let Some(smoothed) = self.eng.step_wma() else {
311            return (f64::NAN, f64::NAN);
312        };
313        let today = self.eng.today();
314        let adj = 0.075 * self.eng.hs.period + 0.54;
315        let inphase = if today % 2 == 0 {
316            self.eng.hs.i1_for_even_prev3
317        } else {
318            self.eng.hs.i1_for_odd_prev3
319        };
320        let (_, q1, _, _) = self.eng.hs.step_hilbert(today, smoothed, adj);
321        self.eng.hs.adjust_period();
322        if today >= HtEngine32::LOOKBACK {
323            (inphase, q1)
324        } else {
325            (f64::NAN, f64::NAN)
326        }
327    }
328}
329
330#[derive(Debug, Clone)]
331struct HtEngine63 {
332    prices: Vec<f64>,
333    wma: Option<HtWma>,
334    warmup_left: u8,
335    hs: HilbertPeriodState,
336    smooth_price: [f64; SMOOTH_PRICE_SIZE],
337    smooth_price_idx: usize,
338    dc_phase: f64,
339    prev_dc_phase: f64,
340    i_trend1: f64,
341    i_trend2: f64,
342    i_trend3: f64,
343    days_in_trend: i32,
344    prev_sine: f64,
345    prev_lead_sine: f64,
346    sine: f64,
347    lead_sine: f64,
348    last_smoothed: f64,
349    last_trendline: f64,
350    last_trend: f64,
351}
352
353impl HtEngine63 {
354    const LOOKBACK: usize = 63;
355    const WARMUP: u8 = 34;
356
357    fn new() -> Self {
358        Self {
359            prices: Vec::new(),
360            wma: None,
361            warmup_left: Self::WARMUP,
362            hs: HilbertPeriodState::default(),
363            smooth_price: [0.0; SMOOTH_PRICE_SIZE],
364            smooth_price_idx: 0,
365            dc_phase: 0.0,
366            prev_dc_phase: 0.0,
367            i_trend1: 0.0,
368            i_trend2: 0.0,
369            i_trend3: 0.0,
370            days_in_trend: 0,
371            prev_sine: 0.0,
372            prev_lead_sine: 0.0,
373            sine: 0.0,
374            lead_sine: 0.0,
375            last_smoothed: 0.0,
376            last_trendline: 0.0,
377            last_trend: 0.0,
378        }
379    }
380
381    fn push(&mut self, price: f64) {
382        self.prices.push(price);
383    }
384
385    fn today(&self) -> usize {
386        self.prices.len().saturating_sub(1)
387    }
388
389    fn step_wma(&mut self) -> Option<f64> {
390        let n = self.prices.len();
391        if n < 3 {
392            return None;
393        }
394        if self.wma.is_none() {
395            self.wma = Some(HtWma::from_first_three(
396                self.prices[0],
397                self.prices[1],
398                self.prices[2],
399            ));
400            return None;
401        }
402        let wma = self.wma.as_mut().unwrap();
403        let price = *self.prices.last().unwrap_or(&0.0);
404        if self.warmup_left > 0 {
405            self.warmup_left -= 1;
406            let _ = wma.next(price);
407            return None;
408        }
409        Some(wma.next(price))
410    }
411
412    fn compute_dc_phase(&mut self) {
413        self.prev_dc_phase = self.dc_phase;
414        let dc_period = self.hs.smooth_period + 0.5;
415        let dc_period_int = dc_period as i32;
416        let mut real_part = 0.0_f64;
417        let mut imag_part = 0.0_f64;
418        let mut idx = self.smooth_price_idx;
419        for i in 0..dc_period_int {
420            let angle = (i as f64 * CONST_DEG2RAD_BY360) / dc_period_int as f64;
421            let price = self.smooth_price[idx];
422            real_part += angle.sin() * price;
423            imag_part += angle.cos() * price;
424            if idx == 0 {
425                idx = SMOOTH_PRICE_SIZE - 1;
426            } else {
427                idx -= 1;
428            }
429        }
430        let abs_imag = imag_part.abs();
431        if abs_imag > 0.0 {
432            self.dc_phase = (real_part / imag_part).atan() * RAD2DEG;
433        } else if abs_imag <= 0.01 {
434            if real_part < 0.0 {
435                self.dc_phase -= 90.0;
436            } else if real_part > 0.0 {
437                self.dc_phase += 90.0;
438            }
439        }
440        self.dc_phase += 90.0;
441        self.dc_phase += 360.0 / self.hs.smooth_period;
442        if imag_part < 0.0 {
443            self.dc_phase += 180.0;
444        }
445        if self.dc_phase > 315.0 {
446            self.dc_phase -= 360.0;
447        }
448    }
449
450    fn sum_prices_back(&self, count: i32) -> f64 {
451        let mut temp = 0.0_f64;
452        let mut price_idx = self.today();
453        for _ in 0..count {
454            temp += self.prices[price_idx];
455            if price_idx == 0 {
456                break;
457            }
458            price_idx -= 1;
459        }
460        if count > 0 {
461            temp / count as f64
462        } else {
463            temp
464        }
465    }
466
467    fn step_core(&mut self) -> bool {
468        let Some(smoothed) = self.step_wma() else {
469            return false;
470        };
471        let today = self.today();
472        let adj = 0.075 * self.hs.period + 0.54;
473        self.last_smoothed = smoothed;
474        self.smooth_price[self.smooth_price_idx] = smoothed;
475        self.hs.step_hilbert(today, smoothed, adj);
476        self.hs.adjust_period();
477        self.compute_dc_phase();
478        self.prev_sine = self.sine;
479        self.prev_lead_sine = self.lead_sine;
480        self.sine = (self.dc_phase * DEG2RAD).sin();
481        self.lead_sine = ((self.dc_phase + 45.0) * DEG2RAD).sin();
482        self.smooth_price_idx += 1;
483        if self.smooth_price_idx >= SMOOTH_PRICE_SIZE {
484            self.smooth_price_idx = 0;
485        }
486        self.update_trend_mode();
487        true
488    }
489
490    fn trendline(&mut self) -> f64 {
491        let dc_period_int = (self.hs.smooth_period + 0.5) as i32;
492        let temp = self.sum_prices_back(dc_period_int);
493        let trendline = (4.0 * temp + 3.0 * self.i_trend1 + 2.0 * self.i_trend2 + self.i_trend3) / 10.0;
494        self.i_trend3 = self.i_trend2;
495        self.i_trend2 = self.i_trend1;
496        self.i_trend1 = temp;
497        self.last_trendline = trendline;
498        trendline
499    }
500
501    /// TA-Lib trend-mode decision — must run every bar after WMA warmup, not only at output index.
502    fn update_trend_mode(&mut self) {
503        let trendline = self.trendline();
504        let mut trend = 1.0_f64;
505        if (self.sine > self.lead_sine && self.prev_sine <= self.prev_lead_sine)
506            || (self.sine < self.lead_sine && self.prev_sine >= self.prev_lead_sine)
507        {
508            self.days_in_trend = 0;
509            trend = 0.0;
510        }
511        self.days_in_trend += 1;
512        if (self.days_in_trend as f64) < 0.5 * self.hs.smooth_period {
513            trend = 0.0;
514        }
515        let phase_change = self.dc_phase - self.prev_dc_phase;
516        if self.hs.smooth_period != 0.0
517            && phase_change > 0.67 * 360.0 / self.hs.smooth_period
518            && phase_change < 1.5 * 360.0 / self.hs.smooth_period
519        {
520            trend = 0.0;
521        }
522        if trendline != 0.0 && ((self.last_smoothed - trendline) / trendline).abs() >= 0.015 {
523            trend = 1.0;
524        }
525        self.last_trend = trend;
526    }
527}
528
529#[derive(Debug, Clone)]
530#[allow(non_camel_case_types)]
531pub struct HT_DCPHASE {
532    eng: HtEngine63,
533}
534
535impl Default for HT_DCPHASE {
536    fn default() -> Self {
537        Self::new()
538    }
539}
540
541impl HT_DCPHASE {
542    pub fn new() -> Self {
543        Self {
544            eng: HtEngine63::new(),
545        }
546    }
547}
548
549impl Next<f64> for HT_DCPHASE {
550    type Output = f64;
551
552    fn next(&mut self, input: f64) -> Self::Output {
553        self.eng.push(input);
554        if !self.eng.step_core() {
555            return f64::NAN;
556        }
557        if self.eng.today() >= HtEngine63::LOOKBACK {
558            self.eng.dc_phase
559        } else {
560            f64::NAN
561        }
562    }
563}
564
565#[derive(Debug, Clone)]
566#[allow(non_camel_case_types)]
567pub struct HT_SINE {
568    eng: HtEngine63,
569}
570
571impl Default for HT_SINE {
572    fn default() -> Self {
573        Self::new()
574    }
575}
576
577impl HT_SINE {
578    pub fn new() -> Self {
579        Self {
580            eng: HtEngine63::new(),
581        }
582    }
583}
584
585impl Next<f64> for HT_SINE {
586    type Output = (f64, f64);
587
588    fn next(&mut self, input: f64) -> Self::Output {
589        self.eng.push(input);
590        if !self.eng.step_core() {
591            return (f64::NAN, f64::NAN);
592        }
593        if self.eng.today() >= HtEngine63::LOOKBACK {
594            (self.eng.sine, self.eng.lead_sine)
595        } else {
596            (f64::NAN, f64::NAN)
597        }
598    }
599}
600
601#[derive(Debug, Clone)]
602#[allow(non_camel_case_types)]
603pub struct HT_TRENDMODE {
604    eng: HtEngine63,
605}
606
607impl Default for HT_TRENDMODE {
608    fn default() -> Self {
609        Self::new()
610    }
611}
612
613impl HT_TRENDMODE {
614    pub fn new() -> Self {
615        Self {
616            eng: HtEngine63::new(),
617        }
618    }
619}
620
621impl Next<f64> for HT_TRENDMODE {
622    type Output = f64;
623
624    fn next(&mut self, input: f64) -> Self::Output {
625        self.eng.push(input);
626        if !self.eng.step_core() {
627            return f64::NAN;
628        }
629        if self.eng.today() >= HtEngine63::LOOKBACK {
630            self.eng.last_trend
631        } else {
632            f64::NAN
633        }
634    }
635}
636
637#[derive(Debug, Clone)]
638#[allow(non_camel_case_types)]
639pub struct HT_TRENDLINE {
640    eng: HtEngine63,
641}
642
643impl Default for HT_TRENDLINE {
644    fn default() -> Self {
645        Self::new()
646    }
647}
648
649impl HT_TRENDLINE {
650    pub fn new() -> Self {
651        Self {
652            eng: HtEngine63::new(),
653        }
654    }
655}
656
657impl Next<f64> for HT_TRENDLINE {
658    type Output = f64;
659
660    fn next(&mut self, input: f64) -> Self::Output {
661        self.eng.push(input);
662        if !self.eng.step_core() {
663            return f64::NAN;
664        }
665        if self.eng.today() >= HtEngine63::LOOKBACK {
666            self.eng.last_trendline
667        } else {
668            f64::NAN
669        }
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use proptest::prelude::*;
677
678    proptest! {
679        #[test]
680        fn test_ht_dcperiod_parity(input in prop::collection::vec(0.1..100.0, 33..100)) {
681            let mut ht = HT_DCPERIOD::new();
682            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
683            let batch = talib_rs::cycle::ht_dcperiod(&input).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
684            for (s, b) in streaming.iter().zip(batch.iter()) {
685                if s.is_nan() { assert!(b.is_nan()); }
686                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
687            }
688        }
689
690        #[test]
691        fn test_ht_phasor_parity(input in prop::collection::vec(0.1..100.0, 33..100)) {
692            let mut ht = HT_PHASOR::new();
693            let streaming: Vec<_> = input.iter().map(|&x| ht.next(x)).collect();
694            let (bi, bq) = talib_rs::cycle::ht_phasor(&input).unwrap_or_else(|_| {
695                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
696            });
697            for (i, &(s_i, s_q)) in streaming.iter().enumerate() {
698                if s_i.is_nan() { assert!(bi[i].is_nan()); }
699                else { approx::assert_relative_eq!(s_i, bi[i], epsilon = 1e-6); }
700                if s_q.is_nan() { assert!(bq[i].is_nan()); }
701                else { approx::assert_relative_eq!(s_q, bq[i], epsilon = 1e-6); }
702            }
703        }
704
705        #[test]
706        fn test_ht_dcphase_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
707            let mut ht = HT_DCPHASE::new();
708            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
709            let batch = talib_rs::cycle::ht_dcphase(&input).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
710            for (s, b) in streaming.iter().zip(batch.iter()) {
711                if s.is_nan() { assert!(b.is_nan()); }
712                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
713            }
714        }
715
716        #[test]
717        fn test_ht_sine_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
718            let mut ht = HT_SINE::new();
719            let streaming: Vec<_> = input.iter().map(|&x| ht.next(x)).collect();
720            let (bs, bl) = talib_rs::cycle::ht_sine(&input).unwrap_or_else(|_| {
721                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
722            });
723            for (i, &(s_s, s_l)) in streaming.iter().enumerate() {
724                if s_s.is_nan() { assert!(bs[i].is_nan()); }
725                else { approx::assert_relative_eq!(s_s, bs[i], epsilon = 1e-6); }
726                if s_l.is_nan() { assert!(bl[i].is_nan()); }
727                else { approx::assert_relative_eq!(s_l, bl[i], epsilon = 1e-6); }
728            }
729        }
730
731        #[test]
732        fn test_ht_trendmode_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
733            let mut ht = HT_TRENDMODE::new();
734            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
735            let batch = talib_rs::cycle::ht_trendmode(&input).unwrap_or_else(|_| vec![0; input.len()]);
736            for (s, b) in streaming.iter().zip(batch.iter()) {
737                assert_eq!(*s as i32, *b);
738            }
739        }
740
741        #[test]
742        fn test_ht_trendline_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
743            let mut ht = HT_TRENDLINE::new();
744            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
745            let batch = talib_rs::overlap::ht_trendline(&input).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
746            for (s, b) in streaming.iter().zip(batch.iter()) {
747                if s.is_nan() { assert!(b.is_nan()); }
748                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
749            }
750        }
751    }
752}