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