Skip to main content

vector_ta/indicators/
stc.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::oscillators::CudaStc;
3#[cfg(all(feature = "python", feature = "cuda"))]
4use crate::indicators::moving_averages::alma::{make_device_array_py, DeviceArrayF32Py};
5use crate::utilities::data_loader::{source_type, Candles};
6use crate::utilities::enums::Kernel;
7use crate::utilities::helpers::{
8    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
9    make_uninit_matrix,
10};
11#[cfg(feature = "python")]
12use crate::utilities::kernel_validation::validate_kernel;
13#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
14use core::arch::x86_64::*;
15use core::mem::MaybeUninit;
16#[cfg(feature = "python")]
17use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
18#[cfg(feature = "python")]
19use pyo3::exceptions::PyValueError;
20#[cfg(feature = "python")]
21use pyo3::prelude::*;
22#[cfg(feature = "python")]
23use pyo3::types::PyDict;
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
27use serde::{Deserialize, Serialize};
28use std::convert::AsRef;
29use std::error::Error;
30use thiserror::Error;
31#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
32use wasm_bindgen::prelude::*;
33
34#[derive(Debug, Clone)]
35pub enum StcData<'a> {
36    Candles {
37        candles: &'a Candles,
38        source: &'a str,
39    },
40    Slice(&'a [f64]),
41}
42
43impl<'a> AsRef<[f64]> for StcInput<'a> {
44    #[inline(always)]
45    fn as_ref(&self) -> &[f64] {
46        match &self.data {
47            StcData::Slice(slice) => slice,
48            StcData::Candles { candles, source } => source_type(candles, source),
49        }
50    }
51}
52
53#[derive(Debug, Clone)]
54pub struct StcOutput {
55    pub values: Vec<f64>,
56}
57
58#[derive(Debug, Clone)]
59#[cfg_attr(
60    all(target_arch = "wasm32", feature = "wasm"),
61    derive(Serialize, Deserialize)
62)]
63pub struct StcParams {
64    pub fast_period: Option<usize>,
65    pub slow_period: Option<usize>,
66    pub k_period: Option<usize>,
67    pub d_period: Option<usize>,
68    pub fast_ma_type: Option<String>,
69    pub slow_ma_type: Option<String>,
70}
71
72impl Default for StcParams {
73    fn default() -> Self {
74        Self {
75            fast_period: Some(23),
76            slow_period: Some(50),
77            k_period: Some(10),
78            d_period: Some(3),
79            fast_ma_type: Some("ema".to_string()),
80            slow_ma_type: Some("ema".to_string()),
81        }
82    }
83}
84
85#[derive(Debug, Clone)]
86pub struct StcInput<'a> {
87    pub data: StcData<'a>,
88    pub params: StcParams,
89}
90
91impl<'a> StcInput<'a> {
92    #[inline]
93    pub fn from_candles(c: &'a Candles, s: &'a str, p: StcParams) -> Self {
94        Self {
95            data: StcData::Candles {
96                candles: c,
97                source: s,
98            },
99            params: p,
100        }
101    }
102    #[inline]
103    pub fn from_slice(sl: &'a [f64], p: StcParams) -> Self {
104        Self {
105            data: StcData::Slice(sl),
106            params: p,
107        }
108    }
109    #[inline]
110    pub fn with_default_candles(c: &'a Candles) -> Self {
111        Self::from_candles(c, "close", StcParams::default())
112    }
113    #[inline]
114    pub fn get_fast_period(&self) -> usize {
115        self.params.fast_period.unwrap_or(23)
116    }
117    #[inline]
118    pub fn get_slow_period(&self) -> usize {
119        self.params.slow_period.unwrap_or(50)
120    }
121    #[inline]
122    pub fn get_k_period(&self) -> usize {
123        self.params.k_period.unwrap_or(10)
124    }
125    #[inline]
126    pub fn get_d_period(&self) -> usize {
127        self.params.d_period.unwrap_or(3)
128    }
129    #[inline]
130    pub fn get_fast_ma_type(&self) -> &str {
131        self.params.fast_ma_type.as_deref().unwrap_or("ema")
132    }
133    #[inline]
134    pub fn get_slow_ma_type(&self) -> &str {
135        self.params.slow_ma_type.as_deref().unwrap_or("ema")
136    }
137}
138
139#[derive(Clone, Debug)]
140pub struct StcBuilder {
141    fast_period: Option<usize>,
142    slow_period: Option<usize>,
143    k_period: Option<usize>,
144    d_period: Option<usize>,
145    fast_ma_type: Option<String>,
146    slow_ma_type: Option<String>,
147    kernel: Kernel,
148}
149
150impl Default for StcBuilder {
151    fn default() -> Self {
152        Self {
153            fast_period: None,
154            slow_period: None,
155            k_period: None,
156            d_period: None,
157            fast_ma_type: None,
158            slow_ma_type: None,
159            kernel: Kernel::Auto,
160        }
161    }
162}
163
164impl StcBuilder {
165    #[inline(always)]
166    pub fn new() -> Self {
167        Self::default()
168    }
169    #[inline(always)]
170    pub fn fast_period(mut self, n: usize) -> Self {
171        self.fast_period = Some(n);
172        self
173    }
174    #[inline(always)]
175    pub fn slow_period(mut self, n: usize) -> Self {
176        self.slow_period = Some(n);
177        self
178    }
179    #[inline(always)]
180    pub fn k_period(mut self, n: usize) -> Self {
181        self.k_period = Some(n);
182        self
183    }
184    #[inline(always)]
185    pub fn d_period(mut self, n: usize) -> Self {
186        self.d_period = Some(n);
187        self
188    }
189    #[inline(always)]
190    pub fn fast_ma_type<T: Into<String>>(mut self, s: T) -> Self {
191        self.fast_ma_type = Some(s.into());
192        self
193    }
194    #[inline(always)]
195    pub fn slow_ma_type<T: Into<String>>(mut self, s: T) -> Self {
196        self.slow_ma_type = Some(s.into());
197        self
198    }
199    #[inline(always)]
200    pub fn kernel(mut self, k: Kernel) -> Self {
201        self.kernel = k;
202        self
203    }
204
205    #[inline(always)]
206    pub fn apply(self, c: &Candles) -> Result<StcOutput, StcError> {
207        let p = StcParams {
208            fast_period: self.fast_period,
209            slow_period: self.slow_period,
210            k_period: self.k_period,
211            d_period: self.d_period,
212            fast_ma_type: self.fast_ma_type,
213            slow_ma_type: self.slow_ma_type,
214        };
215        let i = StcInput::from_candles(c, "close", p);
216        stc_with_kernel(&i, self.kernel)
217    }
218
219    #[inline(always)]
220    pub fn apply_slice(self, d: &[f64]) -> Result<StcOutput, StcError> {
221        let p = StcParams {
222            fast_period: self.fast_period,
223            slow_period: self.slow_period,
224            k_period: self.k_period,
225            d_period: self.d_period,
226            fast_ma_type: self.fast_ma_type,
227            slow_ma_type: self.slow_ma_type,
228        };
229        let i = StcInput::from_slice(d, p);
230        stc_with_kernel(&i, self.kernel)
231    }
232
233    #[inline(always)]
234    pub fn into_stream(self) -> Result<StcStream, StcError> {
235        let p = StcParams {
236            fast_period: self.fast_period,
237            slow_period: self.slow_period,
238            k_period: self.k_period,
239            d_period: self.d_period,
240            fast_ma_type: self.fast_ma_type,
241            slow_ma_type: self.slow_ma_type,
242        };
243        StcStream::try_new(p)
244    }
245}
246
247#[derive(Debug, Error)]
248pub enum StcError {
249    #[error("stc: Empty data provided.")]
250    EmptyInputData,
251    #[error("stc: All values are NaN.")]
252    AllValuesNaN,
253    #[error("stc: Invalid period: period = {period}, data length = {data_len}")]
254    InvalidPeriod { period: usize, data_len: usize },
255    #[error("stc: Not enough valid data: needed = {needed}, valid = {valid}")]
256    NotEnoughValidData { needed: usize, valid: usize },
257    #[error("stc: Output length mismatch: expected {expected}, got {got}")]
258    OutputLengthMismatch { expected: usize, got: usize },
259    #[error("stc: Invalid range: start={start}, end={end}, step={step}")]
260    InvalidRange {
261        start: String,
262        end: String,
263        step: String,
264    },
265    #[error("stc: Invalid kernel for batch: {0:?}")]
266    InvalidKernelForBatch(crate::utilities::enums::Kernel),
267    #[error("stc: Internal error: {0}")]
268    Internal(String),
269}
270
271#[inline]
272pub fn stc(input: &StcInput) -> Result<StcOutput, StcError> {
273    stc_with_kernel(input, Kernel::Auto)
274}
275
276pub fn stc_with_kernel(input: &StcInput, kernel: Kernel) -> Result<StcOutput, StcError> {
277    let data: &[f64] = input.as_ref();
278    let len = data.len();
279    if len == 0 {
280        return Err(StcError::EmptyInputData);
281    }
282    let first = data
283        .iter()
284        .position(|x| !x.is_nan())
285        .ok_or(StcError::AllValuesNaN)?;
286
287    let fast_period = input.get_fast_period();
288    let slow_period = input.get_slow_period();
289    let k_period = input.get_k_period();
290    let d_period = input.get_d_period();
291    let needed = fast_period.max(slow_period).max(k_period).max(d_period);
292
293    if (len - first) < needed {
294        return Err(StcError::NotEnoughValidData {
295            needed,
296            valid: len - first,
297        });
298    }
299
300    let chosen = match kernel {
301        Kernel::Auto => Kernel::Scalar,
302        other => other,
303    };
304
305    let warmup = first + needed - 1;
306    let mut output = alloc_with_nan_prefix(len, warmup);
307
308    unsafe {
309        match chosen {
310            Kernel::Scalar | Kernel::ScalarBatch => stc_scalar(
311                data,
312                fast_period,
313                slow_period,
314                k_period,
315                d_period,
316                input.get_fast_ma_type(),
317                input.get_slow_ma_type(),
318                first,
319                &mut output,
320            )?,
321            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
322            Kernel::Avx2 | Kernel::Avx2Batch => stc_avx2(
323                data,
324                fast_period,
325                slow_period,
326                k_period,
327                d_period,
328                input.get_fast_ma_type(),
329                input.get_slow_ma_type(),
330                first,
331                &mut output,
332            )?,
333            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
334            Kernel::Avx512 | Kernel::Avx512Batch => stc_avx512(
335                data,
336                fast_period,
337                slow_period,
338                k_period,
339                d_period,
340                input.get_fast_ma_type(),
341                input.get_slow_ma_type(),
342                first,
343                &mut output,
344            )?,
345            _ => unreachable!(),
346        }
347    }
348
349    Ok(StcOutput { values: output })
350}
351
352#[inline]
353pub fn stc_into_slice(dst: &mut [f64], input: &StcInput, kern: Kernel) -> Result<(), StcError> {
354    let data: &[f64] = input.as_ref();
355    let len = data.len();
356    if len == 0 {
357        return Err(StcError::EmptyInputData);
358    }
359
360    let first = data
361        .iter()
362        .position(|x| !x.is_nan())
363        .ok_or(StcError::AllValuesNaN)?;
364    if dst.len() != len {
365        return Err(StcError::OutputLengthMismatch {
366            expected: len,
367            got: dst.len(),
368        });
369    }
370
371    let needed = input
372        .get_fast_period()
373        .max(input.get_slow_period())
374        .max(input.get_k_period())
375        .max(input.get_d_period());
376
377    if (len - first) < needed {
378        return Err(StcError::NotEnoughValidData {
379            needed,
380            valid: len - first,
381        });
382    }
383
384    let warmup_end = first + needed - 1;
385    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
386    for v in &mut dst[..warmup_end.min(len)] {
387        *v = qnan;
388    }
389
390    let chosen = match kern {
391        Kernel::Auto => Kernel::Scalar,
392        other => other,
393    };
394
395    unsafe {
396        match chosen {
397            Kernel::Scalar | Kernel::ScalarBatch => stc_scalar(
398                data,
399                input.get_fast_period(),
400                input.get_slow_period(),
401                input.get_k_period(),
402                input.get_d_period(),
403                input.get_fast_ma_type(),
404                input.get_slow_ma_type(),
405                first,
406                dst,
407            )?,
408            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
409            Kernel::Avx2 | Kernel::Avx2Batch => stc_avx2(
410                data,
411                input.get_fast_period(),
412                input.get_slow_period(),
413                input.get_k_period(),
414                input.get_d_period(),
415                input.get_fast_ma_type(),
416                input.get_slow_ma_type(),
417                first,
418                dst,
419            )?,
420            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
421            Kernel::Avx512 | Kernel::Avx512Batch => stc_avx512(
422                data,
423                input.get_fast_period(),
424                input.get_slow_period(),
425                input.get_k_period(),
426                input.get_d_period(),
427                input.get_fast_ma_type(),
428                input.get_slow_ma_type(),
429                first,
430                dst,
431            )?,
432            _ => unreachable!(),
433        }
434    }
435    Ok(())
436}
437
438#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
439#[inline]
440pub fn stc_into(input: &StcInput, out: &mut [f64]) -> Result<(), StcError> {
441    stc_into_slice(out, input, Kernel::Auto)
442}
443
444#[inline]
445pub fn stc_scalar(
446    data: &[f64],
447    fast: usize,
448    slow: usize,
449    k: usize,
450    d: usize,
451    fast_type: &str,
452    slow_type: &str,
453    first: usize,
454    out: &mut [f64],
455) -> Result<(), StcError> {
456    if fast_type == "ema" && slow_type == "ema" {
457        return unsafe { stc_scalar_classic_ema(data, fast, slow, k, d, first, out) };
458    } else if fast_type == "sma" && slow_type == "sma" {
459        return unsafe { stc_scalar_classic_sma(data, fast, slow, k, d, first, out) };
460    }
461
462    use crate::indicators::ema::{ema, EmaInput, EmaParams};
463    use crate::indicators::moving_averages::ma::{ma, MaData};
464    use crate::indicators::utility_functions::{max_rolling, min_rolling};
465    use crate::utilities::helpers::alloc_with_nan_prefix;
466
467    let len = data.len();
468    let slice = &data[first..];
469
470    let fast_ma = ma(fast_type, MaData::Slice(slice), fast)
471        .map_err(|e| StcError::Internal(format!("Fast MA error: {}", e)))?;
472    let slow_ma = ma(slow_type, MaData::Slice(slice), slow)
473        .map_err(|e| StcError::Internal(format!("Slow MA error: {}", e)))?;
474
475    let working_len = slice.len();
476    let mut macd = alloc_with_nan_prefix(working_len, 0);
477
478    for i in 0..working_len {
479        macd[i] = fast_ma[i] - slow_ma[i];
480    }
481
482    let macd_min = min_rolling(&macd, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
483    let macd_max = max_rolling(&macd, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
484
485    let mut stok = alloc_with_nan_prefix(working_len, 0);
486    for i in 0..working_len {
487        let range = macd_max[i] - macd_min[i];
488        if range.abs() > f64::EPSILON && !range.is_nan() {
489            stok[i] = (macd[i] - macd_min[i]) / range * 100.0;
490        } else if !macd[i].is_nan() {
491            stok[i] = 50.0;
492        }
493    }
494
495    let d_ema = ema(&EmaInput::from_slice(&stok, EmaParams { period: Some(d) }))
496        .map_err(|e| StcError::Internal(format!("{:?}", e)))?;
497    let d_vals = &d_ema.values;
498
499    let d_min = min_rolling(&d_vals, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
500    let d_max = max_rolling(&d_vals, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
501
502    let mut kd = alloc_with_nan_prefix(working_len, 0);
503    for i in 0..working_len {
504        let range = d_max[i] - d_min[i];
505        if range.abs() > f64::EPSILON && !range.is_nan() {
506            kd[i] = (d_vals[i] - d_min[i]) / range * 100.0;
507        } else if !d_vals[i].is_nan() {
508            kd[i] = 50.0;
509        }
510    }
511
512    let kd_ema = ema(&EmaInput::from_slice(&kd, EmaParams { period: Some(d) }))
513        .map_err(|e| StcError::Internal(format!("{:?}", e)))?;
514    let final_stc = &kd_ema.values;
515
516    for (i, &val) in final_stc.iter().enumerate() {
517        out[first + i] = val;
518    }
519
520    Ok(())
521}
522
523#[inline]
524pub unsafe fn stc_scalar_classic_ema(
525    data: &[f64],
526    fast: usize,
527    slow: usize,
528    k: usize,
529    d: usize,
530    first: usize,
531    out: &mut [f64],
532) -> Result<(), StcError> {
533    #[inline(always)]
534    fn fma(prev: f64, a: f64, x: f64) -> f64 {
535        (x - prev).mul_add(a, prev)
536    }
537
538    const HUNDRED: f64 = 100.0;
539    const EPS: f64 = f64::EPSILON;
540
541    let slice = &data.get_unchecked(first..);
542    let n = slice.len();
543    if n == 0 {
544        return Ok(());
545    }
546
547    let fast_a = 2.0 / (fast as f64 + 1.0);
548    let slow_a = 2.0 / (slow as f64 + 1.0);
549    let d_a = 2.0 / (d as f64 + 1.0);
550
551    let mut fast_sum = 0.0;
552    let mut slow_sum = 0.0;
553    let mut fast_init_cnt: usize = 0;
554    let mut slow_init_cnt: usize = 0;
555
556    let mut fast_ema = f64::NAN;
557    let mut slow_ema = f64::NAN;
558
559    let mut macd_ring: Vec<f64> = vec![f64::NAN; k];
560    let mut macd_valid_ring: Vec<u8> = vec![0; k];
561    let mut macd_valid_sum: usize = 0;
562    let mut macd_vpos: usize = 0;
563
564    let mut d_ring: Vec<f64> = vec![f64::NAN; k];
565    let mut d_valid_ring: Vec<u8> = vec![0; k];
566    let mut d_valid_sum: usize = 0;
567    let mut d_vpos: usize = 0;
568
569    let mut d_ema = f64::NAN;
570    let mut d_sum = 0.0;
571    let mut d_init_cnt = 0usize;
572
573    let mut final_ema = f64::NAN;
574    let mut final_sum = 0.0;
575    let mut final_init_cnt = 0usize;
576
577    let mut i = 0usize;
578    while i < n {
579        let x = *slice.get_unchecked(i);
580
581        if x.is_finite() {
582            if fast_init_cnt < fast {
583                fast_init_cnt += 1;
584                fast_sum += x;
585                if fast_init_cnt == fast {
586                    fast_ema = fast_sum / fast as f64;
587                }
588            } else {
589                fast_ema = fma(fast_ema, fast_a, x);
590            }
591        } else {
592        }
593
594        if x.is_finite() {
595            if slow_init_cnt < slow {
596                slow_init_cnt += 1;
597                slow_sum += x;
598                if slow_init_cnt == slow {
599                    slow_ema = slow_sum / slow as f64;
600                }
601            } else {
602                slow_ema = fma(slow_ema, slow_a, x);
603            }
604        } else {
605        }
606
607        let macd = if slow_init_cnt >= slow {
608            fast_ema - slow_ema
609        } else {
610            f64::NAN
611        };
612
613        if i >= k {
614            macd_valid_sum -= *macd_valid_ring.get_unchecked(macd_vpos) as usize;
615        }
616        let macd_is_valid = (!macd.is_nan()) as u8;
617        *macd_valid_ring.get_unchecked_mut(macd_vpos) = macd_is_valid;
618        macd_valid_sum += macd_is_valid as usize;
619        if macd_is_valid != 0 {
620            *macd_ring.get_unchecked_mut(macd_vpos) = macd;
621        }
622        macd_vpos += 1;
623        if macd_vpos == k {
624            macd_vpos = 0;
625        }
626
627        let stok = if macd_valid_sum == k && macd_is_valid != 0 {
628            let mut mn = *macd_ring.get_unchecked(0);
629            let mut mx = mn;
630            let mut j = 1usize;
631            while j < k {
632                let v = *macd_ring.get_unchecked(j);
633
634                if v < mn {
635                    mn = v;
636                }
637                if v > mx {
638                    mx = v;
639                }
640                j += 1;
641            }
642            let range = mx - mn;
643            if range.abs() > EPS {
644                (macd - mn) * (HUNDRED / range)
645            } else {
646                50.0
647            }
648        } else if macd_is_valid != 0 {
649            50.0
650        } else {
651            f64::NAN
652        };
653
654        let d_val = if !stok.is_nan() {
655            if d_init_cnt < d {
656                d_sum += stok;
657                d_init_cnt += 1;
658                if d_init_cnt == d {
659                    d_ema = d_sum / d as f64;
660                    d_ema
661                } else {
662                    d_sum / (d_init_cnt as f64)
663                }
664            } else {
665                d_ema = fma(d_ema, d_a, stok);
666                d_ema
667            }
668        } else {
669            if d_init_cnt == 0 {
670                f64::NAN
671            } else if d_init_cnt < d {
672                d_sum / (d_init_cnt as f64)
673            } else {
674                d_ema
675            }
676        };
677
678        if i >= k {
679            d_valid_sum -= *d_valid_ring.get_unchecked(d_vpos) as usize;
680        }
681        let d_is_valid = (!d_val.is_nan()) as u8;
682        *d_valid_ring.get_unchecked_mut(d_vpos) = d_is_valid;
683        d_valid_sum += d_is_valid as usize;
684        if d_is_valid != 0 {
685            *d_ring.get_unchecked_mut(d_vpos) = d_val;
686        }
687        d_vpos += 1;
688        if d_vpos == k {
689            d_vpos = 0;
690        }
691
692        let kd = if d_valid_sum == k && d_is_valid != 0 {
693            let mut mn = *d_ring.get_unchecked(0);
694            let mut mx = mn;
695            let mut j = 1usize;
696            while j < k {
697                let v = *d_ring.get_unchecked(j);
698                if v < mn {
699                    mn = v;
700                }
701                if v > mx {
702                    mx = v;
703                }
704                j += 1;
705            }
706            let range = mx - mn;
707            if range.abs() > EPS {
708                (d_val - mn) * (HUNDRED / range)
709            } else {
710                50.0
711            }
712        } else if d_is_valid != 0 {
713            50.0
714        } else {
715            f64::NAN
716        };
717
718        let dst = out.get_unchecked_mut(first + i);
719        if !kd.is_nan() {
720            if final_init_cnt < d {
721                final_sum += kd;
722                final_init_cnt += 1;
723                if final_init_cnt == d {
724                    final_ema = final_sum / d as f64;
725                    *dst = final_ema;
726                } else {
727                    *dst = final_sum / (final_init_cnt as f64);
728                }
729            } else {
730                final_ema = fma(final_ema, d_a, kd);
731                *dst = final_ema;
732            }
733        } else {
734            if final_init_cnt == 0 {
735                *dst = f64::NAN;
736            } else if final_init_cnt < d {
737                *dst = final_sum / (final_init_cnt as f64);
738            } else {
739                *dst = final_ema;
740            }
741        }
742
743        i += 1;
744    }
745
746    Ok(())
747}
748
749#[inline]
750pub unsafe fn stc_scalar_classic_sma(
751    data: &[f64],
752    fast: usize,
753    slow: usize,
754    k: usize,
755    d: usize,
756    first: usize,
757    out: &mut [f64],
758) -> Result<(), StcError> {
759    use crate::indicators::utility_functions::{max_rolling, min_rolling};
760    use crate::utilities::helpers::alloc_with_nan_prefix;
761
762    let slice = &data[first..];
763    let working_len = slice.len();
764
765    let mut macd = alloc_with_nan_prefix(working_len, 0);
766
767    let mut fast_sum = 0.0;
768    let mut slow_sum = 0.0;
769
770    for i in 0..fast.min(working_len) {
771        fast_sum += slice[i];
772    }
773    for i in 0..slow.min(working_len) {
774        slow_sum += slice[i];
775    }
776
777    for i in 0..working_len {
778        if i >= fast {
779            fast_sum = fast_sum - slice[i - fast] + slice[i];
780        }
781        if i >= slow {
782            slow_sum = slow_sum - slice[i - slow] + slice[i];
783        }
784
785        if i >= slow - 1 {
786            let fast_ma = if i >= fast - 1 {
787                fast_sum / fast as f64
788            } else {
789                let mut sum = 0.0;
790                let start = if i >= fast - 1 { i - fast + 1 } else { 0 };
791                for j in start..=i {
792                    sum += slice[j];
793                }
794                sum / ((i - start + 1) as f64)
795            };
796            let slow_ma = slow_sum / slow as f64;
797            macd[i] = fast_ma - slow_ma;
798        } else {
799            macd[i] = f64::NAN;
800        }
801    }
802
803    let macd_min = min_rolling(&macd, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
804    let macd_max = max_rolling(&macd, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
805
806    let mut stok = alloc_with_nan_prefix(working_len, 0);
807    for i in 0..working_len {
808        let range = macd_max[i] - macd_min[i];
809        if range.abs() > f64::EPSILON && !range.is_nan() {
810            stok[i] = (macd[i] - macd_min[i]) / range * 100.0;
811        } else if !macd[i].is_nan() {
812            stok[i] = 50.0;
813        }
814    }
815
816    let d_alpha = 2.0 / (d as f64 + 1.0);
817    let mut d_vals = alloc_with_nan_prefix(working_len, 0);
818    let mut d_ema = f64::NAN;
819    let mut d_sum = 0.0;
820    let mut d_count = 0;
821
822    for i in 0..working_len {
823        if !stok[i].is_nan() {
824            if d_count < d {
825                d_sum += stok[i];
826                d_count += 1;
827                if d_count == d {
828                    d_ema = d_sum / d as f64;
829                    d_vals[i] = d_ema;
830                } else {
831                    d_vals[i] = f64::NAN;
832                }
833            } else {
834                d_ema = d_alpha * stok[i] + (1.0 - d_alpha) * d_ema;
835                d_vals[i] = d_ema;
836            }
837        } else {
838            d_vals[i] = f64::NAN;
839        }
840    }
841
842    let d_min = min_rolling(&d_vals, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
843    let d_max = max_rolling(&d_vals, k).map_err(|e| StcError::Internal(format!("{:?}", e)))?;
844
845    let mut kd = alloc_with_nan_prefix(working_len, 0);
846    for i in 0..working_len {
847        let range = d_max[i] - d_min[i];
848        if range.abs() > f64::EPSILON && !range.is_nan() {
849            kd[i] = (d_vals[i] - d_min[i]) / range * 100.0;
850        } else if !d_vals[i].is_nan() {
851            kd[i] = 50.0;
852        }
853    }
854
855    let mut final_ema = f64::NAN;
856    let mut final_sum = 0.0;
857    let mut final_count = 0;
858
859    for i in 0..working_len {
860        if !kd[i].is_nan() {
861            if final_count < d {
862                final_sum += kd[i];
863                final_count += 1;
864                if final_count == d {
865                    final_ema = final_sum / d as f64;
866                    out[first + i] = final_ema;
867                } else {
868                    out[first + i] = f64::NAN;
869                }
870            } else {
871                final_ema = d_alpha * kd[i] + (1.0 - d_alpha) * final_ema;
872                out[first + i] = final_ema;
873            }
874        } else {
875            out[first + i] = f64::NAN;
876        }
877    }
878
879    Ok(())
880}
881
882#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
883#[inline]
884pub fn stc_avx2(
885    data: &[f64],
886    fast: usize,
887    slow: usize,
888    k: usize,
889    d: usize,
890    fast_type: &str,
891    slow_type: &str,
892    first: usize,
893    out: &mut [f64],
894) -> Result<(), StcError> {
895    stc_scalar(data, fast, slow, k, d, fast_type, slow_type, first, out)
896}
897
898#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
899#[inline]
900pub fn stc_avx512(
901    data: &[f64],
902    fast: usize,
903    slow: usize,
904    k: usize,
905    d: usize,
906    fast_type: &str,
907    slow_type: &str,
908    first: usize,
909    out: &mut [f64],
910) -> Result<(), StcError> {
911    if fast <= 32 && slow <= 32 {
912        unsafe { stc_avx512_short(data, fast, slow, k, d, fast_type, slow_type, first, out) }
913    } else {
914        unsafe { stc_avx512_long(data, fast, slow, k, d, fast_type, slow_type, first, out) }
915    }
916}
917
918#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
919#[inline]
920pub unsafe fn stc_avx512_short(
921    data: &[f64],
922    fast: usize,
923    slow: usize,
924    k: usize,
925    d: usize,
926    fast_type: &str,
927    slow_type: &str,
928    first: usize,
929    out: &mut [f64],
930) -> Result<(), StcError> {
931    stc_scalar(data, fast, slow, k, d, fast_type, slow_type, first, out)
932}
933
934#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
935#[inline]
936pub unsafe fn stc_avx512_long(
937    data: &[f64],
938    fast: usize,
939    slow: usize,
940    k: usize,
941    d: usize,
942    fast_type: &str,
943    slow_type: &str,
944    first: usize,
945    out: &mut [f64],
946) -> Result<(), StcError> {
947    stc_scalar(data, fast, slow, k, d, fast_type, slow_type, first, out)
948}
949
950#[derive(Clone, Debug)]
951pub struct StcBatchRange {
952    pub fast_period: (usize, usize, usize),
953    pub slow_period: (usize, usize, usize),
954    pub k_period: (usize, usize, usize),
955    pub d_period: (usize, usize, usize),
956}
957
958impl Default for StcBatchRange {
959    fn default() -> Self {
960        Self {
961            fast_period: (23, 23, 0),
962            slow_period: (50, 299, 1),
963            k_period: (10, 10, 0),
964            d_period: (3, 3, 0),
965        }
966    }
967}
968
969#[derive(Clone, Debug, Default)]
970pub struct StcBatchBuilder {
971    range: StcBatchRange,
972    kernel: Kernel,
973}
974
975impl StcBatchBuilder {
976    pub fn new() -> Self {
977        Self::default()
978    }
979    pub fn kernel(mut self, k: Kernel) -> Self {
980        self.kernel = k;
981        self
982    }
983    pub fn fast_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
984        self.range.fast_period = (start, end, step);
985        self
986    }
987    pub fn slow_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
988        self.range.slow_period = (start, end, step);
989        self
990    }
991    pub fn k_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
992        self.range.k_period = (start, end, step);
993        self
994    }
995    pub fn d_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
996        self.range.d_period = (start, end, step);
997        self
998    }
999
1000    pub fn apply_slice(self, data: &[f64]) -> Result<StcBatchOutput, StcError> {
1001        stc_batch_with_kernel(data, &self.range, self.kernel)
1002    }
1003
1004    pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<StcBatchOutput, StcError> {
1005        StcBatchBuilder::new().kernel(k).apply_slice(data)
1006    }
1007
1008    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<StcBatchOutput, StcError> {
1009        let slice = source_type(c, src);
1010        self.apply_slice(slice)
1011    }
1012
1013    pub fn with_default_candles(c: &Candles) -> Result<StcBatchOutput, StcError> {
1014        StcBatchBuilder::new()
1015            .kernel(Kernel::Auto)
1016            .apply_candles(c, "close")
1017    }
1018}
1019
1020pub fn stc_batch_with_kernel(
1021    data: &[f64],
1022    sweep: &StcBatchRange,
1023    k: Kernel,
1024) -> Result<StcBatchOutput, StcError> {
1025    let kernel = match k {
1026        Kernel::Auto => detect_best_batch_kernel(),
1027        other if other.is_batch() => other,
1028        _ => return Err(StcError::InvalidKernelForBatch(k)),
1029    };
1030    let simd = match kernel {
1031        Kernel::Avx512Batch => Kernel::Avx512,
1032        Kernel::Avx2Batch => Kernel::Avx2,
1033        Kernel::ScalarBatch => Kernel::Scalar,
1034        _ => unreachable!(),
1035    };
1036    stc_batch_par_slice(data, sweep, simd)
1037}
1038
1039#[derive(Clone, Debug)]
1040pub struct StcBatchOutput {
1041    pub values: Vec<f64>,
1042    pub combos: Vec<StcParams>,
1043    pub rows: usize,
1044    pub cols: usize,
1045}
1046
1047impl StcBatchOutput {
1048    pub fn row_for_params(&self, p: &StcParams) -> Option<usize> {
1049        self.combos.iter().position(|c| {
1050            c.fast_period == p.fast_period
1051                && c.slow_period == p.slow_period
1052                && c.k_period == p.k_period
1053                && c.d_period == p.d_period
1054        })
1055    }
1056    pub fn values_for(&self, p: &StcParams) -> Option<&[f64]> {
1057        self.row_for_params(p).map(|row| {
1058            let start = row * self.cols;
1059            &self.values[start..start + self.cols]
1060        })
1061    }
1062}
1063
1064#[inline(always)]
1065fn expand_grid(r: &StcBatchRange) -> Result<Vec<StcParams>, StcError> {
1066    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, StcError> {
1067        if step == 0 || start == end {
1068            return Ok(vec![start]);
1069        }
1070        if start < end {
1071            return Ok((start..=end).step_by(step.max(1)).collect());
1072        }
1073        let mut v = Vec::new();
1074        let mut x = start as isize;
1075        let end_i = end as isize;
1076        let st = (step as isize).max(1);
1077        while x >= end_i {
1078            v.push(x as usize);
1079            x -= st;
1080        }
1081        if v.is_empty() {
1082            return Err(StcError::InvalidRange {
1083                start: start.to_string(),
1084                end: end.to_string(),
1085                step: step.to_string(),
1086            });
1087        }
1088        Ok(v)
1089    }
1090
1091    let fasts = axis_usize(r.fast_period)?;
1092    let slows = axis_usize(r.slow_period)?;
1093    let ks = axis_usize(r.k_period)?;
1094    let ds = axis_usize(r.d_period)?;
1095
1096    let cap = fasts
1097        .len()
1098        .checked_mul(slows.len())
1099        .and_then(|v| v.checked_mul(ks.len()))
1100        .and_then(|v| v.checked_mul(ds.len()))
1101        .ok_or_else(|| StcError::InvalidRange {
1102            start: "cap".into(),
1103            end: "overflow".into(),
1104            step: "mul".into(),
1105        })?;
1106
1107    let mut out = Vec::with_capacity(cap);
1108    for &f in &fasts {
1109        for &s in &slows {
1110            for &k in &ks {
1111                for &d in &ds {
1112                    out.push(StcParams {
1113                        fast_period: Some(f),
1114                        slow_period: Some(s),
1115                        k_period: Some(k),
1116                        d_period: Some(d),
1117                        fast_ma_type: None,
1118                        slow_ma_type: None,
1119                    });
1120                }
1121            }
1122        }
1123    }
1124    Ok(out)
1125}
1126
1127#[inline(always)]
1128pub fn stc_batch_slice(
1129    data: &[f64],
1130    sweep: &StcBatchRange,
1131    kern: Kernel,
1132) -> Result<StcBatchOutput, StcError> {
1133    stc_batch_inner(data, sweep, kern, false)
1134}
1135
1136#[inline(always)]
1137pub fn stc_batch_par_slice(
1138    data: &[f64],
1139    sweep: &StcBatchRange,
1140    kern: Kernel,
1141) -> Result<StcBatchOutput, StcError> {
1142    stc_batch_inner(data, sweep, kern, true)
1143}
1144
1145#[inline(always)]
1146fn stc_batch_inner(
1147    data: &[f64],
1148    sweep: &StcBatchRange,
1149    kern: Kernel,
1150    parallel: bool,
1151) -> Result<StcBatchOutput, StcError> {
1152    let combos = expand_grid(sweep)?;
1153    if combos.is_empty() {
1154        return Err(StcError::InvalidRange {
1155            start: "range".into(),
1156            end: "range".into(),
1157            step: "empty".into(),
1158        });
1159    }
1160    let first = data
1161        .iter()
1162        .position(|x| !x.is_nan())
1163        .ok_or(StcError::AllValuesNaN)?;
1164    let max_needed = combos
1165        .iter()
1166        .map(|c| {
1167            c.fast_period
1168                .unwrap()
1169                .max(c.slow_period.unwrap())
1170                .max(c.k_period.unwrap())
1171                .max(c.d_period.unwrap())
1172        })
1173        .max()
1174        .unwrap();
1175    if data.len() - first < max_needed {
1176        return Err(StcError::NotEnoughValidData {
1177            needed: max_needed,
1178            valid: data.len() - first,
1179        });
1180    }
1181
1182    let rows = combos.len();
1183    let cols = data.len();
1184
1185    let _ = rows
1186        .checked_mul(cols)
1187        .ok_or_else(|| StcError::InvalidRange {
1188            start: rows.to_string(),
1189            end: cols.to_string(),
1190            step: "rows*cols".into(),
1191        })?;
1192
1193    let mut buf_mu = make_uninit_matrix(rows, cols);
1194
1195    let warm: Vec<usize> = combos
1196        .iter()
1197        .map(|c| {
1198            first
1199                + c.fast_period
1200                    .unwrap()
1201                    .max(c.slow_period.unwrap())
1202                    .max(c.k_period.unwrap())
1203                    .max(c.d_period.unwrap())
1204                - 1
1205        })
1206        .collect();
1207    init_matrix_prefixes(&mut buf_mu, cols, &warm);
1208
1209    let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
1210    let values_slice: &mut [f64] = unsafe {
1211        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
1212    };
1213
1214    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
1215        let prm = &combos[row];
1216        match kern {
1217            Kernel::Scalar => stc_row_scalar(data, first, prm, out_row),
1218            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1219            Kernel::Avx2 => stc_row_avx2(data, first, prm, out_row),
1220            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1221            Kernel::Avx512 => stc_row_avx512(data, first, prm, out_row),
1222            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
1223            Kernel::Avx2 | Kernel::Avx512 => stc_row_scalar(data, first, prm, out_row),
1224            _ => unreachable!(),
1225        }
1226    };
1227
1228    if parallel {
1229        #[cfg(not(target_arch = "wasm32"))]
1230        {
1231            values_slice
1232                .par_chunks_mut(cols)
1233                .enumerate()
1234                .for_each(|(row, slice)| {
1235                    do_row(row, slice).unwrap();
1236                });
1237        }
1238        #[cfg(target_arch = "wasm32")]
1239        {
1240            for (row, slice) in values_slice.chunks_mut(cols).enumerate() {
1241                do_row(row, slice).unwrap();
1242            }
1243        }
1244    } else {
1245        for (row, slice) in values_slice.chunks_mut(cols).enumerate() {
1246            do_row(row, slice).unwrap();
1247        }
1248    }
1249
1250    let values = unsafe {
1251        Vec::from_raw_parts(
1252            buf_guard.as_mut_ptr() as *mut f64,
1253            buf_guard.len(),
1254            buf_guard.capacity(),
1255        )
1256    };
1257
1258    Ok(StcBatchOutput {
1259        values,
1260        combos,
1261        rows,
1262        cols,
1263    })
1264}
1265
1266#[inline(always)]
1267pub unsafe fn stc_row_scalar(
1268    data: &[f64],
1269    first: usize,
1270    prm: &StcParams,
1271    out: &mut [f64],
1272) -> Result<(), StcError> {
1273    let fast_type = prm.fast_ma_type.as_deref().unwrap_or("ema");
1274    let slow_type = prm.slow_ma_type.as_deref().unwrap_or("ema");
1275
1276    if fast_type == "ema" && slow_type == "ema" {
1277        return stc_row_scalar_classic_ema(data, first, prm, out);
1278    } else if fast_type == "sma" && slow_type == "sma" {
1279        return stc_row_scalar_classic_sma(data, first, prm, out);
1280    }
1281
1282    stc_scalar(
1283        data,
1284        prm.fast_period.unwrap(),
1285        prm.slow_period.unwrap(),
1286        prm.k_period.unwrap(),
1287        prm.d_period.unwrap(),
1288        fast_type,
1289        slow_type,
1290        first,
1291        out,
1292    )
1293}
1294
1295#[inline(always)]
1296pub unsafe fn stc_row_scalar_classic_ema(
1297    data: &[f64],
1298    first: usize,
1299    prm: &StcParams,
1300    out: &mut [f64],
1301) -> Result<(), StcError> {
1302    stc_scalar_classic_ema(
1303        data,
1304        prm.fast_period.unwrap(),
1305        prm.slow_period.unwrap(),
1306        prm.k_period.unwrap(),
1307        prm.d_period.unwrap(),
1308        first,
1309        out,
1310    )
1311}
1312
1313#[inline(always)]
1314pub unsafe fn stc_row_scalar_classic_sma(
1315    data: &[f64],
1316    first: usize,
1317    prm: &StcParams,
1318    out: &mut [f64],
1319) -> Result<(), StcError> {
1320    stc_scalar_classic_sma(
1321        data,
1322        prm.fast_period.unwrap(),
1323        prm.slow_period.unwrap(),
1324        prm.k_period.unwrap(),
1325        prm.d_period.unwrap(),
1326        first,
1327        out,
1328    )
1329}
1330
1331#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1332#[inline(always)]
1333pub unsafe fn stc_row_avx2(
1334    data: &[f64],
1335    first: usize,
1336    prm: &StcParams,
1337    out: &mut [f64],
1338) -> Result<(), StcError> {
1339    stc_row_scalar(data, first, prm, out)
1340}
1341
1342#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1343#[inline(always)]
1344pub unsafe fn stc_row_avx512(
1345    data: &[f64],
1346    first: usize,
1347    prm: &StcParams,
1348    out: &mut [f64],
1349) -> Result<(), StcError> {
1350    if prm.fast_period.unwrap() <= 32 && prm.slow_period.unwrap() <= 32 {
1351        stc_row_avx512_short(data, first, prm, out)
1352    } else {
1353        stc_row_avx512_long(data, first, prm, out)
1354    }
1355}
1356
1357#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1358#[inline(always)]
1359pub unsafe fn stc_row_avx512_short(
1360    data: &[f64],
1361    first: usize,
1362    prm: &StcParams,
1363    out: &mut [f64],
1364) -> Result<(), StcError> {
1365    stc_row_scalar(data, first, prm, out)
1366}
1367
1368#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1369#[inline(always)]
1370pub unsafe fn stc_row_avx512_long(
1371    data: &[f64],
1372    first: usize,
1373    prm: &StcParams,
1374    out: &mut [f64],
1375) -> Result<(), StcError> {
1376    stc_row_scalar(data, first, prm, out)
1377}
1378
1379use std::collections::VecDeque;
1380
1381#[derive(Debug, Clone)]
1382pub struct StcStream {
1383    pub fast_period: usize,
1384    pub slow_period: usize,
1385    pub k_period: usize,
1386    pub d_period: usize,
1387
1388    fast_ma_type: String,
1389    slow_ma_type: String,
1390
1391    started: bool,
1392    poisoned: bool,
1393    ticks: usize,
1394
1395    min_data: usize,
1396
1397    fast_ema: EmaSeed,
1398    slow_ema: EmaSeed,
1399
1400    fast_sma: SmaState,
1401    slow_sma: SmaState,
1402
1403    macd_last: f64,
1404    macd_valid_flags: Vec<u8>,
1405    macd_vpos: usize,
1406    macd_valid_sum: usize,
1407    macd_min: MonoMin,
1408    macd_max: MonoMax,
1409
1410    d_ema: EmaSeed,
1411
1412    d_valid_flags: Vec<u8>,
1413    d_vpos: usize,
1414    d_valid_sum: usize,
1415    d_min: MonoMin,
1416    d_max: MonoMax,
1417
1418    final_ema: EmaSeed,
1419
1420    fallback: bool,
1421    buffer: Vec<f64>,
1422    params: StcParams,
1423}
1424
1425#[derive(Debug, Clone)]
1426struct EmaSeed {
1427    period: usize,
1428    alpha: f64,
1429    sum: f64,
1430    cnt: usize,
1431    ema: f64,
1432    seeded: bool,
1433}
1434impl EmaSeed {
1435    #[inline(always)]
1436    fn new(period: usize) -> Self {
1437        Self {
1438            period,
1439            alpha: 2.0 / (period as f64 + 1.0),
1440            sum: 0.0,
1441            cnt: 0,
1442            ema: f64::NAN,
1443            seeded: false,
1444        }
1445    }
1446
1447    #[inline(always)]
1448    fn step(&mut self, x: f64) -> f64 {
1449        if !self.seeded {
1450            self.cnt += 1;
1451            self.sum += x;
1452            if self.cnt == self.period {
1453                self.ema = self.sum / self.period as f64;
1454                self.seeded = true;
1455                self.ema
1456            } else {
1457                self.sum / self.cnt as f64
1458            }
1459        } else {
1460            let e = (x - self.ema).mul_add(self.alpha, self.ema);
1461            self.ema = e;
1462            e
1463        }
1464    }
1465    #[inline(always)]
1466    fn is_seeded(&self) -> bool {
1467        self.seeded
1468    }
1469    #[inline(always)]
1470    fn current(&self) -> f64 {
1471        self.ema
1472    }
1473
1474    #[inline(always)]
1475    fn value_or_carry(&self) -> f64 {
1476        if self.cnt == 0 {
1477            f64::NAN
1478        } else if !self.seeded {
1479            self.sum / self.cnt as f64
1480        } else {
1481            self.ema
1482        }
1483    }
1484}
1485
1486#[derive(Debug, Clone)]
1487struct SmaState {
1488    period: usize,
1489    sum: f64,
1490    ring: Vec<f64>,
1491    pos: usize,
1492    cnt: usize,
1493}
1494impl SmaState {
1495    #[inline(always)]
1496    fn new(period: usize) -> Self {
1497        Self {
1498            period,
1499            sum: 0.0,
1500            ring: vec![0.0; period],
1501            pos: 0,
1502            cnt: 0,
1503        }
1504    }
1505
1506    #[inline(always)]
1507    fn step(&mut self, x: f64) -> (f64, bool) {
1508        if self.cnt < self.period {
1509            self.ring[self.pos] = x;
1510            self.pos += 1;
1511            if self.pos == self.period {
1512                self.pos = 0;
1513            }
1514            self.sum += x;
1515            self.cnt += 1;
1516            (self.sum / self.cnt as f64, false)
1517        } else {
1518            let old = self.ring[self.pos];
1519            self.ring[self.pos] = x;
1520            self.pos += 1;
1521            if self.pos == self.period {
1522                self.pos = 0;
1523            }
1524            self.sum += x - old;
1525            (self.sum / self.period as f64, true)
1526        }
1527    }
1528    #[inline(always)]
1529    fn is_seeded(&self) -> bool {
1530        self.cnt >= self.period
1531    }
1532
1533    #[inline(always)]
1534    fn current(&self) -> (f64, bool) {
1535        if self.cnt == 0 {
1536            (f64::NAN, false)
1537        } else if self.cnt < self.period {
1538            (self.sum / self.cnt as f64, false)
1539        } else {
1540            (self.sum / self.period as f64, true)
1541        }
1542    }
1543}
1544
1545#[derive(Debug, Clone)]
1546struct MonoMin {
1547    q: VecDeque<(usize, f64)>,
1548}
1549#[derive(Debug, Clone)]
1550struct MonoMax {
1551    q: VecDeque<(usize, f64)>,
1552}
1553
1554impl MonoMin {
1555    #[inline(always)]
1556    fn with_capacity(c: usize) -> Self {
1557        Self {
1558            q: VecDeque::with_capacity(c + 1),
1559        }
1560    }
1561    #[inline(always)]
1562    fn push(&mut self, idx: usize, v: f64) {
1563        while let Some(&(_, back_v)) = self.q.back() {
1564            if back_v <= v {
1565                break;
1566            }
1567            self.q.pop_back();
1568        }
1569        self.q.push_back((idx, v));
1570    }
1571    #[inline(always)]
1572    fn evict_older_than(&mut self, cutoff_exclusive: usize, k: usize) {
1573        while let Some(&(j, _)) = self.q.front() {
1574            if j + k <= cutoff_exclusive {
1575                self.q.pop_front();
1576            } else {
1577                break;
1578            }
1579        }
1580    }
1581    #[inline(always)]
1582    fn min(&self) -> f64 {
1583        self.q.front().map(|x| x.1).unwrap_or(f64::NAN)
1584    }
1585}
1586impl MonoMax {
1587    #[inline(always)]
1588    fn with_capacity(c: usize) -> Self {
1589        Self {
1590            q: VecDeque::with_capacity(c + 1),
1591        }
1592    }
1593    #[inline(always)]
1594    fn push(&mut self, idx: usize, v: f64) {
1595        while let Some(&(_, back_v)) = self.q.back() {
1596            if back_v >= v {
1597                break;
1598            }
1599            self.q.pop_back();
1600        }
1601        self.q.push_back((idx, v));
1602    }
1603    #[inline(always)]
1604    fn evict_older_than(&mut self, cutoff_exclusive: usize, k: usize) {
1605        while let Some(&(j, _)) = self.q.front() {
1606            if j + k <= cutoff_exclusive {
1607                self.q.pop_front();
1608            } else {
1609                break;
1610            }
1611        }
1612    }
1613    #[inline(always)]
1614    fn max(&self) -> f64 {
1615        self.q.front().map(|x| x.1).unwrap_or(f64::NAN)
1616    }
1617}
1618
1619impl StcStream {
1620    pub fn try_new(params: StcParams) -> Result<Self, StcError> {
1621        let fast = params.fast_period.unwrap_or(23);
1622        let slow = params.slow_period.unwrap_or(50);
1623        let k = params.k_period.unwrap_or(10);
1624        let d = params.d_period.unwrap_or(3);
1625        if fast == 0 || slow == 0 || k == 0 || d == 0 {
1626            return Err(StcError::NotEnoughValidData {
1627                needed: 1,
1628                valid: 0,
1629            });
1630        }
1631
1632        let fast_ma = params.fast_ma_type.as_deref().unwrap_or("ema").to_string();
1633        let slow_ma = params.slow_ma_type.as_deref().unwrap_or("ema").to_string();
1634        let min_data = fast.max(slow).max(k).max(d);
1635
1636        let fallback =
1637            !((fast_ma == "ema" && slow_ma == "ema") || (fast_ma == "sma" && slow_ma == "sma"));
1638
1639        Ok(Self {
1640            fast_period: fast,
1641            slow_period: slow,
1642            k_period: k,
1643            d_period: d,
1644            fast_ma_type: fast_ma.clone(),
1645            slow_ma_type: slow_ma.clone(),
1646
1647            started: false,
1648            poisoned: false,
1649            ticks: 0,
1650
1651            min_data,
1652
1653            fast_ema: EmaSeed::new(fast),
1654            slow_ema: EmaSeed::new(slow),
1655
1656            fast_sma: SmaState::new(fast),
1657            slow_sma: SmaState::new(slow),
1658
1659            macd_last: f64::NAN,
1660            macd_valid_flags: vec![0u8; k],
1661            macd_vpos: 0,
1662            macd_valid_sum: 0,
1663            macd_min: MonoMin::with_capacity(k),
1664            macd_max: MonoMax::with_capacity(k),
1665
1666            d_ema: EmaSeed::new(d),
1667
1668            d_valid_flags: vec![0u8; k],
1669            d_vpos: 0,
1670            d_valid_sum: 0,
1671            d_min: MonoMin::with_capacity(k),
1672            d_max: MonoMax::with_capacity(k),
1673
1674            final_ema: EmaSeed::new(d),
1675
1676            fallback,
1677            buffer: Vec::new(),
1678            params,
1679        })
1680    }
1681
1682    #[inline(always)]
1683    pub fn update(&mut self, value: f64) -> Option<f64> {
1684        if !self.started {
1685            if value.is_nan() {
1686                return None;
1687            }
1688            self.started = true;
1689            self.ticks = 0;
1690        }
1691
1692        let (macd, macd_valid) = if self.fast_ma_type == "ema" && self.slow_ma_type == "ema" {
1693            if value.is_nan() {
1694                let valid = self.slow_ema.is_seeded();
1695                let macd = if valid {
1696                    self.fast_ema.current() - self.slow_ema.current()
1697                } else {
1698                    f64::NAN
1699                };
1700                (macd, valid)
1701            } else {
1702                let _f = self.fast_ema.step(value);
1703                let _s = self.slow_ema.step(value);
1704                let valid = self.slow_ema.is_seeded();
1705                let macd = if valid {
1706                    self.fast_ema.current() - self.slow_ema.current()
1707                } else {
1708                    f64::NAN
1709                };
1710                (macd, valid)
1711            }
1712        } else if self.fast_ma_type == "sma" && self.slow_ma_type == "sma" {
1713            if value.is_nan() {
1714                let (fast_val, _fast_seeded) = self.fast_sma.current();
1715                let (slow_val, slow_seeded) = self.slow_sma.current();
1716                let macd = if slow_seeded {
1717                    fast_val - slow_val
1718                } else {
1719                    f64::NAN
1720                };
1721                (macd, slow_seeded)
1722            } else {
1723                let (fast_val, _fast_seeded_or_partial) = self.fast_sma.step(value);
1724                let (slow_val, slow_seeded) = self.slow_sma.step(value);
1725                let macd = if slow_seeded {
1726                    fast_val - slow_val
1727                } else {
1728                    f64::NAN
1729                };
1730                (macd, slow_seeded)
1731            }
1732        } else {
1733            self.buffer.push(value);
1734            if self.buffer.len() < self.min_data {
1735                return None;
1736            }
1737            let input = StcInput::from_slice(&self.buffer, self.params.clone());
1738            match stc(&input) {
1739                Ok(res) => return res.values.last().cloned(),
1740                Err(_) => return Some(f64::NAN),
1741            }
1742        };
1743
1744        self.macd_last = macd;
1745
1746        if self.ticks >= self.k_period {
1747            self.macd_valid_sum -= self.macd_valid_flags[self.macd_vpos] as usize;
1748        }
1749        let macd_is_valid = if macd_valid && !macd.is_nan() {
1750            1u8
1751        } else {
1752            0u8
1753        };
1754        self.macd_valid_flags[self.macd_vpos] = macd_is_valid;
1755        self.macd_valid_sum += macd_is_valid as usize;
1756
1757        if macd_is_valid == 1 {
1758            self.macd_min.push(self.ticks, macd);
1759            self.macd_max.push(self.ticks, macd);
1760        }
1761        self.macd_min.evict_older_than(self.ticks, self.k_period);
1762        self.macd_max.evict_older_than(self.ticks, self.k_period);
1763
1764        self.macd_vpos += 1;
1765        if self.macd_vpos == self.k_period {
1766            self.macd_vpos = 0;
1767        }
1768
1769        let stok = if self.macd_valid_sum == self.k_period && macd_is_valid == 1 {
1770            let mn = self.macd_min.min();
1771            let mx = self.macd_max.max();
1772            let range = mx - mn;
1773            if range.abs() > f64::EPSILON {
1774                (macd - mn) * (100.0 / range)
1775            } else {
1776                50.0
1777            }
1778        } else if macd_is_valid == 1 {
1779            50.0
1780        } else {
1781            f64::NAN
1782        };
1783
1784        let d_val = if !stok.is_nan() {
1785            self.d_ema.step(stok)
1786        } else {
1787            self.d_ema.value_or_carry()
1788        };
1789
1790        let d_is_valid = (!d_val.is_nan()) as u8;
1791        if self.ticks >= self.k_period {
1792            self.d_valid_sum -= self.d_valid_flags[self.d_vpos] as usize;
1793        }
1794        self.d_valid_flags[self.d_vpos] = d_is_valid;
1795        self.d_valid_sum += d_is_valid as usize;
1796
1797        if d_is_valid == 1 {
1798            self.d_min.push(self.ticks, d_val);
1799            self.d_max.push(self.ticks, d_val);
1800        }
1801        self.d_min.evict_older_than(self.ticks, self.k_period);
1802        self.d_max.evict_older_than(self.ticks, self.k_period);
1803
1804        self.d_vpos += 1;
1805        if self.d_vpos == self.k_period {
1806            self.d_vpos = 0;
1807        }
1808
1809        let kd = if self.d_valid_sum == self.k_period && d_is_valid == 1 {
1810            let mn = self.d_min.min();
1811            let mx = self.d_max.max();
1812            let range = mx - mn;
1813            if range.abs() > f64::EPSILON {
1814                (d_val - mn) * (100.0 / range)
1815            } else {
1816                50.0
1817            }
1818        } else if d_is_valid == 1 {
1819            50.0
1820        } else {
1821            f64::NAN
1822        };
1823
1824        let out = if !kd.is_nan() {
1825            self.final_ema.step(kd)
1826        } else {
1827            self.final_ema.value_or_carry()
1828        };
1829
1830        self.ticks += 1;
1831        if self.ticks < self.min_data {
1832            None
1833        } else {
1834            Some(out)
1835        }
1836    }
1837}
1838
1839#[cfg(feature = "python")]
1840#[pyfunction(name = "stc")]
1841#[pyo3(signature = (data, fast_period=23, slow_period=50, k_period=10, d_period=3, fast_ma_type="ema", slow_ma_type="ema", kernel=None))]
1842pub fn stc_py<'py>(
1843    py: Python<'py>,
1844    data: PyReadonlyArray1<'py, f64>,
1845    fast_period: usize,
1846    slow_period: usize,
1847    k_period: usize,
1848    d_period: usize,
1849    fast_ma_type: &str,
1850    slow_ma_type: &str,
1851    kernel: Option<&str>,
1852) -> PyResult<Bound<'py, PyArray1<f64>>> {
1853    use numpy::{IntoPyArray, PyArrayMethods};
1854
1855    let slice_in = data.as_slice()?;
1856    let kern = validate_kernel(kernel, false)?;
1857
1858    let params = StcParams {
1859        fast_period: Some(fast_period),
1860        slow_period: Some(slow_period),
1861        k_period: Some(k_period),
1862        d_period: Some(d_period),
1863        fast_ma_type: Some(fast_ma_type.to_string()),
1864        slow_ma_type: Some(slow_ma_type.to_string()),
1865    };
1866    let stc_in = StcInput::from_slice(slice_in, params);
1867
1868    let result_vec: Vec<f64> = py
1869        .allow_threads(|| stc_with_kernel(&stc_in, kern).map(|o| o.values))
1870        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1871
1872    Ok(result_vec.into_pyarray(py))
1873}
1874
1875#[cfg(feature = "python")]
1876#[pyclass(name = "StcStream")]
1877pub struct StcStreamPy {
1878    stream: StcStream,
1879}
1880
1881#[cfg(feature = "python")]
1882#[pymethods]
1883impl StcStreamPy {
1884    #[new]
1885    fn new(
1886        fast_period: usize,
1887        slow_period: usize,
1888        k_period: usize,
1889        d_period: usize,
1890    ) -> PyResult<Self> {
1891        let params = StcParams {
1892            fast_period: Some(fast_period),
1893            slow_period: Some(slow_period),
1894            k_period: Some(k_period),
1895            d_period: Some(d_period),
1896            fast_ma_type: Some("ema".to_string()),
1897            slow_ma_type: Some("ema".to_string()),
1898        };
1899        let stream =
1900            StcStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1901        Ok(StcStreamPy { stream })
1902    }
1903
1904    fn update(&mut self, value: f64) -> Option<f64> {
1905        self.stream.update(value)
1906    }
1907}
1908
1909#[cfg(feature = "python")]
1910#[pyfunction(name = "stc_batch")]
1911#[pyo3(signature = (data, fast_period_range, slow_period_range, k_period_range, d_period_range, kernel=None))]
1912pub fn stc_batch_py<'py>(
1913    py: Python<'py>,
1914    data: PyReadonlyArray1<'py, f64>,
1915    fast_period_range: (usize, usize, usize),
1916    slow_period_range: (usize, usize, usize),
1917    k_period_range: (usize, usize, usize),
1918    d_period_range: (usize, usize, usize),
1919    kernel: Option<&str>,
1920) -> PyResult<Bound<'py, PyDict>> {
1921    let slice_in = data.as_slice()?;
1922
1923    let sweep = StcBatchRange {
1924        fast_period: fast_period_range,
1925        slow_period: slow_period_range,
1926        k_period: k_period_range,
1927        d_period: d_period_range,
1928    };
1929
1930    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1931    let rows = combos.len();
1932    let cols = slice_in.len();
1933    let expected = rows
1934        .checked_mul(cols)
1935        .ok_or_else(|| PyValueError::new_err("stc_batch: rows*cols overflow"))?;
1936
1937    let out_arr = unsafe { PyArray1::<f64>::new(py, [expected], false) };
1938    let slice_out = unsafe { out_arr.as_slice_mut()? };
1939
1940    let kern = validate_kernel(kernel, true)?;
1941
1942    let combos = py
1943        .allow_threads(|| {
1944            let k = match kern {
1945                Kernel::Auto => detect_best_batch_kernel(),
1946                k => k,
1947            };
1948            let simd = match k {
1949                Kernel::Avx512Batch => Kernel::Avx512,
1950                Kernel::Avx2Batch => Kernel::Avx2,
1951                Kernel::ScalarBatch => Kernel::Scalar,
1952                _ => unreachable!(),
1953            };
1954            stc_batch_inner_into(slice_in, &sweep, simd, true, slice_out)
1955        })
1956        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1957
1958    let dict = PyDict::new(py);
1959    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1960    dict.set_item(
1961        "fast_periods",
1962        combos
1963            .iter()
1964            .map(|p| p.fast_period.unwrap() as u64)
1965            .collect::<Vec<_>>()
1966            .into_pyarray(py),
1967    )?;
1968    dict.set_item(
1969        "slow_periods",
1970        combos
1971            .iter()
1972            .map(|p| p.slow_period.unwrap() as u64)
1973            .collect::<Vec<_>>()
1974            .into_pyarray(py),
1975    )?;
1976    dict.set_item(
1977        "k_periods",
1978        combos
1979            .iter()
1980            .map(|p| p.k_period.unwrap() as u64)
1981            .collect::<Vec<_>>()
1982            .into_pyarray(py),
1983    )?;
1984    dict.set_item(
1985        "d_periods",
1986        combos
1987            .iter()
1988            .map(|p| p.d_period.unwrap() as u64)
1989            .collect::<Vec<_>>()
1990            .into_pyarray(py),
1991    )?;
1992
1993    Ok(dict)
1994}
1995
1996#[cfg(feature = "python")]
1997pub fn register_stc_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1998    m.add_function(wrap_pyfunction!(stc_py, m)?)?;
1999    m.add_function(wrap_pyfunction!(stc_batch_py, m)?)?;
2000    m.add_class::<StcStreamPy>()?;
2001    #[cfg(feature = "cuda")]
2002    {
2003        m.add_function(wrap_pyfunction!(stc_cuda_batch_dev_py, m)?)?;
2004        m.add_function(wrap_pyfunction!(stc_cuda_many_series_one_param_dev_py, m)?)?;
2005    }
2006    Ok(())
2007}
2008
2009#[cfg(all(feature = "python", feature = "cuda"))]
2010#[pyfunction(name = "stc_cuda_batch_dev")]
2011#[pyo3(signature = (data_f32, fast_period_range, slow_period_range, k_period_range, d_period_range, device_id=0))]
2012pub fn stc_cuda_batch_dev_py<'py>(
2013    py: Python<'py>,
2014    data_f32: numpy::PyReadonlyArray1<'py, f32>,
2015    fast_period_range: (usize, usize, usize),
2016    slow_period_range: (usize, usize, usize),
2017    k_period_range: (usize, usize, usize),
2018    d_period_range: (usize, usize, usize),
2019    device_id: usize,
2020) -> PyResult<(DeviceArrayF32Py, Bound<'py, pyo3::types::PyDict>)> {
2021    use crate::cuda::cuda_available;
2022    if !cuda_available() {
2023        return Err(PyValueError::new_err("CUDA not available"));
2024    }
2025    let slice_in = data_f32.as_slice()?;
2026    let sweep = StcBatchRange {
2027        fast_period: fast_period_range,
2028        slow_period: slow_period_range,
2029        k_period: k_period_range,
2030        d_period: d_period_range,
2031    };
2032    let (inner, combos) = py.allow_threads(|| {
2033        let cuda = CudaStc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2034        cuda.stc_batch_dev(slice_in, &sweep)
2035            .map_err(|e| PyValueError::new_err(e.to_string()))
2036    })?;
2037
2038    let dict = pyo3::types::PyDict::new(py);
2039    dict.set_item(
2040        "fast_periods",
2041        combos
2042            .iter()
2043            .map(|c| c.fast_period.unwrap() as u64)
2044            .collect::<Vec<_>>()
2045            .into_pyarray(py),
2046    )?;
2047    dict.set_item(
2048        "slow_periods",
2049        combos
2050            .iter()
2051            .map(|c| c.slow_period.unwrap() as u64)
2052            .collect::<Vec<_>>()
2053            .into_pyarray(py),
2054    )?;
2055    dict.set_item(
2056        "k_periods",
2057        combos
2058            .iter()
2059            .map(|c| c.k_period.unwrap() as u64)
2060            .collect::<Vec<_>>()
2061            .into_pyarray(py),
2062    )?;
2063    dict.set_item(
2064        "d_periods",
2065        combos
2066            .iter()
2067            .map(|c| c.d_period.unwrap() as u64)
2068            .collect::<Vec<_>>()
2069            .into_pyarray(py),
2070    )?;
2071    let handle = make_device_array_py(device_id, inner)?;
2072    Ok((handle, dict))
2073}
2074
2075#[cfg(all(feature = "python", feature = "cuda"))]
2076#[pyfunction(name = "stc_cuda_many_series_one_param_dev")]
2077#[pyo3(signature = (data_tm_f32, cols, rows, fast_period=23, slow_period=50, k_period=10, d_period=3, device_id=0))]
2078pub fn stc_cuda_many_series_one_param_dev_py<'py>(
2079    py: Python<'py>,
2080    data_tm_f32: numpy::PyReadonlyArray1<'py, f32>,
2081    cols: usize,
2082    rows: usize,
2083    fast_period: usize,
2084    slow_period: usize,
2085    k_period: usize,
2086    d_period: usize,
2087    device_id: usize,
2088) -> PyResult<DeviceArrayF32Py> {
2089    use crate::cuda::cuda_available;
2090    if !cuda_available() {
2091        return Err(PyValueError::new_err("CUDA not available"));
2092    }
2093    let tm = data_tm_f32.as_slice()?;
2094    let params = StcParams {
2095        fast_period: Some(fast_period),
2096        slow_period: Some(slow_period),
2097        k_period: Some(k_period),
2098        d_period: Some(d_period),
2099        fast_ma_type: None,
2100        slow_ma_type: None,
2101    };
2102    let inner = py.allow_threads(|| {
2103        let cuda = CudaStc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2104        cuda.stc_many_series_one_param_time_major_dev(tm, cols, rows, &params)
2105            .map_err(|e| PyValueError::new_err(e.to_string()))
2106    })?;
2107    make_device_array_py(device_id, inner)
2108}
2109
2110#[inline(always)]
2111fn stc_batch_inner_into(
2112    data: &[f64],
2113    sweep: &StcBatchRange,
2114    kern: Kernel,
2115    parallel: bool,
2116    out: &mut [f64],
2117) -> Result<Vec<StcParams>, StcError> {
2118    let combos = expand_grid(sweep)?;
2119    if combos.is_empty() {
2120        return Err(StcError::InvalidRange {
2121            start: "range".into(),
2122            end: "range".into(),
2123            step: "empty".into(),
2124        });
2125    }
2126
2127    let len = data.len();
2128    if len == 0 {
2129        return Err(StcError::EmptyInputData);
2130    }
2131    let first = data
2132        .iter()
2133        .position(|x| !x.is_nan())
2134        .ok_or(StcError::AllValuesNaN)?;
2135
2136    let max_needed = combos
2137        .iter()
2138        .map(|c| {
2139            c.fast_period
2140                .unwrap()
2141                .max(c.slow_period.unwrap())
2142                .max(c.k_period.unwrap())
2143                .max(c.d_period.unwrap())
2144        })
2145        .max()
2146        .unwrap();
2147
2148    if (len - first) < max_needed {
2149        return Err(StcError::NotEnoughValidData {
2150            needed: max_needed,
2151            valid: len - first,
2152        });
2153    }
2154
2155    let rows = combos.len();
2156    let cols = len;
2157    let expected = rows
2158        .checked_mul(cols)
2159        .ok_or_else(|| StcError::InvalidRange {
2160            start: rows.to_string(),
2161            end: cols.to_string(),
2162            step: "rows*cols".into(),
2163        })?;
2164    if out.len() != expected {
2165        return Err(StcError::OutputLengthMismatch {
2166            expected,
2167            got: out.len(),
2168        });
2169    }
2170
2171    let mut out_mu = unsafe {
2172        core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
2173    };
2174    let warm: Vec<usize> = combos
2175        .iter()
2176        .map(|c| {
2177            first
2178                + c.fast_period
2179                    .unwrap()
2180                    .max(c.slow_period.unwrap())
2181                    .max(c.k_period.unwrap())
2182                    .max(c.d_period.unwrap())
2183                - 1
2184        })
2185        .collect();
2186    init_matrix_prefixes(&mut out_mu, cols, &warm);
2187
2188    let chosen = match kern {
2189        Kernel::Auto => detect_best_batch_kernel(),
2190        k => k,
2191    };
2192    let simd = match chosen {
2193        Kernel::Avx512Batch => Kernel::Avx512,
2194        Kernel::Avx2Batch => Kernel::Avx2,
2195        Kernel::ScalarBatch => Kernel::Scalar,
2196        Kernel::Avx512 => Kernel::Avx512,
2197        Kernel::Avx2 => Kernel::Avx2,
2198        Kernel::Scalar => Kernel::Scalar,
2199        _ => Kernel::Scalar,
2200    };
2201
2202    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
2203        match simd {
2204            Kernel::Scalar => stc_row_scalar(data, first, &combos[row], out_row),
2205            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2206            Kernel::Avx2 => stc_row_avx2(data, first, &combos[row], out_row),
2207            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2208            Kernel::Avx512 => stc_row_avx512(data, first, &combos[row], out_row),
2209            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
2210            Kernel::Avx2 | Kernel::Avx512 => stc_row_scalar(data, first, &combos[row], out_row),
2211            _ => unreachable!(),
2212        }
2213    };
2214
2215    if parallel {
2216        #[cfg(not(target_arch = "wasm32"))]
2217        {
2218            out_mu.par_chunks_mut(cols).enumerate().for_each(|(r, mr)| {
2219                let row_slice =
2220                    unsafe { core::slice::from_raw_parts_mut(mr.as_mut_ptr() as *mut f64, cols) };
2221                do_row(r, row_slice).unwrap();
2222            });
2223        }
2224        #[cfg(target_arch = "wasm32")]
2225        for (r, mr) in out_mu.chunks_mut(cols).enumerate() {
2226            let row_slice =
2227                unsafe { core::slice::from_raw_parts_mut(mr.as_mut_ptr() as *mut f64, cols) };
2228            do_row(r, row_slice).unwrap();
2229        }
2230    } else {
2231        for (r, mr) in out_mu.chunks_mut(cols).enumerate() {
2232            let row_slice =
2233                unsafe { core::slice::from_raw_parts_mut(mr.as_mut_ptr() as *mut f64, cols) };
2234            do_row(r, row_slice).unwrap();
2235        }
2236    }
2237
2238    Ok(combos)
2239}
2240
2241#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2242#[wasm_bindgen]
2243pub fn stc_js(
2244    data: &[f64],
2245    fast_period: usize,
2246    slow_period: usize,
2247    k_period: usize,
2248    d_period: usize,
2249    fast_ma_type: &str,
2250    slow_ma_type: &str,
2251) -> Result<Vec<f64>, JsValue> {
2252    let params = StcParams {
2253        fast_period: Some(fast_period),
2254        slow_period: Some(slow_period),
2255        k_period: Some(k_period),
2256        d_period: Some(d_period),
2257        fast_ma_type: Some(fast_ma_type.to_string()),
2258        slow_ma_type: Some(slow_ma_type.to_string()),
2259    };
2260    let input = StcInput::from_slice(data, params);
2261
2262    let mut output = vec![0.0; data.len()];
2263    stc_into_slice(&mut output, &input, Kernel::Auto)
2264        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2265
2266    Ok(output)
2267}
2268
2269#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2270#[wasm_bindgen]
2271pub fn stc_alloc(len: usize) -> *mut f64 {
2272    let mut vec = Vec::<f64>::with_capacity(len);
2273    let ptr = vec.as_mut_ptr();
2274    std::mem::forget(vec);
2275    ptr
2276}
2277
2278#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2279#[wasm_bindgen]
2280pub fn stc_free(ptr: *mut f64, len: usize) {
2281    if !ptr.is_null() {
2282        unsafe {
2283            let _ = Vec::from_raw_parts(ptr, len, len);
2284        }
2285    }
2286}
2287
2288#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2289#[wasm_bindgen]
2290pub fn stc_into(
2291    in_ptr: *const f64,
2292    out_ptr: *mut f64,
2293    len: usize,
2294    fast_period: usize,
2295    slow_period: usize,
2296    k_period: usize,
2297    d_period: usize,
2298    fast_ma_type: &str,
2299    slow_ma_type: &str,
2300) -> Result<(), JsValue> {
2301    if in_ptr.is_null() || out_ptr.is_null() {
2302        return Err(JsValue::from_str("Null pointer provided"));
2303    }
2304
2305    unsafe {
2306        let data = std::slice::from_raw_parts(in_ptr, len);
2307        let params = StcParams {
2308            fast_period: Some(fast_period),
2309            slow_period: Some(slow_period),
2310            k_period: Some(k_period),
2311            d_period: Some(d_period),
2312            fast_ma_type: Some(fast_ma_type.to_string()),
2313            slow_ma_type: Some(slow_ma_type.to_string()),
2314        };
2315        let input = StcInput::from_slice(data, params);
2316
2317        if in_ptr == out_ptr {
2318            let mut temp = vec![0.0; len];
2319            stc_into_slice(&mut temp, &input, Kernel::Auto)
2320                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2321            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2322            out.copy_from_slice(&temp);
2323        } else {
2324            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2325            stc_into_slice(out, &input, Kernel::Auto)
2326                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2327        }
2328
2329        Ok(())
2330    }
2331}
2332
2333#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2334#[derive(Serialize, Deserialize)]
2335pub struct StcBatchConfig {
2336    pub fast_period_range: (usize, usize, usize),
2337    pub slow_period_range: (usize, usize, usize),
2338    pub k_period_range: (usize, usize, usize),
2339    pub d_period_range: (usize, usize, usize),
2340}
2341
2342#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2343#[derive(Serialize, Deserialize)]
2344pub struct StcBatchJsOutput {
2345    pub values: Vec<f64>,
2346    pub combos: Vec<StcParams>,
2347    pub rows: usize,
2348    pub cols: usize,
2349}
2350
2351#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2352#[wasm_bindgen(js_name = stc_batch)]
2353pub fn stc_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2354    let config: StcBatchConfig = serde_wasm_bindgen::from_value(config)
2355        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2356
2357    let sweep = StcBatchRange {
2358        fast_period: config.fast_period_range,
2359        slow_period: config.slow_period_range,
2360        k_period: config.k_period_range,
2361        d_period: config.d_period_range,
2362    };
2363
2364    let result = stc_batch_with_kernel(data, &sweep, Kernel::Auto)
2365        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2366
2367    let output = StcBatchJsOutput {
2368        values: result.values,
2369        combos: result.combos,
2370        rows: result.rows,
2371        cols: result.cols,
2372    };
2373
2374    serde_wasm_bindgen::to_value(&output)
2375        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2376}
2377
2378#[cfg(test)]
2379mod tests {
2380    use super::*;
2381    use crate::skip_if_unsupported;
2382    use crate::utilities::data_loader::read_candles_from_csv;
2383
2384    fn check_stc_default_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2385        skip_if_unsupported!(kernel, test_name);
2386        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2387        let candles = read_candles_from_csv(file_path)?;
2388        let input = StcInput::with_default_candles(&candles);
2389        let output = stc_with_kernel(&input, kernel)?;
2390        assert_eq!(output.values.len(), candles.close.len());
2391        Ok(())
2392    }
2393
2394    #[test]
2395    fn test_stc_into_matches_api() -> Result<(), Box<dyn Error>> {
2396        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2397        let candles = read_candles_from_csv(file_path)?;
2398        let input = StcInput::with_default_candles(&candles);
2399
2400        let baseline = stc(&input)?;
2401
2402        let mut out = vec![0.0f64; baseline.values.len()];
2403        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2404        stc_into(&input, &mut out)?;
2405        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2406        stc_into_slice(&mut out, &input, Kernel::Auto)?;
2407
2408        assert_eq!(out.len(), baseline.values.len());
2409
2410        #[inline]
2411        fn eq_or_both_nan(a: f64, b: f64) -> bool {
2412            (a.is_nan() && b.is_nan()) || (a == b)
2413        }
2414
2415        for (i, (&a, &b)) in baseline.values.iter().zip(out.iter()).enumerate() {
2416            assert!(
2417                eq_or_both_nan(a, b),
2418                "Mismatch at idx {}: baseline={} into={}",
2419                i,
2420                a,
2421                b
2422            );
2423        }
2424
2425        Ok(())
2426    }
2427
2428    fn check_stc_last_five(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2429        skip_if_unsupported!(kernel, test_name);
2430        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2431        let candles = read_candles_from_csv(file_path)?;
2432        let input = StcInput::with_default_candles(&candles);
2433        let result = stc_with_kernel(&input, kernel)?;
2434        let expected = [
2435            0.21394384188858884,
2436            0.10697192094429442,
2437            0.05348596047214721,
2438            50.02674298023607,
2439            49.98686202668157,
2440        ];
2441        let n = result.values.len();
2442        for (i, &exp) in expected.iter().enumerate() {
2443            let val = result.values[n - 5 + i];
2444            assert!(
2445                (val - exp).abs() < 1e-5,
2446                "Expected {}, got {} at idx {}",
2447                exp,
2448                val,
2449                n - 5 + i
2450            );
2451        }
2452        Ok(())
2453    }
2454
2455    fn check_stc_with_slice_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2456        skip_if_unsupported!(kernel, test_name);
2457        let slice_data = [10.0, 11.0, 12.0, 13.0, 14.0];
2458        let params = StcParams {
2459            fast_period: Some(2),
2460            slow_period: Some(3),
2461            k_period: Some(2),
2462            d_period: Some(1),
2463            fast_ma_type: Some("ema".to_string()),
2464            slow_ma_type: Some("ema".to_string()),
2465        };
2466        let input = StcInput::from_slice(&slice_data, params);
2467        let result = stc_with_kernel(&input, kernel)?;
2468        assert_eq!(result.values.len(), slice_data.len());
2469        Ok(())
2470    }
2471
2472    fn check_stc_empty_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2473        skip_if_unsupported!(kernel, test_name);
2474        let data: [f64; 0] = [];
2475        let input = StcInput::from_slice(&data, StcParams::default());
2476        let result = stc_with_kernel(&input, kernel);
2477        assert!(result.is_err());
2478        Ok(())
2479    }
2480
2481    fn check_stc_all_nan_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2482        skip_if_unsupported!(kernel, test_name);
2483        let data = [f64::NAN, f64::NAN, f64::NAN];
2484        let input = StcInput::from_slice(&data, StcParams::default());
2485        let result = stc_with_kernel(&input, kernel);
2486        assert!(result.is_err());
2487        Ok(())
2488    }
2489
2490    fn check_stc_not_enough_valid_data(
2491        test_name: &str,
2492        kernel: Kernel,
2493    ) -> Result<(), Box<dyn Error>> {
2494        skip_if_unsupported!(kernel, test_name);
2495        let data = [f64::NAN, 2.0, 3.0];
2496        let params = StcParams {
2497            fast_period: Some(5),
2498            ..Default::default()
2499        };
2500        let input = StcInput::from_slice(&data, params);
2501        let result = stc_with_kernel(&input, kernel);
2502        assert!(result.is_err());
2503        Ok(())
2504    }
2505
2506    #[cfg(debug_assertions)]
2507    fn check_stc_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2508        skip_if_unsupported!(kernel, test_name);
2509
2510        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2511        let candles = read_candles_from_csv(file_path)?;
2512
2513        let test_params = vec![
2514            StcParams::default(),
2515            StcParams {
2516                fast_period: Some(2),
2517                slow_period: Some(3),
2518                k_period: Some(2),
2519                d_period: Some(1),
2520                fast_ma_type: Some("ema".to_string()),
2521                slow_ma_type: Some("ema".to_string()),
2522            },
2523            StcParams {
2524                fast_period: Some(5),
2525                slow_period: Some(10),
2526                k_period: Some(5),
2527                d_period: Some(2),
2528                fast_ma_type: Some("ema".to_string()),
2529                slow_ma_type: Some("ema".to_string()),
2530            },
2531            StcParams {
2532                fast_period: Some(10),
2533                slow_period: Some(20),
2534                k_period: Some(7),
2535                d_period: Some(3),
2536                fast_ma_type: Some("ema".to_string()),
2537                slow_ma_type: Some("ema".to_string()),
2538            },
2539            StcParams {
2540                fast_period: Some(20),
2541                slow_period: Some(40),
2542                k_period: Some(10),
2543                d_period: Some(5),
2544                fast_ma_type: Some("ema".to_string()),
2545                slow_ma_type: Some("ema".to_string()),
2546            },
2547            StcParams {
2548                fast_period: Some(30),
2549                slow_period: Some(60),
2550                k_period: Some(15),
2551                d_period: Some(7),
2552                fast_ma_type: Some("ema".to_string()),
2553                slow_ma_type: Some("ema".to_string()),
2554            },
2555            StcParams {
2556                fast_period: Some(50),
2557                slow_period: Some(100),
2558                k_period: Some(20),
2559                d_period: Some(10),
2560                fast_ma_type: Some("ema".to_string()),
2561                slow_ma_type: Some("ema".to_string()),
2562            },
2563            StcParams {
2564                fast_period: Some(2),
2565                slow_period: Some(2),
2566                k_period: Some(2),
2567                d_period: Some(1),
2568                fast_ma_type: Some("ema".to_string()),
2569                slow_ma_type: Some("ema".to_string()),
2570            },
2571            StcParams {
2572                fast_period: Some(25),
2573                slow_period: Some(15),
2574                k_period: Some(10),
2575                d_period: Some(3),
2576                fast_ma_type: Some("ema".to_string()),
2577                slow_ma_type: Some("ema".to_string()),
2578            },
2579        ];
2580
2581        for (param_idx, params) in test_params.iter().enumerate() {
2582            let input = StcInput::from_candles(&candles, "close", params.clone());
2583            let output = stc_with_kernel(&input, kernel)?;
2584
2585            for (i, &val) in output.values.iter().enumerate() {
2586                if val.is_nan() {
2587                    continue;
2588                }
2589
2590                let bits = val.to_bits();
2591
2592                if bits == 0x11111111_11111111 {
2593                    panic!(
2594                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2595						 with params: fast={}, slow={}, k={}, d={} (param set {})",
2596                        test_name,
2597                        val,
2598                        bits,
2599                        i,
2600                        params.fast_period.unwrap_or(23),
2601                        params.slow_period.unwrap_or(50),
2602                        params.k_period.unwrap_or(10),
2603                        params.d_period.unwrap_or(3),
2604                        param_idx
2605                    );
2606                }
2607
2608                if bits == 0x22222222_22222222 {
2609                    panic!(
2610                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2611						 with params: fast={}, slow={}, k={}, d={} (param set {})",
2612                        test_name,
2613                        val,
2614                        bits,
2615                        i,
2616                        params.fast_period.unwrap_or(23),
2617                        params.slow_period.unwrap_or(50),
2618                        params.k_period.unwrap_or(10),
2619                        params.d_period.unwrap_or(3),
2620                        param_idx
2621                    );
2622                }
2623
2624                if bits == 0x33333333_33333333 {
2625                    panic!(
2626                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2627						 with params: fast={}, slow={}, k={}, d={} (param set {})",
2628                        test_name,
2629                        val,
2630                        bits,
2631                        i,
2632                        params.fast_period.unwrap_or(23),
2633                        params.slow_period.unwrap_or(50),
2634                        params.k_period.unwrap_or(10),
2635                        params.d_period.unwrap_or(3),
2636                        param_idx
2637                    );
2638                }
2639            }
2640        }
2641
2642        Ok(())
2643    }
2644
2645    #[cfg(not(debug_assertions))]
2646    fn check_stc_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2647        Ok(())
2648    }
2649
2650    macro_rules! generate_all_stc_tests {
2651        ($($test_fn:ident),*) => {
2652            paste::paste! {
2653                $(#[test] fn [<$test_fn _scalar_f64>]() { let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar); })*
2654                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2655                $(
2656                    #[test] fn [<$test_fn _avx2_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2); }
2657                    #[test] fn [<$test_fn _avx512_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512); }
2658                )*
2659            }
2660        }
2661    }
2662    generate_all_stc_tests!(
2663        check_stc_default_params,
2664        check_stc_last_five,
2665        check_stc_with_slice_data,
2666        check_stc_empty_data,
2667        check_stc_all_nan_data,
2668        check_stc_not_enough_valid_data,
2669        check_stc_no_poison
2670    );
2671
2672    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2673        skip_if_unsupported!(kernel, test);
2674        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2675        let c = read_candles_from_csv(file)?;
2676        let output = StcBatchBuilder::new()
2677            .kernel(kernel)
2678            .apply_candles(&c, "close")?;
2679        let def = StcParams::default();
2680        let row = output.values_for(&def).expect("default row missing");
2681        assert_eq!(row.len(), c.close.len());
2682        Ok(())
2683    }
2684
2685    #[cfg(debug_assertions)]
2686    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2687        skip_if_unsupported!(kernel, test);
2688
2689        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2690        let c = read_candles_from_csv(file)?;
2691
2692        let test_configs = vec![
2693            (2, 10, 2, 3, 15, 3, 2, 8, 2, 1, 3, 1),
2694            (5, 25, 5, 10, 50, 10, 5, 15, 5, 2, 5, 1),
2695            (20, 40, 10, 40, 80, 20, 10, 20, 5, 3, 6, 1),
2696            (2, 5, 1, 3, 6, 1, 2, 4, 1, 1, 2, 1),
2697            (10, 10, 0, 20, 20, 0, 10, 10, 0, 3, 3, 0),
2698            (15, 30, 5, 30, 60, 10, 7, 14, 7, 3, 5, 2),
2699            (50, 100, 25, 100, 200, 50, 20, 30, 10, 5, 10, 5),
2700        ];
2701
2702        for (
2703            cfg_idx,
2704            &(
2705                f_start,
2706                f_end,
2707                f_step,
2708                s_start,
2709                s_end,
2710                s_step,
2711                k_start,
2712                k_end,
2713                k_step,
2714                d_start,
2715                d_end,
2716                d_step,
2717            ),
2718        ) in test_configs.iter().enumerate()
2719        {
2720            let output = StcBatchBuilder::new()
2721                .kernel(kernel)
2722                .fast_period_range(f_start, f_end, f_step)
2723                .slow_period_range(s_start, s_end, s_step)
2724                .k_period_range(k_start, k_end, k_step)
2725                .d_period_range(d_start, d_end, d_step)
2726                .apply_candles(&c, "close")?;
2727
2728            for (idx, &val) in output.values.iter().enumerate() {
2729                if val.is_nan() {
2730                    continue;
2731                }
2732
2733                let bits = val.to_bits();
2734                let row = idx / output.cols;
2735                let col = idx % output.cols;
2736                let combo = &output.combos[row];
2737
2738                if bits == 0x11111111_11111111 {
2739                    panic!(
2740                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2741						 at row {} col {} (flat index {}) with params: fast={}, slow={}, k={}, d={}",
2742                        test,
2743                        cfg_idx,
2744                        val,
2745                        bits,
2746                        row,
2747                        col,
2748                        idx,
2749                        combo.fast_period.unwrap_or(23),
2750                        combo.slow_period.unwrap_or(50),
2751                        combo.k_period.unwrap_or(10),
2752                        combo.d_period.unwrap_or(3)
2753                    );
2754                }
2755
2756                if bits == 0x22222222_22222222 {
2757                    panic!(
2758                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2759						 at row {} col {} (flat index {}) with params: fast={}, slow={}, k={}, d={}",
2760                        test,
2761                        cfg_idx,
2762                        val,
2763                        bits,
2764                        row,
2765                        col,
2766                        idx,
2767                        combo.fast_period.unwrap_or(23),
2768                        combo.slow_period.unwrap_or(50),
2769                        combo.k_period.unwrap_or(10),
2770                        combo.d_period.unwrap_or(3)
2771                    );
2772                }
2773
2774                if bits == 0x33333333_33333333 {
2775                    panic!(
2776                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2777						 at row {} col {} (flat index {}) with params: fast={}, slow={}, k={}, d={}",
2778                        test,
2779                        cfg_idx,
2780                        val,
2781                        bits,
2782                        row,
2783                        col,
2784                        idx,
2785                        combo.fast_period.unwrap_or(23),
2786                        combo.slow_period.unwrap_or(50),
2787                        combo.k_period.unwrap_or(10),
2788                        combo.d_period.unwrap_or(3)
2789                    );
2790                }
2791            }
2792        }
2793
2794        Ok(())
2795    }
2796
2797    #[cfg(not(debug_assertions))]
2798    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2799        Ok(())
2800    }
2801
2802    macro_rules! gen_batch_tests {
2803        ($fn_name:ident) => {
2804            paste::paste! {
2805                #[test] fn [<$fn_name _scalar>]()      {
2806                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2807                }
2808                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2809                #[test] fn [<$fn_name _avx2>]()        {
2810                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2811                }
2812                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2813                #[test] fn [<$fn_name _avx512>]()      {
2814                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2815                }
2816                #[test] fn [<$fn_name _auto_detect>]() {
2817                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2818                }
2819            }
2820        };
2821    }
2822    gen_batch_tests!(check_batch_default_row);
2823    gen_batch_tests!(check_batch_no_poison);
2824}