Skip to main content

vector_ta/indicators/
mass.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::cuda_available;
3#[cfg(all(feature = "python", feature = "cuda"))]
4use crate::cuda::mass_wrapper::CudaMass;
5#[cfg(all(feature = "python", feature = "cuda"))]
6use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
7#[cfg(feature = "python")]
8use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
9#[cfg(feature = "python")]
10use pyo3::exceptions::PyValueError;
11#[cfg(feature = "python")]
12use pyo3::prelude::*;
13#[cfg(feature = "python")]
14use pyo3::types::PyDict;
15#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
16use serde::{Deserialize, Serialize};
17#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
18use wasm_bindgen::prelude::*;
19
20use crate::utilities::data_loader::{source_type, Candles};
21use crate::utilities::enums::Kernel;
22use crate::utilities::helpers::{
23    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
24    make_uninit_matrix,
25};
26#[cfg(feature = "python")]
27use crate::utilities::kernel_validation::validate_kernel;
28use aligned_vec::{AVec, CACHELINE_ALIGN};
29#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
30use core::arch::x86_64::*;
31#[cfg(not(target_arch = "wasm32"))]
32use rayon::prelude::*;
33use std::convert::AsRef;
34use thiserror::Error;
35
36#[derive(Debug, Clone)]
37pub enum MassData<'a> {
38    Candles {
39        candles: &'a Candles,
40        high_source: &'a str,
41        low_source: &'a str,
42    },
43    Slices {
44        high: &'a [f64],
45        low: &'a [f64],
46    },
47}
48
49#[derive(Debug, Clone)]
50pub struct MassOutput {
51    pub values: Vec<f64>,
52}
53
54#[derive(Debug, Clone)]
55#[cfg_attr(
56    all(target_arch = "wasm32", feature = "wasm"),
57    derive(Serialize, Deserialize)
58)]
59pub struct MassParams {
60    pub period: Option<usize>,
61}
62
63impl Default for MassParams {
64    fn default() -> Self {
65        Self { period: Some(5) }
66    }
67}
68
69#[derive(Debug, Clone)]
70pub struct MassInput<'a> {
71    pub data: MassData<'a>,
72    pub params: MassParams,
73}
74
75impl<'a> MassInput<'a> {
76    #[inline]
77    pub fn from_candles(
78        candles: &'a Candles,
79        high_source: &'a str,
80        low_source: &'a str,
81        params: MassParams,
82    ) -> Self {
83        Self {
84            data: MassData::Candles {
85                candles,
86                high_source,
87                low_source,
88            },
89            params,
90        }
91    }
92
93    #[inline]
94    pub fn from_slices(high: &'a [f64], low: &'a [f64], params: MassParams) -> Self {
95        Self {
96            data: MassData::Slices { high, low },
97            params,
98        }
99    }
100
101    #[inline]
102    pub fn with_default_candles(candles: &'a Candles) -> Self {
103        Self {
104            data: MassData::Candles {
105                candles,
106                high_source: "high",
107                low_source: "low",
108            },
109            params: MassParams::default(),
110        }
111    }
112
113    #[inline]
114    pub fn get_period(&self) -> usize {
115        self.params
116            .period
117            .unwrap_or_else(|| MassParams::default().period.unwrap())
118    }
119}
120
121#[derive(Debug, Error)]
122pub enum MassError {
123    #[error("mass: Empty data provided.")]
124    EmptyInputData,
125    #[error("mass: High and low slices must have the same length.")]
126    DifferentLengthHL,
127    #[error("mass: Invalid period: period = {period}, data length = {data_len}")]
128    InvalidPeriod { period: usize, data_len: usize },
129    #[error("mass: Not enough valid data: needed = {needed}, valid = {valid}")]
130    NotEnoughValidData { needed: usize, valid: usize },
131    #[error("mass: All values are NaN.")]
132    AllValuesNaN,
133    #[error("mass: Output length mismatch: expected {expected}, got {got}")]
134    OutputLengthMismatch { expected: usize, got: usize },
135    #[error("mass: Invalid range expansion: start={start}, end={end}, step={step}")]
136    InvalidRange {
137        start: usize,
138        end: usize,
139        step: usize,
140    },
141    #[error("mass: Invalid kernel for batch: {0:?}")]
142    InvalidKernelForBatch(Kernel),
143}
144
145#[derive(Clone, Debug)]
146pub struct MassBuilder {
147    period: Option<usize>,
148    kernel: Kernel,
149}
150
151impl Default for MassBuilder {
152    fn default() -> Self {
153        Self {
154            period: None,
155            kernel: Kernel::Auto,
156        }
157    }
158}
159
160impl MassBuilder {
161    #[inline(always)]
162    pub fn new() -> Self {
163        Self::default()
164    }
165    #[inline(always)]
166    pub fn period(mut self, n: usize) -> Self {
167        self.period = Some(n);
168        self
169    }
170    #[inline(always)]
171    pub fn kernel(mut self, k: Kernel) -> Self {
172        self.kernel = k;
173        self
174    }
175
176    #[inline(always)]
177    pub fn apply(self, c: &Candles) -> Result<MassOutput, MassError> {
178        let p = MassParams {
179            period: self.period,
180        };
181        let i = MassInput::from_candles(c, "high", "low", p);
182        mass_with_kernel(&i, self.kernel)
183    }
184
185    #[inline(always)]
186    pub fn apply_slices(self, high: &[f64], low: &[f64]) -> Result<MassOutput, MassError> {
187        let p = MassParams {
188            period: self.period,
189        };
190        let i = MassInput::from_slices(high, low, p);
191        mass_with_kernel(&i, self.kernel)
192    }
193
194    #[inline(always)]
195    pub fn into_stream(self) -> Result<MassStream, MassError> {
196        let p = MassParams {
197            period: self.period,
198        };
199        MassStream::try_new(p)
200    }
201}
202
203#[inline(always)]
204fn mass_prepare<'a>(
205    input: &'a MassInput,
206    kernel: Kernel,
207) -> Result<(&'a [f64], &'a [f64], usize, usize, Kernel), MassError> {
208    let (high, low) = match &input.data {
209        MassData::Candles {
210            candles,
211            high_source,
212            low_source,
213        } => (
214            source_type(candles, high_source),
215            source_type(candles, low_source),
216        ),
217        MassData::Slices { high, low } => (*high, *low),
218    };
219
220    if high.is_empty() || low.is_empty() {
221        return Err(MassError::EmptyInputData);
222    }
223    if high.len() != low.len() {
224        return Err(MassError::DifferentLengthHL);
225    }
226
227    let period = input.get_period();
228    if period == 0 || period > high.len() {
229        return Err(MassError::InvalidPeriod {
230            period,
231            data_len: high.len(),
232        });
233    }
234
235    let first = (0..high.len())
236        .find(|&i| !high[i].is_nan() && !low[i].is_nan())
237        .ok_or(MassError::AllValuesNaN)?;
238
239    let needed_bars = 16 + period - 1;
240    if high.len() - first < needed_bars {
241        return Err(MassError::NotEnoughValidData {
242            needed: needed_bars,
243            valid: high.len() - first,
244        });
245    }
246
247    let chosen = match kernel {
248        Kernel::Auto => Kernel::Scalar,
249        k => k,
250    };
251    Ok((high, low, period, first, chosen))
252}
253
254#[inline(always)]
255fn mass_compute_into(
256    high: &[f64],
257    low: &[f64],
258    period: usize,
259    first: usize,
260    kern: Kernel,
261    out: &mut [f64],
262) {
263    unsafe {
264        match kern {
265            Kernel::Scalar | Kernel::ScalarBatch => mass_scalar(high, low, period, first, out),
266            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
267            Kernel::Avx2 | Kernel::Avx2Batch => mass_avx2(high, low, period, first, out),
268            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
269            Kernel::Avx512 | Kernel::Avx512Batch => mass_avx512(high, low, period, first, out),
270            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
271            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
272                mass_scalar(high, low, period, first, out)
273            }
274            _ => unreachable!(),
275        }
276    }
277}
278
279#[inline]
280pub fn mass(input: &MassInput) -> Result<MassOutput, MassError> {
281    mass_with_kernel(input, Kernel::Auto)
282}
283
284pub fn mass_with_kernel(input: &MassInput, kernel: Kernel) -> Result<MassOutput, MassError> {
285    let (high, low, period, first, chosen) = mass_prepare(input, kernel)?;
286    let warmup_end = first + 16 + period - 1;
287    let mut out = alloc_with_nan_prefix(high.len(), warmup_end);
288    mass_compute_into(high, low, period, first, chosen, &mut out);
289    Ok(MassOutput { values: out })
290}
291
292#[inline]
293pub fn mass_into_slice(
294    dst: &mut [f64],
295    input: &MassInput,
296    kernel: Kernel,
297) -> Result<(), MassError> {
298    let (high, low, period, first, chosen) = mass_prepare(input, kernel)?;
299    if dst.len() != high.len() {
300        return Err(MassError::OutputLengthMismatch {
301            expected: high.len(),
302            got: dst.len(),
303        });
304    }
305    mass_compute_into(high, low, period, first, chosen, dst);
306    let warmup_end = first + 16 + period - 1;
307
308    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
309    for v in &mut dst[..warmup_end] {
310        *v = qnan;
311    }
312    Ok(())
313}
314
315#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
316#[inline]
317pub fn mass_into(input: &MassInput, out: &mut [f64]) -> Result<(), MassError> {
318    mass_into_slice(out, input, Kernel::Auto)
319}
320
321#[inline]
322pub fn mass_scalar(
323    high: &[f64],
324    low: &[f64],
325    period: usize,
326    first_valid_idx: usize,
327    out: &mut [f64],
328) {
329    const ALPHA: f64 = 2.0 / 10.0;
330    const INV_ALPHA: f64 = 1.0 - ALPHA;
331
332    let n = high.len();
333    if n == 0 {
334        return;
335    }
336
337    let start_ema2 = first_valid_idx + 8;
338    let start_ratio = first_valid_idx + 16;
339    let start_out = start_ratio + (period - 1);
340
341    let mut ema1 = high[first_valid_idx] - low[first_valid_idx];
342    let mut ema2 = ema1;
343
344    let mut ring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, period);
345    ring.resize(period, 0.0);
346
347    let mut ring_index: usize = 0;
348    let mut sum_ratio: f64 = 0.0;
349
350    unsafe {
351        let hp = high.as_ptr();
352        let lp = low.as_ptr();
353        let outp = out.as_mut_ptr();
354        let rp = ring.as_mut_ptr();
355
356        let mut i = first_valid_idx;
357
358        while i < start_ema2 {
359            let hl = *hp.add(i) - *lp.add(i);
360            ema1 = ema1.mul_add(INV_ALPHA, hl * ALPHA);
361            i += 1;
362        }
363
364        {
365            let hl = *hp.add(i) - *lp.add(i);
366            ema1 = ema1.mul_add(INV_ALPHA, hl * ALPHA);
367            ema2 = ema1;
368            ema2 = ema2.mul_add(INV_ALPHA, ema1 * ALPHA);
369            i += 1;
370        }
371
372        while i < start_ratio {
373            let hl = *hp.add(i) - *lp.add(i);
374            ema1 = ema1.mul_add(INV_ALPHA, hl * ALPHA);
375            ema2 = ema2.mul_add(INV_ALPHA, ema1 * ALPHA);
376            i += 1;
377        }
378
379        while i < start_out {
380            let hl = *hp.add(i) - *lp.add(i);
381            ema1 = ema1.mul_add(INV_ALPHA, hl * ALPHA);
382            ema2 = ema2.mul_add(INV_ALPHA, ema1 * ALPHA);
383
384            let ratio = ema1 / ema2;
385            sum_ratio -= *rp.add(ring_index);
386            *rp.add(ring_index) = ratio;
387            sum_ratio += ratio;
388
389            ring_index += 1;
390            if ring_index == period {
391                ring_index = 0;
392            }
393
394            i += 1;
395        }
396
397        while i < n {
398            let hl = *hp.add(i) - *lp.add(i);
399            ema1 = ema1.mul_add(INV_ALPHA, hl * ALPHA);
400            ema2 = ema2.mul_add(INV_ALPHA, ema1 * ALPHA);
401
402            let ratio = ema1 / ema2;
403            sum_ratio -= *rp.add(ring_index);
404            *rp.add(ring_index) = ratio;
405            sum_ratio += ratio;
406
407            ring_index += 1;
408            if ring_index == period {
409                ring_index = 0;
410            }
411
412            *outp.add(i) = sum_ratio;
413            i += 1;
414        }
415    }
416}
417
418#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
419#[inline]
420pub fn mass_avx512(
421    high: &[f64],
422    low: &[f64],
423    period: usize,
424    first_valid_idx: usize,
425    out: &mut [f64],
426) {
427    if period <= 32 {
428        unsafe { mass_avx512_short(high, low, period, first_valid_idx, out) }
429    } else {
430        unsafe { mass_avx512_long(high, low, period, first_valid_idx, out) }
431    }
432}
433
434#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
435#[inline]
436pub fn mass_avx2(
437    high: &[f64],
438    low: &[f64],
439    period: usize,
440    first_valid_idx: usize,
441    out: &mut [f64],
442) {
443    use core::arch::x86_64::{_mm_prefetch, _MM_HINT_T0};
444
445    const ALPHA: f64 = 2.0 / 10.0;
446    const INV_ALPHA: f64 = 1.0 - ALPHA;
447
448    let n = high.len();
449    if n == 0 {
450        return;
451    }
452
453    let start_ema2 = first_valid_idx + 8;
454    let start_ratio = first_valid_idx + 16;
455    let start_out = start_ratio + (period - 1);
456
457    let mut ema1 = high[first_valid_idx] - low[first_valid_idx];
458    let mut ema2 = ema1;
459
460    let mut ring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, period);
461    ring.resize(period, 0.0);
462
463    let mut ring_index: usize = 0;
464    let mut sum_ratio: f64 = 0.0;
465
466    unsafe {
467        let hp = high.as_ptr();
468        let lp = low.as_ptr();
469        let outp = out.as_mut_ptr();
470        let rp = ring.as_mut_ptr();
471
472        const PF_DIST: usize = 64;
473
474        let mut i = first_valid_idx;
475        while i < n {
476            let pf = i + PF_DIST;
477            if pf < n {
478                _mm_prefetch(hp.add(pf) as *const i8, _MM_HINT_T0);
479                _mm_prefetch(lp.add(pf) as *const i8, _MM_HINT_T0);
480                _mm_prefetch(outp.add(pf) as *const i8, _MM_HINT_T0);
481            }
482
483            let hl = *hp.add(i) - *lp.add(i);
484            ema1 = ema1.mul_add(INV_ALPHA, hl * ALPHA);
485
486            if i == start_ema2 {
487                ema2 = ema1;
488            }
489            if i >= start_ema2 {
490                ema2 = ema2.mul_add(INV_ALPHA, ema1 * ALPHA);
491
492                if i >= start_ratio {
493                    let ratio = ema1 / ema2;
494
495                    sum_ratio -= *rp.add(ring_index);
496                    *rp.add(ring_index) = ratio;
497                    sum_ratio += ratio;
498
499                    ring_index += 1;
500                    if ring_index == period {
501                        ring_index = 0;
502                    }
503
504                    if i >= start_out {
505                        *outp.add(i) = sum_ratio;
506                    }
507                }
508            }
509
510            i += 1;
511        }
512    }
513}
514
515#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
516#[inline]
517pub unsafe fn mass_avx512_short(
518    high: &[f64],
519    low: &[f64],
520    period: usize,
521    first_valid_idx: usize,
522    out: &mut [f64],
523) {
524    mass_avx2(high, low, period, first_valid_idx, out);
525}
526
527#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
528#[inline]
529pub unsafe fn mass_avx512_long(
530    high: &[f64],
531    low: &[f64],
532    period: usize,
533    first_valid_idx: usize,
534    out: &mut [f64],
535) {
536    mass_avx2(high, low, period, first_valid_idx, out);
537}
538
539#[derive(Debug, Clone)]
540pub struct MassStream {
541    period: usize,
542
543    ring: Box<[f64]>,
544    idx: usize,
545    mask: usize,
546    sum_ratio: f64,
547
548    alpha: f64,
549    inv_alpha: f64,
550    ema1: f64,
551    ema2: f64,
552
553    t: usize,
554    warm_ema2: usize,
555    warm_ratio: usize,
556    warm_out: usize,
557}
558
559impl MassStream {
560    #[inline]
561    pub fn try_new(params: MassParams) -> Result<Self, MassError> {
562        let period = params.period.unwrap_or(5);
563        if period == 0 {
564            return Err(MassError::InvalidPeriod {
565                period,
566                data_len: 0,
567            });
568        }
569
570        let ring = vec![0.0; period].into_boxed_slice();
571
572        let mask = if period.is_power_of_two() {
573            period - 1
574        } else {
575            usize::MAX
576        };
577
578        Ok(Self {
579            period,
580            ring,
581            idx: 0,
582            mask,
583            sum_ratio: 0.0,
584
585            alpha: 2.0 / 10.0,
586            inv_alpha: 1.0 - (2.0 / 10.0),
587
588            ema1: f64::NAN,
589            ema2: f64::NAN,
590
591            t: 0,
592            warm_ema2: 8,
593            warm_ratio: 16,
594            warm_out: 16 + (period - 1),
595        })
596    }
597
598    #[inline(always)]
599    pub fn update(&mut self, high: f64, low: f64) -> Option<f64> {
600        let hl = high - low;
601
602        if self.t == 0 {
603            self.ema1 = hl;
604            self.ema2 = hl;
605            self.t = 1;
606            return None;
607        }
608
609        self.ema1 = self.ema1.mul_add(self.inv_alpha, hl * self.alpha);
610
611        if self.t == self.warm_ema2 {
612            self.ema2 = self.ema1;
613        }
614        if self.t >= self.warm_ema2 {
615            self.ema2 = self.ema2.mul_add(self.inv_alpha, self.ema1 * self.alpha);
616        }
617
618        let mut out = None;
619
620        if self.t >= self.warm_ratio {
621            let ratio = self.ema1 / self.ema2;
622
623            let old = self.ring[self.idx];
624            self.sum_ratio = (self.sum_ratio - old) + ratio;
625            self.ring[self.idx] = ratio;
626
627            if self.mask != usize::MAX {
628                self.idx = (self.idx + 1) & self.mask;
629            } else {
630                self.idx += 1;
631                if self.idx == self.period {
632                    self.idx = 0;
633                }
634            }
635
636            if self.t >= self.warm_out {
637                out = Some(self.sum_ratio);
638            }
639        }
640
641        self.t += 1;
642        out
643    }
644}
645
646#[derive(Clone, Debug)]
647pub struct MassBatchRange {
648    pub period: (usize, usize, usize),
649}
650
651impl Default for MassBatchRange {
652    fn default() -> Self {
653        Self {
654            period: (5, 254, 1),
655        }
656    }
657}
658
659#[derive(Clone, Debug, Default)]
660pub struct MassBatchBuilder {
661    range: MassBatchRange,
662    kernel: Kernel,
663}
664
665impl MassBatchBuilder {
666    pub fn new() -> Self {
667        Self::default()
668    }
669    pub fn kernel(mut self, k: Kernel) -> Self {
670        self.kernel = k;
671        self
672    }
673    #[inline]
674    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
675        self.range.period = (start, end, step);
676        self
677    }
678    #[inline]
679    pub fn period_static(mut self, p: usize) -> Self {
680        self.range.period = (p, p, 0);
681        self
682    }
683    pub fn apply_slices(self, high: &[f64], low: &[f64]) -> Result<MassBatchOutput, MassError> {
684        mass_batch_with_kernel(high, low, &self.range, self.kernel)
685    }
686    pub fn with_default_slices(
687        high: &[f64],
688        low: &[f64],
689        k: Kernel,
690    ) -> Result<MassBatchOutput, MassError> {
691        MassBatchBuilder::new().kernel(k).apply_slices(high, low)
692    }
693    pub fn apply_candles(self, c: &Candles) -> Result<MassBatchOutput, MassError> {
694        let high = source_type(c, "high");
695        let low = source_type(c, "low");
696        self.apply_slices(high, low)
697    }
698    pub fn with_default_candles(c: &Candles) -> Result<MassBatchOutput, MassError> {
699        MassBatchBuilder::new()
700            .kernel(Kernel::Auto)
701            .apply_candles(c)
702    }
703}
704
705pub fn mass_batch_with_kernel(
706    high: &[f64],
707    low: &[f64],
708    sweep: &MassBatchRange,
709    k: Kernel,
710) -> Result<MassBatchOutput, MassError> {
711    let kernel = match k {
712        Kernel::Auto => Kernel::ScalarBatch,
713        other if other.is_batch() => other,
714        other => return Err(MassError::InvalidKernelForBatch(other)),
715    };
716    let simd = match kernel {
717        Kernel::Avx512Batch => Kernel::Avx512,
718        Kernel::Avx2Batch => Kernel::Avx2,
719        Kernel::ScalarBatch => Kernel::Scalar,
720        _ => unreachable!(),
721    };
722    mass_batch_par_slice(high, low, sweep, simd)
723}
724
725#[derive(Clone, Debug)]
726pub struct MassBatchOutput {
727    pub values: Vec<f64>,
728    pub combos: Vec<MassParams>,
729    pub rows: usize,
730    pub cols: usize,
731}
732
733impl MassBatchOutput {
734    pub fn row_for_params(&self, p: &MassParams) -> Option<usize> {
735        self.combos
736            .iter()
737            .position(|c| c.period.unwrap_or(5) == p.period.unwrap_or(5))
738    }
739    pub fn values_for(&self, p: &MassParams) -> Option<&[f64]> {
740        self.row_for_params(p).map(|row| {
741            let start = row * self.cols;
742            &self.values[start..start + self.cols]
743        })
744    }
745}
746
747#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
748#[derive(Serialize, Deserialize)]
749pub struct MassBatchConfig {
750    pub period_range: (usize, usize, usize),
751}
752
753#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
754#[derive(Serialize, Deserialize)]
755pub struct MassBatchJsOutput {
756    pub values: Vec<f64>,
757    pub combos: Vec<MassParams>,
758    pub rows: usize,
759    pub cols: usize,
760}
761
762#[inline(always)]
763fn expand_grid_mass(r: &MassBatchRange) -> Result<Vec<MassParams>, MassError> {
764    #[inline]
765    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, MassError> {
766        if step == 0 || start == end {
767            return Ok(vec![start]);
768        }
769        if start < end {
770            let v: Vec<usize> = (start..=end).step_by(step).collect();
771            if v.is_empty() {
772                return Err(MassError::InvalidRange { start, end, step });
773            }
774            Ok(v)
775        } else {
776            let mut v = Vec::new();
777            let mut cur = start;
778            loop {
779                v.push(cur);
780                if cur <= end {
781                    break;
782                }
783                match cur.checked_sub(step) {
784                    Some(next) => {
785                        cur = next;
786                    }
787                    None => break,
788                }
789            }
790            if v.is_empty() {
791                Err(MassError::InvalidRange { start, end, step })
792            } else {
793                Ok(v)
794            }
795        }
796    }
797
798    let periods = axis_usize(r.period)?;
799    if periods.is_empty() {
800        return Err(MassError::InvalidRange {
801            start: r.period.0,
802            end: r.period.1,
803            step: r.period.2,
804        });
805    }
806    let mut out = Vec::with_capacity(periods.len());
807    for &p in &periods {
808        out.push(MassParams { period: Some(p) });
809    }
810    Ok(out)
811}
812
813#[inline(always)]
814pub fn mass_batch_slice(
815    high: &[f64],
816    low: &[f64],
817    sweep: &MassBatchRange,
818    kern: Kernel,
819) -> Result<MassBatchOutput, MassError> {
820    mass_batch_inner(high, low, sweep, kern, false)
821}
822
823#[inline(always)]
824pub fn mass_batch_par_slice(
825    high: &[f64],
826    low: &[f64],
827    sweep: &MassBatchRange,
828    kern: Kernel,
829) -> Result<MassBatchOutput, MassError> {
830    mass_batch_inner(high, low, sweep, kern, true)
831}
832
833#[inline(always)]
834fn mass_batch_inner(
835    high: &[f64],
836    low: &[f64],
837    sweep: &MassBatchRange,
838    kern: Kernel,
839    parallel: bool,
840) -> Result<MassBatchOutput, MassError> {
841    let combos = expand_grid_mass(sweep)?;
842
843    if high.is_empty() || low.is_empty() || high.len() != low.len() {
844        return Err(MassError::DifferentLengthHL);
845    }
846
847    let first = (0..high.len())
848        .find(|&i| !high[i].is_nan() && !low[i].is_nan())
849        .ok_or(MassError::AllValuesNaN)?;
850    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
851    let needed_bars = 16 + max_p - 1;
852    if high.len() - first < needed_bars {
853        return Err(MassError::NotEnoughValidData {
854            needed: needed_bars,
855            valid: high.len() - first,
856        });
857    }
858
859    let rows = combos.len();
860    let cols = high.len();
861    rows.checked_mul(cols).ok_or(MassError::InvalidRange {
862        start: sweep.period.0,
863        end: sweep.period.1,
864        step: sweep.period.2,
865    })?;
866
867    let mut buf_mu = make_uninit_matrix(rows, cols);
868
869    let warm: Vec<usize> = combos
870        .iter()
871        .map(|c| first + 16 + c.period.unwrap() - 1)
872        .collect();
873    init_matrix_prefixes(&mut buf_mu, cols, &warm);
874
875    let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
876    let values_slice: &mut [f64] = unsafe {
877        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
878    };
879
880    let actual_kern = match kern {
881        Kernel::Auto => Kernel::Scalar,
882        other => other,
883    };
884
885    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
886        let period = combos[row].period.unwrap();
887        match actual_kern {
888            Kernel::Scalar => mass_row_scalar(high, low, period, first, out_row),
889            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
890            Kernel::Avx2 => mass_row_avx2(high, low, period, first, out_row),
891            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
892            Kernel::Avx512 => mass_row_avx512(high, low, period, first, out_row),
893            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
894            Kernel::Avx2 | Kernel::Avx512 => mass_row_scalar(high, low, period, first, out_row),
895            _ => mass_row_scalar(high, low, period, first, out_row),
896        }
897    };
898
899    if parallel {
900        #[cfg(not(target_arch = "wasm32"))]
901        {
902            values_slice
903                .par_chunks_mut(cols)
904                .enumerate()
905                .for_each(|(row, slice)| do_row(row, slice));
906        }
907
908        #[cfg(target_arch = "wasm32")]
909        {
910            for (row, slice) in values_slice.chunks_mut(cols).enumerate() {
911                do_row(row, slice);
912            }
913        }
914    } else {
915        for (row, slice) in values_slice.chunks_mut(cols).enumerate() {
916            do_row(row, slice);
917        }
918    }
919
920    let values = unsafe {
921        Vec::from_raw_parts(
922            buf_guard.as_mut_ptr() as *mut f64,
923            buf_guard.len(),
924            buf_guard.capacity(),
925        )
926    };
927
928    Ok(MassBatchOutput {
929        values,
930        combos,
931        rows,
932        cols,
933    })
934}
935
936#[inline(always)]
937unsafe fn mass_row_scalar(high: &[f64], low: &[f64], period: usize, first: usize, out: &mut [f64]) {
938    mass_scalar(high, low, period, first, out);
939}
940
941#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
942#[inline(always)]
943unsafe fn mass_row_avx2(high: &[f64], low: &[f64], period: usize, first: usize, out: &mut [f64]) {
944    mass_avx2(high, low, period, first, out);
945}
946
947#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
948#[inline(always)]
949unsafe fn mass_row_avx512(high: &[f64], low: &[f64], period: usize, first: usize, out: &mut [f64]) {
950    if period <= 32 {
951        mass_row_avx512_short(high, low, period, first, out);
952    } else {
953        mass_row_avx512_long(high, low, period, first, out);
954    }
955}
956
957#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
958#[inline(always)]
959unsafe fn mass_row_avx512_short(
960    high: &[f64],
961    low: &[f64],
962    period: usize,
963    first: usize,
964    out: &mut [f64],
965) {
966    mass_avx2(high, low, period, first, out);
967}
968
969#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
970#[inline(always)]
971unsafe fn mass_row_avx512_long(
972    high: &[f64],
973    low: &[f64],
974    period: usize,
975    first: usize,
976    out: &mut [f64],
977) {
978    mass_avx2(high, low, period, first, out);
979}
980
981#[cfg(test)]
982mod tests {
983    use super::*;
984    use crate::skip_if_unsupported;
985    use crate::utilities::data_loader::read_candles_from_csv;
986
987    #[test]
988    fn test_mass_into_matches_api() {
989        let len = 256usize;
990        let mut ts = Vec::with_capacity(len);
991        let mut open = Vec::with_capacity(len);
992        let mut high = Vec::with_capacity(len);
993        let mut low = Vec::with_capacity(len);
994        let mut close = Vec::with_capacity(len);
995        let mut volume = Vec::with_capacity(len);
996
997        for i in 0..len {
998            let x = i as f64;
999
1000            let base = (x * 0.01).mul_add(100.0, (x * 0.07).sin() * 2.0);
1001
1002            let range = 1.0 + (x * 0.005).sin().abs() * 3.0;
1003            let h = base + range * 0.5;
1004            let l = base - range * 0.5;
1005
1006            ts.push(i as i64);
1007            open.push(base);
1008            high.push(h);
1009            low.push(l);
1010            close.push(base * 0.999 + 0.001 * h);
1011            volume.push(1000.0 + (i % 10) as f64);
1012        }
1013
1014        let candles = crate::utilities::data_loader::Candles::new(
1015            ts,
1016            open,
1017            high.clone(),
1018            low.clone(),
1019            close,
1020            volume,
1021        );
1022
1023        let input = MassInput::from_candles(&candles, "high", "low", MassParams::default());
1024
1025        let base = mass(&input).expect("mass() should succeed");
1026
1027        let mut out = vec![0.0f64; len];
1028        mass_into(&input, &mut out).expect("mass_into() should succeed");
1029
1030        assert_eq!(base.values.len(), out.len());
1031
1032        fn eq_or_both_nan(a: f64, b: f64) -> bool {
1033            (a.is_nan() && b.is_nan()) || (a == b)
1034        }
1035
1036        for i in 0..len {
1037            assert!(
1038                eq_or_both_nan(base.values[i], out[i]),
1039                "Mismatch at index {}: got {}, expected {}",
1040                i,
1041                out[i],
1042                base.values[i]
1043            );
1044        }
1045    }
1046
1047    fn check_mass_partial_params(
1048        test_name: &str,
1049        kernel: Kernel,
1050    ) -> Result<(), Box<dyn std::error::Error>> {
1051        skip_if_unsupported!(kernel, test_name);
1052        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1053        let candles = read_candles_from_csv(file_path)?;
1054        let default_params = MassParams { period: None };
1055        let input_default = MassInput::from_candles(&candles, "high", "low", default_params);
1056        let output_default = mass_with_kernel(&input_default, kernel)?;
1057        assert_eq!(output_default.values.len(), candles.high.len());
1058        Ok(())
1059    }
1060
1061    fn check_mass_accuracy(
1062        test_name: &str,
1063        kernel: Kernel,
1064    ) -> Result<(), Box<dyn std::error::Error>> {
1065        skip_if_unsupported!(kernel, test_name);
1066        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1067        let candles = read_candles_from_csv(file_path)?;
1068        let params = MassParams { period: Some(5) };
1069        let input = MassInput::from_candles(&candles, "high", "low", params);
1070        let mass_result = mass_with_kernel(&input, kernel)?;
1071        assert_eq!(
1072            mass_result.values.len(),
1073            candles.high.len(),
1074            "MASS length mismatch"
1075        );
1076        let expected_last_five = [
1077            4.512263952194651,
1078            4.126178935431121,
1079            3.838738456245828,
1080            3.6450956734739375,
1081            3.6748009093527125,
1082        ];
1083        let result_len = mass_result.values.len();
1084        assert!(
1085            result_len >= 5,
1086            "MASS output length is too short for comparison"
1087        );
1088        let start_idx = result_len - 5;
1089        let result_slice = &mass_result.values[start_idx..];
1090        for (i, &value) in result_slice.iter().enumerate() {
1091            let expected = expected_last_five[i];
1092            assert!(
1093                (value - expected).abs() < 1e-7,
1094                "MASS mismatch at index {}: expected {}, got {}",
1095                start_idx + i,
1096                expected,
1097                value
1098            );
1099        }
1100        Ok(())
1101    }
1102
1103    fn check_mass_default_candles(
1104        test_name: &str,
1105        kernel: Kernel,
1106    ) -> Result<(), Box<dyn std::error::Error>> {
1107        skip_if_unsupported!(kernel, test_name);
1108        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1109        let candles = read_candles_from_csv(file_path)?;
1110        let input = MassInput::with_default_candles(&candles);
1111        match input.data {
1112            MassData::Candles {
1113                high_source,
1114                low_source,
1115                ..
1116            } => {
1117                assert_eq!(high_source, "high");
1118                assert_eq!(low_source, "low");
1119            }
1120            _ => panic!("Expected MassData::Candles variant"),
1121        }
1122        let output = mass_with_kernel(&input, kernel)?;
1123        assert_eq!(output.values.len(), candles.high.len());
1124        Ok(())
1125    }
1126
1127    fn check_mass_zero_period(
1128        test_name: &str,
1129        kernel: Kernel,
1130    ) -> Result<(), Box<dyn std::error::Error>> {
1131        skip_if_unsupported!(kernel, test_name);
1132        let high_data = [10.0, 15.0, 20.0];
1133        let low_data = [5.0, 10.0, 12.0];
1134        let params = MassParams { period: Some(0) };
1135        let input = MassInput::from_slices(&high_data, &low_data, params);
1136        let result = mass_with_kernel(&input, kernel);
1137        assert!(result.is_err(), "Expected an error for zero period");
1138        Ok(())
1139    }
1140
1141    fn check_mass_period_exceeds_length(
1142        test_name: &str,
1143        kernel: Kernel,
1144    ) -> Result<(), Box<dyn std::error::Error>> {
1145        skip_if_unsupported!(kernel, test_name);
1146        let high_data = [10.0, 15.0, 20.0];
1147        let low_data = [5.0, 10.0, 12.0];
1148        let params = MassParams { period: Some(10) };
1149        let input = MassInput::from_slices(&high_data, &low_data, params);
1150        let result = mass_with_kernel(&input, kernel);
1151        assert!(result.is_err(), "Expected an error for period > data.len()");
1152        Ok(())
1153    }
1154
1155    fn check_mass_very_small_data_set(
1156        test_name: &str,
1157        kernel: Kernel,
1158    ) -> Result<(), Box<dyn std::error::Error>> {
1159        skip_if_unsupported!(kernel, test_name);
1160        let high_data = [10.0];
1161        let low_data = [5.0];
1162        let params = MassParams { period: Some(5) };
1163        let input = MassInput::from_slices(&high_data, &low_data, params);
1164        let result = mass_with_kernel(&input, kernel);
1165        assert!(
1166            result.is_err(),
1167            "Expected error for data smaller than needed bars"
1168        );
1169        Ok(())
1170    }
1171
1172    fn check_mass_reinput(
1173        test_name: &str,
1174        kernel: Kernel,
1175    ) -> Result<(), Box<dyn std::error::Error>> {
1176        skip_if_unsupported!(kernel, test_name);
1177        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1178        let candles = read_candles_from_csv(file_path)?;
1179        let first_params = MassParams { period: Some(5) };
1180        let first_input = MassInput::from_candles(&candles, "high", "low", first_params);
1181        let first_result = mass_with_kernel(&first_input, kernel)?;
1182        let second_params = MassParams { period: Some(5) };
1183        let second_input =
1184            MassInput::from_slices(&first_result.values, &first_result.values, second_params);
1185        let second_result = mass_with_kernel(&second_input, kernel)?;
1186        assert_eq!(
1187            second_result.values.len(),
1188            first_result.values.len(),
1189            "Second MASS output length mismatch"
1190        );
1191        Ok(())
1192    }
1193
1194    fn check_mass_nan_handling(
1195        test_name: &str,
1196        kernel: Kernel,
1197    ) -> Result<(), Box<dyn std::error::Error>> {
1198        skip_if_unsupported!(kernel, test_name);
1199        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1200        let candles = read_candles_from_csv(file_path)?;
1201        let period = 5;
1202        let params = MassParams {
1203            period: Some(period),
1204        };
1205        let input = MassInput::from_candles(&candles, "high", "low", params);
1206        let mass_result = mass_with_kernel(&input, kernel)?;
1207        assert_eq!(
1208            mass_result.values.len(),
1209            candles.high.len(),
1210            "MASS length mismatch"
1211        );
1212        if mass_result.values.len() > 240 {
1213            for i in 240..mass_result.values.len() {
1214                assert!(
1215                    !mass_result.values[i].is_nan(),
1216                    "Expected no NaN after index 240, but found NaN at index {}",
1217                    i
1218                );
1219            }
1220        }
1221        Ok(())
1222    }
1223
1224    #[cfg(debug_assertions)]
1225    fn check_mass_no_poison(
1226        test_name: &str,
1227        kernel: Kernel,
1228    ) -> Result<(), Box<dyn std::error::Error>> {
1229        skip_if_unsupported!(kernel, test_name);
1230
1231        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1232        let candles = read_candles_from_csv(file_path)?;
1233
1234        let test_params = vec![
1235            MassParams::default(),
1236            MassParams { period: Some(2) },
1237            MassParams { period: Some(3) },
1238            MassParams { period: Some(4) },
1239            MassParams { period: Some(5) },
1240            MassParams { period: Some(10) },
1241            MassParams { period: Some(20) },
1242            MassParams { period: Some(30) },
1243            MassParams { period: Some(50) },
1244            MassParams { period: Some(100) },
1245            MassParams { period: Some(200) },
1246            MassParams { period: Some(255) },
1247        ];
1248
1249        for (param_idx, params) in test_params.iter().enumerate() {
1250            let input = MassInput::from_candles(&candles, "high", "low", params.clone());
1251            let output = mass_with_kernel(&input, kernel)?;
1252
1253            for (i, &val) in output.values.iter().enumerate() {
1254                if val.is_nan() {
1255                    continue;
1256                }
1257
1258                let bits = val.to_bits();
1259
1260                if bits == 0x11111111_11111111 {
1261                    panic!(
1262                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1263						 with params: period={} (param set {})",
1264                        test_name,
1265                        val,
1266                        bits,
1267                        i,
1268                        params.period.unwrap_or(5),
1269                        param_idx
1270                    );
1271                }
1272
1273                if bits == 0x22222222_22222222 {
1274                    panic!(
1275                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1276						 with params: period={} (param set {})",
1277                        test_name,
1278                        val,
1279                        bits,
1280                        i,
1281                        params.period.unwrap_or(5),
1282                        param_idx
1283                    );
1284                }
1285
1286                if bits == 0x33333333_33333333 {
1287                    panic!(
1288                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1289						 with params: period={} (param set {})",
1290                        test_name,
1291                        val,
1292                        bits,
1293                        i,
1294                        params.period.unwrap_or(5),
1295                        param_idx
1296                    );
1297                }
1298            }
1299        }
1300
1301        Ok(())
1302    }
1303
1304    #[cfg(not(debug_assertions))]
1305    fn check_mass_no_poison(
1306        _test_name: &str,
1307        _kernel: Kernel,
1308    ) -> Result<(), Box<dyn std::error::Error>> {
1309        Ok(())
1310    }
1311
1312    #[cfg(test)]
1313    fn check_mass_property(
1314        test_name: &str,
1315        kernel: Kernel,
1316    ) -> Result<(), Box<dyn std::error::Error>> {
1317        use proptest::prelude::*;
1318        skip_if_unsupported!(kernel, test_name);
1319
1320        let strat = (2usize..=100)
1321            .prop_flat_map(|period| {
1322                (
1323                    prop::collection::vec(
1324                        (0f64..1000f64).prop_filter("finite", |x| x.is_finite()),
1325                        (16 + period)..=500,
1326                    ),
1327                    Just(period),
1328                    0usize..=6,
1329                )
1330            })
1331            .prop_map(|(mut base_data, period, scenario)| {
1332                let mut high = Vec::with_capacity(base_data.len());
1333                let mut low = Vec::with_capacity(base_data.len());
1334
1335                match scenario {
1336                    0 => {
1337                        for val in base_data {
1338                            let range = val * 0.1;
1339                            high.push(val + range / 2.0);
1340                            low.push(val - range / 2.0);
1341                        }
1342                    }
1343                    1 => {
1344                        for val in base_data {
1345                            high.push(val);
1346                            low.push(val);
1347                        }
1348                    }
1349                    2 => {
1350                        let constant_range = 10.0;
1351                        for val in base_data {
1352                            high.push(val + constant_range / 2.0);
1353                            low.push(val - constant_range / 2.0);
1354                        }
1355                    }
1356                    3 => {
1357                        for (i, val) in base_data.iter().enumerate() {
1358                            let range = 1.0 + (i as f64 * 0.1).min(20.0);
1359                            high.push(val + range);
1360                            low.push(val - range);
1361                        }
1362                    }
1363                    4 => {
1364                        for (i, val) in base_data.iter().enumerate() {
1365                            let range = (20.0 - (i as f64 * 0.1)).max(0.5);
1366                            high.push(val + range);
1367                            low.push(val - range);
1368                        }
1369                    }
1370                    5 => {
1371                        for (i, val) in base_data.iter().enumerate() {
1372                            let range = if i % 20 == 0 { 50.0 } else { 5.0 };
1373                            high.push(val + range);
1374                            low.push(val - range);
1375                        }
1376                    }
1377                    6 => {
1378                        for (i, val) in base_data.iter().enumerate() {
1379                            let range = 10.0 * (0.95_f64).powi(i as i32);
1380                            high.push(val + range);
1381                            low.push(val - range);
1382                        }
1383                    }
1384                    _ => unreachable!(),
1385                }
1386
1387                (high, low, period)
1388            });
1389
1390        proptest::test_runner::TestRunner::default()
1391			.run(&strat, |(high, low, period)| {
1392				let params = MassParams { period: Some(period) };
1393				let input = MassInput::from_slices(&high, &low, params);
1394
1395
1396				let MassOutput { values: out } =
1397					mass_with_kernel(&input, kernel).unwrap();
1398
1399
1400				let MassOutput { values: ref_out } =
1401					mass_with_kernel(&input, Kernel::Scalar).unwrap();
1402
1403
1404
1405				let warmup_end = 16 + period - 1;
1406				for i in 0..warmup_end.min(high.len()) {
1407					prop_assert!(
1408						out[i].is_nan(),
1409						"Expected NaN during warmup at index {}, got {}", i, out[i]
1410					);
1411				}
1412
1413
1414				for i in warmup_end..high.len() {
1415					let y = out[i];
1416					let r = ref_out[i];
1417
1418
1419
1420					if y.is_finite() && r.is_finite() {
1421						let y_bits = y.to_bits();
1422						let r_bits = r.to_bits();
1423						let ulp_diff: u64 = y_bits.abs_diff(r_bits);
1424
1425						prop_assert!(
1426							(y - r).abs() <= 1e-9 || ulp_diff <= 8,
1427							"Kernel mismatch at idx {}: {} vs {} (ULP={})",
1428							i, y, r, ulp_diff
1429						);
1430					} else {
1431
1432						prop_assert_eq!(
1433							y.is_nan(), r.is_nan(),
1434							"NaN mismatch at idx {}: {} vs {}", i, y, r
1435						);
1436					}
1437
1438
1439
1440					if y.is_finite() {
1441						prop_assert!(
1442							y > 0.0,
1443							"Mass Index should be positive at idx {}, got {}", i, y
1444						);
1445
1446
1447						prop_assert!(
1448							y <= (period as f64) * 2.5,
1449							"Mass Index unusually high at idx {}: {} (period={})", i, y, period
1450						);
1451					}
1452
1453
1454
1455					let window_start = i.saturating_sub(period - 1);
1456					let window_end = i + 1;
1457					let ranges: Vec<f64> = (window_start..window_end)
1458						.map(|j| high[j] - low[j])
1459						.collect();
1460
1461
1462					let is_constant_range = ranges.windows(2)
1463						.all(|w| (w[0] - w[1]).abs() < 1e-9);
1464
1465
1466
1467					if is_constant_range && y.is_finite() && i >= warmup_end + 2 * period {
1468						let avg_range = ranges.iter().sum::<f64>() / ranges.len() as f64;
1469
1470
1471						if avg_range < f64::EPSILON {
1472							prop_assert!(
1473								(y - period as f64).abs() <= 1e-6,
1474								"Zero range Mass Index should be ~{} at idx {}, got {}", period, i, y
1475							);
1476						}
1477
1478						else if avg_range > 0.01 && avg_range < 100.0 {
1479
1480
1481							let tolerance = (period as f64) * 0.2 + 2.0;
1482							prop_assert!(
1483								(y - period as f64).abs() <= tolerance,
1484								"Constant range Mass Index should be close to {} at idx {}, got {} (tolerance: {})",
1485								period, i, y, tolerance
1486							);
1487						}
1488					}
1489
1490
1491
1492					for j in window_start..window_end {
1493						prop_assert!(
1494							high[j] >= low[j] - f64::EPSILON,
1495							"High should be >= Low at index {}: high={}, low={}", j, high[j], low[j]
1496						);
1497					}
1498
1499
1500
1501					prop_assert!(
1502						!y.is_infinite(),
1503						"Found infinite value at idx {}: {}", i, y
1504					);
1505
1506
1507
1508					if i >= warmup_end + period && y.is_finite() {
1509
1510						let avg_range = ranges.iter().sum::<f64>() / ranges.len() as f64;
1511
1512
1513
1514						if avg_range < 0.001 {
1515
1516							let tolerance = if avg_range < 1e-10 {
1517								1.0
1518							} else {
1519
1520								(period as f64) * 0.25 + 2.0
1521							};
1522							prop_assert!(
1523								(y - period as f64).abs() <= tolerance,
1524								"Low volatility Mass Index should be near {} at idx {}, got {} (avg_range: {}, tolerance: {})",
1525								period, i, y, avg_range, tolerance
1526							);
1527						}
1528
1529
1530						if i > warmup_end + period + 5 {
1531
1532							let prev_window_start = (i - 5).saturating_sub(period - 1);
1533							let prev_window_end = i - 4;
1534							let prev_ranges: Vec<f64> = (prev_window_start..prev_window_end)
1535								.map(|j| high[j] - low[j])
1536								.collect();
1537							let prev_avg_range = prev_ranges.iter().sum::<f64>() / prev_ranges.len() as f64;
1538
1539
1540							if avg_range > prev_avg_range * 2.0 && prev_avg_range > 0.1 {
1541								let prev_mass = out[i - 5];
1542								if prev_mass.is_finite() {
1543									prop_assert!(
1544										y >= prev_mass - 0.5,
1545										"Mass Index should respond to doubling volatility: {} at idx {} vs {} at idx {}",
1546										y, i, prev_mass, i - 5
1547									);
1548								}
1549							}
1550						}
1551
1552
1553						prop_assert!(
1554							y >= (period as f64) * 0.3 && y <= (period as f64) * 2.5,
1555							"Mass Index out of reasonable bounds at idx {}: {} (period={})",
1556							i, y, period
1557						);
1558					}
1559				}
1560
1561				Ok(())
1562			})
1563			.unwrap();
1564
1565        Ok(())
1566    }
1567
1568    macro_rules! generate_all_mass_tests {
1569        ($($test_fn:ident),*) => {
1570            paste::paste! {
1571                $(
1572                    #[test]
1573                    fn [<$test_fn _scalar_f64>]() {
1574                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1575                    }
1576                )*
1577                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1578                $(
1579                    #[test]
1580                    fn [<$test_fn _avx2_f64>]() {
1581                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1582                    }
1583                    #[test]
1584                    fn [<$test_fn _avx512_f64>]() {
1585                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1586                    }
1587                )*
1588            }
1589        }
1590    }
1591
1592    generate_all_mass_tests!(
1593        check_mass_partial_params,
1594        check_mass_accuracy,
1595        check_mass_default_candles,
1596        check_mass_zero_period,
1597        check_mass_period_exceeds_length,
1598        check_mass_very_small_data_set,
1599        check_mass_reinput,
1600        check_mass_nan_handling,
1601        check_mass_no_poison
1602    );
1603
1604    #[cfg(test)]
1605    generate_all_mass_tests!(check_mass_property);
1606    fn check_batch_default_row(
1607        test: &str,
1608        kernel: Kernel,
1609    ) -> Result<(), Box<dyn std::error::Error>> {
1610        skip_if_unsupported!(kernel, test);
1611        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1612        let candles = read_candles_from_csv(file)?;
1613        let output = MassBatchBuilder::new()
1614            .kernel(kernel)
1615            .apply_candles(&candles)?;
1616        let def = MassParams::default();
1617        let row = output.values_for(&def).expect("default row missing");
1618        assert_eq!(row.len(), candles.high.len());
1619
1620        let expected = [
1621            4.512263952194651,
1622            4.126178935431121,
1623            3.838738456245828,
1624            3.6450956734739375,
1625            3.6748009093527125,
1626        ];
1627        let start = row.len().saturating_sub(5);
1628        for (i, &v) in row[start..].iter().enumerate() {
1629            assert!(
1630                (v - expected[i]).abs() < 1e-7,
1631                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1632            );
1633        }
1634        Ok(())
1635    }
1636
1637    #[cfg(debug_assertions)]
1638    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1639        skip_if_unsupported!(kernel, test);
1640
1641        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1642        let c = read_candles_from_csv(file)?;
1643
1644        let test_configs = vec![
1645            (2, 10, 2),
1646            (5, 25, 5),
1647            (30, 60, 15),
1648            (2, 5, 1),
1649            (10, 10, 0),
1650            (50, 100, 25),
1651            (3, 15, 3),
1652            (20, 40, 10),
1653        ];
1654
1655        for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
1656            let output = MassBatchBuilder::new()
1657                .kernel(kernel)
1658                .period_range(p_start, p_end, p_step)
1659                .apply_candles(&c)?;
1660
1661            for (idx, &val) in output.values.iter().enumerate() {
1662                if val.is_nan() {
1663                    continue;
1664                }
1665
1666                let bits = val.to_bits();
1667                let row = idx / output.cols;
1668                let col = idx % output.cols;
1669                let combo = &output.combos[row];
1670
1671                if bits == 0x11111111_11111111 {
1672                    panic!(
1673                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1674						 at row {} col {} (flat index {}) with params: period={}",
1675                        test,
1676                        cfg_idx,
1677                        val,
1678                        bits,
1679                        row,
1680                        col,
1681                        idx,
1682                        combo.period.unwrap_or(5)
1683                    );
1684                }
1685
1686                if bits == 0x22222222_22222222 {
1687                    panic!(
1688                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1689						 at row {} col {} (flat index {}) with params: period={}",
1690                        test,
1691                        cfg_idx,
1692                        val,
1693                        bits,
1694                        row,
1695                        col,
1696                        idx,
1697                        combo.period.unwrap_or(5)
1698                    );
1699                }
1700
1701                if bits == 0x33333333_33333333 {
1702                    panic!(
1703                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1704						 at row {} col {} (flat index {}) with params: period={}",
1705                        test,
1706                        cfg_idx,
1707                        val,
1708                        bits,
1709                        row,
1710                        col,
1711                        idx,
1712                        combo.period.unwrap_or(5)
1713                    );
1714                }
1715            }
1716        }
1717
1718        Ok(())
1719    }
1720
1721    #[cfg(not(debug_assertions))]
1722    fn check_batch_no_poison(
1723        _test: &str,
1724        _kernel: Kernel,
1725    ) -> Result<(), Box<dyn std::error::Error>> {
1726        Ok(())
1727    }
1728
1729    macro_rules! gen_batch_tests {
1730        ($fn_name:ident) => {
1731            paste::paste! {
1732                #[test] fn [<$fn_name _scalar>]()      {
1733                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1734                }
1735                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1736                #[test] fn [<$fn_name _avx2>]()        {
1737                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1738                }
1739                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1740                #[test] fn [<$fn_name _avx512>]()      {
1741                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1742                }
1743                #[test] fn [<$fn_name _auto_detect>]() {
1744                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1745                }
1746            }
1747        };
1748    }
1749    gen_batch_tests!(check_batch_default_row);
1750    gen_batch_tests!(check_batch_no_poison);
1751}
1752
1753#[cfg(feature = "python")]
1754#[pyfunction(name = "mass")]
1755#[pyo3(signature = (high, low, period, kernel=None))]
1756pub fn mass_py<'py>(
1757    py: Python<'py>,
1758    high: PyReadonlyArray1<'py, f64>,
1759    low: PyReadonlyArray1<'py, f64>,
1760    period: usize,
1761    kernel: Option<&str>,
1762) -> PyResult<Bound<'py, PyArray1<f64>>> {
1763    use numpy::{IntoPyArray, PyArrayMethods};
1764
1765    let high_slice = high.as_slice()?;
1766    let low_slice = low.as_slice()?;
1767    let kern = validate_kernel(kernel, false)?;
1768
1769    let params = MassParams {
1770        period: Some(period),
1771    };
1772    let input = MassInput::from_slices(high_slice, low_slice, params);
1773
1774    let result_vec: Vec<f64> = py
1775        .allow_threads(|| mass_with_kernel(&input, kern).map(|o| o.values))
1776        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1777
1778    Ok(result_vec.into_pyarray(py))
1779}
1780
1781#[cfg(feature = "python")]
1782#[pyclass(name = "MassStream")]
1783pub struct MassStreamPy {
1784    stream: MassStream,
1785}
1786
1787#[cfg(feature = "python")]
1788#[pymethods]
1789impl MassStreamPy {
1790    #[new]
1791    fn new(period: usize) -> PyResult<Self> {
1792        let params = MassParams {
1793            period: Some(period),
1794        };
1795        let stream =
1796            MassStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1797        Ok(MassStreamPy { stream })
1798    }
1799
1800    fn update(&mut self, high: f64, low: f64) -> Option<f64> {
1801        self.stream.update(high, low)
1802    }
1803}
1804
1805#[cfg(feature = "python")]
1806#[pyfunction(name = "mass_batch")]
1807#[pyo3(signature = (high, low, period_range, kernel=None))]
1808pub fn mass_batch_py<'py>(
1809    py: Python<'py>,
1810    high: PyReadonlyArray1<'py, f64>,
1811    low: PyReadonlyArray1<'py, f64>,
1812    period_range: (usize, usize, usize),
1813    kernel: Option<&str>,
1814) -> PyResult<Bound<'py, PyDict>> {
1815    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1816    use pyo3::types::PyDict;
1817
1818    let high_slice = high.as_slice()?;
1819    let low_slice = low.as_slice()?;
1820
1821    let sweep = MassBatchRange {
1822        period: period_range,
1823    };
1824
1825    let combos = expand_grid_mass(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1826    let rows = combos.len();
1827    let cols = high_slice.len();
1828
1829    let expected = rows
1830        .checked_mul(cols)
1831        .ok_or_else(|| PyValueError::new_err("mass_batch: output size overflow"))?;
1832
1833    let out_arr = unsafe { PyArray1::<f64>::new(py, [expected], false) };
1834    let slice_out = unsafe { out_arr.as_slice_mut()? };
1835
1836    let kern = validate_kernel(kernel, true)?;
1837
1838    let combos = py
1839        .allow_threads(|| {
1840            let kernel = match kern {
1841                Kernel::Auto => Kernel::ScalarBatch,
1842                k => k,
1843            };
1844            let simd = match kernel {
1845                Kernel::Avx512Batch => Kernel::Avx512,
1846                Kernel::Avx2Batch => Kernel::Avx2,
1847                Kernel::ScalarBatch => Kernel::Scalar,
1848                _ => unreachable!(),
1849            };
1850            mass_batch_inner_into(high_slice, low_slice, &sweep, simd, true, slice_out)
1851        })
1852        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1853
1854    let dict = PyDict::new(py);
1855    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1856    dict.set_item(
1857        "periods",
1858        combos
1859            .iter()
1860            .map(|p| p.period.unwrap() as u64)
1861            .collect::<Vec<_>>()
1862            .into_pyarray(py),
1863    )?;
1864
1865    Ok(dict)
1866}
1867
1868#[cfg(all(feature = "python", feature = "cuda"))]
1869#[pyfunction(name = "mass_cuda_batch_dev")]
1870#[pyo3(signature = (high_f32, low_f32, period_range, device_id=0))]
1871pub fn mass_cuda_batch_dev_py<'py>(
1872    py: Python<'py>,
1873    high_f32: numpy::PyReadonlyArray1<'py, f32>,
1874    low_f32: numpy::PyReadonlyArray1<'py, f32>,
1875    period_range: (usize, usize, usize),
1876    device_id: usize,
1877) -> PyResult<(DeviceArrayF32Py, Bound<'py, pyo3::types::PyDict>)> {
1878    use numpy::{IntoPyArray, PyArrayMethods};
1879    if !cuda_available() {
1880        return Err(PyValueError::new_err("CUDA not available"));
1881    }
1882    let high = high_f32.as_slice()?;
1883    let low = low_f32.as_slice()?;
1884    let sweep = MassBatchRange {
1885        period: period_range,
1886    };
1887
1888    let (inner, combos) = py.allow_threads(|| {
1889        let mut cuda =
1890            CudaMass::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1891        cuda.mass_batch_dev(high, low, &sweep)
1892            .map_err(|e| PyValueError::new_err(e.to_string()))
1893    })?;
1894
1895    let dict = pyo3::types::PyDict::new(py);
1896    let periods: Vec<u64> = combos
1897        .iter()
1898        .map(|c| c.period.unwrap_or(0) as u64)
1899        .collect();
1900    dict.set_item("periods", periods.into_pyarray(py))?;
1901
1902    let handle = make_device_array_py(device_id, inner)?;
1903    Ok((handle, dict))
1904}
1905
1906#[cfg(all(feature = "python", feature = "cuda"))]
1907#[pyfunction(name = "mass_cuda_many_series_one_param_dev")]
1908#[pyo3(signature = (high_tm_f32, low_tm_f32, period, device_id=0))]
1909pub fn mass_cuda_many_series_one_param_dev_py<'py>(
1910    py: Python<'py>,
1911    high_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1912    low_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1913    period: usize,
1914    device_id: usize,
1915) -> PyResult<DeviceArrayF32Py> {
1916    use numpy::PyUntypedArrayMethods;
1917    if !cuda_available() {
1918        return Err(PyValueError::new_err("CUDA not available"));
1919    }
1920
1921    let hs = high_tm_f32.shape();
1922    let ls = low_tm_f32.shape();
1923    if hs != ls || hs.len() != 2 {
1924        return Err(PyValueError::new_err("expected matching 2D arrays"));
1925    }
1926    let rows = hs[0];
1927    let cols = hs[1];
1928    let high = high_tm_f32.as_slice()?;
1929    let low = low_tm_f32.as_slice()?;
1930    let params = MassParams {
1931        period: Some(period),
1932    };
1933
1934    let inner = py.allow_threads(|| {
1935        let mut cuda =
1936            CudaMass::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1937        cuda.mass_many_series_one_param_time_major_dev(high, low, cols, rows, &params)
1938            .map_err(|e| PyValueError::new_err(e.to_string()))
1939    })?;
1940
1941    Ok(make_device_array_py(device_id, inner)?)
1942}
1943
1944#[cfg(feature = "python")]
1945pub fn register_mass_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1946    m.add_function(wrap_pyfunction!(mass_py, m)?)?;
1947    m.add_function(wrap_pyfunction!(mass_batch_py, m)?)?;
1948    #[cfg(feature = "cuda")]
1949    {
1950        m.add_function(wrap_pyfunction!(mass_cuda_batch_dev_py, m)?)?;
1951        m.add_function(wrap_pyfunction!(mass_cuda_many_series_one_param_dev_py, m)?)?;
1952    }
1953    Ok(())
1954}
1955
1956#[cfg(any(feature = "python", feature = "wasm"))]
1957#[inline(always)]
1958fn mass_batch_inner_into(
1959    high: &[f64],
1960    low: &[f64],
1961    sweep: &MassBatchRange,
1962    kern: Kernel,
1963    parallel: bool,
1964    out: &mut [f64],
1965) -> Result<Vec<MassParams>, MassError> {
1966    let combos = expand_grid_mass(sweep)?;
1967
1968    if high.is_empty() || low.is_empty() || high.len() != low.len() {
1969        return Err(MassError::DifferentLengthHL);
1970    }
1971
1972    let first = (0..high.len())
1973        .find(|&i| !high[i].is_nan() && !low[i].is_nan())
1974        .ok_or(MassError::AllValuesNaN)?;
1975    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
1976    let needed_bars = 16 + max_p - 1;
1977    if high.len() - first < needed_bars {
1978        return Err(MassError::NotEnoughValidData {
1979            needed: needed_bars,
1980            valid: high.len() - first,
1981        });
1982    }
1983
1984    let cols = high.len();
1985    let rows = combos.len();
1986    let expected = rows.checked_mul(cols).ok_or(MassError::InvalidRange {
1987        start: sweep.period.0,
1988        end: sweep.period.1,
1989        step: sweep.period.2,
1990    })?;
1991    if out.len() != expected {
1992        return Err(MassError::OutputLengthMismatch {
1993            expected,
1994            got: out.len(),
1995        });
1996    }
1997
1998    for (row, combo) in combos.iter().enumerate() {
1999        let period = combo.period.unwrap();
2000        let warmup_end = first + 16 + period - 1;
2001        let row_start = row * cols;
2002        for i in 0..warmup_end.min(cols) {
2003            out[row_start + i] = f64::NAN;
2004        }
2005    }
2006
2007    let actual_kern = match kern {
2008        Kernel::Auto => Kernel::Scalar,
2009        other => other,
2010    };
2011
2012    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
2013        let period = combos[row].period.unwrap();
2014        match actual_kern {
2015            Kernel::Scalar => mass_row_scalar(high, low, period, first, out_row),
2016            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2017            Kernel::Avx2 => mass_row_avx2(high, low, period, first, out_row),
2018            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2019            Kernel::Avx512 => mass_row_avx512(high, low, period, first, out_row),
2020            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
2021            Kernel::Avx2 | Kernel::Avx512 => mass_row_scalar(high, low, period, first, out_row),
2022            _ => mass_row_scalar(high, low, period, first, out_row),
2023        }
2024    };
2025
2026    if parallel {
2027        #[cfg(not(target_arch = "wasm32"))]
2028        {
2029            out.par_chunks_mut(cols)
2030                .enumerate()
2031                .for_each(|(row, slice)| do_row(row, slice));
2032        }
2033
2034        #[cfg(target_arch = "wasm32")]
2035        {
2036            for (row, slice) in out.chunks_mut(cols).enumerate() {
2037                do_row(row, slice);
2038            }
2039        }
2040    } else {
2041        for (row, slice) in out.chunks_mut(cols).enumerate() {
2042            do_row(row, slice);
2043        }
2044    }
2045
2046    Ok(combos)
2047}
2048
2049#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2050#[wasm_bindgen]
2051pub fn mass_js(high: &[f64], low: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
2052    let params = MassParams {
2053        period: Some(period),
2054    };
2055    let input = MassInput::from_slices(high, low, params);
2056
2057    let mut output = vec![0.0; high.len()];
2058
2059    mass_into_slice(&mut output, &input, Kernel::Auto)
2060        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2061
2062    Ok(output)
2063}
2064
2065#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2066#[wasm_bindgen]
2067pub fn mass_into(
2068    high_ptr: *const f64,
2069    low_ptr: *const f64,
2070    out_ptr: *mut f64,
2071    len: usize,
2072    period: usize,
2073) -> Result<(), JsValue> {
2074    if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
2075        return Err(JsValue::from_str("null pointer passed to mass_into"));
2076    }
2077
2078    unsafe {
2079        let high = std::slice::from_raw_parts(high_ptr, len);
2080        let low = std::slice::from_raw_parts(low_ptr, len);
2081
2082        if period == 0 || period > len {
2083            return Err(JsValue::from_str("Invalid period"));
2084        }
2085
2086        let params = MassParams {
2087            period: Some(period),
2088        };
2089        let input = MassInput::from_slices(high, low, params);
2090
2091        if high_ptr == out_ptr || low_ptr == out_ptr {
2092            let mut temp = vec![0.0; len];
2093            mass_into_slice(&mut temp, &input, Kernel::Auto)
2094                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2095            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2096            out.copy_from_slice(&temp);
2097        } else {
2098            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2099            mass_into_slice(out, &input, Kernel::Auto)
2100                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2101        }
2102
2103        Ok(())
2104    }
2105}
2106
2107#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2108#[wasm_bindgen]
2109pub fn mass_alloc(len: usize) -> *mut f64 {
2110    let mut vec = Vec::<f64>::with_capacity(len);
2111    let ptr = vec.as_mut_ptr();
2112    std::mem::forget(vec);
2113    ptr
2114}
2115
2116#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2117#[wasm_bindgen]
2118pub fn mass_free(ptr: *mut f64, len: usize) {
2119    if !ptr.is_null() {
2120        unsafe {
2121            let _ = Vec::from_raw_parts(ptr, len, len);
2122        }
2123    }
2124}
2125
2126#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2127#[wasm_bindgen(js_name = mass_batch)]
2128pub fn mass_batch_unified_js(
2129    high: &[f64],
2130    low: &[f64],
2131    config: JsValue,
2132) -> Result<JsValue, JsValue> {
2133    let config: MassBatchConfig = serde_wasm_bindgen::from_value(config)
2134        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2135
2136    let sweep = MassBatchRange {
2137        period: config.period_range,
2138    };
2139
2140    let output = mass_batch_inner(high, low, &sweep, Kernel::Auto, false)
2141        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2142
2143    let js_output = MassBatchJsOutput {
2144        values: output.values,
2145        combos: output.combos,
2146        rows: output.rows,
2147        cols: output.cols,
2148    };
2149
2150    serde_wasm_bindgen::to_value(&js_output)
2151        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2152}
2153
2154#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2155#[wasm_bindgen]
2156pub fn mass_batch_into(
2157    high_ptr: *const f64,
2158    low_ptr: *const f64,
2159    out_ptr: *mut f64,
2160    len: usize,
2161    period_start: usize,
2162    period_end: usize,
2163    period_step: usize,
2164) -> Result<usize, JsValue> {
2165    if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
2166        return Err(JsValue::from_str("null pointer passed to mass_batch_into"));
2167    }
2168
2169    unsafe {
2170        let high = std::slice::from_raw_parts(high_ptr, len);
2171        let low = std::slice::from_raw_parts(low_ptr, len);
2172
2173        let sweep = MassBatchRange {
2174            period: (period_start, period_end, period_step),
2175        };
2176
2177        let combos = expand_grid_mass(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2178        let rows = combos.len();
2179        let cols = len;
2180
2181        let total = rows
2182            .checked_mul(cols)
2183            .ok_or_else(|| JsValue::from_str("mass_batch_into: rows*cols overflow"))?;
2184
2185        let out = std::slice::from_raw_parts_mut(out_ptr, total);
2186
2187        mass_batch_inner_into(high, low, &sweep, Kernel::Auto, false, out)
2188            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2189
2190        Ok(rows)
2191    }
2192}
2193
2194#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2195#[wasm_bindgen]
2196pub struct MassStreamWasm {
2197    stream: MassStream,
2198}
2199
2200#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2201#[wasm_bindgen]
2202impl MassStreamWasm {
2203    #[wasm_bindgen(constructor)]
2204    pub fn new(period: usize) -> Result<MassStreamWasm, JsValue> {
2205        let params = MassParams {
2206            period: Some(period),
2207        };
2208        let stream = MassStream::try_new(params).map_err(|e| JsValue::from_str(&e.to_string()))?;
2209        Ok(MassStreamWasm { stream })
2210    }
2211
2212    pub fn update(&mut self, high: f64, low: f64) -> Option<f64> {
2213        self.stream.update(high, low)
2214    }
2215}