Skip to main content

vector_ta/indicators/
multi_length_stochastic_average.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::{PyDict, PyList};
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::convert::AsRef;
28use std::mem::ManuallyDrop;
29use thiserror::Error;
30
31const DEFAULT_LENGTH: usize = 14;
32const DEFAULT_SOURCE: &str = "close";
33const DEFAULT_PRESMOOTH: usize = 10;
34const DEFAULT_POSTSMOOTH: usize = 10;
35const DEFAULT_SMOOTHING_METHOD: &str = "sma";
36const MIN_STOCH_LENGTH: usize = 4;
37const FLOAT_TOL: f64 = 1e-12;
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40enum SmoothingMethod {
41    None,
42    Sma,
43    Tma,
44    Lsma,
45}
46
47impl SmoothingMethod {
48    #[inline]
49    fn parse(name: &str) -> Option<Self> {
50        if name.eq_ignore_ascii_case("none") {
51            Some(Self::None)
52        } else if name.eq_ignore_ascii_case("sma") {
53            Some(Self::Sma)
54        } else if name.eq_ignore_ascii_case("tma") {
55            Some(Self::Tma)
56        } else if name.eq_ignore_ascii_case("lsma") {
57            Some(Self::Lsma)
58        } else {
59            None
60        }
61    }
62
63    #[inline]
64    fn as_str(self) -> &'static str {
65        match self {
66            Self::None => "none",
67            Self::Sma => "sma",
68            Self::Tma => "tma",
69            Self::Lsma => "lsma",
70        }
71    }
72}
73
74impl<'a> AsRef<[f64]> for MultiLengthStochasticAverageInput<'a> {
75    #[inline(always)]
76    fn as_ref(&self) -> &[f64] {
77        match &self.data {
78            MultiLengthStochasticAverageData::Slice(slice) => slice,
79            MultiLengthStochasticAverageData::Candles { candles, source } => {
80                source_type(candles, source)
81            }
82        }
83    }
84}
85
86#[derive(Debug, Clone)]
87pub enum MultiLengthStochasticAverageData<'a> {
88    Candles {
89        candles: &'a Candles,
90        source: &'a str,
91    },
92    Slice(&'a [f64]),
93}
94
95#[derive(Debug, Clone)]
96pub struct MultiLengthStochasticAverageOutput {
97    pub values: Vec<f64>,
98}
99
100#[derive(Debug, Clone, PartialEq)]
101#[cfg_attr(
102    all(target_arch = "wasm32", feature = "wasm"),
103    derive(Serialize, Deserialize)
104)]
105pub struct MultiLengthStochasticAverageParams {
106    pub length: Option<usize>,
107    pub presmooth: Option<usize>,
108    pub premethod: Option<String>,
109    pub postsmooth: Option<usize>,
110    pub postmethod: Option<String>,
111}
112
113impl Default for MultiLengthStochasticAverageParams {
114    fn default() -> Self {
115        Self {
116            length: Some(DEFAULT_LENGTH),
117            presmooth: Some(DEFAULT_PRESMOOTH),
118            premethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
119            postsmooth: Some(DEFAULT_POSTSMOOTH),
120            postmethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
121        }
122    }
123}
124
125#[derive(Debug, Clone)]
126pub struct MultiLengthStochasticAverageInput<'a> {
127    pub data: MultiLengthStochasticAverageData<'a>,
128    pub params: MultiLengthStochasticAverageParams,
129}
130
131impl<'a> MultiLengthStochasticAverageInput<'a> {
132    #[inline]
133    pub fn from_candles(
134        candles: &'a Candles,
135        source: &'a str,
136        params: MultiLengthStochasticAverageParams,
137    ) -> Self {
138        Self {
139            data: MultiLengthStochasticAverageData::Candles { candles, source },
140            params,
141        }
142    }
143
144    #[inline]
145    pub fn from_slice(slice: &'a [f64], params: MultiLengthStochasticAverageParams) -> Self {
146        Self {
147            data: MultiLengthStochasticAverageData::Slice(slice),
148            params,
149        }
150    }
151
152    #[inline]
153    pub fn with_default_candles(candles: &'a Candles) -> Self {
154        Self::from_candles(
155            candles,
156            DEFAULT_SOURCE,
157            MultiLengthStochasticAverageParams::default(),
158        )
159    }
160}
161
162#[derive(Clone, Debug)]
163pub struct MultiLengthStochasticAverageBuilder {
164    length: Option<usize>,
165    presmooth: Option<usize>,
166    premethod: Option<String>,
167    postsmooth: Option<usize>,
168    postmethod: Option<String>,
169    kernel: Kernel,
170}
171
172impl Default for MultiLengthStochasticAverageBuilder {
173    fn default() -> Self {
174        Self {
175            length: None,
176            presmooth: None,
177            premethod: None,
178            postsmooth: None,
179            postmethod: None,
180            kernel: Kernel::Auto,
181        }
182    }
183}
184
185impl MultiLengthStochasticAverageBuilder {
186    #[inline]
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    #[inline]
192    pub fn length(mut self, length: usize) -> Self {
193        self.length = Some(length);
194        self
195    }
196
197    #[inline]
198    pub fn presmooth(mut self, presmooth: usize) -> Self {
199        self.presmooth = Some(presmooth);
200        self
201    }
202
203    #[inline]
204    pub fn premethod<T: Into<String>>(mut self, premethod: T) -> Self {
205        self.premethod = Some(premethod.into());
206        self
207    }
208
209    #[inline]
210    pub fn postsmooth(mut self, postsmooth: usize) -> Self {
211        self.postsmooth = Some(postsmooth);
212        self
213    }
214
215    #[inline]
216    pub fn postmethod<T: Into<String>>(mut self, postmethod: T) -> Self {
217        self.postmethod = Some(postmethod.into());
218        self
219    }
220
221    #[inline]
222    pub fn kernel(mut self, kernel: Kernel) -> Self {
223        self.kernel = kernel;
224        self
225    }
226
227    #[inline]
228    pub fn apply(
229        self,
230        candles: &Candles,
231        source: &str,
232    ) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
233        let input = MultiLengthStochasticAverageInput::from_candles(
234            candles,
235            source,
236            MultiLengthStochasticAverageParams {
237                length: self.length,
238                presmooth: self.presmooth,
239                premethod: self.premethod,
240                postsmooth: self.postsmooth,
241                postmethod: self.postmethod,
242            },
243        );
244        multi_length_stochastic_average_with_kernel(&input, self.kernel)
245    }
246
247    #[inline]
248    pub fn apply_slice(
249        self,
250        data: &[f64],
251    ) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
252        let input = MultiLengthStochasticAverageInput::from_slice(
253            data,
254            MultiLengthStochasticAverageParams {
255                length: self.length,
256                presmooth: self.presmooth,
257                premethod: self.premethod,
258                postsmooth: self.postsmooth,
259                postmethod: self.postmethod,
260            },
261        );
262        multi_length_stochastic_average_with_kernel(&input, self.kernel)
263    }
264
265    #[inline]
266    pub fn into_stream(
267        self,
268    ) -> Result<MultiLengthStochasticAverageStream, MultiLengthStochasticAverageError> {
269        MultiLengthStochasticAverageStream::try_new(MultiLengthStochasticAverageParams {
270            length: self.length,
271            presmooth: self.presmooth,
272            premethod: self.premethod,
273            postsmooth: self.postsmooth,
274            postmethod: self.postmethod,
275        })
276    }
277}
278
279#[derive(Debug, Error)]
280pub enum MultiLengthStochasticAverageError {
281    #[error("multi_length_stochastic_average: Input data slice is empty.")]
282    EmptyInputData,
283    #[error("multi_length_stochastic_average: All values are NaN.")]
284    AllValuesNaN,
285    #[error(
286        "multi_length_stochastic_average: Invalid length: length = {length}, data length = {data_len}"
287    )]
288    InvalidLength { length: usize, data_len: usize },
289    #[error("multi_length_stochastic_average: Invalid presmooth: {presmooth}")]
290    InvalidPresmooth { presmooth: usize },
291    #[error("multi_length_stochastic_average: Invalid postsmooth: {postsmooth}")]
292    InvalidPostsmooth { postsmooth: usize },
293    #[error("multi_length_stochastic_average: Invalid premethod: {premethod}")]
294    InvalidPreMethod { premethod: String },
295    #[error("multi_length_stochastic_average: Invalid postmethod: {postmethod}")]
296    InvalidPostMethod { postmethod: String },
297    #[error(
298        "multi_length_stochastic_average: Not enough valid data: needed = {needed}, valid = {valid}"
299    )]
300    NotEnoughValidData { needed: usize, valid: usize },
301    #[error(
302        "multi_length_stochastic_average: Output length mismatch: expected = {expected}, got = {got}"
303    )]
304    OutputLengthMismatch { expected: usize, got: usize },
305    #[error(
306        "multi_length_stochastic_average: Invalid range: start={start}, end={end}, step={step}"
307    )]
308    InvalidRange {
309        start: String,
310        end: String,
311        step: String,
312    },
313    #[error("multi_length_stochastic_average: Invalid kernel for batch: {0:?}")]
314    InvalidKernelForBatch(Kernel),
315}
316
317#[derive(Clone, Copy, Debug)]
318struct ResolvedParams {
319    length: usize,
320    presmooth: usize,
321    premethod: SmoothingMethod,
322    postsmooth: usize,
323    postmethod: SmoothingMethod,
324}
325
326#[inline(always)]
327fn first_valid_value(data: &[f64]) -> usize {
328    let mut i = 0usize;
329    while i < data.len() {
330        if data[i].is_finite() {
331            return i;
332        }
333        i += 1;
334    }
335    data.len()
336}
337
338#[inline(always)]
339fn max_consecutive_valid_values(data: &[f64]) -> usize {
340    let mut best = 0usize;
341    let mut run = 0usize;
342    for &value in data {
343        if value.is_finite() {
344            run += 1;
345            if run > best {
346                best = run;
347            }
348        } else {
349            run = 0;
350        }
351    }
352    best
353}
354
355#[inline(always)]
356fn smoothing_warmup(method: SmoothingMethod, length: usize) -> usize {
357    match method {
358        SmoothingMethod::None => 0,
359        SmoothingMethod::Sma | SmoothingMethod::Lsma => length.saturating_sub(1),
360        SmoothingMethod::Tma => length.saturating_sub(1).saturating_mul(2),
361    }
362}
363
364#[inline(always)]
365fn total_warmup(params: ResolvedParams) -> usize {
366    smoothing_warmup(params.premethod, params.presmooth)
367        + params.length.saturating_sub(1)
368        + smoothing_warmup(params.postmethod, params.postsmooth)
369}
370
371#[inline(always)]
372fn canonical_method_name(name: Option<&str>, default: &str) -> String {
373    name.unwrap_or(default).to_ascii_lowercase()
374}
375
376#[inline]
377fn resolve_params(
378    params: &MultiLengthStochasticAverageParams,
379    data_len: Option<usize>,
380) -> Result<ResolvedParams, MultiLengthStochasticAverageError> {
381    let length = params.length.unwrap_or(DEFAULT_LENGTH);
382    let presmooth = params.presmooth.unwrap_or(DEFAULT_PRESMOOTH);
383    let postsmooth = params.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH);
384    let premethod_name =
385        canonical_method_name(params.premethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
386    let postmethod_name =
387        canonical_method_name(params.postmethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
388    let premethod = SmoothingMethod::parse(&premethod_name).ok_or_else(|| {
389        MultiLengthStochasticAverageError::InvalidPreMethod {
390            premethod: premethod_name.clone(),
391        }
392    })?;
393    let postmethod = SmoothingMethod::parse(&postmethod_name).ok_or_else(|| {
394        MultiLengthStochasticAverageError::InvalidPostMethod {
395            postmethod: postmethod_name.clone(),
396        }
397    })?;
398
399    if length < MIN_STOCH_LENGTH {
400        return Err(MultiLengthStochasticAverageError::InvalidLength {
401            length,
402            data_len: data_len.unwrap_or(0),
403        });
404    }
405    if presmooth == 0 {
406        return Err(MultiLengthStochasticAverageError::InvalidPresmooth { presmooth });
407    }
408    if postsmooth == 0 {
409        return Err(MultiLengthStochasticAverageError::InvalidPostsmooth { postsmooth });
410    }
411    if let Some(data_len) = data_len {
412        if length > data_len {
413            return Err(MultiLengthStochasticAverageError::InvalidLength { length, data_len });
414        }
415    }
416
417    Ok(ResolvedParams {
418        length,
419        presmooth,
420        premethod,
421        postsmooth,
422        postmethod,
423    })
424}
425
426#[derive(Clone, Debug)]
427struct SmaState {
428    ring: Vec<f64>,
429    head: usize,
430    count: usize,
431    sum: f64,
432}
433
434impl SmaState {
435    #[inline]
436    fn new(length: usize) -> Self {
437        Self {
438            ring: vec![0.0; length.max(1)],
439            head: 0,
440            count: 0,
441            sum: 0.0,
442        }
443    }
444
445    #[inline]
446    fn reset(&mut self) {
447        self.head = 0;
448        self.count = 0;
449        self.sum = 0.0;
450    }
451
452    #[inline]
453    fn update(&mut self, value: f64) -> Option<f64> {
454        let len = self.ring.len();
455        if self.count == len {
456            self.sum -= self.ring[self.head];
457        } else {
458            self.count += 1;
459        }
460        self.ring[self.head] = value;
461        self.sum += value;
462        self.head += 1;
463        if self.head == len {
464            self.head = 0;
465        }
466        if self.count == len {
467            Some(self.sum / len as f64)
468        } else {
469            None
470        }
471    }
472}
473
474#[derive(Clone, Debug)]
475struct LsmaState {
476    ring: Vec<f64>,
477    head: usize,
478    count: usize,
479    sum_y: f64,
480    sum_xy: f64,
481    x_sum: f64,
482    denom: f64,
483}
484
485impl LsmaState {
486    #[inline]
487    fn new(length: usize) -> Self {
488        let n = length.max(1);
489        let n_f = n as f64;
490        let x_sum = ((n * (n - 1)) / 2) as f64;
491        let x2_sum = ((n * (n - 1) * (2 * n - 1)) / 6) as f64;
492        Self {
493            ring: vec![0.0; n],
494            head: 0,
495            count: 0,
496            sum_y: 0.0,
497            sum_xy: 0.0,
498            x_sum,
499            denom: n_f * x2_sum - x_sum * x_sum,
500        }
501    }
502
503    #[inline]
504    fn reset(&mut self) {
505        self.head = 0;
506        self.count = 0;
507        self.sum_y = 0.0;
508        self.sum_xy = 0.0;
509    }
510
511    #[inline]
512    fn update(&mut self, value: f64) -> Option<f64> {
513        let n = self.ring.len();
514        if self.count < n {
515            let idx = self.count;
516            self.ring[self.head] = value;
517            self.head += 1;
518            if self.head == n {
519                self.head = 0;
520            }
521            self.count += 1;
522            self.sum_y += value;
523            self.sum_xy += idx as f64 * value;
524            if self.count == n {
525                Some(self.endpoint())
526            } else {
527                None
528            }
529        } else {
530            let old = self.ring[self.head];
531            let old_sum_y = self.sum_y;
532            self.ring[self.head] = value;
533            self.head += 1;
534            if self.head == n {
535                self.head = 0;
536            }
537            self.sum_y = old_sum_y - old + value;
538            self.sum_xy = self.sum_xy - (old_sum_y - old) + (n - 1) as f64 * value;
539            Some(self.endpoint())
540        }
541    }
542
543    #[inline]
544    fn endpoint(&self) -> f64 {
545        let n = self.ring.len() as f64;
546        let slope = (n * self.sum_xy - self.x_sum * self.sum_y) / self.denom;
547        let intercept = (self.sum_y - slope * self.x_sum) / n;
548        intercept + slope * (self.ring.len() - 1) as f64
549    }
550}
551
552#[derive(Clone, Debug)]
553enum SmoothingState {
554    None,
555    Sma(SmaState),
556    Tma { inner: SmaState, outer: SmaState },
557    Lsma(LsmaState),
558}
559
560impl SmoothingState {
561    #[inline]
562    fn new(method: SmoothingMethod, length: usize) -> Self {
563        match method {
564            SmoothingMethod::None => Self::None,
565            SmoothingMethod::Sma => Self::Sma(SmaState::new(length)),
566            SmoothingMethod::Tma => Self::Tma {
567                inner: SmaState::new(length),
568                outer: SmaState::new(length),
569            },
570            SmoothingMethod::Lsma => Self::Lsma(LsmaState::new(length)),
571        }
572    }
573
574    #[inline]
575    fn reset(&mut self) {
576        match self {
577            Self::None => {}
578            Self::Sma(state) => state.reset(),
579            Self::Tma { inner, outer } => {
580                inner.reset();
581                outer.reset();
582            }
583            Self::Lsma(state) => state.reset(),
584        }
585    }
586
587    #[inline]
588    fn update(&mut self, value: f64) -> Option<f64> {
589        match self {
590            Self::None => Some(value),
591            Self::Sma(state) => state.update(value),
592            Self::Tma { inner, outer } => inner.update(value).and_then(|x| outer.update(x)),
593            Self::Lsma(state) => state.update(value),
594        }
595    }
596}
597
598#[derive(Clone, Debug)]
599pub struct MultiLengthStochasticAverageStream {
600    params: ResolvedParams,
601    pre_smoother: SmoothingState,
602    post_smoother: SmoothingState,
603    ring: Vec<f64>,
604    head: usize,
605    count: usize,
606}
607
608impl MultiLengthStochasticAverageStream {
609    #[inline]
610    pub fn try_new(
611        params: MultiLengthStochasticAverageParams,
612    ) -> Result<Self, MultiLengthStochasticAverageError> {
613        let params = resolve_params(&params, None)?;
614        Ok(Self::new_resolved(params))
615    }
616
617    #[inline]
618    fn new_resolved(params: ResolvedParams) -> Self {
619        Self {
620            params,
621            pre_smoother: SmoothingState::new(params.premethod, params.presmooth),
622            post_smoother: SmoothingState::new(params.postmethod, params.postsmooth),
623            ring: vec![0.0; params.length],
624            head: 0,
625            count: 0,
626        }
627    }
628
629    #[inline]
630    pub fn reset(&mut self) {
631        self.pre_smoother.reset();
632        self.post_smoother.reset();
633        self.head = 0;
634        self.count = 0;
635    }
636
637    #[inline]
638    pub fn get_warmup_period(&self) -> usize {
639        total_warmup(self.params)
640    }
641
642    #[inline]
643    pub fn update(&mut self, value: f64) -> Option<f64> {
644        if !value.is_finite() {
645            self.reset();
646            return None;
647        }
648
649        let pre = self.pre_smoother.update(value)?;
650        self.push_pre_value(pre);
651        if self.count < self.params.length {
652            return None;
653        }
654
655        let norm = self.current_norm()?;
656        self.post_smoother.update(norm)
657    }
658
659    #[inline]
660    fn push_pre_value(&mut self, value: f64) {
661        self.ring[self.head] = value;
662        self.head += 1;
663        if self.head == self.params.length {
664            self.head = 0;
665        }
666        if self.count < self.params.length {
667            self.count += 1;
668        }
669    }
670
671    #[inline]
672    fn current_norm(&mut self) -> Option<f64> {
673        let len = self.params.length;
674        let newest = (self.head + len - 1) % len;
675        let current = self.ring[newest];
676        let mut min_value = current;
677        let mut max_value = current;
678        let mut idx = newest;
679        let mut sum = 0.0;
680
681        for window in 1..=len {
682            let value = self.ring[idx];
683            if value < min_value {
684                min_value = value;
685            }
686            if value > max_value {
687                max_value = value;
688            }
689            if window >= MIN_STOCH_LENGTH {
690                let denom = max_value - min_value;
691                if denom.abs() <= FLOAT_TOL {
692                    self.post_smoother.reset();
693                    return None;
694                }
695                sum += (current - min_value) / denom;
696            }
697            idx = if idx == 0 { len - 1 } else { idx - 1 };
698        }
699
700        Some(sum / (len - (MIN_STOCH_LENGTH - 1)) as f64 * 100.0)
701    }
702}
703
704#[inline(always)]
705fn multi_length_stochastic_average_prepare<'a>(
706    input: &'a MultiLengthStochasticAverageInput,
707    kernel: Kernel,
708) -> Result<(&'a [f64], usize, ResolvedParams, Kernel), MultiLengthStochasticAverageError> {
709    let data = input.as_ref();
710    if data.is_empty() {
711        return Err(MultiLengthStochasticAverageError::EmptyInputData);
712    }
713
714    let first = first_valid_value(data);
715    if first >= data.len() {
716        return Err(MultiLengthStochasticAverageError::AllValuesNaN);
717    }
718
719    let params = resolve_params(&input.params, Some(data.len()))?;
720    let needed = total_warmup(params) + 1;
721    let valid = max_consecutive_valid_values(data);
722    if valid < needed {
723        return Err(MultiLengthStochasticAverageError::NotEnoughValidData { needed, valid });
724    }
725
726    let chosen = match kernel {
727        Kernel::Auto => detect_best_kernel(),
728        other => other.to_non_batch(),
729    };
730
731    Ok((data, first, params, chosen))
732}
733
734#[inline(always)]
735fn multi_length_stochastic_average_row_from_slice(
736    data: &[f64],
737    params: ResolvedParams,
738    out: &mut [f64],
739) {
740    out.fill(f64::NAN);
741    let mut stream = MultiLengthStochasticAverageStream::new_resolved(params);
742    for (slot, &value) in out.iter_mut().zip(data.iter()) {
743        if let Some(result) = stream.update(value) {
744            *slot = result;
745        }
746    }
747}
748
749#[inline]
750pub fn multi_length_stochastic_average(
751    input: &MultiLengthStochasticAverageInput,
752) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
753    multi_length_stochastic_average_with_kernel(input, Kernel::Auto)
754}
755
756#[inline]
757pub fn multi_length_stochastic_average_with_kernel(
758    input: &MultiLengthStochasticAverageInput,
759    kernel: Kernel,
760) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
761    let (data, first, params, _chosen) = multi_length_stochastic_average_prepare(input, kernel)?;
762    let warmup = first.saturating_add(total_warmup(params)).min(data.len());
763    let mut values = alloc_with_nan_prefix(data.len(), warmup);
764    multi_length_stochastic_average_row_from_slice(data, params, &mut values);
765    Ok(MultiLengthStochasticAverageOutput { values })
766}
767
768#[inline]
769pub fn multi_length_stochastic_average_into_slice(
770    dst: &mut [f64],
771    input: &MultiLengthStochasticAverageInput,
772    kernel: Kernel,
773) -> Result<(), MultiLengthStochasticAverageError> {
774    let expected = input.as_ref().len();
775    if dst.len() != expected {
776        return Err(MultiLengthStochasticAverageError::OutputLengthMismatch {
777            expected,
778            got: dst.len(),
779        });
780    }
781    let (data, _first, params, _chosen) = multi_length_stochastic_average_prepare(input, kernel)?;
782    multi_length_stochastic_average_row_from_slice(data, params, dst);
783    Ok(())
784}
785
786#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
787#[inline]
788pub fn multi_length_stochastic_average_into(
789    input: &MultiLengthStochasticAverageInput,
790    out: &mut [f64],
791) -> Result<(), MultiLengthStochasticAverageError> {
792    multi_length_stochastic_average_into_slice(out, input, Kernel::Auto)
793}
794
795#[derive(Debug, Clone, PartialEq)]
796#[cfg_attr(
797    all(target_arch = "wasm32", feature = "wasm"),
798    derive(Serialize, Deserialize)
799)]
800pub struct MultiLengthStochasticAverageBatchRange {
801    pub length: (usize, usize, usize),
802    pub presmooth: (usize, usize, usize),
803    pub postsmooth: (usize, usize, usize),
804    pub premethod: Option<String>,
805    pub postmethod: Option<String>,
806}
807
808impl Default for MultiLengthStochasticAverageBatchRange {
809    fn default() -> Self {
810        Self {
811            length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
812            presmooth: (DEFAULT_PRESMOOTH, DEFAULT_PRESMOOTH, 0),
813            postsmooth: (DEFAULT_POSTSMOOTH, DEFAULT_POSTSMOOTH, 0),
814            premethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
815            postmethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
816        }
817    }
818}
819
820#[derive(Clone, Debug)]
821pub struct MultiLengthStochasticAverageBatchOutput {
822    pub values: Vec<f64>,
823    pub combos: Vec<MultiLengthStochasticAverageParams>,
824    pub rows: usize,
825    pub cols: usize,
826}
827
828impl MultiLengthStochasticAverageBatchOutput {
829    #[inline]
830    pub fn row_for_params(&self, params: &MultiLengthStochasticAverageParams) -> Option<usize> {
831        let target = MultiLengthStochasticAverageParams {
832            length: Some(params.length.unwrap_or(DEFAULT_LENGTH)),
833            presmooth: Some(params.presmooth.unwrap_or(DEFAULT_PRESMOOTH)),
834            premethod: Some(canonical_method_name(
835                params.premethod.as_deref(),
836                DEFAULT_SMOOTHING_METHOD,
837            )),
838            postsmooth: Some(params.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH)),
839            postmethod: Some(canonical_method_name(
840                params.postmethod.as_deref(),
841                DEFAULT_SMOOTHING_METHOD,
842            )),
843        };
844        self.combos.iter().position(|combo| combo == &target)
845    }
846
847    #[inline]
848    pub fn values_for(&self, params: &MultiLengthStochasticAverageParams) -> Option<&[f64]> {
849        self.row_for_params(params).map(|row| {
850            let start = row * self.cols;
851            &self.values[start..start + self.cols]
852        })
853    }
854}
855
856#[derive(Clone, Debug)]
857pub struct MultiLengthStochasticAverageBatchBuilder {
858    range: MultiLengthStochasticAverageBatchRange,
859    kernel: Kernel,
860}
861
862impl Default for MultiLengthStochasticAverageBatchBuilder {
863    fn default() -> Self {
864        Self {
865            range: MultiLengthStochasticAverageBatchRange::default(),
866            kernel: Kernel::Auto,
867        }
868    }
869}
870
871impl MultiLengthStochasticAverageBatchBuilder {
872    #[inline]
873    pub fn new() -> Self {
874        Self::default()
875    }
876
877    #[inline]
878    pub fn kernel(mut self, kernel: Kernel) -> Self {
879        self.kernel = kernel;
880        self
881    }
882
883    #[inline]
884    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
885        self.range.length = (start, end, step);
886        self
887    }
888
889    #[inline]
890    pub fn presmooth_range(mut self, start: usize, end: usize, step: usize) -> Self {
891        self.range.presmooth = (start, end, step);
892        self
893    }
894
895    #[inline]
896    pub fn postsmooth_range(mut self, start: usize, end: usize, step: usize) -> Self {
897        self.range.postsmooth = (start, end, step);
898        self
899    }
900
901    #[inline]
902    pub fn premethod<T: Into<String>>(mut self, premethod: T) -> Self {
903        self.range.premethod = Some(premethod.into());
904        self
905    }
906
907    #[inline]
908    pub fn postmethod<T: Into<String>>(mut self, postmethod: T) -> Self {
909        self.range.postmethod = Some(postmethod.into());
910        self
911    }
912
913    #[inline]
914    pub fn apply_slice(
915        self,
916        data: &[f64],
917    ) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
918        multi_length_stochastic_average_batch_with_kernel(data, &self.range, self.kernel)
919    }
920
921    #[inline]
922    pub fn apply_candles(
923        self,
924        candles: &Candles,
925        source: &str,
926    ) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
927        self.apply_slice(source_type(candles, source))
928    }
929}
930
931#[inline]
932fn expand_axis_usize(
933    start: usize,
934    end: usize,
935    step: usize,
936) -> Result<Vec<usize>, MultiLengthStochasticAverageError> {
937    if start == end {
938        return Ok(vec![start]);
939    }
940    if step == 0 {
941        return Err(MultiLengthStochasticAverageError::InvalidRange {
942            start: start.to_string(),
943            end: end.to_string(),
944            step: step.to_string(),
945        });
946    }
947
948    let mut out = Vec::new();
949    if start < end {
950        let mut value = start;
951        while value <= end {
952            out.push(value);
953            let next = value.saturating_add(step);
954            if next == value {
955                break;
956            }
957            value = next;
958        }
959    } else {
960        let mut value = start;
961        while value >= end {
962            out.push(value);
963            let next = value.saturating_sub(step);
964            if next == value {
965                break;
966            }
967            value = next;
968        }
969    }
970
971    if out.is_empty() {
972        return Err(MultiLengthStochasticAverageError::InvalidRange {
973            start: start.to_string(),
974            end: end.to_string(),
975            step: step.to_string(),
976        });
977    }
978
979    Ok(out)
980}
981
982fn expand_grid_multi_length_stochastic_average(
983    range: &MultiLengthStochasticAverageBatchRange,
984) -> Result<Vec<MultiLengthStochasticAverageParams>, MultiLengthStochasticAverageError> {
985    let lengths = expand_axis_usize(range.length.0, range.length.1, range.length.2)?;
986    let presmooths = expand_axis_usize(range.presmooth.0, range.presmooth.1, range.presmooth.2)?;
987    let postsmooths =
988        expand_axis_usize(range.postsmooth.0, range.postsmooth.1, range.postsmooth.2)?;
989    let premethod = canonical_method_name(range.premethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
990    let postmethod = canonical_method_name(range.postmethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
991
992    let mut combos = Vec::with_capacity(
993        lengths
994            .len()
995            .saturating_mul(presmooths.len())
996            .saturating_mul(postsmooths.len()),
997    );
998
999    for &length in &lengths {
1000        for &presmooth in &presmooths {
1001            for &postsmooth in &postsmooths {
1002                combos.push(MultiLengthStochasticAverageParams {
1003                    length: Some(length),
1004                    presmooth: Some(presmooth),
1005                    premethod: Some(premethod.clone()),
1006                    postsmooth: Some(postsmooth),
1007                    postmethod: Some(postmethod.clone()),
1008                });
1009            }
1010        }
1011    }
1012
1013    if combos.is_empty() {
1014        return Err(MultiLengthStochasticAverageError::InvalidRange {
1015            start: range.length.0.to_string(),
1016            end: range.length.1.to_string(),
1017            step: range.length.2.to_string(),
1018        });
1019    }
1020
1021    Ok(combos)
1022}
1023
1024#[inline]
1025pub fn multi_length_stochastic_average_batch_with_kernel(
1026    data: &[f64],
1027    sweep: &MultiLengthStochasticAverageBatchRange,
1028    kernel: Kernel,
1029) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1030    let batch = match kernel {
1031        Kernel::Auto => detect_best_batch_kernel(),
1032        other if other.is_batch() => other,
1033        other => {
1034            return Err(MultiLengthStochasticAverageError::InvalidKernelForBatch(
1035                other,
1036            ))
1037        }
1038    };
1039    multi_length_stochastic_average_batch_par_slice(data, sweep, batch.to_non_batch())
1040}
1041
1042#[inline]
1043pub fn multi_length_stochastic_average_batch_slice(
1044    data: &[f64],
1045    sweep: &MultiLengthStochasticAverageBatchRange,
1046    kernel: Kernel,
1047) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1048    multi_length_stochastic_average_batch_inner(data, sweep, kernel, false)
1049}
1050
1051#[inline]
1052pub fn multi_length_stochastic_average_batch_par_slice(
1053    data: &[f64],
1054    sweep: &MultiLengthStochasticAverageBatchRange,
1055    kernel: Kernel,
1056) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1057    multi_length_stochastic_average_batch_inner(data, sweep, kernel, true)
1058}
1059
1060pub fn multi_length_stochastic_average_batch_inner(
1061    data: &[f64],
1062    sweep: &MultiLengthStochasticAverageBatchRange,
1063    _kernel: Kernel,
1064    parallel: bool,
1065) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1066    if data.is_empty() {
1067        return Err(MultiLengthStochasticAverageError::EmptyInputData);
1068    }
1069    let first = first_valid_value(data);
1070    if first >= data.len() {
1071        return Err(MultiLengthStochasticAverageError::AllValuesNaN);
1072    }
1073
1074    let combos = expand_grid_multi_length_stochastic_average(sweep)?;
1075    let max_valid = max_consecutive_valid_values(data);
1076    let rows = combos.len();
1077    let cols = data.len();
1078    let total =
1079        rows.checked_mul(cols)
1080            .ok_or(MultiLengthStochasticAverageError::OutputLengthMismatch {
1081                expected: usize::MAX,
1082                got: 0,
1083            })?;
1084
1085    let resolved = combos
1086        .iter()
1087        .map(|params| resolve_params(params, Some(cols)))
1088        .collect::<Result<Vec<_>, _>>()?;
1089
1090    for params in &resolved {
1091        let needed = total_warmup(*params) + 1;
1092        if max_valid < needed {
1093            return Err(MultiLengthStochasticAverageError::NotEnoughValidData {
1094                needed,
1095                valid: max_valid,
1096            });
1097        }
1098    }
1099
1100    let warmups = resolved
1101        .iter()
1102        .map(|params| first.saturating_add(total_warmup(*params)).min(cols))
1103        .collect::<Vec<_>>();
1104
1105    let mut values_mu = make_uninit_matrix(rows, cols);
1106    init_matrix_prefixes(&mut values_mu, cols, &warmups);
1107    let mut values_guard = ManuallyDrop::new(values_mu);
1108    let values_out =
1109        unsafe { std::slice::from_raw_parts_mut(values_guard.as_mut_ptr() as *mut f64, total) };
1110
1111    if parallel {
1112        #[cfg(not(target_arch = "wasm32"))]
1113        {
1114            values_out
1115                .par_chunks_mut(cols)
1116                .zip(resolved.par_iter())
1117                .for_each(|(row, params)| {
1118                    multi_length_stochastic_average_row_from_slice(data, *params, row);
1119                });
1120        }
1121
1122        #[cfg(target_arch = "wasm32")]
1123        for (row, params) in resolved.iter().enumerate() {
1124            let start = row * cols;
1125            let end = start + cols;
1126            multi_length_stochastic_average_row_from_slice(
1127                data,
1128                *params,
1129                &mut values_out[start..end],
1130            );
1131        }
1132    } else {
1133        for (row, params) in resolved.iter().enumerate() {
1134            let start = row * cols;
1135            let end = start + cols;
1136            multi_length_stochastic_average_row_from_slice(
1137                data,
1138                *params,
1139                &mut values_out[start..end],
1140            );
1141        }
1142    }
1143
1144    let values = unsafe {
1145        Vec::from_raw_parts(
1146            values_guard.as_mut_ptr() as *mut f64,
1147            values_guard.len(),
1148            values_guard.capacity(),
1149        )
1150    };
1151    core::mem::forget(values_guard);
1152
1153    Ok(MultiLengthStochasticAverageBatchOutput {
1154        values,
1155        combos,
1156        rows,
1157        cols,
1158    })
1159}
1160
1161pub fn multi_length_stochastic_average_batch_inner_into(
1162    data: &[f64],
1163    sweep: &MultiLengthStochasticAverageBatchRange,
1164    kernel: Kernel,
1165    values_out: &mut [f64],
1166) -> Result<Vec<MultiLengthStochasticAverageParams>, MultiLengthStochasticAverageError> {
1167    let out = multi_length_stochastic_average_batch_inner(data, sweep, kernel, false)?;
1168    let total = out.rows * out.cols;
1169    if values_out.len() != total {
1170        return Err(MultiLengthStochasticAverageError::OutputLengthMismatch {
1171            expected: total,
1172            got: values_out.len(),
1173        });
1174    }
1175    values_out.copy_from_slice(&out.values);
1176    Ok(out.combos)
1177}
1178
1179#[cfg(feature = "python")]
1180#[pyfunction(name = "multi_length_stochastic_average")]
1181#[pyo3(signature = (
1182    data,
1183    length=DEFAULT_LENGTH,
1184    presmooth=DEFAULT_PRESMOOTH,
1185    premethod=DEFAULT_SMOOTHING_METHOD,
1186    postsmooth=DEFAULT_POSTSMOOTH,
1187    postmethod=DEFAULT_SMOOTHING_METHOD,
1188    kernel=None
1189))]
1190pub fn multi_length_stochastic_average_py<'py>(
1191    py: Python<'py>,
1192    data: PyReadonlyArray1<'py, f64>,
1193    length: usize,
1194    presmooth: usize,
1195    premethod: &str,
1196    postsmooth: usize,
1197    postmethod: &str,
1198    kernel: Option<&str>,
1199) -> PyResult<Bound<'py, PyArray1<f64>>> {
1200    let data = data.as_slice()?;
1201    let kernel = validate_kernel(kernel, false)?;
1202    let input = MultiLengthStochasticAverageInput::from_slice(
1203        data,
1204        MultiLengthStochasticAverageParams {
1205            length: Some(length),
1206            presmooth: Some(presmooth),
1207            premethod: Some(premethod.to_string()),
1208            postsmooth: Some(postsmooth),
1209            postmethod: Some(postmethod.to_string()),
1210        },
1211    );
1212    let out = py
1213        .allow_threads(|| multi_length_stochastic_average_with_kernel(&input, kernel))
1214        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1215    Ok(out.values.into_pyarray(py))
1216}
1217
1218#[cfg(feature = "python")]
1219#[pyclass(name = "MultiLengthStochasticAverageStream")]
1220pub struct MultiLengthStochasticAverageStreamPy {
1221    stream: MultiLengthStochasticAverageStream,
1222}
1223
1224#[cfg(feature = "python")]
1225#[pymethods]
1226impl MultiLengthStochasticAverageStreamPy {
1227    #[new]
1228    #[pyo3(signature = (
1229        length=DEFAULT_LENGTH,
1230        presmooth=DEFAULT_PRESMOOTH,
1231        premethod=DEFAULT_SMOOTHING_METHOD,
1232        postsmooth=DEFAULT_POSTSMOOTH,
1233        postmethod=DEFAULT_SMOOTHING_METHOD
1234    ))]
1235    fn new(
1236        length: usize,
1237        presmooth: usize,
1238        premethod: &str,
1239        postsmooth: usize,
1240        postmethod: &str,
1241    ) -> PyResult<Self> {
1242        let stream =
1243            MultiLengthStochasticAverageStream::try_new(MultiLengthStochasticAverageParams {
1244                length: Some(length),
1245                presmooth: Some(presmooth),
1246                premethod: Some(premethod.to_string()),
1247                postsmooth: Some(postsmooth),
1248                postmethod: Some(postmethod.to_string()),
1249            })
1250            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1251        Ok(Self { stream })
1252    }
1253
1254    fn update(&mut self, value: f64) -> Option<f64> {
1255        self.stream.update(value)
1256    }
1257
1258    #[getter]
1259    fn warmup_period(&self) -> usize {
1260        self.stream.get_warmup_period()
1261    }
1262}
1263
1264#[cfg(feature = "python")]
1265#[pyfunction(name = "multi_length_stochastic_average_batch")]
1266#[pyo3(signature = (
1267    data,
1268    length_range=(DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
1269    presmooth_range=(DEFAULT_PRESMOOTH, DEFAULT_PRESMOOTH, 0),
1270    premethod=DEFAULT_SMOOTHING_METHOD,
1271    postsmooth_range=(DEFAULT_POSTSMOOTH, DEFAULT_POSTSMOOTH, 0),
1272    postmethod=DEFAULT_SMOOTHING_METHOD,
1273    kernel=None
1274))]
1275pub fn multi_length_stochastic_average_batch_py<'py>(
1276    py: Python<'py>,
1277    data: PyReadonlyArray1<'py, f64>,
1278    length_range: (usize, usize, usize),
1279    presmooth_range: (usize, usize, usize),
1280    premethod: &str,
1281    postsmooth_range: (usize, usize, usize),
1282    postmethod: &str,
1283    kernel: Option<&str>,
1284) -> PyResult<Bound<'py, PyDict>> {
1285    let data = data.as_slice()?;
1286    let kernel = validate_kernel(kernel, true)?;
1287    let sweep = MultiLengthStochasticAverageBatchRange {
1288        length: length_range,
1289        presmooth: presmooth_range,
1290        postsmooth: postsmooth_range,
1291        premethod: Some(premethod.to_string()),
1292        postmethod: Some(postmethod.to_string()),
1293    };
1294    let combos = expand_grid_multi_length_stochastic_average(&sweep)
1295        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1296    let rows = combos.len();
1297    let cols = data.len();
1298    let total = rows
1299        .checked_mul(cols)
1300        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1301
1302    let values_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1303    let values_slice = unsafe { values_arr.as_slice_mut()? };
1304
1305    let combos = py
1306        .allow_threads(|| {
1307            let batch = match kernel {
1308                Kernel::Auto => detect_best_batch_kernel(),
1309                other => other,
1310            };
1311            multi_length_stochastic_average_batch_inner_into(
1312                data,
1313                &sweep,
1314                batch.to_non_batch(),
1315                values_slice,
1316            )
1317        })
1318        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1319
1320    let dict = PyDict::new(py);
1321    dict.set_item("values", values_arr.reshape((rows, cols))?)?;
1322    dict.set_item(
1323        "lengths",
1324        combos
1325            .iter()
1326            .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
1327            .collect::<Vec<_>>()
1328            .into_pyarray(py),
1329    )?;
1330    dict.set_item(
1331        "presmooths",
1332        combos
1333            .iter()
1334            .map(|combo| combo.presmooth.unwrap_or(DEFAULT_PRESMOOTH) as u64)
1335            .collect::<Vec<_>>()
1336            .into_pyarray(py),
1337    )?;
1338    dict.set_item(
1339        "postsmooths",
1340        combos
1341            .iter()
1342            .map(|combo| combo.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH) as u64)
1343            .collect::<Vec<_>>()
1344            .into_pyarray(py),
1345    )?;
1346    dict.set_item(
1347        "premethods",
1348        PyList::new(
1349            py,
1350            combos.iter().map(|combo| {
1351                combo
1352                    .premethod
1353                    .as_deref()
1354                    .unwrap_or(DEFAULT_SMOOTHING_METHOD)
1355            }),
1356        )?,
1357    )?;
1358    dict.set_item(
1359        "postmethods",
1360        PyList::new(
1361            py,
1362            combos.iter().map(|combo| {
1363                combo
1364                    .postmethod
1365                    .as_deref()
1366                    .unwrap_or(DEFAULT_SMOOTHING_METHOD)
1367            }),
1368        )?,
1369    )?;
1370    dict.set_item("rows", rows)?;
1371    dict.set_item("cols", cols)?;
1372    Ok(dict)
1373}
1374
1375#[cfg(feature = "python")]
1376pub fn register_multi_length_stochastic_average_module(
1377    module: &Bound<'_, pyo3::types::PyModule>,
1378) -> PyResult<()> {
1379    module.add_function(wrap_pyfunction!(
1380        multi_length_stochastic_average_py,
1381        module
1382    )?)?;
1383    module.add_function(wrap_pyfunction!(
1384        multi_length_stochastic_average_batch_py,
1385        module
1386    )?)?;
1387    module.add_class::<MultiLengthStochasticAverageStreamPy>()?;
1388    Ok(())
1389}
1390
1391#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1392#[derive(Serialize, Deserialize)]
1393pub struct MultiLengthStochasticAverageJsOutput {
1394    pub values: Vec<f64>,
1395}
1396
1397#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1398#[wasm_bindgen(js_name = "multi_length_stochastic_average_js")]
1399pub fn multi_length_stochastic_average_js(
1400    data: &[f64],
1401    length: usize,
1402    presmooth: usize,
1403    premethod: String,
1404    postsmooth: usize,
1405    postmethod: String,
1406) -> Result<JsValue, JsValue> {
1407    let input = MultiLengthStochasticAverageInput::from_slice(
1408        data,
1409        MultiLengthStochasticAverageParams {
1410            length: Some(length),
1411            presmooth: Some(presmooth),
1412            premethod: Some(premethod),
1413            postsmooth: Some(postsmooth),
1414            postmethod: Some(postmethod),
1415        },
1416    );
1417    let out =
1418        multi_length_stochastic_average(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1419    serde_wasm_bindgen::to_value(&MultiLengthStochasticAverageJsOutput { values: out.values })
1420        .map_err(|e| JsValue::from_str(&e.to_string()))
1421}
1422
1423#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1424#[wasm_bindgen]
1425pub fn multi_length_stochastic_average_alloc(len: usize) -> *mut f64 {
1426    let mut vec = Vec::<f64>::with_capacity(len);
1427    let ptr = vec.as_mut_ptr();
1428    std::mem::forget(vec);
1429    ptr
1430}
1431
1432#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1433#[wasm_bindgen]
1434pub fn multi_length_stochastic_average_free(ptr: *mut f64, len: usize) {
1435    if !ptr.is_null() {
1436        unsafe {
1437            let _ = Vec::from_raw_parts(ptr, len, len);
1438        }
1439    }
1440}
1441
1442#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1443#[wasm_bindgen]
1444pub fn multi_length_stochastic_average_into(
1445    data_ptr: *const f64,
1446    values_ptr: *mut f64,
1447    len: usize,
1448    length: usize,
1449    presmooth: usize,
1450    premethod: String,
1451    postsmooth: usize,
1452    postmethod: String,
1453) -> Result<(), JsValue> {
1454    if data_ptr.is_null() || values_ptr.is_null() {
1455        return Err(JsValue::from_str("Null pointer provided"));
1456    }
1457
1458    unsafe {
1459        let data = std::slice::from_raw_parts(data_ptr, len);
1460        let input = MultiLengthStochasticAverageInput::from_slice(
1461            data,
1462            MultiLengthStochasticAverageParams {
1463                length: Some(length),
1464                presmooth: Some(presmooth),
1465                premethod: Some(premethod),
1466                postsmooth: Some(postsmooth),
1467                postmethod: Some(postmethod),
1468            },
1469        );
1470        if data_ptr == values_ptr {
1471            let mut tmp = vec![0.0; len];
1472            multi_length_stochastic_average_into_slice(&mut tmp, &input, Kernel::Auto)
1473                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1474            std::slice::from_raw_parts_mut(values_ptr, len).copy_from_slice(&tmp);
1475        } else {
1476            multi_length_stochastic_average_into_slice(
1477                std::slice::from_raw_parts_mut(values_ptr, len),
1478                &input,
1479                Kernel::Auto,
1480            )
1481            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1482        }
1483    }
1484    Ok(())
1485}
1486
1487#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1488#[derive(Serialize, Deserialize)]
1489pub struct MultiLengthStochasticAverageBatchJsConfig {
1490    pub length_range: (usize, usize, usize),
1491    pub presmooth_range: Option<(usize, usize, usize)>,
1492    pub premethod: Option<String>,
1493    pub postsmooth_range: Option<(usize, usize, usize)>,
1494    pub postmethod: Option<String>,
1495}
1496
1497#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1498#[derive(Serialize, Deserialize)]
1499pub struct MultiLengthStochasticAverageBatchJsOutput {
1500    pub values: Vec<f64>,
1501    pub combos: Vec<MultiLengthStochasticAverageParams>,
1502    pub lengths: Vec<usize>,
1503    pub presmooths: Vec<usize>,
1504    pub postsmooths: Vec<usize>,
1505    pub premethods: Vec<String>,
1506    pub postmethods: Vec<String>,
1507    pub rows: usize,
1508    pub cols: usize,
1509}
1510
1511#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1512#[wasm_bindgen(js_name = "multi_length_stochastic_average_batch_js")]
1513pub fn multi_length_stochastic_average_batch_js(
1514    data: &[f64],
1515    config: JsValue,
1516) -> Result<JsValue, JsValue> {
1517    let config: MultiLengthStochasticAverageBatchJsConfig = serde_wasm_bindgen::from_value(config)
1518        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1519    let sweep = MultiLengthStochasticAverageBatchRange {
1520        length: config.length_range,
1521        presmooth: config
1522            .presmooth_range
1523            .unwrap_or((DEFAULT_PRESMOOTH, DEFAULT_PRESMOOTH, 0)),
1524        premethod: config
1525            .premethod
1526            .or_else(|| Some(DEFAULT_SMOOTHING_METHOD.to_string())),
1527        postsmooth: config
1528            .postsmooth_range
1529            .unwrap_or((DEFAULT_POSTSMOOTH, DEFAULT_POSTSMOOTH, 0)),
1530        postmethod: config
1531            .postmethod
1532            .or_else(|| Some(DEFAULT_SMOOTHING_METHOD.to_string())),
1533    };
1534    let out = multi_length_stochastic_average_batch_inner(
1535        data,
1536        &sweep,
1537        detect_best_batch_kernel().to_non_batch(),
1538        false,
1539    )
1540    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1541
1542    serde_wasm_bindgen::to_value(&MultiLengthStochasticAverageBatchJsOutput {
1543        lengths: out
1544            .combos
1545            .iter()
1546            .map(|p| p.length.unwrap_or(DEFAULT_LENGTH))
1547            .collect(),
1548        presmooths: out
1549            .combos
1550            .iter()
1551            .map(|p| p.presmooth.unwrap_or(DEFAULT_PRESMOOTH))
1552            .collect(),
1553        postsmooths: out
1554            .combos
1555            .iter()
1556            .map(|p| p.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH))
1557            .collect(),
1558        premethods: out
1559            .combos
1560            .iter()
1561            .map(|p| canonical_method_name(p.premethod.as_deref(), DEFAULT_SMOOTHING_METHOD))
1562            .collect(),
1563        postmethods: out
1564            .combos
1565            .iter()
1566            .map(|p| canonical_method_name(p.postmethod.as_deref(), DEFAULT_SMOOTHING_METHOD))
1567            .collect(),
1568        values: out.values,
1569        combos: out.combos,
1570        rows: out.rows,
1571        cols: out.cols,
1572    })
1573    .map_err(|e| JsValue::from_str(&e.to_string()))
1574}
1575
1576#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1577#[wasm_bindgen]
1578pub fn multi_length_stochastic_average_batch_into(
1579    data_ptr: *const f64,
1580    values_ptr: *mut f64,
1581    len: usize,
1582    length_start: usize,
1583    length_end: usize,
1584    length_step: usize,
1585    presmooth_start: usize,
1586    presmooth_end: usize,
1587    presmooth_step: usize,
1588    premethod: String,
1589    postsmooth_start: usize,
1590    postsmooth_end: usize,
1591    postsmooth_step: usize,
1592    postmethod: String,
1593) -> Result<usize, JsValue> {
1594    if data_ptr.is_null() || values_ptr.is_null() {
1595        return Err(JsValue::from_str("Null pointer provided"));
1596    }
1597
1598    let sweep = MultiLengthStochasticAverageBatchRange {
1599        length: (length_start, length_end, length_step),
1600        presmooth: (presmooth_start, presmooth_end, presmooth_step),
1601        premethod: Some(premethod),
1602        postsmooth: (postsmooth_start, postsmooth_end, postsmooth_step),
1603        postmethod: Some(postmethod),
1604    };
1605    let combos = expand_grid_multi_length_stochastic_average(&sweep)
1606        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1607    let rows = combos.len();
1608    let total = rows
1609        .checked_mul(len)
1610        .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1611
1612    unsafe {
1613        let data = std::slice::from_raw_parts(data_ptr, len);
1614        let values_out = std::slice::from_raw_parts_mut(values_ptr, total);
1615        multi_length_stochastic_average_batch_inner_into(
1616            data,
1617            &sweep,
1618            detect_best_batch_kernel().to_non_batch(),
1619            values_out,
1620        )
1621        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1622    }
1623    Ok(rows)
1624}
1625
1626#[cfg(test)]
1627mod tests {
1628    use super::*;
1629    use std::error::Error;
1630
1631    fn sample_source(len: usize) -> Vec<f64> {
1632        (0..len)
1633            .map(|i| {
1634                100.0
1635                    + i as f64 * 0.04
1636                    + (i as f64 * 0.17).sin() * 1.8
1637                    + (i as f64 * 0.03).cos() * 0.4
1638            })
1639            .collect()
1640    }
1641
1642    fn sample_candles(len: usize) -> Candles {
1643        let open: Vec<f64> = (0..len)
1644            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.11).sin() * 1.2)
1645            .collect();
1646        let close: Vec<f64> = open
1647            .iter()
1648            .enumerate()
1649            .map(|(i, &o)| o + (i as f64 * 0.19).cos() * 0.7)
1650            .collect();
1651        let high: Vec<f64> = open
1652            .iter()
1653            .zip(close.iter())
1654            .enumerate()
1655            .map(|(i, (&o, &c))| o.max(c) + 0.4 + (i as f64 * 0.07).sin().abs() * 0.2)
1656            .collect();
1657        let low: Vec<f64> = open
1658            .iter()
1659            .zip(close.iter())
1660            .enumerate()
1661            .map(|(i, (&o, &c))| o.min(c) - 0.4 - (i as f64 * 0.05).cos().abs() * 0.2)
1662            .collect();
1663        Candles::new(
1664            (0..len as i64).collect(),
1665            open,
1666            high,
1667            low,
1668            close,
1669            vec![1_000.0; len],
1670        )
1671    }
1672
1673    fn assert_series_eq(lhs: &[f64], rhs: &[f64], tol: f64) {
1674        assert_eq!(lhs.len(), rhs.len());
1675        for (i, (&left, &right)) in lhs.iter().zip(rhs.iter()).enumerate() {
1676            if left.is_nan() && right.is_nan() {
1677                continue;
1678            }
1679            assert!(
1680                (left - right).abs() <= tol,
1681                "mismatch at index {i}: left={left}, right={right}, tol={tol}"
1682            );
1683        }
1684    }
1685
1686    #[test]
1687    fn multi_length_stochastic_average_output_contract() -> Result<(), Box<dyn Error>> {
1688        let data = sample_source(256);
1689        let out = multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1690            &data,
1691            MultiLengthStochasticAverageParams::default(),
1692        ))?;
1693
1694        assert_eq!(out.values.len(), data.len());
1695        let first_finite = out
1696            .values
1697            .iter()
1698            .position(|value| value.is_finite())
1699            .unwrap();
1700        assert!(first_finite >= 22);
1701        for &value in out.values.iter().filter(|value| value.is_finite()) {
1702            assert!((-1e-9..=100.0 + 1e-9).contains(&value));
1703        }
1704        Ok(())
1705    }
1706
1707    #[test]
1708    fn multi_length_stochastic_average_rejects_invalid_parameters() {
1709        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1710
1711        let err = multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1712            &data,
1713            MultiLengthStochasticAverageParams {
1714                length: Some(3),
1715                ..MultiLengthStochasticAverageParams::default()
1716            },
1717        ))
1718        .unwrap_err();
1719        assert!(matches!(
1720            err,
1721            MultiLengthStochasticAverageError::InvalidLength { length: 3, .. }
1722        ));
1723
1724        let err = multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1725            &data,
1726            MultiLengthStochasticAverageParams {
1727                premethod: Some("ema".to_string()),
1728                ..MultiLengthStochasticAverageParams::default()
1729            },
1730        ))
1731        .unwrap_err();
1732        assert!(matches!(
1733            err,
1734            MultiLengthStochasticAverageError::InvalidPreMethod { .. }
1735        ));
1736    }
1737
1738    #[test]
1739    fn multi_length_stochastic_average_builder_supports_candles() -> Result<(), Box<dyn Error>> {
1740        let candles = sample_candles(220);
1741        let built = MultiLengthStochasticAverageBuilder::new()
1742            .length(12)
1743            .presmooth(5)
1744            .premethod("lsma")
1745            .postsmooth(4)
1746            .postmethod("sma")
1747            .apply(&candles, "hlc3")?;
1748
1749        let direct =
1750            multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_candles(
1751                &candles,
1752                "hlc3",
1753                MultiLengthStochasticAverageParams {
1754                    length: Some(12),
1755                    presmooth: Some(5),
1756                    premethod: Some("lsma".to_string()),
1757                    postsmooth: Some(4),
1758                    postmethod: Some("sma".to_string()),
1759                },
1760            ))?;
1761
1762        assert_series_eq(&built.values, &direct.values, 1e-12);
1763        Ok(())
1764    }
1765
1766    #[test]
1767    fn multi_length_stochastic_average_stream_matches_batch_with_reset(
1768    ) -> Result<(), Box<dyn Error>> {
1769        let mut data = sample_source(220);
1770        data[110] = f64::NAN;
1771        let params = MultiLengthStochasticAverageParams {
1772            length: Some(12),
1773            presmooth: Some(5),
1774            premethod: Some("lsma".to_string()),
1775            postsmooth: Some(4),
1776            postmethod: Some("sma".to_string()),
1777        };
1778
1779        let batch = multi_length_stochastic_average(
1780            &MultiLengthStochasticAverageInput::from_slice(&data, params.clone()),
1781        )?;
1782        let mut stream = MultiLengthStochasticAverageStream::try_new(params)?;
1783        let mut streamed = Vec::with_capacity(data.len());
1784
1785        for &value in &data {
1786            streamed.push(stream.update(value).unwrap_or(f64::NAN));
1787        }
1788
1789        assert_eq!(stream.get_warmup_period(), 18);
1790        assert_series_eq(&batch.values, &streamed, 1e-12);
1791        Ok(())
1792    }
1793
1794    #[test]
1795    fn multi_length_stochastic_average_into_matches_api() -> Result<(), Box<dyn Error>> {
1796        let data = sample_source(192);
1797        let input = MultiLengthStochasticAverageInput::from_slice(
1798            &data,
1799            MultiLengthStochasticAverageParams {
1800                length: Some(16),
1801                presmooth: Some(6),
1802                premethod: Some("tma".to_string()),
1803                postsmooth: Some(5),
1804                postmethod: Some("lsma".to_string()),
1805            },
1806        );
1807        let api = multi_length_stochastic_average(&input)?;
1808        let mut out = vec![f64::NAN; data.len()];
1809        multi_length_stochastic_average_into_slice(&mut out, &input, Kernel::Auto)?;
1810        assert_series_eq(&api.values, &out, 1e-12);
1811        Ok(())
1812    }
1813
1814    #[test]
1815    fn multi_length_stochastic_average_batch_single_param_matches_single(
1816    ) -> Result<(), Box<dyn Error>> {
1817        let data = sample_source(128);
1818        let sweep = MultiLengthStochasticAverageBatchRange {
1819            length: (12, 12, 0),
1820            presmooth: (5, 5, 0),
1821            premethod: Some("lsma".to_string()),
1822            postsmooth: (4, 4, 0),
1823            postmethod: Some("sma".to_string()),
1824        };
1825        let batch = multi_length_stochastic_average_batch_with_kernel(&data, &sweep, Kernel::Auto)?;
1826        let single =
1827            multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1828                &data,
1829                MultiLengthStochasticAverageParams {
1830                    length: Some(12),
1831                    presmooth: Some(5),
1832                    premethod: Some("lsma".to_string()),
1833                    postsmooth: Some(4),
1834                    postmethod: Some("sma".to_string()),
1835                },
1836            ))?;
1837
1838        assert_eq!(batch.rows, 1);
1839        assert_eq!(batch.cols, data.len());
1840        assert_eq!(batch.combos[0].length, Some(12));
1841        assert_eq!(batch.combos[0].presmooth, Some(5));
1842        assert_eq!(batch.combos[0].postsmooth, Some(4));
1843        assert_eq!(batch.combos[0].premethod.as_deref(), Some("lsma"));
1844        assert_eq!(batch.combos[0].postmethod.as_deref(), Some("sma"));
1845        assert_series_eq(&batch.values[..data.len()], &single.values, 1e-12);
1846        Ok(())
1847    }
1848
1849    #[test]
1850    fn multi_length_stochastic_average_batch_metadata() -> Result<(), Box<dyn Error>> {
1851        let data = sample_source(96);
1852        let sweep = MultiLengthStochasticAverageBatchRange {
1853            length: (10, 14, 2),
1854            presmooth: (4, 6, 2),
1855            premethod: Some("sma".to_string()),
1856            postsmooth: (3, 5, 2),
1857            postmethod: Some("lsma".to_string()),
1858        };
1859        let batch = multi_length_stochastic_average_batch_with_kernel(&data, &sweep, Kernel::Auto)?;
1860
1861        assert_eq!(batch.rows, 12);
1862        assert_eq!(batch.cols, data.len());
1863        assert_eq!(batch.combos.len(), 12);
1864        assert_eq!(batch.values.len(), 12 * data.len());
1865        for combo in &batch.combos {
1866            assert_eq!(combo.premethod.as_deref(), Some("sma"));
1867            assert_eq!(combo.postmethod.as_deref(), Some("lsma"));
1868        }
1869        Ok(())
1870    }
1871}