Skip to main content

vector_ta/indicators/
bollinger_bands_width.rs

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