1use 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#[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#[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 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}