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/// MAMA / FAMA — MESA Adaptive Moving Average, TA-Lib parity (lookback 32).
327///
328/// Faithful incremental port of `talib_rs::overlap::mama`. Distinct from
329/// [`crate::indicators::mama::MAMA`], which is a simpler formulation that does
330/// not reproduce TA-Lib's values; this one is what [`MaType::Mama`] resolves to.
331///
332/// [`MaType::Mama`]: crate::indicators::ma_type::MaType::Mama
333#[derive(Debug, Clone)]
334pub struct TalibMama {
335    eng: HtEngine32,
336    fast_limit: f64,
337    slow_limit: f64,
338    prev_phase: f64,
339    prev_mama: f64,
340    prev_fama: f64,
341}
342
343impl Default for TalibMama {
344    fn default() -> Self {
345        Self::new(0.5, 0.05)
346    }
347}
348
349impl TalibMama {
350    pub fn new(fast_limit: f64, slow_limit: f64) -> Self {
351        Self {
352            eng: HtEngine32::new(),
353            fast_limit,
354            slow_limit,
355            prev_phase: 0.0,
356            prev_mama: 0.0,
357            prev_fama: 0.0,
358        }
359    }
360}
361
362impl Next<f64> for TalibMama {
363    /// `(mama, fama)`
364    type Output = (f64, f64);
365
366    fn next(&mut self, input: f64) -> Self::Output {
367        self.eng.push(input);
368        let Some(smoothed) = self.eng.step_wma() else {
369            return (f64::NAN, f64::NAN);
370        };
371        let today = self.eng.today();
372        let adj = 0.075 * self.eng.hs.period + 0.54;
373        // I1 used for the phase is the *delayed* detrender, read before the step
374        // rotates it — same convention as HT_PHASOR's in-phase output.
375        let i1 = if today.is_multiple_of(2) {
376            self.eng.hs.i1_for_even_prev3
377        } else {
378            self.eng.hs.i1_for_odd_prev3
379        };
380        let (_, q1, _, _) = self.eng.hs.step_hilbert(today, smoothed, adj);
381        self.eng.hs.adjust_period();
382
383        let phase = if i1 != 0.0 {
384            (q1 / i1).atan() * RAD2DEG
385        } else {
386            0.0
387        };
388        let delta_phase = (self.prev_phase - phase).max(1.0);
389        // Not `clamp` — that panics when slow_limit > fast_limit, which callers
390        // can construct even though TA-Lib rejects it.
391        let mut alpha = self.fast_limit / delta_phase;
392        if alpha < self.slow_limit {
393            alpha = self.slow_limit;
394        }
395        if alpha > self.fast_limit {
396            alpha = self.fast_limit;
397        }
398
399        self.prev_mama = alpha * input + (1.0 - alpha) * self.prev_mama;
400        self.prev_fama = 0.5 * alpha * self.prev_mama + (1.0 - 0.5 * alpha) * self.prev_fama;
401        self.prev_phase = phase;
402
403        if today >= HtEngine32::LOOKBACK {
404            (self.prev_mama, self.prev_fama)
405        } else {
406            (f64::NAN, f64::NAN)
407        }
408    }
409}
410
411#[derive(Debug, Clone)]
412struct HtEngine63 {
413    prices: Vec<f64>,
414    wma: Option<HtWma>,
415    warmup_left: u8,
416    hs: HilbertPeriodState,
417    smooth_price: [f64; SMOOTH_PRICE_SIZE],
418    smooth_price_idx: usize,
419    dc_phase: f64,
420    prev_dc_phase: f64,
421    i_trend1: f64,
422    i_trend2: f64,
423    i_trend3: f64,
424    days_in_trend: i32,
425    prev_sine: f64,
426    prev_lead_sine: f64,
427    sine: f64,
428    lead_sine: f64,
429    last_smoothed: f64,
430    last_trendline: f64,
431    last_trend: f64,
432}
433
434impl HtEngine63 {
435    const LOOKBACK: usize = 63;
436    const WARMUP: u8 = 34;
437
438    fn new() -> Self {
439        Self {
440            prices: Vec::new(),
441            wma: None,
442            warmup_left: Self::WARMUP,
443            hs: HilbertPeriodState::default(),
444            smooth_price: [0.0; SMOOTH_PRICE_SIZE],
445            smooth_price_idx: 0,
446            dc_phase: 0.0,
447            prev_dc_phase: 0.0,
448            i_trend1: 0.0,
449            i_trend2: 0.0,
450            i_trend3: 0.0,
451            days_in_trend: 0,
452            prev_sine: 0.0,
453            prev_lead_sine: 0.0,
454            sine: 0.0,
455            lead_sine: 0.0,
456            last_smoothed: 0.0,
457            last_trendline: 0.0,
458            last_trend: 0.0,
459        }
460    }
461
462    fn push(&mut self, price: f64) {
463        self.prices.push(price);
464    }
465
466    fn today(&self) -> usize {
467        self.prices.len().saturating_sub(1)
468    }
469
470    fn step_wma(&mut self) -> Option<f64> {
471        let n = self.prices.len();
472        if n < 3 {
473            return None;
474        }
475        if self.wma.is_none() {
476            self.wma = Some(HtWma::from_first_three(
477                self.prices[0],
478                self.prices[1],
479                self.prices[2],
480            ));
481            return None;
482        }
483        let wma = self.wma.as_mut()?;
484        let price = *self.prices.last().unwrap_or(&0.0);
485        if self.warmup_left > 0 {
486            self.warmup_left -= 1;
487            let _ = wma.next(price);
488            return None;
489        }
490        Some(wma.next(price))
491    }
492
493    fn compute_dc_phase(&mut self) {
494        self.prev_dc_phase = self.dc_phase;
495        let dc_period = self.hs.smooth_period + 0.5;
496        let dc_period_int = dc_period as i32;
497        let mut real_part = 0.0_f64;
498        let mut imag_part = 0.0_f64;
499        let mut idx = self.smooth_price_idx;
500        for i in 0..dc_period_int {
501            let angle = (i as f64 * CONST_DEG2RAD_BY360) / dc_period_int as f64;
502            let price = self.smooth_price[idx];
503            real_part += angle.sin() * price;
504            imag_part += angle.cos() * price;
505            if idx == 0 {
506                idx = SMOOTH_PRICE_SIZE - 1;
507            } else {
508                idx -= 1;
509            }
510        }
511        let abs_imag = imag_part.abs();
512        if abs_imag > 0.0 {
513            self.dc_phase = (real_part / imag_part).atan() * RAD2DEG;
514        } else if abs_imag <= 0.01 {
515            if real_part < 0.0 {
516                self.dc_phase -= 90.0;
517            } else if real_part > 0.0 {
518                self.dc_phase += 90.0;
519            }
520        }
521        self.dc_phase += 90.0;
522        self.dc_phase += 360.0 / self.hs.smooth_period;
523        if imag_part < 0.0 {
524            self.dc_phase += 180.0;
525        }
526        if self.dc_phase > 315.0 {
527            self.dc_phase -= 360.0;
528        }
529    }
530
531    fn sum_prices_back(&self, count: i32) -> f64 {
532        let mut temp = 0.0_f64;
533        let mut price_idx = self.today();
534        for _ in 0..count {
535            temp += self.prices[price_idx];
536            if price_idx == 0 {
537                break;
538            }
539            price_idx -= 1;
540        }
541        if count > 0 { temp / count as f64 } else { temp }
542    }
543
544    fn step_core(&mut self) -> bool {
545        let Some(smoothed) = self.step_wma() else {
546            return false;
547        };
548        let today = self.today();
549        let adj = 0.075 * self.hs.period + 0.54;
550        self.last_smoothed = smoothed;
551        self.smooth_price[self.smooth_price_idx] = smoothed;
552        self.hs.step_hilbert(today, smoothed, adj);
553        self.hs.adjust_period();
554        self.compute_dc_phase();
555        self.prev_sine = self.sine;
556        self.prev_lead_sine = self.lead_sine;
557        self.sine = (self.dc_phase * DEG2RAD).sin();
558        self.lead_sine = ((self.dc_phase + 45.0) * DEG2RAD).sin();
559        self.smooth_price_idx += 1;
560        if self.smooth_price_idx >= SMOOTH_PRICE_SIZE {
561            self.smooth_price_idx = 0;
562        }
563        self.update_trend_mode();
564        true
565    }
566
567    fn trendline(&mut self) -> f64 {
568        let dc_period_int = (self.hs.smooth_period + 0.5) as i32;
569        let temp = self.sum_prices_back(dc_period_int);
570        let trendline =
571            (4.0 * temp + 3.0 * self.i_trend1 + 2.0 * self.i_trend2 + self.i_trend3) / 10.0;
572        self.i_trend3 = self.i_trend2;
573        self.i_trend2 = self.i_trend1;
574        self.i_trend1 = temp;
575        self.last_trendline = trendline;
576        trendline
577    }
578
579    /// TA-Lib trend-mode decision — must run every bar after WMA warmup, not only at output index.
580    fn update_trend_mode(&mut self) {
581        let trendline = self.trendline();
582        let mut trend = 1.0_f64;
583        if (self.sine > self.lead_sine && self.prev_sine <= self.prev_lead_sine)
584            || (self.sine < self.lead_sine && self.prev_sine >= self.prev_lead_sine)
585        {
586            self.days_in_trend = 0;
587            trend = 0.0;
588        }
589        self.days_in_trend += 1;
590        if (self.days_in_trend as f64) < 0.5 * self.hs.smooth_period {
591            trend = 0.0;
592        }
593        let phase_change = self.dc_phase - self.prev_dc_phase;
594        if self.hs.smooth_period != 0.0
595            && phase_change > 0.67 * 360.0 / self.hs.smooth_period
596            && phase_change < 1.5 * 360.0 / self.hs.smooth_period
597        {
598            trend = 0.0;
599        }
600        if trendline != 0.0 && ((self.last_smoothed - trendline) / trendline).abs() >= 0.015 {
601            trend = 1.0;
602        }
603        self.last_trend = trend;
604    }
605}
606
607#[derive(Debug, Clone)]
608#[allow(non_camel_case_types)]
609pub struct HT_DCPHASE {
610    eng: HtEngine63,
611}
612
613impl Default for HT_DCPHASE {
614    fn default() -> Self {
615        Self::new()
616    }
617}
618
619impl HT_DCPHASE {
620    pub fn new() -> Self {
621        Self {
622            eng: HtEngine63::new(),
623        }
624    }
625}
626
627impl Next<f64> for HT_DCPHASE {
628    type Output = f64;
629
630    fn next(&mut self, input: f64) -> Self::Output {
631        self.eng.push(input);
632        if !self.eng.step_core() {
633            return f64::NAN;
634        }
635        if self.eng.today() >= HtEngine63::LOOKBACK {
636            self.eng.dc_phase
637        } else {
638            f64::NAN
639        }
640    }
641}
642
643#[derive(Debug, Clone)]
644#[allow(non_camel_case_types)]
645pub struct HT_SINE {
646    eng: HtEngine63,
647}
648
649impl Default for HT_SINE {
650    fn default() -> Self {
651        Self::new()
652    }
653}
654
655impl HT_SINE {
656    pub fn new() -> Self {
657        Self {
658            eng: HtEngine63::new(),
659        }
660    }
661}
662
663impl Next<f64> for HT_SINE {
664    type Output = (f64, f64);
665
666    fn next(&mut self, input: f64) -> Self::Output {
667        self.eng.push(input);
668        if !self.eng.step_core() {
669            return (f64::NAN, f64::NAN);
670        }
671        if self.eng.today() >= HtEngine63::LOOKBACK {
672            (self.eng.sine, self.eng.lead_sine)
673        } else {
674            (f64::NAN, f64::NAN)
675        }
676    }
677}
678
679#[derive(Debug, Clone)]
680#[allow(non_camel_case_types)]
681pub struct HT_TRENDMODE {
682    eng: HtEngine63,
683}
684
685impl Default for HT_TRENDMODE {
686    fn default() -> Self {
687        Self::new()
688    }
689}
690
691impl HT_TRENDMODE {
692    pub fn new() -> Self {
693        Self {
694            eng: HtEngine63::new(),
695        }
696    }
697}
698
699impl Next<f64> for HT_TRENDMODE {
700    type Output = f64;
701
702    fn next(&mut self, input: f64) -> Self::Output {
703        self.eng.push(input);
704        if !self.eng.step_core() {
705            return f64::NAN;
706        }
707        if self.eng.today() >= HtEngine63::LOOKBACK {
708            self.eng.last_trend
709        } else {
710            f64::NAN
711        }
712    }
713}
714
715#[derive(Debug, Clone)]
716#[allow(non_camel_case_types)]
717pub struct HT_TRENDLINE {
718    eng: HtEngine63,
719}
720
721impl Default for HT_TRENDLINE {
722    fn default() -> Self {
723        Self::new()
724    }
725}
726
727impl HT_TRENDLINE {
728    pub fn new() -> Self {
729        Self {
730            eng: HtEngine63::new(),
731        }
732    }
733}
734
735impl Next<f64> for HT_TRENDLINE {
736    type Output = f64;
737
738    fn next(&mut self, input: f64) -> Self::Output {
739        self.eng.push(input);
740        if !self.eng.step_core() {
741            return f64::NAN;
742        }
743        if self.eng.today() >= HtEngine63::LOOKBACK {
744            self.eng.last_trendline
745        } else {
746            f64::NAN
747        }
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754    use proptest::prelude::*;
755
756    proptest! {
757        #[test]
758        fn test_ht_dcperiod_parity(input in prop::collection::vec(0.1..100.0, 33..100)) {
759            let mut ht = HT_DCPERIOD::new();
760            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
761            let batch = talib_rs::cycle::ht_dcperiod(&input).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
762            for (s, b) in streaming.iter().zip(batch.iter()) {
763                if s.is_nan() { assert!(b.is_nan()); }
764                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
765            }
766        }
767
768        #[test]
769        fn test_ht_phasor_parity(input in prop::collection::vec(0.1..100.0, 33..100)) {
770            let mut ht = HT_PHASOR::new();
771            let streaming: Vec<_> = input.iter().map(|&x| ht.next(x)).collect();
772            let (bi, bq) = talib_rs::cycle::ht_phasor(&input).unwrap_or_else(|_| {
773                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
774            });
775            for (i, &(s_i, s_q)) in streaming.iter().enumerate() {
776                if s_i.is_nan() { assert!(bi[i].is_nan()); }
777                else { approx::assert_relative_eq!(s_i, bi[i], epsilon = 1e-6); }
778                if s_q.is_nan() { assert!(bq[i].is_nan()); }
779                else { approx::assert_relative_eq!(s_q, bq[i], epsilon = 1e-6); }
780            }
781        }
782
783        #[test]
784        fn test_talib_mama_parity(input in prop::collection::vec(0.1..100.0, 33..120)) {
785            let mut mama = TalibMama::new(0.5, 0.05);
786            let streaming: Vec<_> = input.iter().map(|&x| mama.next(x)).collect();
787            let (bm, bf) = talib_rs::overlap::mama(&input, 0.5, 0.05).unwrap_or_else(|_| {
788                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
789            });
790            for (i, &(s_m, s_f)) in streaming.iter().enumerate() {
791                if s_m.is_nan() { assert!(bm[i].is_nan()); }
792                else { approx::assert_relative_eq!(s_m, bm[i], epsilon = 1e-6); }
793                if s_f.is_nan() { assert!(bf[i].is_nan()); }
794                else { approx::assert_relative_eq!(s_f, bf[i], epsilon = 1e-6); }
795            }
796        }
797
798        #[test]
799        fn test_ht_dcphase_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
800            let mut ht = HT_DCPHASE::new();
801            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
802            let batch = talib_rs::cycle::ht_dcphase(&input).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
803            for (s, b) in streaming.iter().zip(batch.iter()) {
804                if s.is_nan() { assert!(b.is_nan()); }
805                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
806            }
807        }
808
809        #[test]
810        fn test_ht_sine_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
811            let mut ht = HT_SINE::new();
812            let streaming: Vec<_> = input.iter().map(|&x| ht.next(x)).collect();
813            let (bs, bl) = talib_rs::cycle::ht_sine(&input).unwrap_or_else(|_| {
814                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
815            });
816            for (i, &(s_s, s_l)) in streaming.iter().enumerate() {
817                if s_s.is_nan() { assert!(bs[i].is_nan()); }
818                else { approx::assert_relative_eq!(s_s, bs[i], epsilon = 1e-6); }
819                if s_l.is_nan() { assert!(bl[i].is_nan()); }
820                else { approx::assert_relative_eq!(s_l, bl[i], epsilon = 1e-6); }
821            }
822        }
823
824        #[test]
825        fn test_ht_trendmode_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
826            let mut ht = HT_TRENDMODE::new();
827            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
828            let batch = talib_rs::cycle::ht_trendmode(&input).unwrap_or_else(|_| vec![0; input.len()]);
829            for (s, b) in streaming.iter().zip(batch.iter()) {
830                assert_eq!(*s as i32, *b);
831            }
832        }
833
834        #[test]
835        fn test_ht_trendline_parity(input in prop::collection::vec(0.1..100.0, 64..100)) {
836            let mut ht = HT_TRENDLINE::new();
837            let streaming: Vec<f64> = input.iter().map(|&x| ht.next(x)).collect();
838            let batch = talib_rs::overlap::ht_trendline(&input).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
839            for (s, b) in streaming.iter().zip(batch.iter()) {
840                if s.is_nan() { assert!(b.is_nan()); }
841                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
842            }
843        }
844    }
845}