Skip to main content

vector_ta/indicators/
evasive_supertrend.rs

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: &params,
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: &params,
2186            kernel: Kernel::Scalar,
2187        })?;
2188        assert_eq!(state_out.output_id, "state");
2189        Ok(())
2190    }
2191}