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