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, ¶ms)
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}