Skip to main content

vector_ta/indicators/
mesa_stochastic_multi_length.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::detect_best_batch_kernel;
18#[cfg(feature = "python")]
19use crate::utilities::kernel_validation::validate_kernel;
20#[cfg(not(target_arch = "wasm32"))]
21use rayon::prelude::*;
22use std::collections::VecDeque;
23use thiserror::Error;
24
25const DEFAULT_SOURCE: &str = "close";
26const DEFAULT_LENGTH_1: usize = 48;
27const DEFAULT_LENGTH_2: usize = 21;
28const DEFAULT_LENGTH_3: usize = 9;
29const DEFAULT_LENGTH_4: usize = 6;
30const DEFAULT_TRIGGER_LENGTH: usize = 2;
31const PI: f64 = 3.14;
32
33#[derive(Debug, Clone)]
34pub enum MesaStochasticMultiLengthData<'a> {
35    Candles {
36        candles: &'a Candles,
37        source: &'a str,
38    },
39    Slice {
40        source: &'a [f64],
41    },
42}
43
44#[derive(Debug, Clone)]
45pub struct MesaStochasticMultiLengthOutput {
46    pub mesa_1: Vec<f64>,
47    pub mesa_2: Vec<f64>,
48    pub mesa_3: Vec<f64>,
49    pub mesa_4: Vec<f64>,
50    pub trigger_1: Vec<f64>,
51    pub trigger_2: Vec<f64>,
52    pub trigger_3: Vec<f64>,
53    pub trigger_4: Vec<f64>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg_attr(
58    all(target_arch = "wasm32", feature = "wasm"),
59    derive(Serialize, Deserialize)
60)]
61pub struct MesaStochasticMultiLengthParams {
62    pub length_1: Option<usize>,
63    pub length_2: Option<usize>,
64    pub length_3: Option<usize>,
65    pub length_4: Option<usize>,
66    pub trigger_length: Option<usize>,
67}
68
69impl Default for MesaStochasticMultiLengthParams {
70    fn default() -> Self {
71        Self {
72            length_1: Some(DEFAULT_LENGTH_1),
73            length_2: Some(DEFAULT_LENGTH_2),
74            length_3: Some(DEFAULT_LENGTH_3),
75            length_4: Some(DEFAULT_LENGTH_4),
76            trigger_length: Some(DEFAULT_TRIGGER_LENGTH),
77        }
78    }
79}
80
81#[derive(Debug, Clone)]
82pub struct MesaStochasticMultiLengthInput<'a> {
83    pub data: MesaStochasticMultiLengthData<'a>,
84    pub params: MesaStochasticMultiLengthParams,
85}
86
87impl<'a> MesaStochasticMultiLengthInput<'a> {
88    #[inline]
89    pub fn from_candles(
90        candles: &'a Candles,
91        source: &'a str,
92        params: MesaStochasticMultiLengthParams,
93    ) -> Self {
94        Self {
95            data: MesaStochasticMultiLengthData::Candles { candles, source },
96            params,
97        }
98    }
99
100    #[inline]
101    pub fn from_slice(source: &'a [f64], params: MesaStochasticMultiLengthParams) -> Self {
102        Self {
103            data: MesaStochasticMultiLengthData::Slice { source },
104            params,
105        }
106    }
107
108    #[inline]
109    pub fn from_slices(source: &'a [f64], params: MesaStochasticMultiLengthParams) -> Self {
110        Self::from_slice(source, params)
111    }
112
113    #[inline]
114    pub fn with_default_candles(candles: &'a Candles) -> Self {
115        Self::from_candles(
116            candles,
117            DEFAULT_SOURCE,
118            MesaStochasticMultiLengthParams::default(),
119        )
120    }
121}
122
123#[derive(Debug, Clone, Copy)]
124struct ValidatedParams {
125    length_1: usize,
126    length_2: usize,
127    length_3: usize,
128    length_4: usize,
129    trigger_length: usize,
130}
131
132impl ValidatedParams {
133    fn from_params(
134        params: &MesaStochasticMultiLengthParams,
135    ) -> Result<Self, MesaStochasticMultiLengthError> {
136        let out = Self {
137            length_1: params.length_1.unwrap_or(DEFAULT_LENGTH_1),
138            length_2: params.length_2.unwrap_or(DEFAULT_LENGTH_2),
139            length_3: params.length_3.unwrap_or(DEFAULT_LENGTH_3),
140            length_4: params.length_4.unwrap_or(DEFAULT_LENGTH_4),
141            trigger_length: params.trigger_length.unwrap_or(DEFAULT_TRIGGER_LENGTH),
142        };
143        for (name, value) in [
144            ("length_1", out.length_1),
145            ("length_2", out.length_2),
146            ("length_3", out.length_3),
147            ("length_4", out.length_4),
148            ("trigger_length", out.trigger_length),
149        ] {
150            if value == 0 {
151                return Err(MesaStochasticMultiLengthError::InvalidPeriod {
152                    name: name.to_string(),
153                    value,
154                });
155            }
156        }
157        Ok(out)
158    }
159
160    fn into_params(self) -> MesaStochasticMultiLengthParams {
161        MesaStochasticMultiLengthParams {
162            length_1: Some(self.length_1),
163            length_2: Some(self.length_2),
164            length_3: Some(self.length_3),
165            length_4: Some(self.length_4),
166            trigger_length: Some(self.trigger_length),
167        }
168    }
169}
170
171#[derive(Debug, Error)]
172pub enum MesaStochasticMultiLengthError {
173    #[error("mesa_stochastic_multi_length: Input data slice is empty.")]
174    EmptyInputData,
175    #[error("mesa_stochastic_multi_length: All values are NaN.")]
176    AllValuesNaN,
177    #[error("mesa_stochastic_multi_length: Invalid period `{name}`: {value}")]
178    InvalidPeriod { name: String, value: usize },
179    #[error(
180        "mesa_stochastic_multi_length: Output length mismatch: expected={expected}, got={got}"
181    )]
182    OutputLengthMismatch { expected: usize, got: usize },
183    #[error("mesa_stochastic_multi_length: Invalid range: start={start}, end={end}, step={step}")]
184    InvalidRange {
185        start: String,
186        end: String,
187        step: String,
188    },
189    #[error("mesa_stochastic_multi_length: Invalid kernel for batch: {0:?}")]
190    InvalidKernelForBatch(Kernel),
191}
192
193#[inline(always)]
194fn extract_source<'a>(
195    input: &'a MesaStochasticMultiLengthInput<'a>,
196) -> Result<&'a [f64], MesaStochasticMultiLengthError> {
197    let source = match &input.data {
198        MesaStochasticMultiLengthData::Candles { candles, source } => source_type(candles, source),
199        MesaStochasticMultiLengthData::Slice { source } => *source,
200    };
201    if source.is_empty() {
202        return Err(MesaStochasticMultiLengthError::EmptyInputData);
203    }
204    Ok(source)
205}
206
207#[inline(always)]
208fn first_valid(source: &[f64]) -> Option<usize> {
209    source.iter().position(|value| value.is_finite())
210}
211
212#[derive(Debug, Clone, Copy)]
213pub struct MesaStochasticMultiLengthBuilder {
214    source: Option<&'static str>,
215    length_1: Option<usize>,
216    length_2: Option<usize>,
217    length_3: Option<usize>,
218    length_4: Option<usize>,
219    trigger_length: Option<usize>,
220    kernel: Kernel,
221}
222
223impl Default for MesaStochasticMultiLengthBuilder {
224    fn default() -> Self {
225        Self {
226            source: None,
227            length_1: None,
228            length_2: None,
229            length_3: None,
230            length_4: None,
231            trigger_length: None,
232            kernel: Kernel::Auto,
233        }
234    }
235}
236
237impl MesaStochasticMultiLengthBuilder {
238    #[inline(always)]
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    #[inline(always)]
244    pub fn source(mut self, value: &'static str) -> Self {
245        self.source = Some(value);
246        self
247    }
248
249    #[inline(always)]
250    pub fn length_1(mut self, value: usize) -> Self {
251        self.length_1 = Some(value);
252        self
253    }
254
255    #[inline(always)]
256    pub fn length_2(mut self, value: usize) -> Self {
257        self.length_2 = Some(value);
258        self
259    }
260
261    #[inline(always)]
262    pub fn length_3(mut self, value: usize) -> Self {
263        self.length_3 = Some(value);
264        self
265    }
266
267    #[inline(always)]
268    pub fn length_4(mut self, value: usize) -> Self {
269        self.length_4 = Some(value);
270        self
271    }
272
273    #[inline(always)]
274    pub fn trigger_length(mut self, value: usize) -> Self {
275        self.trigger_length = Some(value);
276        self
277    }
278
279    #[inline(always)]
280    pub fn kernel(mut self, value: Kernel) -> Self {
281        self.kernel = value;
282        self
283    }
284
285    #[inline(always)]
286    fn params(self) -> MesaStochasticMultiLengthParams {
287        MesaStochasticMultiLengthParams {
288            length_1: self.length_1,
289            length_2: self.length_2,
290            length_3: self.length_3,
291            length_4: self.length_4,
292            trigger_length: self.trigger_length,
293        }
294    }
295
296    #[inline(always)]
297    pub fn apply(
298        self,
299        candles: &Candles,
300    ) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
301        let input = MesaStochasticMultiLengthInput::from_candles(
302            candles,
303            self.source.unwrap_or(DEFAULT_SOURCE),
304            self.params(),
305        );
306        mesa_stochastic_multi_length_with_kernel(&input, self.kernel)
307    }
308
309    #[inline(always)]
310    pub fn apply_slice(
311        self,
312        source: &[f64],
313    ) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
314        let input = MesaStochasticMultiLengthInput::from_slice(source, self.params());
315        mesa_stochastic_multi_length_with_kernel(&input, self.kernel)
316    }
317
318    #[inline(always)]
319    pub fn into_stream(
320        self,
321    ) -> Result<MesaStochasticMultiLengthStream, MesaStochasticMultiLengthError> {
322        MesaStochasticMultiLengthStream::try_new(self.params())
323    }
324}
325
326#[inline(always)]
327fn nz(value: f64) -> f64 {
328    if value.is_finite() {
329        value
330    } else {
331        0.0
332    }
333}
334
335#[derive(Clone, Debug)]
336struct RollingSmaState {
337    length: usize,
338    window: VecDeque<f64>,
339    finite_sum: f64,
340    finite_count: usize,
341}
342
343impl RollingSmaState {
344    fn new(length: usize) -> Self {
345        Self {
346            length,
347            window: VecDeque::with_capacity(length + 1),
348            finite_sum: 0.0,
349            finite_count: 0,
350        }
351    }
352
353    fn update(&mut self, value: f64) -> f64 {
354        self.window.push_back(value);
355        if value.is_finite() {
356            self.finite_sum += value;
357            self.finite_count += 1;
358        }
359        if self.window.len() > self.length {
360            if let Some(old) = self.window.pop_front() {
361                if old.is_finite() {
362                    self.finite_sum -= old;
363                    self.finite_count -= 1;
364                }
365            }
366        }
367        if self.window.len() == self.length && self.finite_count == self.length {
368            self.finite_sum / self.length as f64
369        } else {
370            f64::NAN
371        }
372    }
373}
374
375#[derive(Clone, Debug)]
376struct MesaLineState {
377    length: usize,
378    filt_window: VecDeque<f64>,
379    prev_1: f64,
380    prev_2: f64,
381}
382
383impl MesaLineState {
384    fn new(length: usize) -> Self {
385        Self {
386            length,
387            filt_window: VecDeque::with_capacity(length + 1),
388            prev_1: f64::NAN,
389            prev_2: f64::NAN,
390        }
391    }
392
393    fn update(&mut self, filt: f64, c1: f64, c2: f64, c3: f64) -> f64 {
394        let filt_nz = nz(filt);
395        self.filt_window.push_back(filt_nz);
396        if self.filt_window.len() > self.length {
397            self.filt_window.pop_front();
398        }
399
400        let out = if filt.is_finite() {
401            let mut highest = filt;
402            let mut lowest = filt;
403            for &value in &self.filt_window {
404                if value > highest {
405                    highest = value;
406                }
407                if value < lowest {
408                    lowest = value;
409                }
410            }
411            if self.filt_window.len() < self.length {
412                if 0.0 > highest {
413                    highest = 0.0;
414                }
415                if 0.0 < lowest {
416                    lowest = 0.0;
417                }
418            }
419            let denom = highest - lowest;
420            if denom != 0.0 && denom.is_finite() {
421                let stoc = (filt - lowest) / denom;
422                if stoc.is_finite() {
423                    c1.mul_add(stoc, c2.mul_add(nz(self.prev_1), c3 * nz(self.prev_2)))
424                } else {
425                    f64::NAN
426                }
427            } else {
428                f64::NAN
429            }
430        } else {
431            f64::NAN
432        };
433
434        self.prev_2 = self.prev_1;
435        self.prev_1 = out;
436        out
437    }
438}
439
440#[derive(Clone, Debug)]
441struct SharedFilterState {
442    c1: f64,
443    c2: f64,
444    c3: f64,
445    hp_coef: f64,
446    hp_feedback_1: f64,
447    hp_feedback_2: f64,
448    prev_src_1: f64,
449    prev_src_2: f64,
450    prev_hp_1: f64,
451    prev_hp_2: f64,
452    prev_filt_1: f64,
453    prev_filt_2: f64,
454}
455
456impl SharedFilterState {
457    fn new() -> Self {
458        let alpha1 = ((0.707 * 2.0 * PI / 48.0).cos() + (0.707 * 2.0 * PI / 48.0).sin() - 1.0)
459            / (0.707 * 2.0 * PI / 48.0).cos();
460        let one_minus_alpha = 1.0 - alpha1;
461        let hp_coef = (1.0 - alpha1 * 0.5) * (1.0 - alpha1 * 0.5);
462        let a1 = (-1.414 * PI / 10.0).exp();
463        let b1 = 2.0 * a1 * (1.414 * PI / 10.0).cos();
464        let c2 = b1;
465        let c3 = -(a1 * a1);
466        let c1 = 1.0 - c2 - c3;
467        Self {
468            c1,
469            c2,
470            c3,
471            hp_coef,
472            hp_feedback_1: 2.0 * one_minus_alpha,
473            hp_feedback_2: -(one_minus_alpha * one_minus_alpha),
474            prev_src_1: f64::NAN,
475            prev_src_2: f64::NAN,
476            prev_hp_1: f64::NAN,
477            prev_hp_2: f64::NAN,
478            prev_filt_1: f64::NAN,
479            prev_filt_2: f64::NAN,
480        }
481    }
482
483    fn update(&mut self, source: f64) -> f64 {
484        let hp = if source.is_finite() {
485            self.hp_coef.mul_add(
486                source - 2.0 * nz(self.prev_src_1) + nz(self.prev_src_2),
487                self.hp_feedback_1
488                    .mul_add(nz(self.prev_hp_1), self.hp_feedback_2 * nz(self.prev_hp_2)),
489            )
490        } else {
491            f64::NAN
492        };
493        let filt = if hp.is_finite() {
494            self.c1.mul_add(
495                hp,
496                self.c2
497                    .mul_add(nz(self.prev_filt_1), self.c3 * nz(self.prev_filt_2)),
498            )
499        } else {
500            f64::NAN
501        };
502
503        self.prev_src_2 = self.prev_src_1;
504        self.prev_src_1 = source;
505        self.prev_hp_2 = self.prev_hp_1;
506        self.prev_hp_1 = hp;
507        self.prev_filt_2 = self.prev_filt_1;
508        self.prev_filt_1 = filt;
509        filt
510    }
511}
512
513#[derive(Clone, Debug)]
514pub struct MesaStochasticMultiLengthStream {
515    filter_state: SharedFilterState,
516    mesa_1_state: MesaLineState,
517    mesa_2_state: MesaLineState,
518    mesa_3_state: MesaLineState,
519    mesa_4_state: MesaLineState,
520    trigger_1_state: RollingSmaState,
521    trigger_2_state: RollingSmaState,
522    trigger_3_state: RollingSmaState,
523    trigger_4_state: RollingSmaState,
524}
525
526impl MesaStochasticMultiLengthStream {
527    pub fn try_new(
528        params: MesaStochasticMultiLengthParams,
529    ) -> Result<Self, MesaStochasticMultiLengthError> {
530        let params = ValidatedParams::from_params(&params)?;
531        Ok(Self {
532            filter_state: SharedFilterState::new(),
533            mesa_1_state: MesaLineState::new(params.length_1),
534            mesa_2_state: MesaLineState::new(params.length_2),
535            mesa_3_state: MesaLineState::new(params.length_3),
536            mesa_4_state: MesaLineState::new(params.length_4),
537            trigger_1_state: RollingSmaState::new(params.trigger_length),
538            trigger_2_state: RollingSmaState::new(params.trigger_length),
539            trigger_3_state: RollingSmaState::new(params.trigger_length),
540            trigger_4_state: RollingSmaState::new(params.trigger_length),
541        })
542    }
543
544    pub fn update(&mut self, source: f64) -> (f64, f64, f64, f64, f64, f64, f64, f64) {
545        let filt = self.filter_state.update(source);
546        let c1 = self.filter_state.c1;
547        let c2 = self.filter_state.c2;
548        let c3 = self.filter_state.c3;
549
550        let mesa_1 = self.mesa_1_state.update(filt, c1, c2, c3);
551        let mesa_2 = self.mesa_2_state.update(filt, c1, c2, c3);
552        let mesa_3 = self.mesa_3_state.update(filt, c1, c2, c3);
553        let mesa_4 = self.mesa_4_state.update(filt, c1, c2, c3);
554
555        let trigger_1 = self.trigger_1_state.update(mesa_1);
556        let trigger_2 = self.trigger_2_state.update(mesa_2);
557        let trigger_3 = self.trigger_3_state.update(mesa_3);
558        let trigger_4 = self.trigger_4_state.update(mesa_4);
559
560        (
561            mesa_1, mesa_2, mesa_3, mesa_4, trigger_1, trigger_2, trigger_3, trigger_4,
562        )
563    }
564}
565
566#[allow(clippy::too_many_arguments)]
567fn compute_mesa_stochastic_multi_length_into(
568    source: &[f64],
569    params: ValidatedParams,
570    mesa_1_out: &mut [f64],
571    mesa_2_out: &mut [f64],
572    mesa_3_out: &mut [f64],
573    mesa_4_out: &mut [f64],
574    trigger_1_out: &mut [f64],
575    trigger_2_out: &mut [f64],
576    trigger_3_out: &mut [f64],
577    trigger_4_out: &mut [f64],
578) -> Result<(), MesaStochasticMultiLengthError> {
579    let n = source.len();
580    if mesa_1_out.len() != n
581        || mesa_2_out.len() != n
582        || mesa_3_out.len() != n
583        || mesa_4_out.len() != n
584        || trigger_1_out.len() != n
585        || trigger_2_out.len() != n
586        || trigger_3_out.len() != n
587        || trigger_4_out.len() != n
588    {
589        let got = [
590            mesa_1_out.len(),
591            mesa_2_out.len(),
592            mesa_3_out.len(),
593            mesa_4_out.len(),
594            trigger_1_out.len(),
595            trigger_2_out.len(),
596            trigger_3_out.len(),
597            trigger_4_out.len(),
598        ]
599        .into_iter()
600        .max()
601        .unwrap_or(0);
602        return Err(MesaStochasticMultiLengthError::OutputLengthMismatch { expected: n, got });
603    }
604
605    mesa_1_out.fill(f64::NAN);
606    mesa_2_out.fill(f64::NAN);
607    mesa_3_out.fill(f64::NAN);
608    mesa_4_out.fill(f64::NAN);
609    trigger_1_out.fill(f64::NAN);
610    trigger_2_out.fill(f64::NAN);
611    trigger_3_out.fill(f64::NAN);
612    trigger_4_out.fill(f64::NAN);
613
614    let mut stream = MesaStochasticMultiLengthStream::try_new(params.into_params())?;
615    for (i, value) in source.iter().copied().enumerate() {
616        let (mesa_1, mesa_2, mesa_3, mesa_4, trigger_1, trigger_2, trigger_3, trigger_4) =
617            stream.update(value);
618        mesa_1_out[i] = mesa_1;
619        mesa_2_out[i] = mesa_2;
620        mesa_3_out[i] = mesa_3;
621        mesa_4_out[i] = mesa_4;
622        trigger_1_out[i] = trigger_1;
623        trigger_2_out[i] = trigger_2;
624        trigger_3_out[i] = trigger_3;
625        trigger_4_out[i] = trigger_4;
626    }
627
628    Ok(())
629}
630
631pub fn mesa_stochastic_multi_length(
632    input: &MesaStochasticMultiLengthInput,
633) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
634    mesa_stochastic_multi_length_with_kernel(input, Kernel::Auto)
635}
636
637pub fn mesa_stochastic_multi_length_with_kernel(
638    input: &MesaStochasticMultiLengthInput,
639    _kernel: Kernel,
640) -> Result<MesaStochasticMultiLengthOutput, MesaStochasticMultiLengthError> {
641    let source = extract_source(input)?;
642    let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
643    let params = ValidatedParams::from_params(&input.params)?;
644    let n = source.len();
645    let mut out = MesaStochasticMultiLengthOutput {
646        mesa_1: vec![f64::NAN; n],
647        mesa_2: vec![f64::NAN; n],
648        mesa_3: vec![f64::NAN; n],
649        mesa_4: vec![f64::NAN; n],
650        trigger_1: vec![f64::NAN; n],
651        trigger_2: vec![f64::NAN; n],
652        trigger_3: vec![f64::NAN; n],
653        trigger_4: vec![f64::NAN; n],
654    };
655    compute_mesa_stochastic_multi_length_into(
656        source,
657        params,
658        &mut out.mesa_1,
659        &mut out.mesa_2,
660        &mut out.mesa_3,
661        &mut out.mesa_4,
662        &mut out.trigger_1,
663        &mut out.trigger_2,
664        &mut out.trigger_3,
665        &mut out.trigger_4,
666    )?;
667    Ok(out)
668}
669
670#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
671#[allow(clippy::too_many_arguments)]
672pub fn mesa_stochastic_multi_length_into(
673    mesa_1_out: &mut [f64],
674    mesa_2_out: &mut [f64],
675    mesa_3_out: &mut [f64],
676    mesa_4_out: &mut [f64],
677    trigger_1_out: &mut [f64],
678    trigger_2_out: &mut [f64],
679    trigger_3_out: &mut [f64],
680    trigger_4_out: &mut [f64],
681    input: &MesaStochasticMultiLengthInput,
682    kernel: Kernel,
683) -> Result<(), MesaStochasticMultiLengthError> {
684    mesa_stochastic_multi_length_into_slice(
685        mesa_1_out,
686        mesa_2_out,
687        mesa_3_out,
688        mesa_4_out,
689        trigger_1_out,
690        trigger_2_out,
691        trigger_3_out,
692        trigger_4_out,
693        input,
694        kernel,
695    )
696}
697
698#[allow(clippy::too_many_arguments)]
699pub fn mesa_stochastic_multi_length_into_slice(
700    mesa_1_out: &mut [f64],
701    mesa_2_out: &mut [f64],
702    mesa_3_out: &mut [f64],
703    mesa_4_out: &mut [f64],
704    trigger_1_out: &mut [f64],
705    trigger_2_out: &mut [f64],
706    trigger_3_out: &mut [f64],
707    trigger_4_out: &mut [f64],
708    input: &MesaStochasticMultiLengthInput,
709    _kernel: Kernel,
710) -> Result<(), MesaStochasticMultiLengthError> {
711    let source = extract_source(input)?;
712    let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
713    let params = ValidatedParams::from_params(&input.params)?;
714    compute_mesa_stochastic_multi_length_into(
715        source,
716        params,
717        mesa_1_out,
718        mesa_2_out,
719        mesa_3_out,
720        mesa_4_out,
721        trigger_1_out,
722        trigger_2_out,
723        trigger_3_out,
724        trigger_4_out,
725    )
726}
727
728#[derive(Clone, Debug)]
729pub struct MesaStochasticMultiLengthBatchRange {
730    pub length_1: (usize, usize, usize),
731    pub length_2: (usize, usize, usize),
732    pub length_3: (usize, usize, usize),
733    pub length_4: (usize, usize, usize),
734    pub trigger_length: (usize, usize, usize),
735}
736
737#[derive(Clone, Debug)]
738pub struct MesaStochasticMultiLengthBatchOutput {
739    pub mesa_1: Vec<f64>,
740    pub mesa_2: Vec<f64>,
741    pub mesa_3: Vec<f64>,
742    pub mesa_4: Vec<f64>,
743    pub trigger_1: Vec<f64>,
744    pub trigger_2: Vec<f64>,
745    pub trigger_3: Vec<f64>,
746    pub trigger_4: Vec<f64>,
747    pub combos: Vec<MesaStochasticMultiLengthParams>,
748    pub rows: usize,
749    pub cols: usize,
750}
751
752#[derive(Clone, Copy, Debug)]
753pub struct MesaStochasticMultiLengthBatchBuilder {
754    source: Option<&'static str>,
755    length_1: (usize, usize, usize),
756    length_2: (usize, usize, usize),
757    length_3: (usize, usize, usize),
758    length_4: (usize, usize, usize),
759    trigger_length: (usize, usize, usize),
760    kernel: Kernel,
761}
762
763impl Default for MesaStochasticMultiLengthBatchBuilder {
764    fn default() -> Self {
765        Self {
766            source: None,
767            length_1: (DEFAULT_LENGTH_1, DEFAULT_LENGTH_1, 0),
768            length_2: (DEFAULT_LENGTH_2, DEFAULT_LENGTH_2, 0),
769            length_3: (DEFAULT_LENGTH_3, DEFAULT_LENGTH_3, 0),
770            length_4: (DEFAULT_LENGTH_4, DEFAULT_LENGTH_4, 0),
771            trigger_length: (DEFAULT_TRIGGER_LENGTH, DEFAULT_TRIGGER_LENGTH, 0),
772            kernel: Kernel::Auto,
773        }
774    }
775}
776
777impl MesaStochasticMultiLengthBatchBuilder {
778    #[inline(always)]
779    pub fn new() -> Self {
780        Self::default()
781    }
782
783    #[inline(always)]
784    pub fn source(mut self, value: &'static str) -> Self {
785        self.source = Some(value);
786        self
787    }
788
789    #[inline(always)]
790    pub fn length_1_range(mut self, value: (usize, usize, usize)) -> Self {
791        self.length_1 = value;
792        self
793    }
794
795    #[inline(always)]
796    pub fn length_2_range(mut self, value: (usize, usize, usize)) -> Self {
797        self.length_2 = value;
798        self
799    }
800
801    #[inline(always)]
802    pub fn length_3_range(mut self, value: (usize, usize, usize)) -> Self {
803        self.length_3 = value;
804        self
805    }
806
807    #[inline(always)]
808    pub fn length_4_range(mut self, value: (usize, usize, usize)) -> Self {
809        self.length_4 = value;
810        self
811    }
812
813    #[inline(always)]
814    pub fn trigger_length_range(mut self, value: (usize, usize, usize)) -> Self {
815        self.trigger_length = value;
816        self
817    }
818
819    #[inline(always)]
820    pub fn kernel(mut self, value: Kernel) -> Self {
821        self.kernel = value;
822        self
823    }
824
825    #[inline(always)]
826    pub fn apply(
827        self,
828        candles: &Candles,
829    ) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
830        let source = source_type(candles, self.source.unwrap_or(DEFAULT_SOURCE));
831        mesa_stochastic_multi_length_batch_with_kernel(
832            source,
833            &MesaStochasticMultiLengthBatchRange {
834                length_1: self.length_1,
835                length_2: self.length_2,
836                length_3: self.length_3,
837                length_4: self.length_4,
838                trigger_length: self.trigger_length,
839            },
840            self.kernel,
841        )
842    }
843
844    #[inline(always)]
845    pub fn apply_slice(
846        self,
847        source: &[f64],
848    ) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
849        mesa_stochastic_multi_length_batch_with_kernel(
850            source,
851            &MesaStochasticMultiLengthBatchRange {
852                length_1: self.length_1,
853                length_2: self.length_2,
854                length_3: self.length_3,
855                length_4: self.length_4,
856                trigger_length: self.trigger_length,
857            },
858            self.kernel,
859        )
860    }
861}
862
863fn expand_one_range(
864    start: usize,
865    end: usize,
866    step: usize,
867) -> Result<Vec<usize>, MesaStochasticMultiLengthError> {
868    if start == 0 {
869        return Err(MesaStochasticMultiLengthError::InvalidRange {
870            start: start.to_string(),
871            end: end.to_string(),
872            step: step.to_string(),
873        });
874    }
875    if step == 0 {
876        if start != end {
877            return Err(MesaStochasticMultiLengthError::InvalidRange {
878                start: start.to_string(),
879                end: end.to_string(),
880                step: step.to_string(),
881            });
882        }
883        return Ok(vec![start]);
884    }
885    if start > end {
886        return Err(MesaStochasticMultiLengthError::InvalidRange {
887            start: start.to_string(),
888            end: end.to_string(),
889            step: step.to_string(),
890        });
891    }
892    let mut values = Vec::new();
893    let mut current = start;
894    while current <= end {
895        values.push(current);
896        current = match current.checked_add(step) {
897            Some(next) => next,
898            None => break,
899        };
900    }
901    Ok(values)
902}
903
904pub fn expand_grid(
905    sweep: &MesaStochasticMultiLengthBatchRange,
906) -> Result<Vec<MesaStochasticMultiLengthParams>, MesaStochasticMultiLengthError> {
907    let lengths_1 = expand_one_range(sweep.length_1.0, sweep.length_1.1, sweep.length_1.2)?;
908    let lengths_2 = expand_one_range(sweep.length_2.0, sweep.length_2.1, sweep.length_2.2)?;
909    let lengths_3 = expand_one_range(sweep.length_3.0, sweep.length_3.1, sweep.length_3.2)?;
910    let lengths_4 = expand_one_range(sweep.length_4.0, sweep.length_4.1, sweep.length_4.2)?;
911    let trigger_lengths = expand_one_range(
912        sweep.trigger_length.0,
913        sweep.trigger_length.1,
914        sweep.trigger_length.2,
915    )?;
916
917    let mut out = Vec::new();
918    for length_1 in lengths_1 {
919        for &length_2 in &lengths_2 {
920            for &length_3 in &lengths_3 {
921                for &length_4 in &lengths_4 {
922                    for &trigger_length in &trigger_lengths {
923                        out.push(MesaStochasticMultiLengthParams {
924                            length_1: Some(length_1),
925                            length_2: Some(length_2),
926                            length_3: Some(length_3),
927                            length_4: Some(length_4),
928                            trigger_length: Some(trigger_length),
929                        });
930                    }
931                }
932            }
933        }
934    }
935    Ok(out)
936}
937
938fn batch_compute_rows(
939    source: &[f64],
940    sweep: &MesaStochasticMultiLengthBatchRange,
941    kernel: Kernel,
942    parallel: bool,
943) -> Result<
944    (
945        Vec<MesaStochasticMultiLengthParams>,
946        Vec<MesaStochasticMultiLengthOutput>,
947    ),
948    MesaStochasticMultiLengthError,
949> {
950    let combos = expand_grid(sweep)?;
951    let kernel = match kernel {
952        Kernel::Auto => detect_best_batch_kernel().to_non_batch(),
953        other if other.is_batch() => other.to_non_batch(),
954        other => other.to_non_batch(),
955    };
956    let compute = |params: &MesaStochasticMultiLengthParams| {
957        let input = MesaStochasticMultiLengthInput::from_slice(source, params.clone());
958        mesa_stochastic_multi_length_with_kernel(&input, kernel)
959    };
960    let rows = if parallel {
961        #[cfg(not(target_arch = "wasm32"))]
962        {
963            combos
964                .par_iter()
965                .map(compute)
966                .collect::<Result<Vec<_>, _>>()?
967        }
968        #[cfg(target_arch = "wasm32")]
969        {
970            combos.iter().map(compute).collect::<Result<Vec<_>, _>>()?
971        }
972    } else {
973        combos.iter().map(compute).collect::<Result<Vec<_>, _>>()?
974    };
975    Ok((combos, rows))
976}
977
978fn flatten_rows(
979    rows: &[MesaStochasticMultiLengthOutput],
980    cols: usize,
981) -> MesaStochasticMultiLengthOutput {
982    let total = rows.len() * cols;
983    let mut out = MesaStochasticMultiLengthOutput {
984        mesa_1: vec![f64::NAN; total],
985        mesa_2: vec![f64::NAN; total],
986        mesa_3: vec![f64::NAN; total],
987        mesa_4: vec![f64::NAN; total],
988        trigger_1: vec![f64::NAN; total],
989        trigger_2: vec![f64::NAN; total],
990        trigger_3: vec![f64::NAN; total],
991        trigger_4: vec![f64::NAN; total],
992    };
993    for (row_idx, row) in rows.iter().enumerate() {
994        let start = row_idx * cols;
995        let end = start + cols;
996        out.mesa_1[start..end].copy_from_slice(&row.mesa_1);
997        out.mesa_2[start..end].copy_from_slice(&row.mesa_2);
998        out.mesa_3[start..end].copy_from_slice(&row.mesa_3);
999        out.mesa_4[start..end].copy_from_slice(&row.mesa_4);
1000        out.trigger_1[start..end].copy_from_slice(&row.trigger_1);
1001        out.trigger_2[start..end].copy_from_slice(&row.trigger_2);
1002        out.trigger_3[start..end].copy_from_slice(&row.trigger_3);
1003        out.trigger_4[start..end].copy_from_slice(&row.trigger_4);
1004    }
1005    out
1006}
1007
1008pub fn mesa_stochastic_multi_length_batch_with_kernel(
1009    source: &[f64],
1010    sweep: &MesaStochasticMultiLengthBatchRange,
1011    kernel: Kernel,
1012) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
1013    let batch_kernel = match kernel {
1014        Kernel::Auto => detect_best_batch_kernel(),
1015        other if other.is_batch() => other,
1016        _ => {
1017            return Err(MesaStochasticMultiLengthError::InvalidKernelForBatch(
1018                kernel,
1019            ))
1020        }
1021    };
1022    mesa_stochastic_multi_length_batch_par_slice(source, sweep, batch_kernel.to_non_batch())
1023}
1024
1025pub fn mesa_stochastic_multi_length_batch_slice(
1026    source: &[f64],
1027    sweep: &MesaStochasticMultiLengthBatchRange,
1028    kernel: Kernel,
1029) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
1030    if source.is_empty() {
1031        return Err(MesaStochasticMultiLengthError::EmptyInputData);
1032    }
1033    let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
1034    let (combos, rows) = batch_compute_rows(source, sweep, kernel, false)?;
1035    let flat = flatten_rows(&rows, source.len());
1036    Ok(MesaStochasticMultiLengthBatchOutput {
1037        mesa_1: flat.mesa_1,
1038        mesa_2: flat.mesa_2,
1039        mesa_3: flat.mesa_3,
1040        mesa_4: flat.mesa_4,
1041        trigger_1: flat.trigger_1,
1042        trigger_2: flat.trigger_2,
1043        trigger_3: flat.trigger_3,
1044        trigger_4: flat.trigger_4,
1045        rows: combos.len(),
1046        cols: source.len(),
1047        combos,
1048    })
1049}
1050
1051pub fn mesa_stochastic_multi_length_batch_par_slice(
1052    source: &[f64],
1053    sweep: &MesaStochasticMultiLengthBatchRange,
1054    kernel: Kernel,
1055) -> Result<MesaStochasticMultiLengthBatchOutput, MesaStochasticMultiLengthError> {
1056    if source.is_empty() {
1057        return Err(MesaStochasticMultiLengthError::EmptyInputData);
1058    }
1059    let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
1060    let (combos, rows) = batch_compute_rows(source, sweep, kernel, true)?;
1061    let flat = flatten_rows(&rows, source.len());
1062    Ok(MesaStochasticMultiLengthBatchOutput {
1063        mesa_1: flat.mesa_1,
1064        mesa_2: flat.mesa_2,
1065        mesa_3: flat.mesa_3,
1066        mesa_4: flat.mesa_4,
1067        trigger_1: flat.trigger_1,
1068        trigger_2: flat.trigger_2,
1069        trigger_3: flat.trigger_3,
1070        trigger_4: flat.trigger_4,
1071        rows: combos.len(),
1072        cols: source.len(),
1073        combos,
1074    })
1075}
1076
1077#[allow(clippy::too_many_arguments)]
1078pub fn mesa_stochastic_multi_length_batch_into_slice(
1079    mesa_1_out: &mut [f64],
1080    mesa_2_out: &mut [f64],
1081    mesa_3_out: &mut [f64],
1082    mesa_4_out: &mut [f64],
1083    trigger_1_out: &mut [f64],
1084    trigger_2_out: &mut [f64],
1085    trigger_3_out: &mut [f64],
1086    trigger_4_out: &mut [f64],
1087    source: &[f64],
1088    sweep: &MesaStochasticMultiLengthBatchRange,
1089    kernel: Kernel,
1090) -> Result<(), MesaStochasticMultiLengthError> {
1091    if source.is_empty() {
1092        return Err(MesaStochasticMultiLengthError::EmptyInputData);
1093    }
1094    let _ = first_valid(source).ok_or(MesaStochasticMultiLengthError::AllValuesNaN)?;
1095    let combos = expand_grid(sweep)?;
1096    let expected = combos.len().checked_mul(source.len()).ok_or_else(|| {
1097        MesaStochasticMultiLengthError::InvalidRange {
1098            start: combos.len().to_string(),
1099            end: source.len().to_string(),
1100            step: "rows*cols".to_string(),
1101        }
1102    })?;
1103    let got = [
1104        mesa_1_out.len(),
1105        mesa_2_out.len(),
1106        mesa_3_out.len(),
1107        mesa_4_out.len(),
1108        trigger_1_out.len(),
1109        trigger_2_out.len(),
1110        trigger_3_out.len(),
1111        trigger_4_out.len(),
1112    ]
1113    .into_iter()
1114    .max()
1115    .unwrap_or(0);
1116    if mesa_1_out.len() != expected
1117        || mesa_2_out.len() != expected
1118        || mesa_3_out.len() != expected
1119        || mesa_4_out.len() != expected
1120        || trigger_1_out.len() != expected
1121        || trigger_2_out.len() != expected
1122        || trigger_3_out.len() != expected
1123        || trigger_4_out.len() != expected
1124    {
1125        return Err(MesaStochasticMultiLengthError::OutputLengthMismatch { expected, got });
1126    }
1127    let (combos, rows) = batch_compute_rows(source, sweep, kernel, false)?;
1128    let cols = source.len();
1129    for (row_idx, row) in rows.iter().enumerate() {
1130        let start = row_idx * cols;
1131        let end = start + cols;
1132        mesa_1_out[start..end].copy_from_slice(&row.mesa_1);
1133        mesa_2_out[start..end].copy_from_slice(&row.mesa_2);
1134        mesa_3_out[start..end].copy_from_slice(&row.mesa_3);
1135        mesa_4_out[start..end].copy_from_slice(&row.mesa_4);
1136        trigger_1_out[start..end].copy_from_slice(&row.trigger_1);
1137        trigger_2_out[start..end].copy_from_slice(&row.trigger_2);
1138        trigger_3_out[start..end].copy_from_slice(&row.trigger_3);
1139        trigger_4_out[start..end].copy_from_slice(&row.trigger_4);
1140    }
1141    debug_assert_eq!(combos.len() * cols, expected);
1142    Ok(())
1143}
1144
1145#[cfg(feature = "python")]
1146#[pyfunction(name = "mesa_stochastic_multi_length")]
1147#[pyo3(signature = (
1148    source,
1149    length_1=48,
1150    length_2=21,
1151    length_3=9,
1152    length_4=6,
1153    trigger_length=2,
1154    kernel=None
1155))]
1156pub fn mesa_stochastic_multi_length_py<'py>(
1157    py: Python<'py>,
1158    source: PyReadonlyArray1<'py, f64>,
1159    length_1: usize,
1160    length_2: usize,
1161    length_3: usize,
1162    length_4: usize,
1163    trigger_length: usize,
1164    kernel: Option<&str>,
1165) -> PyResult<Bound<'py, PyDict>> {
1166    let source = source.as_slice()?;
1167    let kernel = validate_kernel(kernel, false)?;
1168    let input = MesaStochasticMultiLengthInput::from_slice(
1169        source,
1170        MesaStochasticMultiLengthParams {
1171            length_1: Some(length_1),
1172            length_2: Some(length_2),
1173            length_3: Some(length_3),
1174            length_4: Some(length_4),
1175            trigger_length: Some(trigger_length),
1176        },
1177    );
1178    let out = py
1179        .allow_threads(|| mesa_stochastic_multi_length_with_kernel(&input, kernel))
1180        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1181    let dict = PyDict::new(py);
1182    dict.set_item("mesa_1", out.mesa_1.into_pyarray(py))?;
1183    dict.set_item("mesa_2", out.mesa_2.into_pyarray(py))?;
1184    dict.set_item("mesa_3", out.mesa_3.into_pyarray(py))?;
1185    dict.set_item("mesa_4", out.mesa_4.into_pyarray(py))?;
1186    dict.set_item("trigger_1", out.trigger_1.into_pyarray(py))?;
1187    dict.set_item("trigger_2", out.trigger_2.into_pyarray(py))?;
1188    dict.set_item("trigger_3", out.trigger_3.into_pyarray(py))?;
1189    dict.set_item("trigger_4", out.trigger_4.into_pyarray(py))?;
1190    Ok(dict)
1191}
1192
1193#[cfg(feature = "python")]
1194#[pyclass(name = "MesaStochasticMultiLengthStream")]
1195pub struct MesaStochasticMultiLengthStreamPy {
1196    stream: MesaStochasticMultiLengthStream,
1197}
1198
1199#[cfg(feature = "python")]
1200#[pymethods]
1201impl MesaStochasticMultiLengthStreamPy {
1202    #[new]
1203    #[pyo3(signature = (
1204        length_1=48,
1205        length_2=21,
1206        length_3=9,
1207        length_4=6,
1208        trigger_length=2
1209    ))]
1210    fn new(
1211        length_1: usize,
1212        length_2: usize,
1213        length_3: usize,
1214        length_4: usize,
1215        trigger_length: usize,
1216    ) -> PyResult<Self> {
1217        let stream = MesaStochasticMultiLengthStream::try_new(MesaStochasticMultiLengthParams {
1218            length_1: Some(length_1),
1219            length_2: Some(length_2),
1220            length_3: Some(length_3),
1221            length_4: Some(length_4),
1222            trigger_length: Some(trigger_length),
1223        })
1224        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1225        Ok(Self { stream })
1226    }
1227
1228    fn update<'py>(&mut self, py: Python<'py>, source: f64) -> PyResult<Bound<'py, PyDict>> {
1229        let values = self.stream.update(source);
1230        let dict = PyDict::new(py);
1231        dict.set_item("mesa_1", values.0)?;
1232        dict.set_item("mesa_2", values.1)?;
1233        dict.set_item("mesa_3", values.2)?;
1234        dict.set_item("mesa_4", values.3)?;
1235        dict.set_item("trigger_1", values.4)?;
1236        dict.set_item("trigger_2", values.5)?;
1237        dict.set_item("trigger_3", values.6)?;
1238        dict.set_item("trigger_4", values.7)?;
1239        Ok(dict)
1240    }
1241}
1242
1243#[cfg(feature = "python")]
1244#[pyfunction(name = "mesa_stochastic_multi_length_batch")]
1245#[pyo3(signature = (
1246    source,
1247    length_1_range=(48,48,0),
1248    length_2_range=(21,21,0),
1249    length_3_range=(9,9,0),
1250    length_4_range=(6,6,0),
1251    trigger_length_range=(2,2,0),
1252    kernel=None
1253))]
1254pub fn mesa_stochastic_multi_length_batch_py<'py>(
1255    py: Python<'py>,
1256    source: PyReadonlyArray1<'py, f64>,
1257    length_1_range: (usize, usize, usize),
1258    length_2_range: (usize, usize, usize),
1259    length_3_range: (usize, usize, usize),
1260    length_4_range: (usize, usize, usize),
1261    trigger_length_range: (usize, usize, usize),
1262    kernel: Option<&str>,
1263) -> PyResult<Bound<'py, PyDict>> {
1264    let source = source.as_slice()?;
1265    let kernel = validate_kernel(kernel, true)?;
1266    let sweep = MesaStochasticMultiLengthBatchRange {
1267        length_1: length_1_range,
1268        length_2: length_2_range,
1269        length_3: length_3_range,
1270        length_4: length_4_range,
1271        trigger_length: trigger_length_range,
1272    };
1273    let out = py
1274        .allow_threads(|| mesa_stochastic_multi_length_batch_with_kernel(source, &sweep, kernel))
1275        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1276    let dict = PyDict::new(py);
1277    dict.set_item(
1278        "mesa_1",
1279        out.mesa_1.into_pyarray(py).reshape((out.rows, out.cols))?,
1280    )?;
1281    dict.set_item(
1282        "mesa_2",
1283        out.mesa_2.into_pyarray(py).reshape((out.rows, out.cols))?,
1284    )?;
1285    dict.set_item(
1286        "mesa_3",
1287        out.mesa_3.into_pyarray(py).reshape((out.rows, out.cols))?,
1288    )?;
1289    dict.set_item(
1290        "mesa_4",
1291        out.mesa_4.into_pyarray(py).reshape((out.rows, out.cols))?,
1292    )?;
1293    dict.set_item(
1294        "trigger_1",
1295        out.trigger_1
1296            .into_pyarray(py)
1297            .reshape((out.rows, out.cols))?,
1298    )?;
1299    dict.set_item(
1300        "trigger_2",
1301        out.trigger_2
1302            .into_pyarray(py)
1303            .reshape((out.rows, out.cols))?,
1304    )?;
1305    dict.set_item(
1306        "trigger_3",
1307        out.trigger_3
1308            .into_pyarray(py)
1309            .reshape((out.rows, out.cols))?,
1310    )?;
1311    dict.set_item(
1312        "trigger_4",
1313        out.trigger_4
1314            .into_pyarray(py)
1315            .reshape((out.rows, out.cols))?,
1316    )?;
1317    dict.set_item(
1318        "length_1",
1319        out.combos
1320            .iter()
1321            .map(|p| p.length_1.unwrap())
1322            .collect::<Vec<_>>(),
1323    )?;
1324    dict.set_item(
1325        "length_2",
1326        out.combos
1327            .iter()
1328            .map(|p| p.length_2.unwrap())
1329            .collect::<Vec<_>>(),
1330    )?;
1331    dict.set_item(
1332        "length_3",
1333        out.combos
1334            .iter()
1335            .map(|p| p.length_3.unwrap())
1336            .collect::<Vec<_>>(),
1337    )?;
1338    dict.set_item(
1339        "length_4",
1340        out.combos
1341            .iter()
1342            .map(|p| p.length_4.unwrap())
1343            .collect::<Vec<_>>(),
1344    )?;
1345    dict.set_item(
1346        "trigger_length",
1347        out.combos
1348            .iter()
1349            .map(|p| p.trigger_length.unwrap())
1350            .collect::<Vec<_>>(),
1351    )?;
1352    dict.set_item("rows", out.rows)?;
1353    dict.set_item("cols", out.cols)?;
1354    Ok(dict)
1355}
1356
1357#[cfg(feature = "python")]
1358pub fn register_mesa_stochastic_multi_length_module(
1359    m: &Bound<'_, pyo3::types::PyModule>,
1360) -> PyResult<()> {
1361    m.add_function(wrap_pyfunction!(mesa_stochastic_multi_length_py, m)?)?;
1362    m.add_function(wrap_pyfunction!(mesa_stochastic_multi_length_batch_py, m)?)?;
1363    m.add_class::<MesaStochasticMultiLengthStreamPy>()?;
1364    Ok(())
1365}
1366
1367#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1368#[derive(Serialize, Deserialize)]
1369pub struct MesaStochasticMultiLengthJsOutput {
1370    pub mesa_1: Vec<f64>,
1371    pub mesa_2: Vec<f64>,
1372    pub mesa_3: Vec<f64>,
1373    pub mesa_4: Vec<f64>,
1374    pub trigger_1: Vec<f64>,
1375    pub trigger_2: Vec<f64>,
1376    pub trigger_3: Vec<f64>,
1377    pub trigger_4: Vec<f64>,
1378}
1379
1380#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1381#[derive(Serialize, Deserialize)]
1382pub struct MesaStochasticMultiLengthBatchConfig {
1383    pub length_1_range: Vec<f64>,
1384    pub length_2_range: Vec<f64>,
1385    pub length_3_range: Vec<f64>,
1386    pub length_4_range: Vec<f64>,
1387    pub trigger_length_range: Vec<f64>,
1388}
1389
1390#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1391#[derive(Serialize, Deserialize)]
1392pub struct MesaStochasticMultiLengthBatchJsOutput {
1393    pub mesa_1: Vec<f64>,
1394    pub mesa_2: Vec<f64>,
1395    pub mesa_3: Vec<f64>,
1396    pub mesa_4: Vec<f64>,
1397    pub trigger_1: Vec<f64>,
1398    pub trigger_2: Vec<f64>,
1399    pub trigger_3: Vec<f64>,
1400    pub trigger_4: Vec<f64>,
1401    pub length_1: Vec<usize>,
1402    pub length_2: Vec<usize>,
1403    pub length_3: Vec<usize>,
1404    pub length_4: Vec<usize>,
1405    pub trigger_length: Vec<usize>,
1406    pub rows: usize,
1407    pub cols: usize,
1408}
1409
1410#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1411fn js_vec3_to_usize(name: &str, values: &[f64]) -> Result<(usize, usize, usize), JsValue> {
1412    if values.len() != 3 {
1413        return Err(JsValue::from_str(&format!(
1414            "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1415        )));
1416    }
1417    let mut out = [0usize; 3];
1418    for (i, value) in values.iter().copied().enumerate() {
1419        if !value.is_finite() || value < 0.0 {
1420            return Err(JsValue::from_str(&format!(
1421                "Invalid config: {name}[{i}] must be a finite non-negative whole number"
1422            )));
1423        }
1424        let rounded = value.round();
1425        if (value - rounded).abs() > 1e-9 {
1426            return Err(JsValue::from_str(&format!(
1427                "Invalid config: {name}[{i}] must be a whole number"
1428            )));
1429        }
1430        out[i] = rounded as usize;
1431    }
1432    Ok((out[0], out[1], out[2]))
1433}
1434
1435#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1436#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_js")]
1437pub fn mesa_stochastic_multi_length_js(
1438    source: &[f64],
1439    length_1: usize,
1440    length_2: usize,
1441    length_3: usize,
1442    length_4: usize,
1443    trigger_length: usize,
1444) -> Result<JsValue, JsValue> {
1445    let input = MesaStochasticMultiLengthInput::from_slice(
1446        source,
1447        MesaStochasticMultiLengthParams {
1448            length_1: Some(length_1),
1449            length_2: Some(length_2),
1450            length_3: Some(length_3),
1451            length_4: Some(length_4),
1452            trigger_length: Some(trigger_length),
1453        },
1454    );
1455    let out = mesa_stochastic_multi_length_with_kernel(&input, Kernel::Auto)
1456        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1457    serde_wasm_bindgen::to_value(&MesaStochasticMultiLengthJsOutput {
1458        mesa_1: out.mesa_1,
1459        mesa_2: out.mesa_2,
1460        mesa_3: out.mesa_3,
1461        mesa_4: out.mesa_4,
1462        trigger_1: out.trigger_1,
1463        trigger_2: out.trigger_2,
1464        trigger_3: out.trigger_3,
1465        trigger_4: out.trigger_4,
1466    })
1467    .map_err(|e| JsValue::from_str(&e.to_string()))
1468}
1469
1470#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1471#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_batch_js")]
1472pub fn mesa_stochastic_multi_length_batch_js(
1473    source: &[f64],
1474    config: JsValue,
1475) -> Result<JsValue, JsValue> {
1476    let config: MesaStochasticMultiLengthBatchConfig =
1477        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1478    let sweep = MesaStochasticMultiLengthBatchRange {
1479        length_1: js_vec3_to_usize("length_1_range", &config.length_1_range)?,
1480        length_2: js_vec3_to_usize("length_2_range", &config.length_2_range)?,
1481        length_3: js_vec3_to_usize("length_3_range", &config.length_3_range)?,
1482        length_4: js_vec3_to_usize("length_4_range", &config.length_4_range)?,
1483        trigger_length: js_vec3_to_usize("trigger_length_range", &config.trigger_length_range)?,
1484    };
1485    let out = mesa_stochastic_multi_length_batch_with_kernel(source, &sweep, Kernel::Auto)
1486        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1487    serde_wasm_bindgen::to_value(&MesaStochasticMultiLengthBatchJsOutput {
1488        mesa_1: out.mesa_1,
1489        mesa_2: out.mesa_2,
1490        mesa_3: out.mesa_3,
1491        mesa_4: out.mesa_4,
1492        trigger_1: out.trigger_1,
1493        trigger_2: out.trigger_2,
1494        trigger_3: out.trigger_3,
1495        trigger_4: out.trigger_4,
1496        length_1: out.combos.iter().map(|p| p.length_1.unwrap()).collect(),
1497        length_2: out.combos.iter().map(|p| p.length_2.unwrap()).collect(),
1498        length_3: out.combos.iter().map(|p| p.length_3.unwrap()).collect(),
1499        length_4: out.combos.iter().map(|p| p.length_4.unwrap()).collect(),
1500        trigger_length: out
1501            .combos
1502            .iter()
1503            .map(|p| p.trigger_length.unwrap())
1504            .collect(),
1505        rows: out.rows,
1506        cols: out.cols,
1507    })
1508    .map_err(|e| JsValue::from_str(&e.to_string()))
1509}
1510
1511#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1512#[wasm_bindgen]
1513pub fn mesa_stochastic_multi_length_alloc(len: usize) -> *mut f64 {
1514    let mut buf = vec![0.0; len];
1515    let ptr = buf.as_mut_ptr();
1516    std::mem::forget(buf);
1517    ptr
1518}
1519
1520#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1521#[wasm_bindgen]
1522pub fn mesa_stochastic_multi_length_free(ptr: *mut f64, len: usize) {
1523    if ptr.is_null() || len == 0 {
1524        return;
1525    }
1526    unsafe {
1527        drop(Vec::from_raw_parts(ptr, len, len));
1528    }
1529}
1530
1531#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1532#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_into")]
1533#[allow(clippy::too_many_arguments)]
1534pub fn mesa_stochastic_multi_length_into(
1535    source_ptr: *const f64,
1536    mesa_1_ptr: *mut f64,
1537    mesa_2_ptr: *mut f64,
1538    mesa_3_ptr: *mut f64,
1539    mesa_4_ptr: *mut f64,
1540    trigger_1_ptr: *mut f64,
1541    trigger_2_ptr: *mut f64,
1542    trigger_3_ptr: *mut f64,
1543    trigger_4_ptr: *mut f64,
1544    len: usize,
1545    length_1: usize,
1546    length_2: usize,
1547    length_3: usize,
1548    length_4: usize,
1549    trigger_length: usize,
1550) -> Result<(), JsValue> {
1551    if source_ptr.is_null()
1552        || mesa_1_ptr.is_null()
1553        || mesa_2_ptr.is_null()
1554        || mesa_3_ptr.is_null()
1555        || mesa_4_ptr.is_null()
1556        || trigger_1_ptr.is_null()
1557        || trigger_2_ptr.is_null()
1558        || trigger_3_ptr.is_null()
1559        || trigger_4_ptr.is_null()
1560    {
1561        return Err(JsValue::from_str(
1562            "null pointer passed to mesa_stochastic_multi_length_into",
1563        ));
1564    }
1565    let source = unsafe { std::slice::from_raw_parts(source_ptr, len) };
1566    let mesa_1_out = unsafe { std::slice::from_raw_parts_mut(mesa_1_ptr, len) };
1567    let mesa_2_out = unsafe { std::slice::from_raw_parts_mut(mesa_2_ptr, len) };
1568    let mesa_3_out = unsafe { std::slice::from_raw_parts_mut(mesa_3_ptr, len) };
1569    let mesa_4_out = unsafe { std::slice::from_raw_parts_mut(mesa_4_ptr, len) };
1570    let trigger_1_out = unsafe { std::slice::from_raw_parts_mut(trigger_1_ptr, len) };
1571    let trigger_2_out = unsafe { std::slice::from_raw_parts_mut(trigger_2_ptr, len) };
1572    let trigger_3_out = unsafe { std::slice::from_raw_parts_mut(trigger_3_ptr, len) };
1573    let trigger_4_out = unsafe { std::slice::from_raw_parts_mut(trigger_4_ptr, len) };
1574    let input = MesaStochasticMultiLengthInput::from_slice(
1575        source,
1576        MesaStochasticMultiLengthParams {
1577            length_1: Some(length_1),
1578            length_2: Some(length_2),
1579            length_3: Some(length_3),
1580            length_4: Some(length_4),
1581            trigger_length: Some(trigger_length),
1582        },
1583    );
1584    mesa_stochastic_multi_length_into_slice(
1585        mesa_1_out,
1586        mesa_2_out,
1587        mesa_3_out,
1588        mesa_4_out,
1589        trigger_1_out,
1590        trigger_2_out,
1591        trigger_3_out,
1592        trigger_4_out,
1593        &input,
1594        Kernel::Auto,
1595    )
1596    .map_err(|e| JsValue::from_str(&e.to_string()))
1597}
1598
1599#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1600#[wasm_bindgen(js_name = "mesa_stochastic_multi_length_batch_into")]
1601#[allow(clippy::too_many_arguments)]
1602pub fn mesa_stochastic_multi_length_batch_into(
1603    source_ptr: *const f64,
1604    mesa_1_ptr: *mut f64,
1605    mesa_2_ptr: *mut f64,
1606    mesa_3_ptr: *mut f64,
1607    mesa_4_ptr: *mut f64,
1608    trigger_1_ptr: *mut f64,
1609    trigger_2_ptr: *mut f64,
1610    trigger_3_ptr: *mut f64,
1611    trigger_4_ptr: *mut f64,
1612    len: usize,
1613    length_1_start: usize,
1614    length_1_end: usize,
1615    length_1_step: usize,
1616    length_2_start: usize,
1617    length_2_end: usize,
1618    length_2_step: usize,
1619    length_3_start: usize,
1620    length_3_end: usize,
1621    length_3_step: usize,
1622    length_4_start: usize,
1623    length_4_end: usize,
1624    length_4_step: usize,
1625    trigger_length_start: usize,
1626    trigger_length_end: usize,
1627    trigger_length_step: usize,
1628) -> Result<usize, JsValue> {
1629    if source_ptr.is_null()
1630        || mesa_1_ptr.is_null()
1631        || mesa_2_ptr.is_null()
1632        || mesa_3_ptr.is_null()
1633        || mesa_4_ptr.is_null()
1634        || trigger_1_ptr.is_null()
1635        || trigger_2_ptr.is_null()
1636        || trigger_3_ptr.is_null()
1637        || trigger_4_ptr.is_null()
1638    {
1639        return Err(JsValue::from_str(
1640            "null pointer passed to mesa_stochastic_multi_length_batch_into",
1641        ));
1642    }
1643    let source = unsafe { std::slice::from_raw_parts(source_ptr, len) };
1644    let sweep = MesaStochasticMultiLengthBatchRange {
1645        length_1: (length_1_start, length_1_end, length_1_step),
1646        length_2: (length_2_start, length_2_end, length_2_step),
1647        length_3: (length_3_start, length_3_end, length_3_step),
1648        length_4: (length_4_start, length_4_end, length_4_step),
1649        trigger_length: (
1650            trigger_length_start,
1651            trigger_length_end,
1652            trigger_length_step,
1653        ),
1654    };
1655    let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1656    let rows = combos.len();
1657    let total = rows.checked_mul(len).ok_or_else(|| {
1658        JsValue::from_str("rows*cols overflow in mesa_stochastic_multi_length_batch_into")
1659    })?;
1660    let mesa_1_out = unsafe { std::slice::from_raw_parts_mut(mesa_1_ptr, total) };
1661    let mesa_2_out = unsafe { std::slice::from_raw_parts_mut(mesa_2_ptr, total) };
1662    let mesa_3_out = unsafe { std::slice::from_raw_parts_mut(mesa_3_ptr, total) };
1663    let mesa_4_out = unsafe { std::slice::from_raw_parts_mut(mesa_4_ptr, total) };
1664    let trigger_1_out = unsafe { std::slice::from_raw_parts_mut(trigger_1_ptr, total) };
1665    let trigger_2_out = unsafe { std::slice::from_raw_parts_mut(trigger_2_ptr, total) };
1666    let trigger_3_out = unsafe { std::slice::from_raw_parts_mut(trigger_3_ptr, total) };
1667    let trigger_4_out = unsafe { std::slice::from_raw_parts_mut(trigger_4_ptr, total) };
1668    mesa_stochastic_multi_length_batch_into_slice(
1669        mesa_1_out,
1670        mesa_2_out,
1671        mesa_3_out,
1672        mesa_4_out,
1673        trigger_1_out,
1674        trigger_2_out,
1675        trigger_3_out,
1676        trigger_4_out,
1677        source,
1678        &sweep,
1679        Kernel::Auto,
1680    )
1681    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1682    Ok(rows)
1683}
1684
1685#[cfg(test)]
1686mod tests {
1687    use super::*;
1688    use crate::utilities::data_loader::read_candles_from_csv;
1689
1690    fn manual_sma(values: &[f64], length: usize) -> Vec<f64> {
1691        let mut out = vec![f64::NAN; values.len()];
1692        if length == 0 || values.len() < length {
1693            return out;
1694        }
1695        for i in (length - 1)..values.len() {
1696            let window = &values[i + 1 - length..=i];
1697            if window.iter().all(|v| v.is_finite()) {
1698                out[i] = window.iter().sum::<f64>() / length as f64;
1699            }
1700        }
1701        out
1702    }
1703
1704    fn manual_reference(
1705        source: &[f64],
1706        params: ValidatedParams,
1707    ) -> MesaStochasticMultiLengthOutput {
1708        let n = source.len();
1709        let mut out = MesaStochasticMultiLengthOutput {
1710            mesa_1: vec![f64::NAN; n],
1711            mesa_2: vec![f64::NAN; n],
1712            mesa_3: vec![f64::NAN; n],
1713            mesa_4: vec![f64::NAN; n],
1714            trigger_1: vec![f64::NAN; n],
1715            trigger_2: vec![f64::NAN; n],
1716            trigger_3: vec![f64::NAN; n],
1717            trigger_4: vec![f64::NAN; n],
1718        };
1719
1720        let alpha1 = ((0.707 * 2.0 * PI / 48.0).cos() + (0.707 * 2.0 * PI / 48.0).sin() - 1.0)
1721            / (0.707 * 2.0 * PI / 48.0).cos();
1722        let hp_coef = (1.0 - alpha1 * 0.5) * (1.0 - alpha1 * 0.5);
1723        let one_minus_alpha = 1.0 - alpha1;
1724        let hp_feedback_1 = 2.0 * one_minus_alpha;
1725        let hp_feedback_2 = -(one_minus_alpha * one_minus_alpha);
1726        let a1 = (-1.414 * PI / 10.0).exp();
1727        let b1 = 2.0 * a1 * (1.414 * PI / 10.0).cos();
1728        let c2 = b1;
1729        let c3 = -(a1 * a1);
1730        let c1 = 1.0 - c2 - c3;
1731
1732        let mut hp = vec![f64::NAN; n];
1733        let mut filt = vec![f64::NAN; n];
1734        for i in 0..n {
1735            if source[i].is_finite() {
1736                let src1 = if i >= 1 { nz(source[i - 1]) } else { 0.0 };
1737                let src2 = if i >= 2 { nz(source[i - 2]) } else { 0.0 };
1738                let hp1 = if i >= 1 { nz(hp[i - 1]) } else { 0.0 };
1739                let hp2 = if i >= 2 { nz(hp[i - 2]) } else { 0.0 };
1740                hp[i] = hp_coef.mul_add(
1741                    source[i] - 2.0 * src1 + src2,
1742                    hp_feedback_1.mul_add(hp1, hp_feedback_2 * hp2),
1743                );
1744                let filt1 = if i >= 1 { nz(filt[i - 1]) } else { 0.0 };
1745                let filt2 = if i >= 2 { nz(filt[i - 2]) } else { 0.0 };
1746                filt[i] = c1.mul_add(hp[i], c2.mul_add(filt1, c3 * filt2));
1747            }
1748        }
1749
1750        fn mesa_from_filt(filt: &[f64], length: usize, c1: f64, c2: f64, c3: f64) -> Vec<f64> {
1751            let n = filt.len();
1752            let mut out = vec![f64::NAN; n];
1753            for i in 0..n {
1754                if !filt[i].is_finite() {
1755                    continue;
1756                }
1757                let mut highest = filt[i];
1758                let mut lowest = filt[i];
1759                for count in 0..length {
1760                    let value = if i >= count { nz(filt[i - count]) } else { 0.0 };
1761                    if value > highest {
1762                        highest = value;
1763                    }
1764                    if value < lowest {
1765                        lowest = value;
1766                    }
1767                }
1768                let denom = highest - lowest;
1769                if denom == 0.0 || !denom.is_finite() {
1770                    continue;
1771                }
1772                let stoc = (filt[i] - lowest) / denom;
1773                if !stoc.is_finite() {
1774                    continue;
1775                }
1776                let prev1 = if i >= 1 { nz(out[i - 1]) } else { 0.0 };
1777                let prev2 = if i >= 2 { nz(out[i - 2]) } else { 0.0 };
1778                out[i] = c1.mul_add(stoc, c2.mul_add(prev1, c3 * prev2));
1779            }
1780            out
1781        }
1782
1783        out.mesa_1 = mesa_from_filt(&filt, params.length_1, c1, c2, c3);
1784        out.mesa_2 = mesa_from_filt(&filt, params.length_2, c1, c2, c3);
1785        out.mesa_3 = mesa_from_filt(&filt, params.length_3, c1, c2, c3);
1786        out.mesa_4 = mesa_from_filt(&filt, params.length_4, c1, c2, c3);
1787        out.trigger_1 = manual_sma(&out.mesa_1, params.trigger_length);
1788        out.trigger_2 = manual_sma(&out.mesa_2, params.trigger_length);
1789        out.trigger_3 = manual_sma(&out.mesa_3, params.trigger_length);
1790        out.trigger_4 = manual_sma(&out.mesa_4, params.trigger_length);
1791        out
1792    }
1793
1794    fn assert_close(actual: &[f64], expected: &[f64]) {
1795        assert_eq!(actual.len(), expected.len());
1796        for (i, (&a, &b)) in actual.iter().zip(expected.iter()).enumerate() {
1797            if a.is_nan() && b.is_nan() {
1798                continue;
1799            }
1800            assert!(
1801                (a - b).abs() <= 1e-12,
1802                "mismatch at {i}: actual={a:?} expected={b:?}"
1803            );
1804        }
1805    }
1806
1807    #[test]
1808    fn manual_reference_matches_core() {
1809        let candles =
1810            read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1811        let source = &candles.close[..160];
1812        let params = MesaStochasticMultiLengthParams::default();
1813        let input = MesaStochasticMultiLengthInput::from_slice(source, params.clone());
1814        let got = mesa_stochastic_multi_length(&input).unwrap();
1815        let want = manual_reference(source, ValidatedParams::from_params(&params).unwrap());
1816        assert_close(&got.mesa_1, &want.mesa_1);
1817        assert_close(&got.mesa_2, &want.mesa_2);
1818        assert_close(&got.mesa_3, &want.mesa_3);
1819        assert_close(&got.mesa_4, &want.mesa_4);
1820        assert_close(&got.trigger_1, &want.trigger_1);
1821        assert_close(&got.trigger_2, &want.trigger_2);
1822        assert_close(&got.trigger_3, &want.trigger_3);
1823        assert_close(&got.trigger_4, &want.trigger_4);
1824    }
1825
1826    #[test]
1827    fn stream_matches_batch() {
1828        let candles =
1829            read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1830        let source = &candles.close[..160];
1831        let input = MesaStochasticMultiLengthInput::from_slice(
1832            source,
1833            MesaStochasticMultiLengthParams::default(),
1834        );
1835        let batch = mesa_stochastic_multi_length(&input).unwrap();
1836        let mut stream =
1837            MesaStochasticMultiLengthStream::try_new(MesaStochasticMultiLengthParams::default())
1838                .unwrap();
1839        let mut last = (
1840            f64::NAN,
1841            f64::NAN,
1842            f64::NAN,
1843            f64::NAN,
1844            f64::NAN,
1845            f64::NAN,
1846            f64::NAN,
1847            f64::NAN,
1848        );
1849        for &value in source {
1850            last = stream.update(value);
1851        }
1852        assert!(
1853            (last.0 - batch.mesa_1[source.len() - 1]).abs() <= 1e-12
1854                || (last.0.is_nan() && batch.mesa_1[source.len() - 1].is_nan())
1855        );
1856        assert!(
1857            (last.1 - batch.mesa_2[source.len() - 1]).abs() <= 1e-12
1858                || (last.1.is_nan() && batch.mesa_2[source.len() - 1].is_nan())
1859        );
1860        assert!(
1861            (last.2 - batch.mesa_3[source.len() - 1]).abs() <= 1e-12
1862                || (last.2.is_nan() && batch.mesa_3[source.len() - 1].is_nan())
1863        );
1864        assert!(
1865            (last.3 - batch.mesa_4[source.len() - 1]).abs() <= 1e-12
1866                || (last.3.is_nan() && batch.mesa_4[source.len() - 1].is_nan())
1867        );
1868        assert!(
1869            (last.4 - batch.trigger_1[source.len() - 1]).abs() <= 1e-12
1870                || (last.4.is_nan() && batch.trigger_1[source.len() - 1].is_nan())
1871        );
1872        assert!(
1873            (last.5 - batch.trigger_2[source.len() - 1]).abs() <= 1e-12
1874                || (last.5.is_nan() && batch.trigger_2[source.len() - 1].is_nan())
1875        );
1876        assert!(
1877            (last.6 - batch.trigger_3[source.len() - 1]).abs() <= 1e-12
1878                || (last.6.is_nan() && batch.trigger_3[source.len() - 1].is_nan())
1879        );
1880        assert!(
1881            (last.7 - batch.trigger_4[source.len() - 1]).abs() <= 1e-12
1882                || (last.7.is_nan() && batch.trigger_4[source.len() - 1].is_nan())
1883        );
1884    }
1885
1886    #[test]
1887    fn batch_first_row_matches_single() {
1888        let candles =
1889            read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1890        let source = &candles.close[..128];
1891        let single = mesa_stochastic_multi_length(&MesaStochasticMultiLengthInput::from_slice(
1892            source,
1893            MesaStochasticMultiLengthParams::default(),
1894        ))
1895        .unwrap();
1896        let batch = mesa_stochastic_multi_length_batch_with_kernel(
1897            source,
1898            &MesaStochasticMultiLengthBatchRange {
1899                length_1: (48, 50, 2),
1900                length_2: (21, 21, 0),
1901                length_3: (9, 9, 0),
1902                length_4: (6, 6, 0),
1903                trigger_length: (2, 2, 0),
1904            },
1905            Kernel::ScalarBatch,
1906        )
1907        .unwrap();
1908        let cols = source.len();
1909        assert_eq!(batch.rows, 2);
1910        assert_close(&batch.mesa_1[..cols], &single.mesa_1);
1911        assert_close(&batch.trigger_1[..cols], &single.trigger_1);
1912    }
1913
1914    #[test]
1915    fn into_slice_matches_owned_output() {
1916        let candles =
1917            read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1918        let source = &candles.close[..128];
1919        let input = MesaStochasticMultiLengthInput::from_slice(
1920            source,
1921            MesaStochasticMultiLengthParams::default(),
1922        );
1923        let single = mesa_stochastic_multi_length(&input).unwrap();
1924        let mut mesa_1 = vec![f64::NAN; source.len()];
1925        let mut mesa_2 = vec![f64::NAN; source.len()];
1926        let mut mesa_3 = vec![f64::NAN; source.len()];
1927        let mut mesa_4 = vec![f64::NAN; source.len()];
1928        let mut trigger_1 = vec![f64::NAN; source.len()];
1929        let mut trigger_2 = vec![f64::NAN; source.len()];
1930        let mut trigger_3 = vec![f64::NAN; source.len()];
1931        let mut trigger_4 = vec![f64::NAN; source.len()];
1932        mesa_stochastic_multi_length_into_slice(
1933            &mut mesa_1,
1934            &mut mesa_2,
1935            &mut mesa_3,
1936            &mut mesa_4,
1937            &mut trigger_1,
1938            &mut trigger_2,
1939            &mut trigger_3,
1940            &mut trigger_4,
1941            &input,
1942            Kernel::Auto,
1943        )
1944        .unwrap();
1945        assert_close(&mesa_1, &single.mesa_1);
1946        assert_close(&mesa_2, &single.mesa_2);
1947        assert_close(&mesa_3, &single.mesa_3);
1948        assert_close(&mesa_4, &single.mesa_4);
1949        assert_close(&trigger_1, &single.trigger_1);
1950        assert_close(&trigger_2, &single.trigger_2);
1951        assert_close(&trigger_3, &single.trigger_3);
1952        assert_close(&trigger_4, &single.trigger_4);
1953    }
1954
1955    #[test]
1956    fn rejects_invalid_period() {
1957        let candles =
1958            read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
1959        let input = MesaStochasticMultiLengthInput::from_slice(
1960            &candles.close[..64],
1961            MesaStochasticMultiLengthParams {
1962                length_1: Some(0),
1963                ..MesaStochasticMultiLengthParams::default()
1964            },
1965        );
1966        let err = mesa_stochastic_multi_length(&input).unwrap_err();
1967        assert!(err.to_string().contains("Invalid period"));
1968    }
1969}