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::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24use std::error::Error;
25use thiserror::Error;
26
27const DEFAULT_ATR_LENGTH: usize = 10;
28const DEFAULT_BASE_MULTIPLIER: f64 = 3.0;
29const DEFAULT_NOISE_THRESHOLD: f64 = 1.0;
30const DEFAULT_EXPANSION_ALPHA: f64 = 0.5;
31
32#[derive(Debug, Clone)]
33pub enum EvasiveSuperTrendData<'a> {
34 Candles {
35 candles: &'a Candles,
36 },
37 Slices {
38 open: &'a [f64],
39 high: &'a [f64],
40 low: &'a [f64],
41 close: &'a [f64],
42 },
43}
44
45#[derive(Debug, Clone)]
46pub struct EvasiveSuperTrendOutput {
47 pub band: Vec<f64>,
48 pub state: Vec<f64>,
49 pub noisy: Vec<f64>,
50 pub changed: Vec<f64>,
51}
52
53#[derive(Debug, Clone)]
54#[cfg_attr(
55 all(target_arch = "wasm32", feature = "wasm"),
56 derive(Serialize, Deserialize)
57)]
58pub struct EvasiveSuperTrendParams {
59 pub atr_length: Option<usize>,
60 pub base_multiplier: Option<f64>,
61 pub noise_threshold: Option<f64>,
62 pub expansion_alpha: Option<f64>,
63}
64
65impl Default for EvasiveSuperTrendParams {
66 fn default() -> Self {
67 Self {
68 atr_length: Some(DEFAULT_ATR_LENGTH),
69 base_multiplier: Some(DEFAULT_BASE_MULTIPLIER),
70 noise_threshold: Some(DEFAULT_NOISE_THRESHOLD),
71 expansion_alpha: Some(DEFAULT_EXPANSION_ALPHA),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct EvasiveSuperTrendInput<'a> {
78 pub data: EvasiveSuperTrendData<'a>,
79 pub params: EvasiveSuperTrendParams,
80}
81
82impl<'a> EvasiveSuperTrendInput<'a> {
83 #[inline]
84 pub fn from_candles(candles: &'a Candles, params: EvasiveSuperTrendParams) -> Self {
85 Self {
86 data: EvasiveSuperTrendData::Candles { candles },
87 params,
88 }
89 }
90
91 #[inline]
92 pub fn from_slices(
93 open: &'a [f64],
94 high: &'a [f64],
95 low: &'a [f64],
96 close: &'a [f64],
97 params: EvasiveSuperTrendParams,
98 ) -> Self {
99 Self {
100 data: EvasiveSuperTrendData::Slices {
101 open,
102 high,
103 low,
104 close,
105 },
106 params,
107 }
108 }
109
110 #[inline]
111 pub fn with_default_candles(candles: &'a Candles) -> Self {
112 Self::from_candles(candles, EvasiveSuperTrendParams::default())
113 }
114
115 #[inline]
116 pub fn get_atr_length(&self) -> usize {
117 self.params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH)
118 }
119
120 #[inline]
121 pub fn get_base_multiplier(&self) -> f64 {
122 self.params
123 .base_multiplier
124 .unwrap_or(DEFAULT_BASE_MULTIPLIER)
125 }
126
127 #[inline]
128 pub fn get_noise_threshold(&self) -> f64 {
129 self.params
130 .noise_threshold
131 .unwrap_or(DEFAULT_NOISE_THRESHOLD)
132 }
133
134 #[inline]
135 pub fn get_expansion_alpha(&self) -> f64 {
136 self.params
137 .expansion_alpha
138 .unwrap_or(DEFAULT_EXPANSION_ALPHA)
139 }
140}
141
142#[derive(Copy, Clone, Debug)]
143pub struct EvasiveSuperTrendBuilder {
144 atr_length: Option<usize>,
145 base_multiplier: Option<f64>,
146 noise_threshold: Option<f64>,
147 expansion_alpha: Option<f64>,
148 kernel: Kernel,
149}
150
151impl Default for EvasiveSuperTrendBuilder {
152 fn default() -> Self {
153 Self {
154 atr_length: None,
155 base_multiplier: None,
156 noise_threshold: None,
157 expansion_alpha: None,
158 kernel: Kernel::Auto,
159 }
160 }
161}
162
163impl EvasiveSuperTrendBuilder {
164 #[inline(always)]
165 pub fn new() -> Self {
166 Self::default()
167 }
168
169 #[inline(always)]
170 pub fn atr_length(mut self, value: usize) -> Self {
171 self.atr_length = Some(value);
172 self
173 }
174
175 #[inline(always)]
176 pub fn base_multiplier(mut self, value: f64) -> Self {
177 self.base_multiplier = Some(value);
178 self
179 }
180
181 #[inline(always)]
182 pub fn noise_threshold(mut self, value: f64) -> Self {
183 self.noise_threshold = Some(value);
184 self
185 }
186
187 #[inline(always)]
188 pub fn expansion_alpha(mut self, value: f64) -> Self {
189 self.expansion_alpha = Some(value);
190 self
191 }
192
193 #[inline(always)]
194 pub fn kernel(mut self, value: Kernel) -> Self {
195 self.kernel = value;
196 self
197 }
198
199 #[inline(always)]
200 pub fn apply(
201 self,
202 candles: &Candles,
203 ) -> Result<EvasiveSuperTrendOutput, EvasiveSuperTrendError> {
204 evasive_supertrend_with_kernel(
205 &EvasiveSuperTrendInput::from_candles(
206 candles,
207 EvasiveSuperTrendParams {
208 atr_length: self.atr_length,
209 base_multiplier: self.base_multiplier,
210 noise_threshold: self.noise_threshold,
211 expansion_alpha: self.expansion_alpha,
212 },
213 ),
214 self.kernel,
215 )
216 }
217
218 #[inline(always)]
219 pub fn apply_slices(
220 self,
221 open: &[f64],
222 high: &[f64],
223 low: &[f64],
224 close: &[f64],
225 ) -> Result<EvasiveSuperTrendOutput, EvasiveSuperTrendError> {
226 evasive_supertrend_with_kernel(
227 &EvasiveSuperTrendInput::from_slices(
228 open,
229 high,
230 low,
231 close,
232 EvasiveSuperTrendParams {
233 atr_length: self.atr_length,
234 base_multiplier: self.base_multiplier,
235 noise_threshold: self.noise_threshold,
236 expansion_alpha: self.expansion_alpha,
237 },
238 ),
239 self.kernel,
240 )
241 }
242
243 #[inline(always)]
244 pub fn into_stream(self) -> Result<EvasiveSuperTrendStream, EvasiveSuperTrendError> {
245 EvasiveSuperTrendStream::try_new(EvasiveSuperTrendParams {
246 atr_length: self.atr_length,
247 base_multiplier: self.base_multiplier,
248 noise_threshold: self.noise_threshold,
249 expansion_alpha: self.expansion_alpha,
250 })
251 }
252}
253
254#[derive(Debug, Error)]
255pub enum EvasiveSuperTrendError {
256 #[error("evasive_supertrend: Input data slice is empty.")]
257 EmptyInputData,
258 #[error(
259 "evasive_supertrend: Input length mismatch: open = {open_len}, high = {high_len}, low = {low_len}, close = {close_len}"
260 )]
261 InputLengthMismatch {
262 open_len: usize,
263 high_len: usize,
264 low_len: usize,
265 close_len: usize,
266 },
267 #[error("evasive_supertrend: All values are NaN.")]
268 AllValuesNaN,
269 #[error("evasive_supertrend: Invalid atr_length: {atr_length}")]
270 InvalidAtrLength { atr_length: usize },
271 #[error("evasive_supertrend: Invalid base_multiplier: {base_multiplier}")]
272 InvalidBaseMultiplier { base_multiplier: f64 },
273 #[error("evasive_supertrend: Invalid noise_threshold: {noise_threshold}")]
274 InvalidNoiseThreshold { noise_threshold: f64 },
275 #[error("evasive_supertrend: Invalid expansion_alpha: {expansion_alpha}")]
276 InvalidExpansionAlpha { expansion_alpha: f64 },
277 #[error("evasive_supertrend: Not enough valid data: needed = {needed}, valid = {valid}")]
278 NotEnoughValidData { needed: usize, valid: usize },
279 #[error("evasive_supertrend: Output length mismatch: expected = {expected}, got = {got}")]
280 OutputLengthMismatch { expected: usize, got: usize },
281 #[error(
282 "evasive_supertrend: Output length mismatch: dst = {dst_len}, expected = {expected_len}"
283 )]
284 MismatchedOutputLen { dst_len: usize, expected_len: usize },
285 #[error("evasive_supertrend: Invalid range: start={start}, end={end}, step={step}")]
286 InvalidRange {
287 start: String,
288 end: String,
289 step: String,
290 },
291 #[error("evasive_supertrend: Invalid kernel for batch: {0:?}")]
292 InvalidKernelForBatch(Kernel),
293 #[error("evasive_supertrend: Invalid input: {msg}")]
294 InvalidInput { msg: String },
295}
296
297#[derive(Debug, Clone, Copy)]
298struct AtrTracker {
299 period: usize,
300 count: usize,
301 tr_sum: f64,
302 prev_close: Option<f64>,
303 atr: f64,
304}
305
306impl AtrTracker {
307 #[inline(always)]
308 fn new(period: usize) -> Self {
309 Self {
310 period,
311 count: 0,
312 tr_sum: 0.0,
313 prev_close: None,
314 atr: f64::NAN,
315 }
316 }
317
318 #[inline(always)]
319 fn reset(&mut self) {
320 self.count = 0;
321 self.tr_sum = 0.0;
322 self.prev_close = None;
323 self.atr = f64::NAN;
324 }
325
326 #[inline(always)]
327 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
328 let tr = match self.prev_close {
329 Some(prev_close) => {
330 let hl = high - low;
331 let hc = (high - prev_close).abs();
332 let lc = (low - prev_close).abs();
333 hl.max(hc).max(lc)
334 }
335 None => high - low,
336 };
337 self.prev_close = Some(close);
338
339 if self.count < self.period {
340 self.count += 1;
341 self.tr_sum += tr;
342 if self.count == self.period {
343 self.atr = self.tr_sum / self.period as f64;
344 Some(self.atr)
345 } else {
346 None
347 }
348 } else {
349 self.atr = ((self.atr * (self.period as f64 - 1.0)) + tr) / self.period as f64;
350 Some(self.atr)
351 }
352 }
353}
354
355#[inline(always)]
356fn is_valid_ohlc(open: f64, high: f64, low: f64, close: f64) -> bool {
357 open.is_finite() && high.is_finite() && low.is_finite() && close.is_finite()
358}
359
360#[inline(always)]
361fn longest_valid_run(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> usize {
362 let mut best = 0usize;
363 let mut cur = 0usize;
364 for (((&o, &h), &l), &c) in open
365 .iter()
366 .zip(high.iter())
367 .zip(low.iter())
368 .zip(close.iter())
369 {
370 if is_valid_ohlc(o, h, l, c) {
371 cur += 1;
372 best = best.max(cur);
373 } else {
374 cur = 0;
375 }
376 }
377 best
378}
379
380#[inline(always)]
381fn input_slices<'a>(
382 input: &'a EvasiveSuperTrendInput<'a>,
383) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), EvasiveSuperTrendError> {
384 match &input.data {
385 EvasiveSuperTrendData::Candles { candles } => Ok((
386 candles.open.as_slice(),
387 candles.high.as_slice(),
388 candles.low.as_slice(),
389 candles.close.as_slice(),
390 )),
391 EvasiveSuperTrendData::Slices {
392 open,
393 high,
394 low,
395 close,
396 } => Ok((open, high, low, close)),
397 }
398}
399
400#[inline(always)]
401fn validate_params_only(
402 atr_length: usize,
403 base_multiplier: f64,
404 noise_threshold: f64,
405 expansion_alpha: f64,
406) -> Result<(), EvasiveSuperTrendError> {
407 if atr_length == 0 {
408 return Err(EvasiveSuperTrendError::InvalidAtrLength { atr_length });
409 }
410 if !base_multiplier.is_finite() || base_multiplier < 0.1 {
411 return Err(EvasiveSuperTrendError::InvalidBaseMultiplier { base_multiplier });
412 }
413 if !noise_threshold.is_finite() || noise_threshold < 0.1 {
414 return Err(EvasiveSuperTrendError::InvalidNoiseThreshold { noise_threshold });
415 }
416 if !expansion_alpha.is_finite() || expansion_alpha < 0.0 {
417 return Err(EvasiveSuperTrendError::InvalidExpansionAlpha { expansion_alpha });
418 }
419 Ok(())
420}
421
422#[inline(always)]
423fn validate_common(
424 open: &[f64],
425 high: &[f64],
426 low: &[f64],
427 close: &[f64],
428 atr_length: usize,
429 base_multiplier: f64,
430 noise_threshold: f64,
431 expansion_alpha: f64,
432) -> Result<(), EvasiveSuperTrendError> {
433 if open.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
434 return Err(EvasiveSuperTrendError::EmptyInputData);
435 }
436 if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
437 return Err(EvasiveSuperTrendError::InputLengthMismatch {
438 open_len: open.len(),
439 high_len: high.len(),
440 low_len: low.len(),
441 close_len: close.len(),
442 });
443 }
444 validate_params_only(
445 atr_length,
446 base_multiplier,
447 noise_threshold,
448 expansion_alpha,
449 )?;
450 let longest = longest_valid_run(open, high, low, close);
451 if longest == 0 {
452 return Err(EvasiveSuperTrendError::AllValuesNaN);
453 }
454 if longest < atr_length {
455 return Err(EvasiveSuperTrendError::NotEnoughValidData {
456 needed: atr_length,
457 valid: longest,
458 });
459 }
460 Ok(())
461}
462
463#[inline(always)]
464fn compute_point(
465 tracker: &mut AtrTracker,
466 trend: &mut i8,
467 band: &mut f64,
468 high: f64,
469 low: f64,
470 close: f64,
471 base_multiplier: f64,
472 noise_threshold: f64,
473 expansion_alpha: f64,
474) -> Option<(f64, f64, f64, f64)> {
475 let atr = tracker.update(high, low, close)?;
476 let src = (high + low) * 0.5;
477 let upper_base = src + base_multiplier * atr;
478 let lower_base = src - base_multiplier * atr;
479 let prev_band = if band.is_nan() {
480 if *trend == 1 {
481 lower_base
482 } else {
483 upper_base
484 }
485 } else {
486 *band
487 };
488 let is_noisy = (close - prev_band).abs() < atr * noise_threshold;
489 let prev_trend = *trend;
490 let mut next_band;
491
492 if prev_trend == 1 {
493 next_band = if is_noisy {
494 prev_band - atr * expansion_alpha
495 } else {
496 lower_base.max(prev_band)
497 };
498 if close < next_band {
499 *trend = -1;
500 next_band = upper_base;
501 }
502 } else {
503 next_band = if is_noisy {
504 prev_band + atr * expansion_alpha
505 } else {
506 upper_base.min(prev_band)
507 };
508 if close > next_band {
509 *trend = 1;
510 next_band = lower_base;
511 }
512 }
513
514 *band = next_band;
515 Some((
516 next_band,
517 *trend as f64,
518 if is_noisy { 1.0 } else { 0.0 },
519 if *trend != prev_trend { 1.0 } else { 0.0 },
520 ))
521}
522
523fn compute_row(
524 open: &[f64],
525 high: &[f64],
526 low: &[f64],
527 close: &[f64],
528 atr_length: usize,
529 base_multiplier: f64,
530 noise_threshold: f64,
531 expansion_alpha: f64,
532 band_out: &mut [f64],
533 state_out: &mut [f64],
534 noisy_out: &mut [f64],
535 changed_out: &mut [f64],
536) {
537 let mut tracker = AtrTracker::new(atr_length);
538 let mut trend = 1i8;
539 let mut band = f64::NAN;
540
541 for i in 0..close.len() {
542 if !is_valid_ohlc(open[i], high[i], low[i], close[i]) {
543 tracker.reset();
544 trend = 1;
545 band = f64::NAN;
546 continue;
547 }
548
549 if let Some((band_value, state_value, noisy_value, changed_value)) = compute_point(
550 &mut tracker,
551 &mut trend,
552 &mut band,
553 high[i],
554 low[i],
555 close[i],
556 base_multiplier,
557 noise_threshold,
558 expansion_alpha,
559 ) {
560 band_out[i] = band_value;
561 state_out[i] = state_value;
562 noisy_out[i] = noisy_value;
563 changed_out[i] = changed_value;
564 }
565 }
566}
567
568#[inline]
569pub fn evasive_supertrend(
570 input: &EvasiveSuperTrendInput,
571) -> Result<EvasiveSuperTrendOutput, EvasiveSuperTrendError> {
572 evasive_supertrend_with_kernel(input, Kernel::Auto)
573}
574
575pub fn evasive_supertrend_with_kernel(
576 input: &EvasiveSuperTrendInput,
577 kernel: Kernel,
578) -> Result<EvasiveSuperTrendOutput, EvasiveSuperTrendError> {
579 let (open, high, low, close) = input_slices(input)?;
580 let atr_length = input.get_atr_length();
581 let base_multiplier = input.get_base_multiplier();
582 let noise_threshold = input.get_noise_threshold();
583 let expansion_alpha = input.get_expansion_alpha();
584 validate_common(
585 open,
586 high,
587 low,
588 close,
589 atr_length,
590 base_multiplier,
591 noise_threshold,
592 expansion_alpha,
593 )?;
594
595 let mut band = alloc_with_nan_prefix(close.len(), 0);
596 let mut state = alloc_with_nan_prefix(close.len(), 0);
597 let mut noisy = alloc_with_nan_prefix(close.len(), 0);
598 let mut changed = alloc_with_nan_prefix(close.len(), 0);
599 band.fill(f64::NAN);
600 state.fill(f64::NAN);
601 noisy.fill(f64::NAN);
602 changed.fill(f64::NAN);
603
604 let _chosen = match kernel {
605 Kernel::Auto => detect_best_kernel(),
606 other => other,
607 };
608
609 compute_row(
610 open,
611 high,
612 low,
613 close,
614 atr_length,
615 base_multiplier,
616 noise_threshold,
617 expansion_alpha,
618 &mut band,
619 &mut state,
620 &mut noisy,
621 &mut changed,
622 );
623
624 Ok(EvasiveSuperTrendOutput {
625 band,
626 state,
627 noisy,
628 changed,
629 })
630}
631
632pub fn evasive_supertrend_into_slice(
633 out_band: &mut [f64],
634 out_state: &mut [f64],
635 out_noisy: &mut [f64],
636 out_changed: &mut [f64],
637 input: &EvasiveSuperTrendInput,
638 kernel: Kernel,
639) -> Result<(), EvasiveSuperTrendError> {
640 let (open, high, low, close) = input_slices(input)?;
641 let atr_length = input.get_atr_length();
642 let base_multiplier = input.get_base_multiplier();
643 let noise_threshold = input.get_noise_threshold();
644 let expansion_alpha = input.get_expansion_alpha();
645 validate_common(
646 open,
647 high,
648 low,
649 close,
650 atr_length,
651 base_multiplier,
652 noise_threshold,
653 expansion_alpha,
654 )?;
655
656 if out_band.len() != close.len() {
657 return Err(EvasiveSuperTrendError::OutputLengthMismatch {
658 expected: close.len(),
659 got: out_band.len(),
660 });
661 }
662 if out_state.len() != close.len() {
663 return Err(EvasiveSuperTrendError::OutputLengthMismatch {
664 expected: close.len(),
665 got: out_state.len(),
666 });
667 }
668 if out_noisy.len() != close.len() {
669 return Err(EvasiveSuperTrendError::OutputLengthMismatch {
670 expected: close.len(),
671 got: out_noisy.len(),
672 });
673 }
674 if out_changed.len() != close.len() {
675 return Err(EvasiveSuperTrendError::OutputLengthMismatch {
676 expected: close.len(),
677 got: out_changed.len(),
678 });
679 }
680
681 let _chosen = match kernel {
682 Kernel::Auto => detect_best_kernel(),
683 other => other,
684 };
685
686 out_band.fill(f64::NAN);
687 out_state.fill(f64::NAN);
688 out_noisy.fill(f64::NAN);
689 out_changed.fill(f64::NAN);
690 compute_row(
691 open,
692 high,
693 low,
694 close,
695 atr_length,
696 base_multiplier,
697 noise_threshold,
698 expansion_alpha,
699 out_band,
700 out_state,
701 out_noisy,
702 out_changed,
703 );
704 Ok(())
705}
706
707#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
708pub fn evasive_supertrend_into(
709 input: &EvasiveSuperTrendInput,
710 out_band: &mut [f64],
711 out_state: &mut [f64],
712 out_noisy: &mut [f64],
713 out_changed: &mut [f64],
714) -> Result<(), EvasiveSuperTrendError> {
715 evasive_supertrend_into_slice(
716 out_band,
717 out_state,
718 out_noisy,
719 out_changed,
720 input,
721 Kernel::Auto,
722 )
723}
724
725#[derive(Debug, Clone, Copy)]
726pub struct EvasiveSuperTrendBatchRange {
727 pub atr_length: (usize, usize, usize),
728 pub base_multiplier: (f64, f64, f64),
729 pub noise_threshold: (f64, f64, f64),
730 pub expansion_alpha: (f64, f64, f64),
731}
732
733impl Default for EvasiveSuperTrendBatchRange {
734 fn default() -> Self {
735 Self {
736 atr_length: (DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0),
737 base_multiplier: (DEFAULT_BASE_MULTIPLIER, DEFAULT_BASE_MULTIPLIER, 0.0),
738 noise_threshold: (DEFAULT_NOISE_THRESHOLD, DEFAULT_NOISE_THRESHOLD, 0.0),
739 expansion_alpha: (DEFAULT_EXPANSION_ALPHA, DEFAULT_EXPANSION_ALPHA, 0.0),
740 }
741 }
742}
743
744#[derive(Debug, Clone)]
745pub struct EvasiveSuperTrendBatchOutput {
746 pub band: Vec<f64>,
747 pub state: Vec<f64>,
748 pub noisy: Vec<f64>,
749 pub changed: Vec<f64>,
750 pub combos: Vec<EvasiveSuperTrendParams>,
751 pub rows: usize,
752 pub cols: usize,
753}
754
755#[derive(Debug, Clone, Copy)]
756pub struct EvasiveSuperTrendBatchBuilder {
757 range: EvasiveSuperTrendBatchRange,
758 kernel: Kernel,
759}
760
761impl Default for EvasiveSuperTrendBatchBuilder {
762 fn default() -> Self {
763 Self {
764 range: EvasiveSuperTrendBatchRange::default(),
765 kernel: Kernel::Auto,
766 }
767 }
768}
769
770impl EvasiveSuperTrendBatchBuilder {
771 #[inline(always)]
772 pub fn new() -> Self {
773 Self::default()
774 }
775
776 #[inline(always)]
777 pub fn kernel(mut self, value: Kernel) -> Self {
778 self.kernel = value;
779 self
780 }
781
782 #[inline(always)]
783 pub fn atr_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
784 self.range.atr_length = (start, end, step);
785 self
786 }
787
788 #[inline(always)]
789 pub fn atr_length_static(mut self, value: usize) -> Self {
790 self.range.atr_length = (value, value, 0);
791 self
792 }
793
794 #[inline(always)]
795 pub fn base_multiplier_range(mut self, start: f64, end: f64, step: f64) -> Self {
796 self.range.base_multiplier = (start, end, step);
797 self
798 }
799
800 #[inline(always)]
801 pub fn base_multiplier_static(mut self, value: f64) -> Self {
802 self.range.base_multiplier = (value, value, 0.0);
803 self
804 }
805
806 #[inline(always)]
807 pub fn noise_threshold_range(mut self, start: f64, end: f64, step: f64) -> Self {
808 self.range.noise_threshold = (start, end, step);
809 self
810 }
811
812 #[inline(always)]
813 pub fn noise_threshold_static(mut self, value: f64) -> Self {
814 self.range.noise_threshold = (value, value, 0.0);
815 self
816 }
817
818 #[inline(always)]
819 pub fn expansion_alpha_range(mut self, start: f64, end: f64, step: f64) -> Self {
820 self.range.expansion_alpha = (start, end, step);
821 self
822 }
823
824 #[inline(always)]
825 pub fn expansion_alpha_static(mut self, value: f64) -> Self {
826 self.range.expansion_alpha = (value, value, 0.0);
827 self
828 }
829
830 #[inline(always)]
831 pub fn apply_slices(
832 self,
833 open: &[f64],
834 high: &[f64],
835 low: &[f64],
836 close: &[f64],
837 ) -> Result<EvasiveSuperTrendBatchOutput, EvasiveSuperTrendError> {
838 evasive_supertrend_batch_with_kernel(open, high, low, close, &self.range, self.kernel)
839 }
840
841 #[inline(always)]
842 pub fn apply_candles(
843 self,
844 candles: &Candles,
845 ) -> Result<EvasiveSuperTrendBatchOutput, EvasiveSuperTrendError> {
846 evasive_supertrend_batch_with_kernel(
847 candles.open.as_slice(),
848 candles.high.as_slice(),
849 candles.low.as_slice(),
850 candles.close.as_slice(),
851 &self.range,
852 self.kernel,
853 )
854 }
855}
856
857#[inline(always)]
858fn expand_usize_range(
859 field: &'static str,
860 start: usize,
861 end: usize,
862 step: usize,
863) -> Result<Vec<usize>, EvasiveSuperTrendError> {
864 if start == 0 || end == 0 {
865 return Err(EvasiveSuperTrendError::InvalidRange {
866 start: start.to_string(),
867 end: end.to_string(),
868 step: step.to_string(),
869 });
870 }
871 if step == 0 {
872 return Ok(vec![start]);
873 }
874 if start > end {
875 return Err(EvasiveSuperTrendError::InvalidRange {
876 start: start.to_string(),
877 end: end.to_string(),
878 step: step.to_string(),
879 });
880 }
881 let mut out = Vec::new();
882 let mut current = start;
883 loop {
884 out.push(current);
885 if current >= end {
886 break;
887 }
888 let next = current.saturating_add(step);
889 if next <= current {
890 return Err(EvasiveSuperTrendError::InvalidRange {
891 start: field.to_string(),
892 end: end.to_string(),
893 step: step.to_string(),
894 });
895 }
896 current = next.min(end);
897 if current == *out.last().unwrap() {
898 break;
899 }
900 }
901 Ok(out)
902}
903
904#[inline(always)]
905fn expand_f64_range(
906 field: &'static str,
907 start: f64,
908 end: f64,
909 step: f64,
910) -> Result<Vec<f64>, EvasiveSuperTrendError> {
911 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
912 return Err(EvasiveSuperTrendError::InvalidRange {
913 start: start.to_string(),
914 end: end.to_string(),
915 step: step.to_string(),
916 });
917 }
918 if step == 0.0 {
919 return Ok(vec![start]);
920 }
921 if start > end || step < 0.0 {
922 return Err(EvasiveSuperTrendError::InvalidRange {
923 start: start.to_string(),
924 end: end.to_string(),
925 step: step.to_string(),
926 });
927 }
928 let mut out = Vec::new();
929 let mut current = start;
930 loop {
931 out.push(current);
932 if current >= end || (end - current).abs() <= 1e-12 {
933 break;
934 }
935 let next = current + step;
936 if next <= current {
937 return Err(EvasiveSuperTrendError::InvalidRange {
938 start: field.to_string(),
939 end: end.to_string(),
940 step: step.to_string(),
941 });
942 }
943 current = if next > end { end } else { next };
944 }
945 Ok(out)
946}
947
948#[inline(always)]
949fn expand_grid_checked(
950 range: &EvasiveSuperTrendBatchRange,
951) -> Result<Vec<EvasiveSuperTrendParams>, EvasiveSuperTrendError> {
952 let atr_lengths = expand_usize_range(
953 "atr_length",
954 range.atr_length.0,
955 range.atr_length.1,
956 range.atr_length.2,
957 )?;
958 let base_multipliers = expand_f64_range(
959 "base_multiplier",
960 range.base_multiplier.0,
961 range.base_multiplier.1,
962 range.base_multiplier.2,
963 )?;
964 let noise_thresholds = expand_f64_range(
965 "noise_threshold",
966 range.noise_threshold.0,
967 range.noise_threshold.1,
968 range.noise_threshold.2,
969 )?;
970 let expansion_alphas = expand_f64_range(
971 "expansion_alpha",
972 range.expansion_alpha.0,
973 range.expansion_alpha.1,
974 range.expansion_alpha.2,
975 )?;
976
977 let mut out = Vec::new();
978 for &atr_length in &atr_lengths {
979 for &base_multiplier in &base_multipliers {
980 for &noise_threshold in &noise_thresholds {
981 for &expansion_alpha in &expansion_alphas {
982 out.push(EvasiveSuperTrendParams {
983 atr_length: Some(atr_length),
984 base_multiplier: Some(base_multiplier),
985 noise_threshold: Some(noise_threshold),
986 expansion_alpha: Some(expansion_alpha),
987 });
988 }
989 }
990 }
991 }
992 Ok(out)
993}
994
995pub fn expand_grid_evasive_supertrend(
996 range: &EvasiveSuperTrendBatchRange,
997) -> Vec<EvasiveSuperTrendParams> {
998 expand_grid_checked(range).unwrap_or_default()
999}
1000
1001pub fn evasive_supertrend_batch_with_kernel(
1002 open: &[f64],
1003 high: &[f64],
1004 low: &[f64],
1005 close: &[f64],
1006 sweep: &EvasiveSuperTrendBatchRange,
1007 kernel: Kernel,
1008) -> Result<EvasiveSuperTrendBatchOutput, EvasiveSuperTrendError> {
1009 evasive_supertrend_batch_inner(open, high, low, close, sweep, kernel, true)
1010}
1011
1012pub fn evasive_supertrend_batch_slice(
1013 open: &[f64],
1014 high: &[f64],
1015 low: &[f64],
1016 close: &[f64],
1017 sweep: &EvasiveSuperTrendBatchRange,
1018 kernel: Kernel,
1019) -> Result<EvasiveSuperTrendBatchOutput, EvasiveSuperTrendError> {
1020 evasive_supertrend_batch_inner(open, high, low, close, sweep, kernel, false)
1021}
1022
1023pub fn evasive_supertrend_batch_par_slice(
1024 open: &[f64],
1025 high: &[f64],
1026 low: &[f64],
1027 close: &[f64],
1028 sweep: &EvasiveSuperTrendBatchRange,
1029 kernel: Kernel,
1030) -> Result<EvasiveSuperTrendBatchOutput, EvasiveSuperTrendError> {
1031 evasive_supertrend_batch_inner(open, high, low, close, sweep, kernel, true)
1032}
1033
1034fn evasive_supertrend_batch_inner(
1035 open: &[f64],
1036 high: &[f64],
1037 low: &[f64],
1038 close: &[f64],
1039 sweep: &EvasiveSuperTrendBatchRange,
1040 kernel: Kernel,
1041 parallel: bool,
1042) -> Result<EvasiveSuperTrendBatchOutput, EvasiveSuperTrendError> {
1043 match kernel {
1044 Kernel::Auto
1045 | Kernel::Scalar
1046 | Kernel::ScalarBatch
1047 | Kernel::Avx2
1048 | Kernel::Avx2Batch
1049 | Kernel::Avx512
1050 | Kernel::Avx512Batch => {}
1051 other => return Err(EvasiveSuperTrendError::InvalidKernelForBatch(other)),
1052 }
1053
1054 let combos = expand_grid_checked(sweep)?;
1055 let max_atr_length = combos
1056 .iter()
1057 .map(|params| params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH))
1058 .max()
1059 .unwrap_or(0);
1060 let max_base_multiplier = combos
1061 .iter()
1062 .map(|params| params.base_multiplier.unwrap_or(DEFAULT_BASE_MULTIPLIER))
1063 .fold(0.0_f64, f64::max);
1064 let max_noise_threshold = combos
1065 .iter()
1066 .map(|params| params.noise_threshold.unwrap_or(DEFAULT_NOISE_THRESHOLD))
1067 .fold(0.0_f64, f64::max);
1068 let max_expansion_alpha = combos
1069 .iter()
1070 .map(|params| params.expansion_alpha.unwrap_or(DEFAULT_EXPANSION_ALPHA))
1071 .fold(0.0_f64, f64::max);
1072 validate_common(
1073 open,
1074 high,
1075 low,
1076 close,
1077 max_atr_length,
1078 max_base_multiplier,
1079 max_noise_threshold,
1080 max_expansion_alpha,
1081 )?;
1082
1083 let rows = combos.len();
1084 let cols = close.len();
1085 let total = rows
1086 .checked_mul(cols)
1087 .ok_or_else(|| EvasiveSuperTrendError::InvalidInput {
1088 msg: "evasive_supertrend: rows*cols overflow in batch".to_string(),
1089 })?;
1090
1091 let mut band = vec![f64::NAN; total];
1092 let mut state = vec![f64::NAN; total];
1093 let mut noisy = vec![f64::NAN; total];
1094 let mut changed = vec![f64::NAN; total];
1095 evasive_supertrend_batch_inner_into(
1096 open,
1097 high,
1098 low,
1099 close,
1100 sweep,
1101 kernel,
1102 parallel,
1103 &mut band,
1104 &mut state,
1105 &mut noisy,
1106 &mut changed,
1107 )?;
1108
1109 Ok(EvasiveSuperTrendBatchOutput {
1110 band,
1111 state,
1112 noisy,
1113 changed,
1114 combos,
1115 rows,
1116 cols,
1117 })
1118}
1119
1120pub fn evasive_supertrend_batch_inner_into(
1121 open: &[f64],
1122 high: &[f64],
1123 low: &[f64],
1124 close: &[f64],
1125 sweep: &EvasiveSuperTrendBatchRange,
1126 kernel: Kernel,
1127 parallel: bool,
1128 out_band: &mut [f64],
1129 out_state: &mut [f64],
1130 out_noisy: &mut [f64],
1131 out_changed: &mut [f64],
1132) -> Result<Vec<EvasiveSuperTrendParams>, EvasiveSuperTrendError> {
1133 match kernel {
1134 Kernel::Auto
1135 | Kernel::Scalar
1136 | Kernel::ScalarBatch
1137 | Kernel::Avx2
1138 | Kernel::Avx2Batch
1139 | Kernel::Avx512
1140 | Kernel::Avx512Batch => {}
1141 other => return Err(EvasiveSuperTrendError::InvalidKernelForBatch(other)),
1142 }
1143
1144 let combos = expand_grid_checked(sweep)?;
1145 let max_atr_length = combos
1146 .iter()
1147 .map(|params| params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH))
1148 .max()
1149 .unwrap_or(0);
1150 let max_base_multiplier = combos
1151 .iter()
1152 .map(|params| params.base_multiplier.unwrap_or(DEFAULT_BASE_MULTIPLIER))
1153 .fold(0.0_f64, f64::max);
1154 let max_noise_threshold = combos
1155 .iter()
1156 .map(|params| params.noise_threshold.unwrap_or(DEFAULT_NOISE_THRESHOLD))
1157 .fold(0.0_f64, f64::max);
1158 let max_expansion_alpha = combos
1159 .iter()
1160 .map(|params| params.expansion_alpha.unwrap_or(DEFAULT_EXPANSION_ALPHA))
1161 .fold(0.0_f64, f64::max);
1162 validate_common(
1163 open,
1164 high,
1165 low,
1166 close,
1167 max_atr_length,
1168 max_base_multiplier,
1169 max_noise_threshold,
1170 max_expansion_alpha,
1171 )?;
1172
1173 let cols = close.len();
1174 let total =
1175 combos
1176 .len()
1177 .checked_mul(cols)
1178 .ok_or_else(|| EvasiveSuperTrendError::InvalidInput {
1179 msg: "evasive_supertrend: rows*cols overflow in batch_into".to_string(),
1180 })?;
1181 if out_band.len() != total {
1182 return Err(EvasiveSuperTrendError::MismatchedOutputLen {
1183 dst_len: out_band.len(),
1184 expected_len: total,
1185 });
1186 }
1187 if out_state.len() != total {
1188 return Err(EvasiveSuperTrendError::MismatchedOutputLen {
1189 dst_len: out_state.len(),
1190 expected_len: total,
1191 });
1192 }
1193 if out_noisy.len() != total {
1194 return Err(EvasiveSuperTrendError::MismatchedOutputLen {
1195 dst_len: out_noisy.len(),
1196 expected_len: total,
1197 });
1198 }
1199 if out_changed.len() != total {
1200 return Err(EvasiveSuperTrendError::MismatchedOutputLen {
1201 dst_len: out_changed.len(),
1202 expected_len: total,
1203 });
1204 }
1205
1206 let _chosen = match kernel {
1207 Kernel::Auto => detect_best_batch_kernel(),
1208 other => other,
1209 };
1210
1211 let worker = |row: usize,
1212 band_row: &mut [f64],
1213 state_row: &mut [f64],
1214 noisy_row: &mut [f64],
1215 changed_row: &mut [f64]| {
1216 band_row.fill(f64::NAN);
1217 state_row.fill(f64::NAN);
1218 noisy_row.fill(f64::NAN);
1219 changed_row.fill(f64::NAN);
1220 let params = &combos[row];
1221 compute_row(
1222 open,
1223 high,
1224 low,
1225 close,
1226 params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH),
1227 params.base_multiplier.unwrap_or(DEFAULT_BASE_MULTIPLIER),
1228 params.noise_threshold.unwrap_or(DEFAULT_NOISE_THRESHOLD),
1229 params.expansion_alpha.unwrap_or(DEFAULT_EXPANSION_ALPHA),
1230 band_row,
1231 state_row,
1232 noisy_row,
1233 changed_row,
1234 );
1235 };
1236
1237 #[cfg(not(target_arch = "wasm32"))]
1238 if parallel && combos.len() > 1 {
1239 out_band
1240 .par_chunks_mut(cols)
1241 .zip(out_state.par_chunks_mut(cols))
1242 .zip(out_noisy.par_chunks_mut(cols))
1243 .zip(out_changed.par_chunks_mut(cols))
1244 .enumerate()
1245 .for_each(|(row, (((band_row, state_row), noisy_row), changed_row))| {
1246 worker(row, band_row, state_row, noisy_row, changed_row);
1247 });
1248 } else {
1249 for (row, (((band_row, state_row), noisy_row), changed_row)) in out_band
1250 .chunks_mut(cols)
1251 .zip(out_state.chunks_mut(cols))
1252 .zip(out_noisy.chunks_mut(cols))
1253 .zip(out_changed.chunks_mut(cols))
1254 .enumerate()
1255 {
1256 worker(row, band_row, state_row, noisy_row, changed_row);
1257 }
1258 }
1259
1260 #[cfg(target_arch = "wasm32")]
1261 {
1262 let _ = parallel;
1263 for (row, (((band_row, state_row), noisy_row), changed_row)) in out_band
1264 .chunks_mut(cols)
1265 .zip(out_state.chunks_mut(cols))
1266 .zip(out_noisy.chunks_mut(cols))
1267 .zip(out_changed.chunks_mut(cols))
1268 .enumerate()
1269 {
1270 worker(row, band_row, state_row, noisy_row, changed_row);
1271 }
1272 }
1273
1274 Ok(combos)
1275}
1276
1277#[derive(Debug, Clone)]
1278pub struct EvasiveSuperTrendStream {
1279 atr_length: usize,
1280 base_multiplier: f64,
1281 noise_threshold: f64,
1282 expansion_alpha: f64,
1283 tracker: AtrTracker,
1284 trend: i8,
1285 band: f64,
1286}
1287
1288impl EvasiveSuperTrendStream {
1289 pub fn try_new(params: EvasiveSuperTrendParams) -> Result<Self, EvasiveSuperTrendError> {
1290 let atr_length = params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
1291 let base_multiplier = params.base_multiplier.unwrap_or(DEFAULT_BASE_MULTIPLIER);
1292 let noise_threshold = params.noise_threshold.unwrap_or(DEFAULT_NOISE_THRESHOLD);
1293 let expansion_alpha = params.expansion_alpha.unwrap_or(DEFAULT_EXPANSION_ALPHA);
1294 validate_params_only(
1295 atr_length,
1296 base_multiplier,
1297 noise_threshold,
1298 expansion_alpha,
1299 )?;
1300 Ok(Self {
1301 atr_length,
1302 base_multiplier,
1303 noise_threshold,
1304 expansion_alpha,
1305 tracker: AtrTracker::new(atr_length),
1306 trend: 1,
1307 band: f64::NAN,
1308 })
1309 }
1310
1311 #[inline(always)]
1312 fn reset(&mut self) {
1313 self.tracker.reset();
1314 self.trend = 1;
1315 self.band = f64::NAN;
1316 }
1317
1318 pub fn update(
1319 &mut self,
1320 open: f64,
1321 high: f64,
1322 low: f64,
1323 close: f64,
1324 ) -> Option<(f64, f64, f64, f64)> {
1325 if !is_valid_ohlc(open, high, low, close) {
1326 self.reset();
1327 return None;
1328 }
1329 compute_point(
1330 &mut self.tracker,
1331 &mut self.trend,
1332 &mut self.band,
1333 high,
1334 low,
1335 close,
1336 self.base_multiplier,
1337 self.noise_threshold,
1338 self.expansion_alpha,
1339 )
1340 }
1341
1342 #[inline(always)]
1343 pub fn atr_length(&self) -> usize {
1344 self.atr_length
1345 }
1346}
1347
1348#[cfg(feature = "python")]
1349#[pyfunction(
1350 name = "evasive_supertrend",
1351 signature = (open, high, low, close, atr_length=DEFAULT_ATR_LENGTH, base_multiplier=DEFAULT_BASE_MULTIPLIER, noise_threshold=DEFAULT_NOISE_THRESHOLD, expansion_alpha=DEFAULT_EXPANSION_ALPHA, kernel=None)
1352)]
1353pub fn evasive_supertrend_py<'py>(
1354 py: Python<'py>,
1355 open: PyReadonlyArray1<'py, f64>,
1356 high: PyReadonlyArray1<'py, f64>,
1357 low: PyReadonlyArray1<'py, f64>,
1358 close: PyReadonlyArray1<'py, f64>,
1359 atr_length: usize,
1360 base_multiplier: f64,
1361 noise_threshold: f64,
1362 expansion_alpha: f64,
1363 kernel: Option<&str>,
1364) -> PyResult<(
1365 Bound<'py, PyArray1<f64>>,
1366 Bound<'py, PyArray1<f64>>,
1367 Bound<'py, PyArray1<f64>>,
1368 Bound<'py, PyArray1<f64>>,
1369)> {
1370 let open = open.as_slice()?;
1371 let high = high.as_slice()?;
1372 let low = low.as_slice()?;
1373 let close = close.as_slice()?;
1374 let kern = validate_kernel(kernel, false)?;
1375 let input = EvasiveSuperTrendInput::from_slices(
1376 open,
1377 high,
1378 low,
1379 close,
1380 EvasiveSuperTrendParams {
1381 atr_length: Some(atr_length),
1382 base_multiplier: Some(base_multiplier),
1383 noise_threshold: Some(noise_threshold),
1384 expansion_alpha: Some(expansion_alpha),
1385 },
1386 );
1387 let out = py
1388 .allow_threads(|| evasive_supertrend_with_kernel(&input, kern))
1389 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1390 Ok((
1391 out.band.into_pyarray(py),
1392 out.state.into_pyarray(py),
1393 out.noisy.into_pyarray(py),
1394 out.changed.into_pyarray(py),
1395 ))
1396}
1397
1398#[cfg(feature = "python")]
1399#[pyfunction(
1400 name = "evasive_supertrend_batch",
1401 signature = (open, high, low, close, atr_length_range=(DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0), base_multiplier_range=(DEFAULT_BASE_MULTIPLIER, DEFAULT_BASE_MULTIPLIER, 0.0), noise_threshold_range=(DEFAULT_NOISE_THRESHOLD, DEFAULT_NOISE_THRESHOLD, 0.0), expansion_alpha_range=(DEFAULT_EXPANSION_ALPHA, DEFAULT_EXPANSION_ALPHA, 0.0), kernel=None)
1402)]
1403pub fn evasive_supertrend_batch_py<'py>(
1404 py: Python<'py>,
1405 open: PyReadonlyArray1<'py, f64>,
1406 high: PyReadonlyArray1<'py, f64>,
1407 low: PyReadonlyArray1<'py, f64>,
1408 close: PyReadonlyArray1<'py, f64>,
1409 atr_length_range: (usize, usize, usize),
1410 base_multiplier_range: (f64, f64, f64),
1411 noise_threshold_range: (f64, f64, f64),
1412 expansion_alpha_range: (f64, f64, f64),
1413 kernel: Option<&str>,
1414) -> PyResult<Bound<'py, PyDict>> {
1415 let open = open.as_slice()?;
1416 let high = high.as_slice()?;
1417 let low = low.as_slice()?;
1418 let close = close.as_slice()?;
1419 let kern = validate_kernel(kernel, true)?;
1420
1421 let output = py
1422 .allow_threads(|| {
1423 evasive_supertrend_batch_with_kernel(
1424 open,
1425 high,
1426 low,
1427 close,
1428 &EvasiveSuperTrendBatchRange {
1429 atr_length: atr_length_range,
1430 base_multiplier: base_multiplier_range,
1431 noise_threshold: noise_threshold_range,
1432 expansion_alpha: expansion_alpha_range,
1433 },
1434 kern,
1435 )
1436 })
1437 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1438
1439 let dict = PyDict::new(py);
1440 dict.set_item(
1441 "band",
1442 output
1443 .band
1444 .into_pyarray(py)
1445 .reshape((output.rows, output.cols))?,
1446 )?;
1447 dict.set_item(
1448 "state",
1449 output
1450 .state
1451 .into_pyarray(py)
1452 .reshape((output.rows, output.cols))?,
1453 )?;
1454 dict.set_item(
1455 "noisy",
1456 output
1457 .noisy
1458 .into_pyarray(py)
1459 .reshape((output.rows, output.cols))?,
1460 )?;
1461 dict.set_item(
1462 "changed",
1463 output
1464 .changed
1465 .into_pyarray(py)
1466 .reshape((output.rows, output.cols))?,
1467 )?;
1468 dict.set_item(
1469 "atr_lengths",
1470 output
1471 .combos
1472 .iter()
1473 .map(|params| params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH) as u64)
1474 .collect::<Vec<_>>()
1475 .into_pyarray(py),
1476 )?;
1477 dict.set_item(
1478 "base_multipliers",
1479 output
1480 .combos
1481 .iter()
1482 .map(|params| params.base_multiplier.unwrap_or(DEFAULT_BASE_MULTIPLIER))
1483 .collect::<Vec<_>>()
1484 .into_pyarray(py),
1485 )?;
1486 dict.set_item(
1487 "noise_thresholds",
1488 output
1489 .combos
1490 .iter()
1491 .map(|params| params.noise_threshold.unwrap_or(DEFAULT_NOISE_THRESHOLD))
1492 .collect::<Vec<_>>()
1493 .into_pyarray(py),
1494 )?;
1495 dict.set_item(
1496 "expansion_alphas",
1497 output
1498 .combos
1499 .iter()
1500 .map(|params| params.expansion_alpha.unwrap_or(DEFAULT_EXPANSION_ALPHA))
1501 .collect::<Vec<_>>()
1502 .into_pyarray(py),
1503 )?;
1504 dict.set_item("rows", output.rows)?;
1505 dict.set_item("cols", output.cols)?;
1506 Ok(dict)
1507}
1508
1509#[cfg(feature = "python")]
1510#[pyclass(name = "EvasiveSuperTrendStream")]
1511pub struct EvasiveSuperTrendStreamPy {
1512 stream: EvasiveSuperTrendStream,
1513}
1514
1515#[cfg(feature = "python")]
1516#[pymethods]
1517impl EvasiveSuperTrendStreamPy {
1518 #[new]
1519 #[pyo3(signature = (atr_length=DEFAULT_ATR_LENGTH, base_multiplier=DEFAULT_BASE_MULTIPLIER, noise_threshold=DEFAULT_NOISE_THRESHOLD, expansion_alpha=DEFAULT_EXPANSION_ALPHA))]
1520 fn new(
1521 atr_length: usize,
1522 base_multiplier: f64,
1523 noise_threshold: f64,
1524 expansion_alpha: f64,
1525 ) -> PyResult<Self> {
1526 let stream = EvasiveSuperTrendStream::try_new(EvasiveSuperTrendParams {
1527 atr_length: Some(atr_length),
1528 base_multiplier: Some(base_multiplier),
1529 noise_threshold: Some(noise_threshold),
1530 expansion_alpha: Some(expansion_alpha),
1531 })
1532 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1533 Ok(Self { stream })
1534 }
1535
1536 fn update(
1537 &mut self,
1538 open: f64,
1539 high: f64,
1540 low: f64,
1541 close: f64,
1542 ) -> Option<(f64, f64, f64, f64)> {
1543 self.stream.update(open, high, low, close)
1544 }
1545}
1546
1547#[cfg(feature = "python")]
1548pub fn register_evasive_supertrend_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1549 m.add_function(wrap_pyfunction!(evasive_supertrend_py, m)?)?;
1550 m.add_function(wrap_pyfunction!(evasive_supertrend_batch_py, m)?)?;
1551 m.add_class::<EvasiveSuperTrendStreamPy>()?;
1552 Ok(())
1553}
1554
1555#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1556#[derive(Debug, Clone, Serialize, Deserialize)]
1557pub struct EvasiveSuperTrendBatchConfig {
1558 pub atr_length_range: Vec<usize>,
1559 pub base_multiplier_range: Vec<f64>,
1560 pub noise_threshold_range: Vec<f64>,
1561 pub expansion_alpha_range: Vec<f64>,
1562}
1563
1564#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1565#[wasm_bindgen(js_name = evasive_supertrend_js)]
1566pub fn evasive_supertrend_js(
1567 open: &[f64],
1568 high: &[f64],
1569 low: &[f64],
1570 close: &[f64],
1571 atr_length: usize,
1572 base_multiplier: f64,
1573 noise_threshold: f64,
1574 expansion_alpha: f64,
1575) -> Result<JsValue, JsValue> {
1576 let input = EvasiveSuperTrendInput::from_slices(
1577 open,
1578 high,
1579 low,
1580 close,
1581 EvasiveSuperTrendParams {
1582 atr_length: Some(atr_length),
1583 base_multiplier: Some(base_multiplier),
1584 noise_threshold: Some(noise_threshold),
1585 expansion_alpha: Some(expansion_alpha),
1586 },
1587 );
1588 let out = evasive_supertrend_with_kernel(&input, Kernel::Auto)
1589 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1590 let obj = js_sys::Object::new();
1591 js_sys::Reflect::set(
1592 &obj,
1593 &JsValue::from_str("band"),
1594 &serde_wasm_bindgen::to_value(&out.band).unwrap(),
1595 )?;
1596 js_sys::Reflect::set(
1597 &obj,
1598 &JsValue::from_str("state"),
1599 &serde_wasm_bindgen::to_value(&out.state).unwrap(),
1600 )?;
1601 js_sys::Reflect::set(
1602 &obj,
1603 &JsValue::from_str("noisy"),
1604 &serde_wasm_bindgen::to_value(&out.noisy).unwrap(),
1605 )?;
1606 js_sys::Reflect::set(
1607 &obj,
1608 &JsValue::from_str("changed"),
1609 &serde_wasm_bindgen::to_value(&out.changed).unwrap(),
1610 )?;
1611 Ok(obj.into())
1612}
1613
1614#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1615#[wasm_bindgen(js_name = evasive_supertrend_batch_js)]
1616pub fn evasive_supertrend_batch_js(
1617 open: &[f64],
1618 high: &[f64],
1619 low: &[f64],
1620 close: &[f64],
1621 config: JsValue,
1622) -> Result<JsValue, JsValue> {
1623 let config: EvasiveSuperTrendBatchConfig = serde_wasm_bindgen::from_value(config)
1624 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1625 if config.atr_length_range.len() != 3
1626 || config.base_multiplier_range.len() != 3
1627 || config.noise_threshold_range.len() != 3
1628 || config.expansion_alpha_range.len() != 3
1629 {
1630 return Err(JsValue::from_str(
1631 "Invalid config: every range must have exactly 3 elements [start, end, step]",
1632 ));
1633 }
1634
1635 let out = evasive_supertrend_batch_with_kernel(
1636 open,
1637 high,
1638 low,
1639 close,
1640 &EvasiveSuperTrendBatchRange {
1641 atr_length: (
1642 config.atr_length_range[0],
1643 config.atr_length_range[1],
1644 config.atr_length_range[2],
1645 ),
1646 base_multiplier: (
1647 config.base_multiplier_range[0],
1648 config.base_multiplier_range[1],
1649 config.base_multiplier_range[2],
1650 ),
1651 noise_threshold: (
1652 config.noise_threshold_range[0],
1653 config.noise_threshold_range[1],
1654 config.noise_threshold_range[2],
1655 ),
1656 expansion_alpha: (
1657 config.expansion_alpha_range[0],
1658 config.expansion_alpha_range[1],
1659 config.expansion_alpha_range[2],
1660 ),
1661 },
1662 Kernel::Auto,
1663 )
1664 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1665
1666 let obj = js_sys::Object::new();
1667 js_sys::Reflect::set(
1668 &obj,
1669 &JsValue::from_str("band"),
1670 &serde_wasm_bindgen::to_value(&out.band).unwrap(),
1671 )?;
1672 js_sys::Reflect::set(
1673 &obj,
1674 &JsValue::from_str("state"),
1675 &serde_wasm_bindgen::to_value(&out.state).unwrap(),
1676 )?;
1677 js_sys::Reflect::set(
1678 &obj,
1679 &JsValue::from_str("noisy"),
1680 &serde_wasm_bindgen::to_value(&out.noisy).unwrap(),
1681 )?;
1682 js_sys::Reflect::set(
1683 &obj,
1684 &JsValue::from_str("changed"),
1685 &serde_wasm_bindgen::to_value(&out.changed).unwrap(),
1686 )?;
1687 js_sys::Reflect::set(
1688 &obj,
1689 &JsValue::from_str("rows"),
1690 &JsValue::from_f64(out.rows as f64),
1691 )?;
1692 js_sys::Reflect::set(
1693 &obj,
1694 &JsValue::from_str("cols"),
1695 &JsValue::from_f64(out.cols as f64),
1696 )?;
1697 js_sys::Reflect::set(
1698 &obj,
1699 &JsValue::from_str("combos"),
1700 &serde_wasm_bindgen::to_value(&out.combos).unwrap(),
1701 )?;
1702 Ok(obj.into())
1703}
1704
1705#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1706#[wasm_bindgen]
1707pub fn evasive_supertrend_alloc(len: usize) -> *mut f64 {
1708 let mut vec = Vec::<f64>::with_capacity(4 * len);
1709 let ptr = vec.as_mut_ptr();
1710 std::mem::forget(vec);
1711 ptr
1712}
1713
1714#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1715#[wasm_bindgen]
1716pub fn evasive_supertrend_free(ptr: *mut f64, len: usize) {
1717 if !ptr.is_null() {
1718 unsafe {
1719 let _ = Vec::from_raw_parts(ptr, 4 * len, 4 * len);
1720 }
1721 }
1722}
1723
1724#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1725#[wasm_bindgen]
1726pub fn evasive_supertrend_into(
1727 open_ptr: *const f64,
1728 high_ptr: *const f64,
1729 low_ptr: *const f64,
1730 close_ptr: *const f64,
1731 out_ptr: *mut f64,
1732 len: usize,
1733 atr_length: usize,
1734 base_multiplier: f64,
1735 noise_threshold: f64,
1736 expansion_alpha: f64,
1737) -> Result<(), JsValue> {
1738 if open_ptr.is_null()
1739 || high_ptr.is_null()
1740 || low_ptr.is_null()
1741 || close_ptr.is_null()
1742 || out_ptr.is_null()
1743 {
1744 return Err(JsValue::from_str(
1745 "null pointer passed to evasive_supertrend_into",
1746 ));
1747 }
1748
1749 unsafe {
1750 let open = std::slice::from_raw_parts(open_ptr, len);
1751 let high = std::slice::from_raw_parts(high_ptr, len);
1752 let low = std::slice::from_raw_parts(low_ptr, len);
1753 let close = std::slice::from_raw_parts(close_ptr, len);
1754 let out = std::slice::from_raw_parts_mut(out_ptr, 4 * len);
1755 let (band, tail) = out.split_at_mut(len);
1756 let (state, tail) = tail.split_at_mut(len);
1757 let (noisy, changed) = tail.split_at_mut(len);
1758 let input = EvasiveSuperTrendInput::from_slices(
1759 open,
1760 high,
1761 low,
1762 close,
1763 EvasiveSuperTrendParams {
1764 atr_length: Some(atr_length),
1765 base_multiplier: Some(base_multiplier),
1766 noise_threshold: Some(noise_threshold),
1767 expansion_alpha: Some(expansion_alpha),
1768 },
1769 );
1770 evasive_supertrend_into_slice(band, state, noisy, changed, &input, Kernel::Auto)
1771 .map_err(|e| JsValue::from_str(&e.to_string()))
1772 }
1773}
1774
1775#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1776#[wasm_bindgen]
1777pub fn evasive_supertrend_batch_into(
1778 open_ptr: *const f64,
1779 high_ptr: *const f64,
1780 low_ptr: *const f64,
1781 close_ptr: *const f64,
1782 out_ptr: *mut f64,
1783 len: usize,
1784 atr_length_start: usize,
1785 atr_length_end: usize,
1786 atr_length_step: usize,
1787 base_multiplier_start: f64,
1788 base_multiplier_end: f64,
1789 base_multiplier_step: f64,
1790 noise_threshold_start: f64,
1791 noise_threshold_end: f64,
1792 noise_threshold_step: f64,
1793 expansion_alpha_start: f64,
1794 expansion_alpha_end: f64,
1795 expansion_alpha_step: f64,
1796) -> Result<usize, JsValue> {
1797 if open_ptr.is_null()
1798 || high_ptr.is_null()
1799 || low_ptr.is_null()
1800 || close_ptr.is_null()
1801 || out_ptr.is_null()
1802 {
1803 return Err(JsValue::from_str(
1804 "null pointer passed to evasive_supertrend_batch_into",
1805 ));
1806 }
1807
1808 let sweep = EvasiveSuperTrendBatchRange {
1809 atr_length: (atr_length_start, atr_length_end, atr_length_step),
1810 base_multiplier: (
1811 base_multiplier_start,
1812 base_multiplier_end,
1813 base_multiplier_step,
1814 ),
1815 noise_threshold: (
1816 noise_threshold_start,
1817 noise_threshold_end,
1818 noise_threshold_step,
1819 ),
1820 expansion_alpha: (
1821 expansion_alpha_start,
1822 expansion_alpha_end,
1823 expansion_alpha_step,
1824 ),
1825 };
1826 let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1827 let rows = combos.len();
1828 let split = rows
1829 .checked_mul(len)
1830 .ok_or_else(|| JsValue::from_str("rows*cols overflow in evasive_supertrend_batch_into"))?;
1831 let total = split.checked_mul(4).ok_or_else(|| {
1832 JsValue::from_str("4*rows*cols overflow in evasive_supertrend_batch_into")
1833 })?;
1834
1835 unsafe {
1836 let open = std::slice::from_raw_parts(open_ptr, len);
1837 let high = std::slice::from_raw_parts(high_ptr, len);
1838 let low = std::slice::from_raw_parts(low_ptr, len);
1839 let close = std::slice::from_raw_parts(close_ptr, len);
1840 let out = std::slice::from_raw_parts_mut(out_ptr, total);
1841 let (band, tail) = out.split_at_mut(split);
1842 let (state, tail) = tail.split_at_mut(split);
1843 let (noisy, changed) = tail.split_at_mut(split);
1844 evasive_supertrend_batch_inner_into(
1845 open,
1846 high,
1847 low,
1848 close,
1849 &sweep,
1850 Kernel::Auto,
1851 false,
1852 band,
1853 state,
1854 noisy,
1855 changed,
1856 )
1857 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1858 }
1859
1860 Ok(rows)
1861}
1862
1863#[cfg(test)]
1864mod tests {
1865 use super::*;
1866 use crate::indicators::dispatch::{
1867 compute_cpu, IndicatorComputeRequest, IndicatorDataRef, IndicatorSeries, ParamKV,
1868 ParamValue,
1869 };
1870
1871 fn assert_vec_eq_nan(actual: &[f64], expected: &[f64]) {
1872 assert_eq!(actual.len(), expected.len());
1873 for (idx, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
1874 if a.is_nan() && e.is_nan() {
1875 continue;
1876 }
1877 assert!(
1878 (a - e).abs() <= 1e-12,
1879 "mismatch at {idx}: actual={a}, expected={e}"
1880 );
1881 }
1882 }
1883
1884 fn sample_ohlc(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1885 let mut open = Vec::with_capacity(len);
1886 let mut high = Vec::with_capacity(len);
1887 let mut low = Vec::with_capacity(len);
1888 let mut close = Vec::with_capacity(len);
1889 for i in 0..len {
1890 let base = 100.0 + ((i as f64) * 0.19).sin() * 2.5 + (i as f64) * 0.03;
1891 let body = ((i as f64) * 0.13).cos() * 0.4;
1892 let o = base - body;
1893 let c = base + body;
1894 let h = o.max(c) + 0.8 + ((i as f64) * 0.07).sin().abs();
1895 let l = o.min(c) - 0.8 - ((i as f64) * 0.11).cos().abs() * 0.6;
1896 open.push(o);
1897 high.push(h);
1898 low.push(l);
1899 close.push(c);
1900 }
1901 (open, high, low, close)
1902 }
1903
1904 #[test]
1905 fn evasive_supertrend_output_contract() -> Result<(), Box<dyn Error>> {
1906 let (open, high, low, close) = sample_ohlc(256);
1907 let out = evasive_supertrend_with_kernel(
1908 &EvasiveSuperTrendInput::from_slices(
1909 &open,
1910 &high,
1911 &low,
1912 &close,
1913 EvasiveSuperTrendParams::default(),
1914 ),
1915 Kernel::Scalar,
1916 )?;
1917 assert_eq!(out.band.len(), close.len());
1918 assert_eq!(out.state.len(), close.len());
1919 assert_eq!(out.noisy.len(), close.len());
1920 assert_eq!(out.changed.len(), close.len());
1921 assert!(out.band.iter().any(|v| v.is_finite()));
1922 Ok(())
1923 }
1924
1925 #[test]
1926 fn evasive_supertrend_exact_small_case() -> Result<(), Box<dyn Error>> {
1927 let open = vec![10.0, 11.0, 12.0, 13.0];
1928 let high = vec![11.0, 12.0, 13.0, 14.0];
1929 let low = vec![9.0, 10.0, 11.0, 12.0];
1930 let close = vec![10.0, 11.0, 12.0, 13.0];
1931 let out = evasive_supertrend_with_kernel(
1932 &EvasiveSuperTrendInput::from_slices(
1933 &open,
1934 &high,
1935 &low,
1936 &close,
1937 EvasiveSuperTrendParams {
1938 atr_length: Some(2),
1939 base_multiplier: Some(1.0),
1940 noise_threshold: Some(1.0),
1941 expansion_alpha: Some(0.5),
1942 },
1943 ),
1944 Kernel::Scalar,
1945 )?;
1946 assert_vec_eq_nan(&out.band, &[f64::NAN, 9.0, 10.0, 11.0]);
1947 assert_vec_eq_nan(&out.state, &[f64::NAN, 1.0, 1.0, 1.0]);
1948 assert_vec_eq_nan(&out.noisy, &[f64::NAN, 0.0, 0.0, 0.0]);
1949 assert_vec_eq_nan(&out.changed, &[f64::NAN, 0.0, 0.0, 0.0]);
1950 Ok(())
1951 }
1952
1953 #[test]
1954 fn evasive_supertrend_into_matches_api() -> Result<(), Box<dyn Error>> {
1955 let (open, high, low, close) = sample_ohlc(200);
1956 let input = EvasiveSuperTrendInput::from_slices(
1957 &open,
1958 &high,
1959 &low,
1960 &close,
1961 EvasiveSuperTrendParams::default(),
1962 );
1963 let baseline = evasive_supertrend_with_kernel(&input, Kernel::Scalar)?;
1964 let mut band = vec![0.0; close.len()];
1965 let mut state = vec![0.0; close.len()];
1966 let mut noisy = vec![0.0; close.len()];
1967 let mut changed = vec![0.0; close.len()];
1968 evasive_supertrend_into(&input, &mut band, &mut state, &mut noisy, &mut changed)?;
1969 assert_vec_eq_nan(&band, &baseline.band);
1970 assert_vec_eq_nan(&state, &baseline.state);
1971 assert_vec_eq_nan(&noisy, &baseline.noisy);
1972 assert_vec_eq_nan(&changed, &baseline.changed);
1973 Ok(())
1974 }
1975
1976 #[test]
1977 fn evasive_supertrend_batch_single_matches_single() -> Result<(), Box<dyn Error>> {
1978 let (open, high, low, close) = sample_ohlc(180);
1979 let single = evasive_supertrend_with_kernel(
1980 &EvasiveSuperTrendInput::from_slices(
1981 &open,
1982 &high,
1983 &low,
1984 &close,
1985 EvasiveSuperTrendParams::default(),
1986 ),
1987 Kernel::Scalar,
1988 )?;
1989 let batch = evasive_supertrend_batch_with_kernel(
1990 &open,
1991 &high,
1992 &low,
1993 &close,
1994 &EvasiveSuperTrendBatchRange::default(),
1995 Kernel::ScalarBatch,
1996 )?;
1997 assert_eq!(batch.rows, 1);
1998 assert_eq!(batch.cols, close.len());
1999 assert_vec_eq_nan(&batch.band[..close.len()], &single.band);
2000 assert_vec_eq_nan(&batch.state[..close.len()], &single.state);
2001 assert_vec_eq_nan(&batch.noisy[..close.len()], &single.noisy);
2002 assert_vec_eq_nan(&batch.changed[..close.len()], &single.changed);
2003 Ok(())
2004 }
2005
2006 #[test]
2007 fn evasive_supertrend_stream_matches_batch() -> Result<(), Box<dyn Error>> {
2008 let (open, high, low, close) = sample_ohlc(220);
2009 let batch = evasive_supertrend_with_kernel(
2010 &EvasiveSuperTrendInput::from_slices(
2011 &open,
2012 &high,
2013 &low,
2014 &close,
2015 EvasiveSuperTrendParams::default(),
2016 ),
2017 Kernel::Scalar,
2018 )?;
2019 let mut stream = EvasiveSuperTrendBuilder::new().into_stream()?;
2020 let mut band = Vec::with_capacity(close.len());
2021 let mut state = Vec::with_capacity(close.len());
2022 let mut noisy = Vec::with_capacity(close.len());
2023 let mut changed = Vec::with_capacity(close.len());
2024 for i in 0..close.len() {
2025 match stream.update(open[i], high[i], low[i], close[i]) {
2026 Some((b, s, n, c)) => {
2027 band.push(b);
2028 state.push(s);
2029 noisy.push(n);
2030 changed.push(c);
2031 }
2032 None => {
2033 band.push(f64::NAN);
2034 state.push(f64::NAN);
2035 noisy.push(f64::NAN);
2036 changed.push(f64::NAN);
2037 }
2038 }
2039 }
2040 assert_vec_eq_nan(&band, &batch.band);
2041 assert_vec_eq_nan(&state, &batch.state);
2042 assert_vec_eq_nan(&noisy, &batch.noisy);
2043 assert_vec_eq_nan(&changed, &batch.changed);
2044 Ok(())
2045 }
2046
2047 #[test]
2048 fn evasive_supertrend_rejects_invalid_params() {
2049 let (open, high, low, close) = sample_ohlc(32);
2050 let err = evasive_supertrend_with_kernel(
2051 &EvasiveSuperTrendInput::from_slices(
2052 &open,
2053 &high,
2054 &low,
2055 &close,
2056 EvasiveSuperTrendParams {
2057 atr_length: Some(0),
2058 base_multiplier: Some(3.0),
2059 noise_threshold: Some(1.0),
2060 expansion_alpha: Some(0.5),
2061 },
2062 ),
2063 Kernel::Scalar,
2064 )
2065 .unwrap_err();
2066 assert!(matches!(
2067 err,
2068 EvasiveSuperTrendError::InvalidAtrLength { .. }
2069 ));
2070
2071 let err = evasive_supertrend_with_kernel(
2072 &EvasiveSuperTrendInput::from_slices(
2073 &open,
2074 &high,
2075 &low,
2076 &close,
2077 EvasiveSuperTrendParams {
2078 atr_length: Some(10),
2079 base_multiplier: Some(0.0),
2080 noise_threshold: Some(1.0),
2081 expansion_alpha: Some(0.5),
2082 },
2083 ),
2084 Kernel::Scalar,
2085 )
2086 .unwrap_err();
2087 assert!(matches!(
2088 err,
2089 EvasiveSuperTrendError::InvalidBaseMultiplier { .. }
2090 ));
2091
2092 let err = evasive_supertrend_with_kernel(
2093 &EvasiveSuperTrendInput::from_slices(
2094 &open,
2095 &high,
2096 &low,
2097 &close,
2098 EvasiveSuperTrendParams {
2099 atr_length: Some(10),
2100 base_multiplier: Some(3.0),
2101 noise_threshold: Some(0.0),
2102 expansion_alpha: Some(0.5),
2103 },
2104 ),
2105 Kernel::Scalar,
2106 )
2107 .unwrap_err();
2108 assert!(matches!(
2109 err,
2110 EvasiveSuperTrendError::InvalidNoiseThreshold { .. }
2111 ));
2112
2113 let err = evasive_supertrend_with_kernel(
2114 &EvasiveSuperTrendInput::from_slices(
2115 &open,
2116 &high,
2117 &low,
2118 &close,
2119 EvasiveSuperTrendParams {
2120 atr_length: Some(10),
2121 base_multiplier: Some(3.0),
2122 noise_threshold: Some(1.0),
2123 expansion_alpha: Some(-0.1),
2124 },
2125 ),
2126 Kernel::Scalar,
2127 )
2128 .unwrap_err();
2129 assert!(matches!(
2130 err,
2131 EvasiveSuperTrendError::InvalidExpansionAlpha { .. }
2132 ));
2133 }
2134
2135 #[test]
2136 fn evasive_supertrend_dispatch_compute_returns_expected_outputs() -> Result<(), Box<dyn Error>>
2137 {
2138 let (open, high, low, close) = sample_ohlc(192);
2139 let params = [
2140 ParamKV {
2141 key: "atr_length",
2142 value: ParamValue::Int(10),
2143 },
2144 ParamKV {
2145 key: "base_multiplier",
2146 value: ParamValue::Float(3.0),
2147 },
2148 ParamKV {
2149 key: "noise_threshold",
2150 value: ParamValue::Float(1.0),
2151 },
2152 ParamKV {
2153 key: "expansion_alpha",
2154 value: ParamValue::Float(0.5),
2155 },
2156 ];
2157
2158 let out = compute_cpu(IndicatorComputeRequest {
2159 indicator_id: "evasive_supertrend",
2160 output_id: Some("band"),
2161 data: IndicatorDataRef::Ohlc {
2162 open: &open,
2163 high: &high,
2164 low: &low,
2165 close: &close,
2166 },
2167 params: ¶ms,
2168 kernel: Kernel::Scalar,
2169 })?;
2170 assert_eq!(out.output_id, "band");
2171 match out.series {
2172 IndicatorSeries::F64(values) => assert_eq!(values.len(), close.len()),
2173 other => panic!("expected f64 series, got {:?}", other),
2174 }
2175
2176 let state_out = compute_cpu(IndicatorComputeRequest {
2177 indicator_id: "evasive_supertrend",
2178 output_id: Some("state"),
2179 data: IndicatorDataRef::Ohlc {
2180 open: &open,
2181 high: &high,
2182 low: &low,
2183 close: &close,
2184 },
2185 params: ¶ms,
2186 kernel: Kernel::Scalar,
2187 })?;
2188 assert_eq!(state_out.output_id, "state");
2189 Ok(())
2190 }
2191}