Skip to main content

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