1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::mem::{ManuallyDrop, MaybeUninit};
27use thiserror::Error;
28
29const DEFAULT_FACTOR: f64 = 5.0;
30const DEFAULT_SLOPE: f64 = 14.0;
31const DEFAULT_WIDTH_PERCENT: f64 = 80.0;
32const ATR_PERIOD: usize = 200;
33
34#[derive(Debug, Clone)]
35pub enum HyperTrendData<'a> {
36 Candles {
37 candles: &'a Candles,
38 source: &'a str,
39 },
40 Slices {
41 high: &'a [f64],
42 low: &'a [f64],
43 source: &'a [f64],
44 },
45}
46
47#[derive(Debug, Clone)]
48pub struct HyperTrendOutput {
49 pub upper: Vec<f64>,
50 pub average: Vec<f64>,
51 pub lower: Vec<f64>,
52 pub trend: Vec<f64>,
53 pub changed: Vec<f64>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg_attr(
58 all(target_arch = "wasm32", feature = "wasm"),
59 derive(Serialize, Deserialize)
60)]
61pub struct HyperTrendParams {
62 pub factor: Option<f64>,
63 pub slope: Option<f64>,
64 pub width_percent: Option<f64>,
65}
66
67impl Default for HyperTrendParams {
68 fn default() -> Self {
69 Self {
70 factor: Some(DEFAULT_FACTOR),
71 slope: Some(DEFAULT_SLOPE),
72 width_percent: Some(DEFAULT_WIDTH_PERCENT),
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
78pub struct HyperTrendInput<'a> {
79 pub data: HyperTrendData<'a>,
80 pub params: HyperTrendParams,
81}
82
83impl<'a> HyperTrendInput<'a> {
84 #[inline]
85 pub fn from_candles(candles: &'a Candles, source: &'a str, params: HyperTrendParams) -> Self {
86 Self {
87 data: HyperTrendData::Candles { candles, source },
88 params,
89 }
90 }
91
92 #[inline]
93 pub fn from_slices(
94 high: &'a [f64],
95 low: &'a [f64],
96 source: &'a [f64],
97 params: HyperTrendParams,
98 ) -> Self {
99 Self {
100 data: HyperTrendData::Slices { high, low, source },
101 params,
102 }
103 }
104
105 #[inline]
106 pub fn with_default_candles(candles: &'a Candles) -> Self {
107 Self::from_candles(candles, "close", HyperTrendParams::default())
108 }
109
110 #[inline]
111 pub fn get_factor(&self) -> f64 {
112 self.params.factor.unwrap_or(DEFAULT_FACTOR)
113 }
114
115 #[inline]
116 pub fn get_slope(&self) -> f64 {
117 self.params.slope.unwrap_or(DEFAULT_SLOPE)
118 }
119
120 #[inline]
121 pub fn get_width_percent(&self) -> f64 {
122 self.params.width_percent.unwrap_or(DEFAULT_WIDTH_PERCENT)
123 }
124
125 #[inline]
126 pub fn as_refs(&'a self) -> (&'a [f64], &'a [f64], &'a [f64]) {
127 match &self.data {
128 HyperTrendData::Candles { candles, source } => (
129 candles.high.as_slice(),
130 candles.low.as_slice(),
131 source_type(candles, source),
132 ),
133 HyperTrendData::Slices { high, low, source } => (*high, *low, *source),
134 }
135 }
136}
137
138#[derive(Clone, Debug)]
139pub struct HyperTrendBuilder {
140 factor: Option<f64>,
141 slope: Option<f64>,
142 width_percent: Option<f64>,
143 source: Option<String>,
144 kernel: Kernel,
145}
146
147impl Default for HyperTrendBuilder {
148 fn default() -> Self {
149 Self {
150 factor: None,
151 slope: None,
152 width_percent: None,
153 source: None,
154 kernel: Kernel::Auto,
155 }
156 }
157}
158
159impl HyperTrendBuilder {
160 #[inline]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 #[inline]
166 pub fn factor(mut self, value: f64) -> Self {
167 self.factor = Some(value);
168 self
169 }
170
171 #[inline]
172 pub fn slope(mut self, value: f64) -> Self {
173 self.slope = Some(value);
174 self
175 }
176
177 #[inline]
178 pub fn width_percent(mut self, value: f64) -> Self {
179 self.width_percent = Some(value);
180 self
181 }
182
183 #[inline]
184 pub fn source<S: Into<String>>(mut self, value: S) -> Self {
185 self.source = Some(value.into());
186 self
187 }
188
189 #[inline]
190 pub fn kernel(mut self, value: Kernel) -> Self {
191 self.kernel = value;
192 self
193 }
194
195 #[inline]
196 pub fn apply(self, candles: &Candles) -> Result<HyperTrendOutput, HyperTrendError> {
197 let input = HyperTrendInput::from_candles(
198 candles,
199 self.source.as_deref().unwrap_or("close"),
200 HyperTrendParams {
201 factor: self.factor,
202 slope: self.slope,
203 width_percent: self.width_percent,
204 },
205 );
206 hypertrend_with_kernel(&input, self.kernel)
207 }
208
209 #[inline]
210 pub fn apply_slices(
211 self,
212 high: &[f64],
213 low: &[f64],
214 source: &[f64],
215 ) -> Result<HyperTrendOutput, HyperTrendError> {
216 let input = HyperTrendInput::from_slices(
217 high,
218 low,
219 source,
220 HyperTrendParams {
221 factor: self.factor,
222 slope: self.slope,
223 width_percent: self.width_percent,
224 },
225 );
226 hypertrend_with_kernel(&input, self.kernel)
227 }
228
229 #[inline]
230 pub fn into_stream(self) -> Result<HyperTrendStream, HyperTrendError> {
231 HyperTrendStream::try_new(HyperTrendParams {
232 factor: self.factor,
233 slope: self.slope,
234 width_percent: self.width_percent,
235 })
236 }
237}
238
239#[derive(Debug, Error)]
240pub enum HyperTrendError {
241 #[error("hypertrend: Empty input data.")]
242 EmptyInputData,
243 #[error("hypertrend: Input length mismatch: high={high}, low={low}, source={source_len}")]
244 DataLengthMismatch {
245 high: usize,
246 low: usize,
247 source_len: usize,
248 },
249 #[error("hypertrend: All input values are invalid.")]
250 AllValuesNaN,
251 #[error("hypertrend: Invalid factor: {factor}")]
252 InvalidFactor { factor: f64 },
253 #[error("hypertrend: Invalid slope: {slope}")]
254 InvalidSlope { slope: f64 },
255 #[error("hypertrend: Invalid width_percent: {width_percent}")]
256 InvalidWidthPercent { width_percent: f64 },
257 #[error("hypertrend: Output length mismatch: expected={expected}, got={got}")]
258 OutputLengthMismatch { expected: usize, got: usize },
259 #[error("hypertrend: Invalid range: start={start}, end={end}, step={step}")]
260 InvalidRange {
261 start: String,
262 end: String,
263 step: String,
264 },
265 #[error("hypertrend: Invalid float range: start={start}, end={end}, step={step}")]
266 InvalidFloatRange { start: f64, end: f64, step: f64 },
267 #[error("hypertrend: Invalid kernel for batch: {0:?}")]
268 InvalidKernelForBatch(Kernel),
269}
270
271#[inline(always)]
272fn valid_bar(high: f64, low: f64, source: f64) -> bool {
273 high.is_finite() && low.is_finite() && source.is_finite() && high >= low
274}
275
276#[inline(always)]
277fn first_valid_bar(high: &[f64], low: &[f64], source: &[f64]) -> Option<usize> {
278 (0..source.len()).find(|&i| valid_bar(high[i], low[i], source[i]))
279}
280
281#[inline(always)]
282fn normalize_kernel(kernel: Kernel) -> Kernel {
283 match kernel {
284 Kernel::Auto => detect_best_kernel(),
285 other if other.is_batch() => other.to_non_batch(),
286 other => other,
287 }
288}
289
290#[inline(always)]
291fn validate_lengths(high: &[f64], low: &[f64], source: &[f64]) -> Result<(), HyperTrendError> {
292 if high.is_empty() || low.is_empty() || source.is_empty() {
293 return Err(HyperTrendError::EmptyInputData);
294 }
295 if high.len() != low.len() || low.len() != source.len() {
296 return Err(HyperTrendError::DataLengthMismatch {
297 high: high.len(),
298 low: low.len(),
299 source_len: source.len(),
300 });
301 }
302 Ok(())
303}
304
305#[inline(always)]
306fn validate_params(factor: f64, slope: f64, width_percent: f64) -> Result<(), HyperTrendError> {
307 if !factor.is_finite() || factor <= 0.0 {
308 return Err(HyperTrendError::InvalidFactor { factor });
309 }
310 if !slope.is_finite() || slope <= 0.0 {
311 return Err(HyperTrendError::InvalidSlope { slope });
312 }
313 if !width_percent.is_finite() || !(0.0..=100.0).contains(&width_percent) {
314 return Err(HyperTrendError::InvalidWidthPercent { width_percent });
315 }
316 Ok(())
317}
318
319#[inline(always)]
320fn pine_sign(value: f64) -> f64 {
321 if value > 0.0 {
322 1.0
323 } else if value < 0.0 {
324 -1.0
325 } else {
326 0.0
327 }
328}
329
330#[inline(always)]
331fn true_range(high: f64, low: f64, prev_close: f64) -> f64 {
332 if prev_close.is_finite() {
333 let a = high - low;
334 let b = (high - prev_close).abs();
335 let c = (low - prev_close).abs();
336 a.max(b).max(c)
337 } else {
338 high - low
339 }
340}
341
342fn compute_atr_zeroed(high: &[f64], low: &[f64], source: &[f64]) -> Vec<f64> {
343 let mut out = vec![0.0; source.len()];
344 let mut prev_close = f64::NAN;
345 let mut seed_sum = 0.0;
346 let mut seed_count = 0usize;
347 let mut atr = f64::NAN;
348
349 for i in 0..source.len() {
350 if !valid_bar(high[i], low[i], source[i]) {
351 out[i] = 0.0;
352 prev_close = f64::NAN;
353 seed_sum = 0.0;
354 seed_count = 0;
355 atr = f64::NAN;
356 continue;
357 }
358
359 let tr = true_range(high[i], low[i], prev_close);
360 prev_close = source[i];
361
362 if seed_count < ATR_PERIOD {
363 seed_sum += tr;
364 seed_count += 1;
365 if seed_count == ATR_PERIOD {
366 atr = seed_sum / ATR_PERIOD as f64;
367 out[i] = atr;
368 }
369 continue;
370 }
371
372 atr = ((atr * (ATR_PERIOD as f64 - 1.0)) + tr) / ATR_PERIOD as f64;
373 out[i] = atr;
374 }
375
376 out
377}
378
379#[inline(always)]
380fn hypertrend_row_scalar(
381 high: &[f64],
382 low: &[f64],
383 source: &[f64],
384 factor: f64,
385 slope: f64,
386 width_ratio: f64,
387 atr_values: &[f64],
388 out_upper: &mut [f64],
389 out_average: &mut [f64],
390 out_lower: &mut [f64],
391 out_trend: &mut [f64],
392 out_changed: &mut [f64],
393) {
394 let mut initialized = false;
395 let mut avg = 0.0;
396 let mut hold = 0.0;
397 let mut os = 1.0;
398
399 for i in 0..source.len() {
400 let src = source[i];
401 if !valid_bar(high[i], low[i], src) {
402 out_upper[i] = f64::NAN;
403 out_average[i] = f64::NAN;
404 out_lower[i] = f64::NAN;
405 out_trend[i] = f64::NAN;
406 out_changed[i] = f64::NAN;
407 initialized = false;
408 avg = 0.0;
409 hold = 0.0;
410 os = 1.0;
411 continue;
412 }
413
414 if !initialized {
415 avg = src;
416 hold = 0.0;
417 os = 1.0;
418 out_average[i] = avg;
419 out_upper[i] = avg;
420 out_lower[i] = avg;
421 out_trend[i] = os;
422 out_changed[i] = 0.0;
423 initialized = true;
424 continue;
425 }
426
427 let atr = atr_values[i] * factor;
428 let next_avg = if (src - avg).abs() > atr {
429 0.5 * (src + avg)
430 } else {
431 avg + os * (hold / factor / slope)
432 };
433 let next_os = pine_sign(next_avg - avg);
434 let changed = if next_os != os { 1.0 } else { 0.0 };
435 let next_hold = if changed != 0.0 { atr } else { hold };
436 let upper = next_avg + width_ratio * next_hold;
437 let lower = next_avg - width_ratio * next_hold;
438
439 out_upper[i] = upper;
440 out_average[i] = next_avg;
441 out_lower[i] = lower;
442 out_trend[i] = next_os;
443 out_changed[i] = changed;
444
445 avg = next_avg;
446 hold = next_hold;
447 os = next_os;
448 }
449}
450
451#[inline]
452pub fn hypertrend(input: &HyperTrendInput) -> Result<HyperTrendOutput, HyperTrendError> {
453 hypertrend_with_kernel(input, Kernel::Auto)
454}
455
456#[inline]
457pub fn hypertrend_with_kernel(
458 input: &HyperTrendInput,
459 kernel: Kernel,
460) -> Result<HyperTrendOutput, HyperTrendError> {
461 let (high, low, source) = input.as_refs();
462 validate_lengths(high, low, source)?;
463
464 let factor = input.get_factor();
465 let slope = input.get_slope();
466 let width_percent = input.get_width_percent();
467 validate_params(factor, slope, width_percent)?;
468
469 let first_valid = first_valid_bar(high, low, source).ok_or(HyperTrendError::AllValuesNaN)?;
470 let _kernel = normalize_kernel(kernel);
471 let atr_values = compute_atr_zeroed(high, low, source);
472 let width_ratio = width_percent * 0.01;
473 let len = source.len();
474
475 let mut upper = alloc_with_nan_prefix(len, first_valid);
476 let mut average = alloc_with_nan_prefix(len, first_valid);
477 let mut lower = alloc_with_nan_prefix(len, first_valid);
478 let mut trend = alloc_with_nan_prefix(len, first_valid);
479 let mut changed = alloc_with_nan_prefix(len, first_valid);
480
481 hypertrend_row_scalar(
482 high,
483 low,
484 source,
485 factor,
486 slope,
487 width_ratio,
488 &atr_values,
489 &mut upper,
490 &mut average,
491 &mut lower,
492 &mut trend,
493 &mut changed,
494 );
495
496 Ok(HyperTrendOutput {
497 upper,
498 average,
499 lower,
500 trend,
501 changed,
502 })
503}
504
505#[inline]
506pub fn hypertrend_into_slice(
507 out_upper: &mut [f64],
508 out_average: &mut [f64],
509 out_lower: &mut [f64],
510 out_trend: &mut [f64],
511 out_changed: &mut [f64],
512 input: &HyperTrendInput,
513 kernel: Kernel,
514) -> Result<(), HyperTrendError> {
515 let (high, low, source) = input.as_refs();
516 validate_lengths(high, low, source)?;
517 let len = source.len();
518 if out_upper.len() != len
519 || out_average.len() != len
520 || out_lower.len() != len
521 || out_trend.len() != len
522 || out_changed.len() != len
523 {
524 return Err(HyperTrendError::OutputLengthMismatch {
525 expected: len,
526 got: out_upper
527 .len()
528 .max(out_average.len())
529 .max(out_lower.len())
530 .max(out_trend.len())
531 .max(out_changed.len()),
532 });
533 }
534
535 let factor = input.get_factor();
536 let slope = input.get_slope();
537 let width_percent = input.get_width_percent();
538 validate_params(factor, slope, width_percent)?;
539 let _kernel = normalize_kernel(kernel);
540 let atr_values = compute_atr_zeroed(high, low, source);
541
542 hypertrend_row_scalar(
543 high,
544 low,
545 source,
546 factor,
547 slope,
548 width_percent * 0.01,
549 &atr_values,
550 out_upper,
551 out_average,
552 out_lower,
553 out_trend,
554 out_changed,
555 );
556 Ok(())
557}
558
559#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
560#[inline]
561pub fn hypertrend_into(
562 input: &HyperTrendInput,
563 out_upper: &mut [f64],
564 out_average: &mut [f64],
565 out_lower: &mut [f64],
566 out_trend: &mut [f64],
567 out_changed: &mut [f64],
568) -> Result<(), HyperTrendError> {
569 hypertrend_into_slice(
570 out_upper,
571 out_average,
572 out_lower,
573 out_trend,
574 out_changed,
575 input,
576 Kernel::Auto,
577 )
578}
579
580#[derive(Clone, Debug)]
581struct HyperTrendAtrState {
582 prev_close: f64,
583 seed_sum: f64,
584 seed_count: usize,
585 atr: f64,
586}
587
588impl HyperTrendAtrState {
589 #[inline]
590 fn new() -> Self {
591 Self {
592 prev_close: f64::NAN,
593 seed_sum: 0.0,
594 seed_count: 0,
595 atr: f64::NAN,
596 }
597 }
598
599 #[inline]
600 fn reset(&mut self) {
601 self.prev_close = f64::NAN;
602 self.seed_sum = 0.0;
603 self.seed_count = 0;
604 self.atr = f64::NAN;
605 }
606
607 #[inline]
608 fn update(&mut self, high: f64, low: f64, source: f64) -> Option<f64> {
609 if !valid_bar(high, low, source) {
610 self.reset();
611 return None;
612 }
613
614 let tr = true_range(high, low, self.prev_close);
615 self.prev_close = source;
616
617 if self.seed_count < ATR_PERIOD {
618 self.seed_sum += tr;
619 self.seed_count += 1;
620 if self.seed_count == ATR_PERIOD {
621 self.atr = self.seed_sum / ATR_PERIOD as f64;
622 return Some(self.atr);
623 }
624 return Some(0.0);
625 }
626
627 self.atr = ((self.atr * (ATR_PERIOD as f64 - 1.0)) + tr) / ATR_PERIOD as f64;
628 Some(self.atr)
629 }
630}
631
632#[derive(Clone, Debug)]
633pub struct HyperTrendStream {
634 factor: f64,
635 slope: f64,
636 width_ratio: f64,
637 atr: HyperTrendAtrState,
638 initialized: bool,
639 avg: f64,
640 hold: f64,
641 os: f64,
642}
643
644impl HyperTrendStream {
645 #[inline]
646 pub fn try_new(params: HyperTrendParams) -> Result<Self, HyperTrendError> {
647 let factor = params.factor.unwrap_or(DEFAULT_FACTOR);
648 let slope = params.slope.unwrap_or(DEFAULT_SLOPE);
649 let width_percent = params.width_percent.unwrap_or(DEFAULT_WIDTH_PERCENT);
650 validate_params(factor, slope, width_percent)?;
651 Ok(Self {
652 factor,
653 slope,
654 width_ratio: width_percent * 0.01,
655 atr: HyperTrendAtrState::new(),
656 initialized: false,
657 avg: 0.0,
658 hold: 0.0,
659 os: 1.0,
660 })
661 }
662
663 #[inline]
664 pub fn update(
665 &mut self,
666 high: f64,
667 low: f64,
668 source: f64,
669 ) -> Option<(f64, f64, f64, f64, f64)> {
670 let atr_raw = self.atr.update(high, low, source)?;
671 if !self.initialized {
672 self.avg = source;
673 self.hold = 0.0;
674 self.os = 1.0;
675 self.initialized = true;
676 return Some((source, source, source, 1.0, 0.0));
677 }
678
679 let atr = atr_raw * self.factor;
680 let next_avg = if (source - self.avg).abs() > atr {
681 0.5 * (source + self.avg)
682 } else {
683 self.avg + self.os * (self.hold / self.factor / self.slope)
684 };
685 let next_os = pine_sign(next_avg - self.avg);
686 let changed = if next_os != self.os { 1.0 } else { 0.0 };
687 let next_hold = if changed != 0.0 { atr } else { self.hold };
688 let upper = next_avg + self.width_ratio * next_hold;
689 let lower = next_avg - self.width_ratio * next_hold;
690
691 self.avg = next_avg;
692 self.hold = next_hold;
693 self.os = next_os;
694
695 Some((upper, next_avg, lower, next_os, changed))
696 }
697}
698
699#[derive(Clone, Debug)]
700pub struct HyperTrendBatchRange {
701 pub factor: (f64, f64, f64),
702 pub slope: (f64, f64, f64),
703 pub width_percent: (f64, f64, f64),
704}
705
706impl Default for HyperTrendBatchRange {
707 fn default() -> Self {
708 Self {
709 factor: (DEFAULT_FACTOR, DEFAULT_FACTOR, 0.0),
710 slope: (DEFAULT_SLOPE, DEFAULT_SLOPE, 0.0),
711 width_percent: (DEFAULT_WIDTH_PERCENT, DEFAULT_WIDTH_PERCENT, 0.0),
712 }
713 }
714}
715
716#[derive(Clone, Debug)]
717pub struct HyperTrendBatchOutput {
718 pub upper: Vec<f64>,
719 pub average: Vec<f64>,
720 pub lower: Vec<f64>,
721 pub trend: Vec<f64>,
722 pub changed: Vec<f64>,
723 pub combos: Vec<HyperTrendParams>,
724 pub rows: usize,
725 pub cols: usize,
726}
727
728#[derive(Clone, Debug)]
729pub struct HyperTrendBatchBuilder {
730 range: HyperTrendBatchRange,
731 kernel: Kernel,
732}
733
734impl Default for HyperTrendBatchBuilder {
735 fn default() -> Self {
736 Self {
737 range: HyperTrendBatchRange::default(),
738 kernel: Kernel::Auto,
739 }
740 }
741}
742
743impl HyperTrendBatchBuilder {
744 #[inline]
745 pub fn new() -> Self {
746 Self::default()
747 }
748
749 #[inline]
750 pub fn factor_range(mut self, range: (f64, f64, f64)) -> Self {
751 self.range.factor = range;
752 self
753 }
754
755 #[inline]
756 pub fn slope_range(mut self, range: (f64, f64, f64)) -> Self {
757 self.range.slope = range;
758 self
759 }
760
761 #[inline]
762 pub fn width_percent_range(mut self, range: (f64, f64, f64)) -> Self {
763 self.range.width_percent = range;
764 self
765 }
766
767 #[inline]
768 pub fn kernel(mut self, kernel: Kernel) -> Self {
769 self.kernel = kernel;
770 self
771 }
772
773 #[inline]
774 pub fn apply_slices(
775 self,
776 high: &[f64],
777 low: &[f64],
778 source: &[f64],
779 ) -> Result<HyperTrendBatchOutput, HyperTrendError> {
780 hypertrend_batch_with_kernel(high, low, source, &self.range, self.kernel)
781 }
782
783 #[inline]
784 pub fn apply(self, candles: &Candles) -> Result<HyperTrendBatchOutput, HyperTrendError> {
785 let source = source_type(candles, "close");
786 hypertrend_batch_with_kernel(
787 &candles.high,
788 &candles.low,
789 source,
790 &self.range,
791 self.kernel,
792 )
793 }
794}
795
796pub fn expand_grid_hypertrend(
797 range: &HyperTrendBatchRange,
798) -> Result<Vec<HyperTrendParams>, HyperTrendError> {
799 fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, HyperTrendError> {
800 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
801 return Err(HyperTrendError::InvalidFloatRange { start, end, step });
802 }
803 if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
804 return Ok(vec![start]);
805 }
806
807 let step = step.abs();
808 let mut out = Vec::new();
809 if start <= end {
810 let mut x = start;
811 while x <= end + 1e-12 {
812 out.push(x);
813 x += step;
814 }
815 } else {
816 let mut x = start;
817 while x + 1e-12 >= end {
818 out.push(x);
819 x -= step;
820 }
821 }
822
823 if out.is_empty() {
824 return Err(HyperTrendError::InvalidFloatRange { start, end, step });
825 }
826 Ok(out)
827 }
828
829 let factors = axis_f64(range.factor)?;
830 let slopes = axis_f64(range.slope)?;
831 let widths = axis_f64(range.width_percent)?;
832
833 let cap = factors
834 .len()
835 .checked_mul(slopes.len())
836 .and_then(|value| value.checked_mul(widths.len()))
837 .ok_or(HyperTrendError::InvalidRange {
838 start: range.factor.0.to_string(),
839 end: range.factor.1.to_string(),
840 step: range.factor.2.to_string(),
841 })?;
842
843 let mut out = Vec::with_capacity(cap);
844 for &factor in &factors {
845 for &slope in &slopes {
846 for &width_percent in &widths {
847 out.push(HyperTrendParams {
848 factor: Some(factor),
849 slope: Some(slope),
850 width_percent: Some(width_percent),
851 });
852 }
853 }
854 }
855 Ok(out)
856}
857
858#[inline]
859pub fn hypertrend_batch_with_kernel(
860 high: &[f64],
861 low: &[f64],
862 source: &[f64],
863 sweep: &HyperTrendBatchRange,
864 kernel: Kernel,
865) -> Result<HyperTrendBatchOutput, HyperTrendError> {
866 let batch_kernel = match kernel {
867 Kernel::Auto => detect_best_batch_kernel(),
868 other if other.is_batch() => other,
869 other => return Err(HyperTrendError::InvalidKernelForBatch(other)),
870 };
871 hypertrend_batch_par_slice(high, low, source, sweep, batch_kernel.to_non_batch())
872}
873
874#[inline]
875pub fn hypertrend_batch_slice(
876 high: &[f64],
877 low: &[f64],
878 source: &[f64],
879 sweep: &HyperTrendBatchRange,
880 kernel: Kernel,
881) -> Result<HyperTrendBatchOutput, HyperTrendError> {
882 hypertrend_batch_inner(high, low, source, sweep, kernel, false)
883}
884
885#[inline]
886pub fn hypertrend_batch_par_slice(
887 high: &[f64],
888 low: &[f64],
889 source: &[f64],
890 sweep: &HyperTrendBatchRange,
891 kernel: Kernel,
892) -> Result<HyperTrendBatchOutput, HyperTrendError> {
893 hypertrend_batch_inner(high, low, source, sweep, kernel, true)
894}
895
896fn hypertrend_batch_inner(
897 high: &[f64],
898 low: &[f64],
899 source: &[f64],
900 sweep: &HyperTrendBatchRange,
901 _kernel: Kernel,
902 parallel: bool,
903) -> Result<HyperTrendBatchOutput, HyperTrendError> {
904 validate_lengths(high, low, source)?;
905 let combos = expand_grid_hypertrend(sweep)?;
906 for params in &combos {
907 validate_params(
908 params.factor.unwrap_or(DEFAULT_FACTOR),
909 params.slope.unwrap_or(DEFAULT_SLOPE),
910 params.width_percent.unwrap_or(DEFAULT_WIDTH_PERCENT),
911 )?;
912 }
913
914 let first_valid = first_valid_bar(high, low, source).ok_or(HyperTrendError::AllValuesNaN)?;
915 let rows = combos.len();
916 let cols = source.len();
917 let total = rows
918 .checked_mul(cols)
919 .ok_or(HyperTrendError::OutputLengthMismatch {
920 expected: usize::MAX,
921 got: 0,
922 })?;
923 let atr_values = compute_atr_zeroed(high, low, source);
924
925 let mut upper_matrix = make_uninit_matrix(rows, cols);
926 let mut average_matrix = make_uninit_matrix(rows, cols);
927 let mut lower_matrix = make_uninit_matrix(rows, cols);
928 let mut trend_matrix = make_uninit_matrix(rows, cols);
929 let mut changed_matrix = make_uninit_matrix(rows, cols);
930 let warmups = vec![first_valid; rows];
931 init_matrix_prefixes(&mut upper_matrix, cols, &warmups);
932 init_matrix_prefixes(&mut average_matrix, cols, &warmups);
933 init_matrix_prefixes(&mut lower_matrix, cols, &warmups);
934 init_matrix_prefixes(&mut trend_matrix, cols, &warmups);
935 init_matrix_prefixes(&mut changed_matrix, cols, &warmups);
936
937 let mut upper_guard = ManuallyDrop::new(upper_matrix);
938 let mut average_guard = ManuallyDrop::new(average_matrix);
939 let mut lower_guard = ManuallyDrop::new(lower_matrix);
940 let mut trend_guard = ManuallyDrop::new(trend_matrix);
941 let mut changed_guard = ManuallyDrop::new(changed_matrix);
942
943 let upper_mu: &mut [MaybeUninit<f64>] =
944 unsafe { std::slice::from_raw_parts_mut(upper_guard.as_mut_ptr(), upper_guard.len()) };
945 let average_mu: &mut [MaybeUninit<f64>] =
946 unsafe { std::slice::from_raw_parts_mut(average_guard.as_mut_ptr(), average_guard.len()) };
947 let lower_mu: &mut [MaybeUninit<f64>] =
948 unsafe { std::slice::from_raw_parts_mut(lower_guard.as_mut_ptr(), lower_guard.len()) };
949 let trend_mu: &mut [MaybeUninit<f64>] =
950 unsafe { std::slice::from_raw_parts_mut(trend_guard.as_mut_ptr(), trend_guard.len()) };
951 let changed_mu: &mut [MaybeUninit<f64>] =
952 unsafe { std::slice::from_raw_parts_mut(changed_guard.as_mut_ptr(), changed_guard.len()) };
953
954 let do_row = |row: usize,
955 row_upper: &mut [MaybeUninit<f64>],
956 row_average: &mut [MaybeUninit<f64>],
957 row_lower: &mut [MaybeUninit<f64>],
958 row_trend: &mut [MaybeUninit<f64>],
959 row_changed: &mut [MaybeUninit<f64>]| {
960 let params = &combos[row];
961 let dst_upper =
962 unsafe { std::slice::from_raw_parts_mut(row_upper.as_mut_ptr() as *mut f64, cols) };
963 let dst_average =
964 unsafe { std::slice::from_raw_parts_mut(row_average.as_mut_ptr() as *mut f64, cols) };
965 let dst_lower =
966 unsafe { std::slice::from_raw_parts_mut(row_lower.as_mut_ptr() as *mut f64, cols) };
967 let dst_trend =
968 unsafe { std::slice::from_raw_parts_mut(row_trend.as_mut_ptr() as *mut f64, cols) };
969 let dst_changed =
970 unsafe { std::slice::from_raw_parts_mut(row_changed.as_mut_ptr() as *mut f64, cols) };
971
972 hypertrend_row_scalar(
973 high,
974 low,
975 source,
976 params.factor.unwrap_or(DEFAULT_FACTOR),
977 params.slope.unwrap_or(DEFAULT_SLOPE),
978 params.width_percent.unwrap_or(DEFAULT_WIDTH_PERCENT) * 0.01,
979 &atr_values,
980 dst_upper,
981 dst_average,
982 dst_lower,
983 dst_trend,
984 dst_changed,
985 );
986 };
987
988 if parallel {
989 #[cfg(not(target_arch = "wasm32"))]
990 upper_mu
991 .par_chunks_mut(cols)
992 .zip(average_mu.par_chunks_mut(cols))
993 .zip(lower_mu.par_chunks_mut(cols))
994 .zip(trend_mu.par_chunks_mut(cols))
995 .zip(changed_mu.par_chunks_mut(cols))
996 .enumerate()
997 .for_each(
998 |(row, ((((row_upper, row_average), row_lower), row_trend), row_changed))| {
999 do_row(
1000 row,
1001 row_upper,
1002 row_average,
1003 row_lower,
1004 row_trend,
1005 row_changed,
1006 )
1007 },
1008 );
1009
1010 #[cfg(target_arch = "wasm32")]
1011 for (row, ((((row_upper, row_average), row_lower), row_trend), row_changed)) in upper_mu
1012 .chunks_mut(cols)
1013 .zip(average_mu.chunks_mut(cols))
1014 .zip(lower_mu.chunks_mut(cols))
1015 .zip(trend_mu.chunks_mut(cols))
1016 .zip(changed_mu.chunks_mut(cols))
1017 .enumerate()
1018 {
1019 do_row(
1020 row,
1021 row_upper,
1022 row_average,
1023 row_lower,
1024 row_trend,
1025 row_changed,
1026 );
1027 }
1028 } else {
1029 for (row, ((((row_upper, row_average), row_lower), row_trend), row_changed)) in upper_mu
1030 .chunks_mut(cols)
1031 .zip(average_mu.chunks_mut(cols))
1032 .zip(lower_mu.chunks_mut(cols))
1033 .zip(trend_mu.chunks_mut(cols))
1034 .zip(changed_mu.chunks_mut(cols))
1035 .enumerate()
1036 {
1037 do_row(
1038 row,
1039 row_upper,
1040 row_average,
1041 row_lower,
1042 row_trend,
1043 row_changed,
1044 );
1045 }
1046 }
1047
1048 let upper = unsafe {
1049 Vec::from_raw_parts(
1050 upper_guard.as_mut_ptr() as *mut f64,
1051 total,
1052 upper_guard.capacity(),
1053 )
1054 };
1055 let average = unsafe {
1056 Vec::from_raw_parts(
1057 average_guard.as_mut_ptr() as *mut f64,
1058 total,
1059 average_guard.capacity(),
1060 )
1061 };
1062 let lower = unsafe {
1063 Vec::from_raw_parts(
1064 lower_guard.as_mut_ptr() as *mut f64,
1065 total,
1066 lower_guard.capacity(),
1067 )
1068 };
1069 let trend = unsafe {
1070 Vec::from_raw_parts(
1071 trend_guard.as_mut_ptr() as *mut f64,
1072 total,
1073 trend_guard.capacity(),
1074 )
1075 };
1076 let changed = unsafe {
1077 Vec::from_raw_parts(
1078 changed_guard.as_mut_ptr() as *mut f64,
1079 total,
1080 changed_guard.capacity(),
1081 )
1082 };
1083
1084 Ok(HyperTrendBatchOutput {
1085 upper,
1086 average,
1087 lower,
1088 trend,
1089 changed,
1090 combos,
1091 rows,
1092 cols,
1093 })
1094}
1095
1096fn hypertrend_batch_inner_into(
1097 high: &[f64],
1098 low: &[f64],
1099 source: &[f64],
1100 sweep: &HyperTrendBatchRange,
1101 kernel: Kernel,
1102 parallel: bool,
1103 out_upper: &mut [f64],
1104 out_average: &mut [f64],
1105 out_lower: &mut [f64],
1106 out_trend: &mut [f64],
1107 out_changed: &mut [f64],
1108) -> Result<Vec<HyperTrendParams>, HyperTrendError> {
1109 validate_lengths(high, low, source)?;
1110 let combos = expand_grid_hypertrend(sweep)?;
1111 for params in &combos {
1112 validate_params(
1113 params.factor.unwrap_or(DEFAULT_FACTOR),
1114 params.slope.unwrap_or(DEFAULT_SLOPE),
1115 params.width_percent.unwrap_or(DEFAULT_WIDTH_PERCENT),
1116 )?;
1117 }
1118
1119 let rows = combos.len();
1120 let cols = source.len();
1121 let total = rows
1122 .checked_mul(cols)
1123 .ok_or(HyperTrendError::OutputLengthMismatch {
1124 expected: usize::MAX,
1125 got: 0,
1126 })?;
1127 if out_upper.len() != total
1128 || out_average.len() != total
1129 || out_lower.len() != total
1130 || out_trend.len() != total
1131 || out_changed.len() != total
1132 {
1133 return Err(HyperTrendError::OutputLengthMismatch {
1134 expected: total,
1135 got: out_upper
1136 .len()
1137 .max(out_average.len())
1138 .max(out_lower.len())
1139 .max(out_trend.len())
1140 .max(out_changed.len()),
1141 });
1142 }
1143
1144 let _kernel = kernel;
1145 let atr_values = compute_atr_zeroed(high, low, source);
1146 let do_row = |row: usize,
1147 dst_upper: &mut [f64],
1148 dst_average: &mut [f64],
1149 dst_lower: &mut [f64],
1150 dst_trend: &mut [f64],
1151 dst_changed: &mut [f64]| {
1152 let params = &combos[row];
1153 hypertrend_row_scalar(
1154 high,
1155 low,
1156 source,
1157 params.factor.unwrap_or(DEFAULT_FACTOR),
1158 params.slope.unwrap_or(DEFAULT_SLOPE),
1159 params.width_percent.unwrap_or(DEFAULT_WIDTH_PERCENT) * 0.01,
1160 &atr_values,
1161 dst_upper,
1162 dst_average,
1163 dst_lower,
1164 dst_trend,
1165 dst_changed,
1166 );
1167 };
1168
1169 if parallel {
1170 #[cfg(not(target_arch = "wasm32"))]
1171 out_upper
1172 .par_chunks_mut(cols)
1173 .zip(out_average.par_chunks_mut(cols))
1174 .zip(out_lower.par_chunks_mut(cols))
1175 .zip(out_trend.par_chunks_mut(cols))
1176 .zip(out_changed.par_chunks_mut(cols))
1177 .enumerate()
1178 .for_each(
1179 |(row, ((((dst_upper, dst_average), dst_lower), dst_trend), dst_changed))| {
1180 do_row(
1181 row,
1182 dst_upper,
1183 dst_average,
1184 dst_lower,
1185 dst_trend,
1186 dst_changed,
1187 )
1188 },
1189 );
1190
1191 #[cfg(target_arch = "wasm32")]
1192 for (row, ((((dst_upper, dst_average), dst_lower), dst_trend), dst_changed)) in out_upper
1193 .chunks_mut(cols)
1194 .zip(out_average.chunks_mut(cols))
1195 .zip(out_lower.chunks_mut(cols))
1196 .zip(out_trend.chunks_mut(cols))
1197 .zip(out_changed.chunks_mut(cols))
1198 .enumerate()
1199 {
1200 do_row(
1201 row,
1202 dst_upper,
1203 dst_average,
1204 dst_lower,
1205 dst_trend,
1206 dst_changed,
1207 );
1208 }
1209 } else {
1210 for (row, ((((dst_upper, dst_average), dst_lower), dst_trend), dst_changed)) in out_upper
1211 .chunks_mut(cols)
1212 .zip(out_average.chunks_mut(cols))
1213 .zip(out_lower.chunks_mut(cols))
1214 .zip(out_trend.chunks_mut(cols))
1215 .zip(out_changed.chunks_mut(cols))
1216 .enumerate()
1217 {
1218 do_row(
1219 row,
1220 dst_upper,
1221 dst_average,
1222 dst_lower,
1223 dst_trend,
1224 dst_changed,
1225 );
1226 }
1227 }
1228
1229 Ok(combos)
1230}
1231
1232#[cfg(feature = "python")]
1233#[pyfunction(name = "hypertrend")]
1234#[pyo3(signature = (high, low, source, factor=DEFAULT_FACTOR, slope=DEFAULT_SLOPE, width_percent=DEFAULT_WIDTH_PERCENT, kernel=None))]
1235pub fn hypertrend_py<'py>(
1236 py: Python<'py>,
1237 high: PyReadonlyArray1<'py, f64>,
1238 low: PyReadonlyArray1<'py, f64>,
1239 source: PyReadonlyArray1<'py, f64>,
1240 factor: f64,
1241 slope: f64,
1242 width_percent: f64,
1243 kernel: Option<&str>,
1244) -> PyResult<(
1245 Bound<'py, PyArray1<f64>>,
1246 Bound<'py, PyArray1<f64>>,
1247 Bound<'py, PyArray1<f64>>,
1248 Bound<'py, PyArray1<f64>>,
1249 Bound<'py, PyArray1<f64>>,
1250)> {
1251 let high = high.as_slice()?;
1252 let low = low.as_slice()?;
1253 let source = source.as_slice()?;
1254 let input = HyperTrendInput::from_slices(
1255 high,
1256 low,
1257 source,
1258 HyperTrendParams {
1259 factor: Some(factor),
1260 slope: Some(slope),
1261 width_percent: Some(width_percent),
1262 },
1263 );
1264 let kernel = validate_kernel(kernel, false)?;
1265 let out = py
1266 .allow_threads(|| hypertrend_with_kernel(&input, kernel))
1267 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1268 Ok((
1269 out.upper.into_pyarray(py),
1270 out.average.into_pyarray(py),
1271 out.lower.into_pyarray(py),
1272 out.trend.into_pyarray(py),
1273 out.changed.into_pyarray(py),
1274 ))
1275}
1276
1277#[cfg(feature = "python")]
1278#[pyclass(name = "HyperTrendStream")]
1279pub struct HyperTrendStreamPy {
1280 stream: HyperTrendStream,
1281}
1282
1283#[cfg(feature = "python")]
1284#[pymethods]
1285impl HyperTrendStreamPy {
1286 #[new]
1287 #[pyo3(signature = (factor=DEFAULT_FACTOR, slope=DEFAULT_SLOPE, width_percent=DEFAULT_WIDTH_PERCENT))]
1288 fn new(factor: f64, slope: f64, width_percent: f64) -> PyResult<Self> {
1289 let stream = HyperTrendStream::try_new(HyperTrendParams {
1290 factor: Some(factor),
1291 slope: Some(slope),
1292 width_percent: Some(width_percent),
1293 })
1294 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1295 Ok(Self { stream })
1296 }
1297
1298 fn update(&mut self, high: f64, low: f64, source: f64) -> Option<(f64, f64, f64, f64, f64)> {
1299 self.stream.update(high, low, source)
1300 }
1301}
1302
1303#[cfg(feature = "python")]
1304#[pyfunction(name = "hypertrend_batch")]
1305#[pyo3(signature = (high, low, source, factor_range, slope_range, width_percent_range, kernel=None))]
1306pub fn hypertrend_batch_py<'py>(
1307 py: Python<'py>,
1308 high: PyReadonlyArray1<'py, f64>,
1309 low: PyReadonlyArray1<'py, f64>,
1310 source: PyReadonlyArray1<'py, f64>,
1311 factor_range: (f64, f64, f64),
1312 slope_range: (f64, f64, f64),
1313 width_percent_range: (f64, f64, f64),
1314 kernel: Option<&str>,
1315) -> PyResult<Bound<'py, PyDict>> {
1316 let high = high.as_slice()?;
1317 let low = low.as_slice()?;
1318 let source = source.as_slice()?;
1319 let sweep = HyperTrendBatchRange {
1320 factor: factor_range,
1321 slope: slope_range,
1322 width_percent: width_percent_range,
1323 };
1324 let combos =
1325 expand_grid_hypertrend(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1326 let rows = combos.len();
1327 let cols = source.len();
1328 let total = rows
1329 .checked_mul(cols)
1330 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1331 let upper_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1332 let average_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1333 let lower_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1334 let trend_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1335 let changed_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1336 let out_upper = unsafe { upper_arr.as_slice_mut()? };
1337 let out_average = unsafe { average_arr.as_slice_mut()? };
1338 let out_lower = unsafe { lower_arr.as_slice_mut()? };
1339 let out_trend = unsafe { trend_arr.as_slice_mut()? };
1340 let out_changed = unsafe { changed_arr.as_slice_mut()? };
1341 let kernel = validate_kernel(kernel, true)?;
1342
1343 py.allow_threads(|| {
1344 let batch_kernel = match kernel {
1345 Kernel::Auto => detect_best_batch_kernel(),
1346 other => other,
1347 };
1348 hypertrend_batch_inner_into(
1349 high,
1350 low,
1351 source,
1352 &sweep,
1353 batch_kernel.to_non_batch(),
1354 true,
1355 out_upper,
1356 out_average,
1357 out_lower,
1358 out_trend,
1359 out_changed,
1360 )
1361 })
1362 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1363
1364 let factors: Vec<f64> = combos
1365 .iter()
1366 .map(|params| params.factor.unwrap_or(DEFAULT_FACTOR))
1367 .collect();
1368 let slopes: Vec<f64> = combos
1369 .iter()
1370 .map(|params| params.slope.unwrap_or(DEFAULT_SLOPE))
1371 .collect();
1372 let width_percents: Vec<f64> = combos
1373 .iter()
1374 .map(|params| params.width_percent.unwrap_or(DEFAULT_WIDTH_PERCENT))
1375 .collect();
1376
1377 let dict = PyDict::new(py);
1378 dict.set_item("upper", upper_arr.reshape((rows, cols))?)?;
1379 dict.set_item("average", average_arr.reshape((rows, cols))?)?;
1380 dict.set_item("lower", lower_arr.reshape((rows, cols))?)?;
1381 dict.set_item("trend", trend_arr.reshape((rows, cols))?)?;
1382 dict.set_item("changed", changed_arr.reshape((rows, cols))?)?;
1383 dict.set_item("rows", rows)?;
1384 dict.set_item("cols", cols)?;
1385 dict.set_item("factors", factors.into_pyarray(py))?;
1386 dict.set_item("slopes", slopes.into_pyarray(py))?;
1387 dict.set_item("width_percents", width_percents.into_pyarray(py))?;
1388 Ok(dict)
1389}
1390
1391#[cfg(feature = "python")]
1392pub fn register_hypertrend_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1393 m.add_function(wrap_pyfunction!(hypertrend_py, m)?)?;
1394 m.add_function(wrap_pyfunction!(hypertrend_batch_py, m)?)?;
1395 m.add_class::<HyperTrendStreamPy>()?;
1396 Ok(())
1397}
1398
1399#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1400#[derive(Debug, Clone, Serialize, Deserialize)]
1401struct HyperTrendJsOutput {
1402 upper: Vec<f64>,
1403 average: Vec<f64>,
1404 lower: Vec<f64>,
1405 trend: Vec<f64>,
1406 changed: Vec<f64>,
1407}
1408
1409#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1410#[derive(Debug, Clone, Serialize, Deserialize)]
1411struct HyperTrendBatchConfig {
1412 factor_range: Vec<f64>,
1413 slope_range: Vec<f64>,
1414 width_percent_range: Vec<f64>,
1415}
1416
1417#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1418#[derive(Debug, Clone, Serialize, Deserialize)]
1419struct HyperTrendBatchJsOutput {
1420 upper: Vec<f64>,
1421 average: Vec<f64>,
1422 lower: Vec<f64>,
1423 trend: Vec<f64>,
1424 changed: Vec<f64>,
1425 rows: usize,
1426 cols: usize,
1427 combos: Vec<HyperTrendParams>,
1428}
1429
1430#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1431#[wasm_bindgen(js_name = "hypertrend")]
1432pub fn hypertrend_js(
1433 high: &[f64],
1434 low: &[f64],
1435 source: &[f64],
1436 factor: f64,
1437 slope: f64,
1438 width_percent: f64,
1439) -> Result<JsValue, JsValue> {
1440 let input = HyperTrendInput::from_slices(
1441 high,
1442 low,
1443 source,
1444 HyperTrendParams {
1445 factor: Some(factor),
1446 slope: Some(slope),
1447 width_percent: Some(width_percent),
1448 },
1449 );
1450 let out = hypertrend(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1451 serde_wasm_bindgen::to_value(&HyperTrendJsOutput {
1452 upper: out.upper,
1453 average: out.average,
1454 lower: out.lower,
1455 trend: out.trend,
1456 changed: out.changed,
1457 })
1458 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1459}
1460
1461#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1462#[wasm_bindgen]
1463pub fn hypertrend_into(
1464 high_ptr: *const f64,
1465 low_ptr: *const f64,
1466 source_ptr: *const f64,
1467 out_ptr: *mut f64,
1468 len: usize,
1469 factor: f64,
1470 slope: f64,
1471 width_percent: f64,
1472) -> Result<(), JsValue> {
1473 if high_ptr.is_null() || low_ptr.is_null() || source_ptr.is_null() || out_ptr.is_null() {
1474 return Err(JsValue::from_str("null pointer passed to hypertrend_into"));
1475 }
1476
1477 unsafe {
1478 let high = std::slice::from_raw_parts(high_ptr, len);
1479 let low = std::slice::from_raw_parts(low_ptr, len);
1480 let source = std::slice::from_raw_parts(source_ptr, len);
1481 let out = std::slice::from_raw_parts_mut(out_ptr, len * 5);
1482 let (out_upper, rest) = out.split_at_mut(len);
1483 let (out_average, rest) = rest.split_at_mut(len);
1484 let (out_lower, rest) = rest.split_at_mut(len);
1485 let (out_trend, out_changed) = rest.split_at_mut(len);
1486 let input = HyperTrendInput::from_slices(
1487 high,
1488 low,
1489 source,
1490 HyperTrendParams {
1491 factor: Some(factor),
1492 slope: Some(slope),
1493 width_percent: Some(width_percent),
1494 },
1495 );
1496 hypertrend_into_slice(
1497 out_upper,
1498 out_average,
1499 out_lower,
1500 out_trend,
1501 out_changed,
1502 &input,
1503 Kernel::Auto,
1504 )
1505 .map_err(|e| JsValue::from_str(&e.to_string()))
1506 }
1507}
1508
1509#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1510#[wasm_bindgen(js_name = "hypertrend_into_host")]
1511pub fn hypertrend_into_host(
1512 high: &[f64],
1513 low: &[f64],
1514 source: &[f64],
1515 out_ptr: *mut f64,
1516 factor: f64,
1517 slope: f64,
1518 width_percent: f64,
1519) -> Result<(), JsValue> {
1520 if out_ptr.is_null() {
1521 return Err(JsValue::from_str(
1522 "null pointer passed to hypertrend_into_host",
1523 ));
1524 }
1525
1526 unsafe {
1527 let out = std::slice::from_raw_parts_mut(out_ptr, source.len() * 5);
1528 let (out_upper, rest) = out.split_at_mut(source.len());
1529 let (out_average, rest) = rest.split_at_mut(source.len());
1530 let (out_lower, rest) = rest.split_at_mut(source.len());
1531 let (out_trend, out_changed) = rest.split_at_mut(source.len());
1532 let input = HyperTrendInput::from_slices(
1533 high,
1534 low,
1535 source,
1536 HyperTrendParams {
1537 factor: Some(factor),
1538 slope: Some(slope),
1539 width_percent: Some(width_percent),
1540 },
1541 );
1542 hypertrend_into_slice(
1543 out_upper,
1544 out_average,
1545 out_lower,
1546 out_trend,
1547 out_changed,
1548 &input,
1549 Kernel::Auto,
1550 )
1551 .map_err(|e| JsValue::from_str(&e.to_string()))
1552 }
1553}
1554
1555#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1556#[wasm_bindgen]
1557pub fn hypertrend_alloc(len: usize) -> *mut f64 {
1558 let mut buf = vec![0.0_f64; len * 5];
1559 let ptr = buf.as_mut_ptr();
1560 std::mem::forget(buf);
1561 ptr
1562}
1563
1564#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1565#[wasm_bindgen]
1566pub fn hypertrend_free(ptr: *mut f64, len: usize) {
1567 if ptr.is_null() {
1568 return;
1569 }
1570 unsafe {
1571 let _ = Vec::from_raw_parts(ptr, len * 5, len * 5);
1572 }
1573}
1574
1575#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1576#[wasm_bindgen(js_name = "hypertrend_batch")]
1577pub fn hypertrend_batch_js(
1578 high: &[f64],
1579 low: &[f64],
1580 source: &[f64],
1581 config: JsValue,
1582) -> Result<JsValue, JsValue> {
1583 let config: HyperTrendBatchConfig = serde_wasm_bindgen::from_value(config)
1584 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1585 if config.factor_range.len() != 3
1586 || config.slope_range.len() != 3
1587 || config.width_percent_range.len() != 3
1588 {
1589 return Err(JsValue::from_str(
1590 "Invalid config: ranges must have exactly 3 elements [start, end, step]",
1591 ));
1592 }
1593
1594 let sweep = HyperTrendBatchRange {
1595 factor: (
1596 config.factor_range[0],
1597 config.factor_range[1],
1598 config.factor_range[2],
1599 ),
1600 slope: (
1601 config.slope_range[0],
1602 config.slope_range[1],
1603 config.slope_range[2],
1604 ),
1605 width_percent: (
1606 config.width_percent_range[0],
1607 config.width_percent_range[1],
1608 config.width_percent_range[2],
1609 ),
1610 };
1611 let batch = hypertrend_batch_slice(high, low, source, &sweep, Kernel::Scalar)
1612 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1613 serde_wasm_bindgen::to_value(&HyperTrendBatchJsOutput {
1614 upper: batch.upper,
1615 average: batch.average,
1616 lower: batch.lower,
1617 trend: batch.trend,
1618 changed: batch.changed,
1619 rows: batch.rows,
1620 cols: batch.cols,
1621 combos: batch.combos,
1622 })
1623 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1624}
1625
1626#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1627#[wasm_bindgen]
1628pub fn hypertrend_batch_into(
1629 high_ptr: *const f64,
1630 low_ptr: *const f64,
1631 source_ptr: *const f64,
1632 upper_ptr: *mut f64,
1633 average_ptr: *mut f64,
1634 lower_ptr: *mut f64,
1635 trend_ptr: *mut f64,
1636 changed_ptr: *mut f64,
1637 len: usize,
1638 factor_start: f64,
1639 factor_end: f64,
1640 factor_step: f64,
1641 slope_start: f64,
1642 slope_end: f64,
1643 slope_step: f64,
1644 width_percent_start: f64,
1645 width_percent_end: f64,
1646 width_percent_step: f64,
1647) -> Result<usize, JsValue> {
1648 if high_ptr.is_null()
1649 || low_ptr.is_null()
1650 || source_ptr.is_null()
1651 || upper_ptr.is_null()
1652 || average_ptr.is_null()
1653 || lower_ptr.is_null()
1654 || trend_ptr.is_null()
1655 || changed_ptr.is_null()
1656 {
1657 return Err(JsValue::from_str(
1658 "null pointer passed to hypertrend_batch_into",
1659 ));
1660 }
1661
1662 unsafe {
1663 let high = std::slice::from_raw_parts(high_ptr, len);
1664 let low = std::slice::from_raw_parts(low_ptr, len);
1665 let source = std::slice::from_raw_parts(source_ptr, len);
1666 let sweep = HyperTrendBatchRange {
1667 factor: (factor_start, factor_end, factor_step),
1668 slope: (slope_start, slope_end, slope_step),
1669 width_percent: (width_percent_start, width_percent_end, width_percent_step),
1670 };
1671 let combos =
1672 expand_grid_hypertrend(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1673 let rows = combos.len();
1674 let total = rows
1675 .checked_mul(len)
1676 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1677 let upper = std::slice::from_raw_parts_mut(upper_ptr, total);
1678 let average = std::slice::from_raw_parts_mut(average_ptr, total);
1679 let lower = std::slice::from_raw_parts_mut(lower_ptr, total);
1680 let trend = std::slice::from_raw_parts_mut(trend_ptr, total);
1681 let changed = std::slice::from_raw_parts_mut(changed_ptr, total);
1682 hypertrend_batch_inner_into(
1683 high,
1684 low,
1685 source,
1686 &sweep,
1687 Kernel::Scalar,
1688 false,
1689 upper,
1690 average,
1691 lower,
1692 trend,
1693 changed,
1694 )
1695 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1696 Ok(rows)
1697 }
1698}
1699
1700#[cfg(test)]
1701mod tests {
1702 use super::*;
1703 use crate::indicators::dispatch::{
1704 compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
1705 ParamValue,
1706 };
1707
1708 fn assert_close(a: &[f64], b: &[f64], tol: f64) {
1709 assert_eq!(a.len(), b.len());
1710 for i in 0..a.len() {
1711 let lhs = a[i];
1712 let rhs = b[i];
1713 if lhs.is_nan() || rhs.is_nan() {
1714 assert!(
1715 lhs.is_nan() && rhs.is_nan(),
1716 "nan mismatch at {i}: {lhs} vs {rhs}"
1717 );
1718 } else {
1719 assert!(
1720 (lhs - rhs).abs() <= tol,
1721 "mismatch at {i}: {lhs} vs {rhs} with tol {tol}"
1722 );
1723 }
1724 }
1725 }
1726
1727 fn sample_hls(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1728 let mut high = Vec::with_capacity(len);
1729 let mut low = Vec::with_capacity(len);
1730 let mut source = Vec::with_capacity(len);
1731
1732 for i in 0..len {
1733 let base = 100.0 + i as f64 * 0.17 + (i as f64 * 0.031).sin() * 2.75;
1734 let spread = 1.0 + (i as f64 * 0.07).cos().abs() * 1.8;
1735 let src = base + (i as f64 * 0.11).sin() * 0.8;
1736 high.push(base + spread);
1737 low.push(base - spread);
1738 source.push(src);
1739 }
1740
1741 (high, low, source)
1742 }
1743
1744 fn check_output_contract(kernel: Kernel) {
1745 let (high, low, source) = sample_hls(320);
1746 let input = HyperTrendInput::from_slices(
1747 &high,
1748 &low,
1749 &source,
1750 HyperTrendParams {
1751 factor: Some(5.0),
1752 slope: Some(14.0),
1753 width_percent: Some(80.0),
1754 },
1755 );
1756 let out = hypertrend_with_kernel(&input, kernel).expect("indicator");
1757 assert_eq!(out.upper.len(), source.len());
1758 assert_eq!(out.average.len(), source.len());
1759 assert_eq!(out.lower.len(), source.len());
1760 assert_eq!(out.trend.len(), source.len());
1761 assert_eq!(out.changed.len(), source.len());
1762 assert!(out.average.iter().any(|v| v.is_finite()));
1763 assert!(out
1764 .upper
1765 .iter()
1766 .zip(&out.lower)
1767 .all(|(u, l)| (u.is_nan() && l.is_nan()) || (*u >= *l)));
1768 }
1769
1770 fn check_into_matches_api(kernel: Kernel) {
1771 let (high, low, source) = sample_hls(240);
1772 let input = HyperTrendInput::from_slices(
1773 &high,
1774 &low,
1775 &source,
1776 HyperTrendParams {
1777 factor: Some(4.5),
1778 slope: Some(10.0),
1779 width_percent: Some(60.0),
1780 },
1781 );
1782 let baseline = hypertrend_with_kernel(&input, kernel).expect("baseline");
1783 let mut upper = vec![0.0; source.len()];
1784 let mut average = vec![0.0; source.len()];
1785 let mut lower = vec![0.0; source.len()];
1786 let mut trend = vec![0.0; source.len()];
1787 let mut changed = vec![0.0; source.len()];
1788 hypertrend_into_slice(
1789 &mut upper,
1790 &mut average,
1791 &mut lower,
1792 &mut trend,
1793 &mut changed,
1794 &input,
1795 kernel,
1796 )
1797 .expect("into");
1798
1799 assert_close(&baseline.upper, &upper, 1e-12);
1800 assert_close(&baseline.average, &average, 1e-12);
1801 assert_close(&baseline.lower, &lower, 1e-12);
1802 assert_close(&baseline.trend, &trend, 1e-12);
1803 assert_close(&baseline.changed, &changed, 1e-12);
1804 }
1805
1806 fn check_stream_matches_batch() {
1807 let (high, low, source) = sample_hls(260);
1808 let input = HyperTrendInput::from_slices(
1809 &high,
1810 &low,
1811 &source,
1812 HyperTrendParams {
1813 factor: Some(4.0),
1814 slope: Some(12.0),
1815 width_percent: Some(55.0),
1816 },
1817 );
1818 let batch = hypertrend(&input).expect("batch");
1819 let mut stream = HyperTrendStream::try_new(HyperTrendParams {
1820 factor: Some(4.0),
1821 slope: Some(12.0),
1822 width_percent: Some(55.0),
1823 })
1824 .expect("stream");
1825
1826 let mut upper = vec![f64::NAN; source.len()];
1827 let mut average = vec![f64::NAN; source.len()];
1828 let mut lower = vec![f64::NAN; source.len()];
1829 let mut trend = vec![f64::NAN; source.len()];
1830 let mut changed = vec![f64::NAN; source.len()];
1831
1832 for i in 0..source.len() {
1833 if let Some((u, a, l, t, c)) = stream.update(high[i], low[i], source[i]) {
1834 upper[i] = u;
1835 average[i] = a;
1836 lower[i] = l;
1837 trend[i] = t;
1838 changed[i] = c;
1839 }
1840 }
1841
1842 assert_close(&batch.upper, &upper, 1e-12);
1843 assert_close(&batch.average, &average, 1e-12);
1844 assert_close(&batch.lower, &lower, 1e-12);
1845 assert_close(&batch.trend, &trend, 1e-12);
1846 assert_close(&batch.changed, &changed, 1e-12);
1847 }
1848
1849 fn check_batch_single_matches_single(kernel: Kernel) {
1850 let (high, low, source) = sample_hls(180);
1851 let batch = hypertrend_batch_with_kernel(
1852 &high,
1853 &low,
1854 &source,
1855 &HyperTrendBatchRange {
1856 factor: (5.0, 5.0, 0.0),
1857 slope: (14.0, 14.0, 0.0),
1858 width_percent: (80.0, 80.0, 0.0),
1859 },
1860 kernel,
1861 )
1862 .expect("batch");
1863 let single = hypertrend(&HyperTrendInput::from_slices(
1864 &high,
1865 &low,
1866 &source,
1867 HyperTrendParams {
1868 factor: Some(5.0),
1869 slope: Some(14.0),
1870 width_percent: Some(80.0),
1871 },
1872 ))
1873 .expect("single");
1874
1875 assert_eq!(batch.rows, 1);
1876 assert_eq!(batch.cols, source.len());
1877 assert_close(&batch.upper[..source.len()], &single.upper, 1e-12);
1878 assert_close(&batch.average[..source.len()], &single.average, 1e-12);
1879 assert_close(&batch.lower[..source.len()], &single.lower, 1e-12);
1880 assert_close(&batch.trend[..source.len()], &single.trend, 1e-12);
1881 assert_close(&batch.changed[..source.len()], &single.changed, 1e-12);
1882 }
1883
1884 #[test]
1885 fn hypertrend_invalid_params() {
1886 let (high, low, source) = sample_hls(64);
1887
1888 let err = hypertrend(&HyperTrendInput::from_slices(
1889 &high,
1890 &low,
1891 &source,
1892 HyperTrendParams {
1893 factor: Some(0.0),
1894 slope: Some(14.0),
1895 width_percent: Some(80.0),
1896 },
1897 ))
1898 .expect_err("invalid factor");
1899 assert!(matches!(err, HyperTrendError::InvalidFactor { .. }));
1900
1901 let err = hypertrend(&HyperTrendInput::from_slices(
1902 &high,
1903 &low,
1904 &source,
1905 HyperTrendParams {
1906 factor: Some(5.0),
1907 slope: Some(0.0),
1908 width_percent: Some(80.0),
1909 },
1910 ))
1911 .expect_err("invalid slope");
1912 assert!(matches!(err, HyperTrendError::InvalidSlope { .. }));
1913
1914 let err = hypertrend(&HyperTrendInput::from_slices(
1915 &high,
1916 &low,
1917 &source,
1918 HyperTrendParams {
1919 factor: Some(5.0),
1920 slope: Some(14.0),
1921 width_percent: Some(120.0),
1922 },
1923 ))
1924 .expect_err("invalid width");
1925 assert!(matches!(err, HyperTrendError::InvalidWidthPercent { .. }));
1926 }
1927
1928 #[test]
1929 fn hypertrend_output_contract() {
1930 check_output_contract(Kernel::Auto);
1931 check_output_contract(Kernel::Scalar);
1932 }
1933
1934 #[test]
1935 fn hypertrend_into_matches_api() {
1936 check_into_matches_api(Kernel::Auto);
1937 check_into_matches_api(Kernel::Scalar);
1938 }
1939
1940 #[test]
1941 fn hypertrend_stream_matches_batch() {
1942 check_stream_matches_batch();
1943 }
1944
1945 #[test]
1946 fn hypertrend_batch_single_matches_single() {
1947 check_batch_single_matches_single(Kernel::Auto);
1948 }
1949
1950 #[test]
1951 fn hypertrend_dispatch_matches_direct() {
1952 let (high, low, source) = sample_hls(160);
1953 let combo = [
1954 ParamKV {
1955 key: "factor",
1956 value: ParamValue::Float(5.0),
1957 },
1958 ParamKV {
1959 key: "slope",
1960 value: ParamValue::Float(14.0),
1961 },
1962 ParamKV {
1963 key: "width_percent",
1964 value: ParamValue::Float(80.0),
1965 },
1966 ];
1967 let combos = [IndicatorParamSet { params: &combo }];
1968 let req = IndicatorBatchRequest {
1969 indicator_id: "hypertrend",
1970 output_id: Some("average"),
1971 data: IndicatorDataRef::Ohlc {
1972 open: &source,
1973 high: &high,
1974 low: &low,
1975 close: &source,
1976 },
1977 combos: &combos,
1978 kernel: Kernel::Auto,
1979 };
1980
1981 let batch = compute_cpu_batch(req).expect("dispatch");
1982 assert_eq!(batch.rows, 1);
1983 assert_eq!(batch.cols, source.len());
1984
1985 let direct = hypertrend(&HyperTrendInput::from_slices(
1986 &high,
1987 &low,
1988 &source,
1989 HyperTrendParams {
1990 factor: Some(5.0),
1991 slope: Some(14.0),
1992 width_percent: Some(80.0),
1993 },
1994 ))
1995 .expect("direct");
1996 let row = &batch.values_f64.as_ref().expect("f64 output")[0..source.len()];
1997 assert_close(row, &direct.average, 1e-12);
1998 }
1999}