Skip to main content

vector_ta/indicators/
bollinger_bands.rs

1use crate::indicators::deviation::{deviation, DevInput, DevParams};
2use crate::indicators::moving_averages::ma::{ma, MaData};
3use crate::utilities::data_loader::{source_type, Candles};
4use crate::utilities::enums::Kernel;
5use crate::utilities::helpers::{
6    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
7    make_uninit_matrix,
8};
9#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
10use core::arch::x86_64::*;
11#[cfg(not(target_arch = "wasm32"))]
12use rayon::prelude::*;
13use std::mem::{ManuallyDrop, MaybeUninit};
14use thiserror::Error;
15
16#[derive(Debug, Clone)]
17pub enum BollingerBandsData<'a> {
18    Candles {
19        candles: &'a Candles,
20        source: &'a str,
21    },
22    Slice(&'a [f64]),
23}
24
25impl<'a> AsRef<[f64]> for BollingerBandsInput<'a> {
26    #[inline(always)]
27    fn as_ref(&self) -> &[f64] {
28        match &self.data {
29            BollingerBandsData::Slice(s) => s,
30            BollingerBandsData::Candles { candles, source } => source_type(candles, source),
31        }
32    }
33}
34
35#[derive(Debug, Clone)]
36pub struct BollingerBandsOutput {
37    pub upper_band: Vec<f64>,
38    pub middle_band: Vec<f64>,
39    pub lower_band: Vec<f64>,
40}
41
42#[derive(Debug, Clone)]
43#[cfg_attr(
44    all(target_arch = "wasm32", feature = "wasm"),
45    derive(serde::Serialize, serde::Deserialize)
46)]
47pub struct BollingerBandsParams {
48    pub period: Option<usize>,
49    pub devup: Option<f64>,
50    pub devdn: Option<f64>,
51    pub matype: Option<String>,
52    pub devtype: Option<usize>,
53}
54
55impl Default for BollingerBandsParams {
56    fn default() -> Self {
57        Self {
58            period: Some(20),
59            devup: Some(2.0),
60            devdn: Some(2.0),
61            matype: Some("sma".to_string()),
62            devtype: Some(0),
63        }
64    }
65}
66
67#[derive(Debug, Clone)]
68pub struct BollingerBandsInput<'a> {
69    pub data: BollingerBandsData<'a>,
70    pub params: BollingerBandsParams,
71}
72
73impl<'a> BollingerBandsInput<'a> {
74    #[inline]
75    pub fn from_candles(c: &'a Candles, s: &'a str, p: BollingerBandsParams) -> Self {
76        Self {
77            data: BollingerBandsData::Candles {
78                candles: c,
79                source: s,
80            },
81            params: p,
82        }
83    }
84    #[inline]
85    pub fn from_slice(sl: &'a [f64], p: BollingerBandsParams) -> Self {
86        Self {
87            data: BollingerBandsData::Slice(sl),
88            params: p,
89        }
90    }
91    #[inline]
92    pub fn with_default_candles(c: &'a Candles) -> Self {
93        Self::from_candles(c, "close", BollingerBandsParams::default())
94    }
95    #[inline]
96    pub fn get_period(&self) -> usize {
97        self.params.period.unwrap_or(20)
98    }
99    #[inline]
100    pub fn get_devup(&self) -> f64 {
101        self.params.devup.unwrap_or(2.0)
102    }
103    #[inline]
104    pub fn get_devdn(&self) -> f64 {
105        self.params.devdn.unwrap_or(2.0)
106    }
107    #[inline]
108    pub fn get_matype(&self) -> String {
109        self.params.matype.clone().unwrap_or_else(|| "sma".into())
110    }
111    #[inline]
112    pub fn get_devtype(&self) -> usize {
113        self.params.devtype.unwrap_or(0)
114    }
115}
116
117#[derive(Clone, Debug)]
118pub struct BollingerBandsBuilder {
119    period: Option<usize>,
120    devup: Option<f64>,
121    devdn: Option<f64>,
122    matype: Option<String>,
123    devtype: Option<usize>,
124    kernel: Kernel,
125}
126
127impl Default for BollingerBandsBuilder {
128    fn default() -> Self {
129        Self {
130            period: None,
131            devup: None,
132            devdn: None,
133            matype: None,
134            devtype: None,
135            kernel: Kernel::Auto,
136        }
137    }
138}
139
140impl BollingerBandsBuilder {
141    #[inline(always)]
142    pub fn new() -> Self {
143        Self::default()
144    }
145    #[inline(always)]
146    pub fn period(mut self, n: usize) -> Self {
147        self.period = Some(n);
148        self
149    }
150    #[inline(always)]
151    pub fn devup(mut self, x: f64) -> Self {
152        self.devup = Some(x);
153        self
154    }
155    #[inline(always)]
156    pub fn devdn(mut self, x: f64) -> Self {
157        self.devdn = Some(x);
158        self
159    }
160    #[inline(always)]
161    pub fn matype(mut self, x: &str) -> Self {
162        self.matype = Some(x.into());
163        self
164    }
165    #[inline(always)]
166    pub fn devtype(mut self, t: usize) -> Self {
167        self.devtype = Some(t);
168        self
169    }
170    #[inline(always)]
171    pub fn kernel(mut self, k: Kernel) -> Self {
172        self.kernel = k;
173        self
174    }
175    #[inline(always)]
176    pub fn apply(self, c: &Candles) -> Result<BollingerBandsOutput, BollingerBandsError> {
177        let p = BollingerBandsParams {
178            period: self.period,
179            devup: self.devup,
180            devdn: self.devdn,
181            matype: self.matype,
182            devtype: self.devtype,
183        };
184        let i = BollingerBandsInput::from_candles(c, "close", p);
185        bollinger_bands_with_kernel(&i, self.kernel)
186    }
187    #[inline(always)]
188    pub fn apply_slice(self, d: &[f64]) -> Result<BollingerBandsOutput, BollingerBandsError> {
189        let p = BollingerBandsParams {
190            period: self.period,
191            devup: self.devup,
192            devdn: self.devdn,
193            matype: self.matype,
194            devtype: self.devtype,
195        };
196        let i = BollingerBandsInput::from_slice(d, p);
197        bollinger_bands_with_kernel(&i, self.kernel)
198    }
199    #[inline(always)]
200    pub fn into_stream(self) -> Result<BollingerBandsStream, BollingerBandsError> {
201        let p = BollingerBandsParams {
202            period: self.period,
203            devup: self.devup,
204            devdn: self.devdn,
205            matype: self.matype,
206            devtype: self.devtype,
207        };
208        BollingerBandsStream::try_new(p)
209    }
210}
211
212#[derive(Debug, Error)]
213pub enum BollingerBandsError {
214    #[error("bollinger_bands: Empty data provided.")]
215    EmptyInputData,
216    #[error("bollinger_bands: Invalid period: period = {period}, data length = {data_len}")]
217    InvalidPeriod { period: usize, data_len: usize },
218    #[error("bollinger_bands: All values are NaN.")]
219    AllValuesNaN,
220    #[error("bollinger_bands: Underlying MA or Deviation function failed: {0}")]
221    UnderlyingFunctionFailed(String),
222    #[error("bollinger_bands: Not enough valid data for period: needed={needed}, valid={valid}")]
223    NotEnoughValidData { needed: usize, valid: usize },
224    #[error("bollinger_bands: output length mismatch: expected={expected}, got={got}")]
225    OutputLengthMismatch { expected: usize, got: usize },
226    #[error("bollinger_bands: invalid kernel for batch: {0:?}")]
227    InvalidKernelForBatch(Kernel),
228    #[error("bollinger_bands: invalid range expansion (start={start}, end={end}, step={step})")]
229    InvalidRange {
230        start: usize,
231        end: usize,
232        step: usize,
233    },
234    #[error("bollinger_bands: invalid range (f64): start={start}, end={end}, step={step}")]
235    InvalidRangeF64 { start: f64, end: f64, step: f64 },
236
237    #[error(
238        "bollinger_bands: Kernel must be a batch variant for batch operations. Got: {kernel:?}"
239    )]
240    KernelNotBatch { kernel: Kernel },
241    #[error("bollinger_bands: invalid input: {0}")]
242    InvalidInput(String),
243}
244
245#[inline]
246pub fn bollinger_bands(
247    input: &BollingerBandsInput,
248) -> Result<BollingerBandsOutput, BollingerBandsError> {
249    bollinger_bands_with_kernel(input, Kernel::Auto)
250}
251
252#[inline(always)]
253fn bb_prepare<'a>(
254    input: &'a BollingerBandsInput,
255    kernel: Kernel,
256) -> Result<(&'a [f64], usize, f64, f64, &'a str, usize, usize, Kernel), BollingerBandsError> {
257    let data: &[f64] = input.as_ref();
258    if data.is_empty() {
259        return Err(BollingerBandsError::EmptyInputData);
260    }
261
262    let period = input.get_period();
263    let devup = input.get_devup();
264    let devdn = input.get_devdn();
265
266    let matype = input.params.matype.as_deref().unwrap_or("sma");
267    let devtype = input.get_devtype();
268
269    let first = data
270        .iter()
271        .position(|x| !x.is_nan())
272        .ok_or(BollingerBandsError::AllValuesNaN)?;
273
274    if period == 0 || period > data.len() {
275        return Err(BollingerBandsError::InvalidPeriod {
276            period,
277            data_len: data.len(),
278        });
279    }
280
281    if (data.len() - first) < period {
282        return Err(BollingerBandsError::NotEnoughValidData {
283            needed: period,
284            valid: data.len() - first,
285        });
286    }
287
288    let chosen = match kernel {
289        Kernel::Auto => Kernel::Scalar,
290        other => other,
291    };
292
293    Ok((data, period, devup, devdn, matype, devtype, first, chosen))
294}
295
296#[inline(always)]
297pub fn bollinger_bands_compute_into(
298    data: &[f64],
299    period: usize,
300    devup: f64,
301    devdn: f64,
302    matype: &str,
303    devtype: usize,
304    first: usize,
305    kernel: Kernel,
306    out_u: &mut [f64],
307    out_m: &mut [f64],
308    out_l: &mut [f64],
309) -> Result<(), BollingerBandsError> {
310    if matype == "sma" || matype == "SMA" {
311        unsafe {
312            bb_row_scalar_classic_sma(
313                data, period, devtype, devup, devdn, first, out_u, out_m, out_l,
314            );
315        }
316        return Ok(());
317    }
318
319    match kernel {
320        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
321        Kernel::Avx2 => unsafe {
322            bb_row_avx2(
323                data, matype, period, devtype, devup, devdn, first, out_u, out_m, out_l,
324            );
325            Ok(())
326        },
327        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
328        Kernel::Avx512 => unsafe {
329            bb_row_avx512(
330                data, matype, period, devtype, devup, devdn, first, out_u, out_m, out_l,
331            );
332            Ok(())
333        },
334        _ => {
335            let middle = ma(matype, MaData::Slice(data), period)
336                .map_err(|e| BollingerBandsError::UnderlyingFunctionFailed(e.to_string()))?;
337            let dev_values = deviation(&DevInput::from_slice(
338                data,
339                DevParams {
340                    period: Some(period),
341                    devtype: Some(devtype),
342                },
343            ))
344            .map_err(|e| BollingerBandsError::UnderlyingFunctionFailed(e.to_string()))?;
345
346            let start = first + period - 1;
347            let n = data.len();
348            let mut i = start;
349            while i < n {
350                let m = unsafe { *middle.get_unchecked(i) };
351                let d = unsafe { *dev_values.values.get_unchecked(i) };
352                unsafe {
353                    *out_m.get_unchecked_mut(i) = m;
354                    *out_u.get_unchecked_mut(i) = devup.mul_add(d, m);
355                    *out_l.get_unchecked_mut(i) = (-devdn).mul_add(d, m);
356                }
357                i += 1;
358            }
359            Ok(())
360        }
361    }
362}
363
364pub fn bollinger_bands_with_kernel(
365    input: &BollingerBandsInput,
366    kernel: Kernel,
367) -> Result<BollingerBandsOutput, BollingerBandsError> {
368    let (data, period, devup, devdn, matype, devtype, first, chosen) = bb_prepare(input, kernel)?;
369    let warm = first + period - 1;
370
371    let mut upper = alloc_with_nan_prefix(data.len(), warm);
372    let mut middle = alloc_with_nan_prefix(data.len(), warm);
373    let mut lower = alloc_with_nan_prefix(data.len(), warm);
374
375    bollinger_bands_compute_into(
376        data,
377        period,
378        devup,
379        devdn,
380        matype,
381        devtype,
382        first,
383        chosen,
384        &mut upper,
385        &mut middle,
386        &mut lower,
387    )?;
388
389    Ok(BollingerBandsOutput {
390        upper_band: upper,
391        middle_band: middle,
392        lower_band: lower,
393    })
394}
395
396#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
397pub fn bollinger_bands_into(
398    input: &BollingerBandsInput,
399    out_upper: &mut [f64],
400    out_middle: &mut [f64],
401    out_lower: &mut [f64],
402) -> Result<(), BollingerBandsError> {
403    let (data, period, devup, devdn, matype, devtype, first, chosen) =
404        bb_prepare(input, Kernel::Auto)?;
405
406    if out_upper.len() != data.len()
407        || out_middle.len() != data.len()
408        || out_lower.len() != data.len()
409    {
410        return Err(BollingerBandsError::OutputLengthMismatch {
411            expected: data.len(),
412            got: out_upper.len().min(out_middle.len()).min(out_lower.len()),
413        });
414    }
415
416    let warm = first + period - 1;
417    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
418    let w = warm.min(data.len());
419    for v in &mut out_upper[..w] {
420        *v = qnan;
421    }
422    for v in &mut out_middle[..w] {
423        *v = qnan;
424    }
425    for v in &mut out_lower[..w] {
426        *v = qnan;
427    }
428
429    bollinger_bands_compute_into(
430        data, period, devup, devdn, matype, devtype, first, chosen, out_upper, out_middle,
431        out_lower,
432    )
433}
434
435#[inline]
436pub fn bollinger_bands_into_slices(
437    out_u: &mut [f64],
438    out_m: &mut [f64],
439    out_l: &mut [f64],
440    input: &BollingerBandsInput,
441    kern: Kernel,
442) -> Result<(), BollingerBandsError> {
443    let (data, period, devup, devdn, matype, devtype, first, chosen) = bb_prepare(input, kern)?;
444
445    if out_u.len() != data.len() || out_m.len() != data.len() || out_l.len() != data.len() {
446        return Err(BollingerBandsError::OutputLengthMismatch {
447            expected: data.len(),
448            got: out_u.len().min(out_m.len()).min(out_l.len()),
449        });
450    }
451
452    bollinger_bands_compute_into(
453        data, period, devup, devdn, matype, devtype, first, chosen, out_u, out_m, out_l,
454    )?;
455
456    let warm = first + period - 1;
457    out_u[..warm].fill(f64::NAN);
458    out_m[..warm].fill(f64::NAN);
459    out_l[..warm].fill(f64::NAN);
460    Ok(())
461}
462
463#[inline]
464pub unsafe fn bollinger_bands_scalar(
465    data: &[f64],
466    matype: &str,
467    ma_data: MaData,
468    period: usize,
469    devtype: usize,
470    dev_input: DevInput,
471    devup: f64,
472    devdn: f64,
473) -> Result<BollingerBandsOutput, BollingerBandsError> {
474    let middle = ma(matype, ma_data, period)
475        .map_err(|e| BollingerBandsError::UnderlyingFunctionFailed(e.to_string()))?;
476    let dev_values = deviation(&dev_input)
477        .map_err(|e| BollingerBandsError::UnderlyingFunctionFailed(e.to_string()))?;
478
479    let first = data.iter().position(|x| !x.is_nan()).unwrap();
480    let warmup_period = first + period - 1;
481    let mut upper_band = alloc_with_nan_prefix(data.len(), warmup_period);
482    let mut middle_band = alloc_with_nan_prefix(data.len(), warmup_period);
483    let mut lower_band = alloc_with_nan_prefix(data.len(), warmup_period);
484    for i in (first + period - 1)..data.len() {
485        middle_band[i] = middle[i];
486        upper_band[i] = middle[i] + devup * dev_values[i];
487        lower_band[i] = middle[i] - devdn * dev_values[i];
488    }
489    Ok(BollingerBandsOutput {
490        upper_band,
491        middle_band,
492        lower_band,
493    })
494}
495
496#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
497pub unsafe fn bollinger_bands_avx512(
498    data: &[f64],
499    matype: &str,
500    ma_data: MaData,
501    period: usize,
502    devtype: usize,
503    dev_input: DevInput,
504    devup: f64,
505    devdn: f64,
506) -> Result<BollingerBandsOutput, BollingerBandsError> {
507    if period <= 32 {
508        bollinger_bands_avx512_short(
509            data, matype, ma_data, period, devtype, dev_input, devup, devdn,
510        )
511    } else {
512        bollinger_bands_avx512_long(
513            data, matype, ma_data, period, devtype, dev_input, devup, devdn,
514        )
515    }
516}
517
518#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
519pub unsafe fn bollinger_bands_avx512_short(
520    data: &[f64],
521    matype: &str,
522    ma_data: MaData,
523    period: usize,
524    devtype: usize,
525    dev_input: DevInput,
526    devup: f64,
527    devdn: f64,
528) -> Result<BollingerBandsOutput, BollingerBandsError> {
529    bollinger_bands_scalar(
530        data, matype, ma_data, period, devtype, dev_input, devup, devdn,
531    )
532}
533
534#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
535pub unsafe fn bollinger_bands_avx512_long(
536    data: &[f64],
537    matype: &str,
538    ma_data: MaData,
539    period: usize,
540    devtype: usize,
541    dev_input: DevInput,
542    devup: f64,
543    devdn: f64,
544) -> Result<BollingerBandsOutput, BollingerBandsError> {
545    bollinger_bands_scalar(
546        data, matype, ma_data, period, devtype, dev_input, devup, devdn,
547    )
548}
549
550#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
551pub unsafe fn bollinger_bands_avx2(
552    data: &[f64],
553    matype: &str,
554    ma_data: MaData,
555    period: usize,
556    devtype: usize,
557    dev_input: DevInput,
558    devup: f64,
559    devdn: f64,
560) -> Result<BollingerBandsOutput, BollingerBandsError> {
561    bollinger_bands_scalar(
562        data, matype, ma_data, period, devtype, dev_input, devup, devdn,
563    )
564}
565
566unsafe fn bb_row_scalar_classic_sma(
567    data: &[f64],
568    period: usize,
569    devtype: usize,
570    devup: f64,
571    devdn: f64,
572    first: usize,
573    out_u: &mut [f64],
574    out_m: &mut [f64],
575    out_l: &mut [f64],
576) {
577    let n = data.len();
578    let warm = first + period - 1;
579    if warm >= n {
580        return;
581    }
582
583    if devtype != 0 {
584        let ma_data = MaData::Slice(data);
585        let dev_input = DevInput::from_slice(
586            data,
587            DevParams {
588                period: Some(period),
589                devtype: Some(devtype),
590            },
591        );
592
593        let middle = ma("sma", ma_data, period)
594            .expect("MA(sma) computation failed in bb_row_scalar_classic_sma");
595        let dev_values = deviation(&dev_input)
596            .expect("Deviation computation failed in bb_row_scalar_classic_sma");
597
598        let mut i = warm;
599        while i < n {
600            let m = *middle.get_unchecked(i);
601            let d = *dev_values.values.get_unchecked(i);
602            *out_m.get_unchecked_mut(i) = m;
603            *out_u.get_unchecked_mut(i) = devup.mul_add(d, m);
604            *out_l.get_unchecked_mut(i) = (-devdn).mul_add(d, m);
605            i += 1;
606        }
607        return;
608    }
609
610    let inv_n = 1.0 / (period as f64);
611    let use_sample = false;
612
613    let mut sum = 0.0f64;
614    let mut sum_sq = 0.0f64;
615
616    let mut p = data.as_ptr().add(first);
617    for _ in 0..period {
618        let v = unsafe { *p };
619        sum += v;
620        sum_sq += v * v;
621        p = unsafe { p.add(1) };
622    }
623
624    let mean = sum * inv_n;
625
626    let var0 = if !use_sample {
627        (sum_sq * inv_n) - (mean * mean)
628    } else if period > 1 {
629        (sum_sq - sum * mean) / ((period - 1) as f64)
630    } else {
631        0.0
632    };
633    let std0 = var0.max(0.0).sqrt();
634
635    unsafe {
636        *out_m.get_unchecked_mut(warm) = mean;
637        *out_u.get_unchecked_mut(warm) = mean + devup * std0;
638        *out_l.get_unchecked_mut(warm) = mean - devdn * std0;
639    }
640
641    let mut i = warm + 1;
642    let mut p_new = unsafe { data.as_ptr().add(i) };
643    let mut p_old = unsafe { data.as_ptr().add(i - period) };
644    while i < n {
645        let new = unsafe { *p_new };
646        let old = unsafe { *p_old };
647
648        sum += new - old;
649        sum_sq = (sum_sq - old * old) + new * new;
650
651        let m = sum * inv_n;
652        let var = if !use_sample {
653            (sum_sq * inv_n) - (m * m)
654        } else if period > 1 {
655            (sum_sq - sum * m) / ((period - 1) as f64)
656        } else {
657            0.0
658        };
659        let sd = var.max(0.0).sqrt();
660
661        unsafe {
662            *out_m.get_unchecked_mut(i) = m;
663            *out_u.get_unchecked_mut(i) = m + devup * sd;
664            *out_l.get_unchecked_mut(i) = m - devdn * sd;
665        }
666
667        i += 1;
668        p_new = unsafe { p_new.add(1) };
669        p_old = unsafe { p_old.add(1) };
670    }
671}
672
673unsafe fn bb_row_scalar(
674    data: &[f64],
675    matype: &str,
676    period: usize,
677    devtype: usize,
678    devup: f64,
679    devdn: f64,
680    first: usize,
681    out_u: &mut [f64],
682    out_m: &mut [f64],
683    out_l: &mut [f64],
684) {
685    if matype.eq_ignore_ascii_case("sma") {
686        bb_row_scalar_classic_sma(
687            data, period, devtype, devup, devdn, first, out_u, out_m, out_l,
688        );
689        return;
690    }
691
692    let ma_data = MaData::Slice(data);
693    let dev_input = DevInput::from_slice(
694        data,
695        DevParams {
696            period: Some(period),
697            devtype: Some(devtype),
698        },
699    );
700
701    let middle = ma(matype, ma_data, period).expect("MA computation failed in bb_row_scalar");
702    let dev_values = deviation(&dev_input).expect("Deviation computation failed in bb_row_scalar");
703
704    let n = data.len();
705    let warm = first + period - 1;
706    if warm >= n {
707        return;
708    }
709
710    let mut i = warm;
711    while i < n {
712        let m = *middle.get_unchecked(i);
713        let d = *dev_values.values.get_unchecked(i);
714
715        *out_m.get_unchecked_mut(i) = m;
716        *out_u.get_unchecked_mut(i) = devup.mul_add(d, m);
717        *out_l.get_unchecked_mut(i) = (-devdn).mul_add(d, m);
718
719        i += 1;
720    }
721}
722
723#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
724unsafe fn bb_row_avx2(
725    data: &[f64],
726    matype: &str,
727    period: usize,
728    devtype: usize,
729    devup: f64,
730    devdn: f64,
731    first: usize,
732    out_u: &mut [f64],
733    out_m: &mut [f64],
734    out_l: &mut [f64],
735) {
736    use core::arch::x86_64::*;
737
738    let n = data.len();
739    let warm = first + period - 1;
740    if warm >= n {
741        return;
742    }
743
744    if matype.eq_ignore_ascii_case("sma") && devtype == 0 {
745        bb_row_scalar_classic_sma(
746            data, period, devtype, devup, devdn, first, out_u, out_m, out_l,
747        );
748        return;
749    }
750
751    let ma_data = MaData::Slice(data);
752    let dev_input = DevInput::from_slice(
753        data,
754        DevParams {
755            period: Some(period),
756            devtype: Some(devtype),
757        },
758    );
759    let middle = ma(matype, ma_data, period).expect("MA computation failed in bb_row_avx2");
760    let dev_values = deviation(&dev_input).expect("Deviation computation failed in bb_row_avx2");
761
762    let du = unsafe { _mm256_set1_pd(devup) };
763    let dd = unsafe { _mm256_set1_pd(devdn) };
764
765    let mut i = warm;
766    let step = 4usize;
767
768    while i + step <= n {
769        let m = unsafe { _mm256_loadu_pd(middle.as_ptr().add(i)) };
770        let dv = unsafe { _mm256_loadu_pd(dev_values.values.as_ptr().add(i)) };
771
772        #[cfg(target_feature = "fma")]
773        let up = unsafe { _mm256_fmadd_pd(du, dv, m) };
774        #[cfg(not(target_feature = "fma"))]
775        let up = unsafe { _mm256_add_pd(m, _mm256_mul_pd(du, dv)) };
776
777        #[cfg(target_feature = "fma")]
778        let lo = unsafe { _mm256_fnmadd_pd(dd, dv, m) };
779        #[cfg(not(target_feature = "fma"))]
780        let lo = unsafe { _mm256_sub_pd(m, _mm256_mul_pd(dd, dv)) };
781
782        unsafe {
783            _mm256_storeu_pd(out_m.as_mut_ptr().add(i), m);
784            _mm256_storeu_pd(out_u.as_mut_ptr().add(i), up);
785            _mm256_storeu_pd(out_l.as_mut_ptr().add(i), lo);
786        }
787
788        i += step;
789    }
790
791    while i < n {
792        let m = unsafe { *middle.get_unchecked(i) };
793        let d = unsafe { *dev_values.values.get_unchecked(i) };
794        unsafe {
795            *out_m.get_unchecked_mut(i) = m;
796            *out_u.get_unchecked_mut(i) = devup.mul_add(d, m);
797            *out_l.get_unchecked_mut(i) = (-devdn).mul_add(d, m);
798        }
799        i += 1;
800    }
801}
802
803#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
804unsafe fn bb_row_avx512(
805    data: &[f64],
806    matype: &str,
807    period: usize,
808    devtype: usize,
809    devup: f64,
810    devdn: f64,
811    first: usize,
812    out_u: &mut [f64],
813    out_m: &mut [f64],
814    out_l: &mut [f64],
815) {
816    use core::arch::x86_64::*;
817
818    let n = data.len();
819    let warm = first + period - 1;
820    if warm >= n {
821        return;
822    }
823
824    if matype.eq_ignore_ascii_case("sma") && devtype == 0 {
825        bb_row_scalar_classic_sma(
826            data, period, devtype, devup, devdn, first, out_u, out_m, out_l,
827        );
828        return;
829    }
830
831    let ma_data = MaData::Slice(data);
832    let dev_input = DevInput::from_slice(
833        data,
834        DevParams {
835            period: Some(period),
836            devtype: Some(devtype),
837        },
838    );
839    let middle = ma(matype, ma_data, period).expect("MA computation failed in bb_row_avx512");
840    let dev_values = deviation(&dev_input).expect("Deviation computation failed in bb_row_avx512");
841
842    let du = unsafe { _mm512_set1_pd(devup) };
843    let dd = unsafe { _mm512_set1_pd(devdn) };
844
845    let mut i = warm;
846    let step = 8usize;
847
848    while i + step <= n {
849        let m = unsafe { _mm512_loadu_pd(middle.as_ptr().add(i)) };
850        let dv = unsafe { _mm512_loadu_pd(dev_values.values.as_ptr().add(i)) };
851
852        #[cfg(target_feature = "fma")]
853        let up = unsafe { _mm512_fmadd_pd(du, dv, m) };
854        #[cfg(not(target_feature = "fma"))]
855        let up = unsafe { _mm512_add_pd(m, _mm512_mul_pd(du, dv)) };
856
857        #[cfg(target_feature = "fma")]
858        let lo = unsafe { _mm512_fnmadd_pd(dd, dv, m) };
859        #[cfg(not(target_feature = "fma"))]
860        let lo = unsafe { _mm512_sub_pd(m, _mm512_mul_pd(dd, dv)) };
861
862        unsafe {
863            _mm512_storeu_pd(out_m.as_mut_ptr().add(i), m);
864            _mm512_storeu_pd(out_u.as_mut_ptr().add(i), up);
865            _mm512_storeu_pd(out_l.as_mut_ptr().add(i), lo);
866        }
867
868        i += step;
869    }
870
871    while i < n {
872        let m = unsafe { *middle.get_unchecked(i) };
873        let d = unsafe { *dev_values.values.get_unchecked(i) };
874        unsafe {
875            *out_m.get_unchecked_mut(i) = m;
876            *out_u.get_unchecked_mut(i) = devup.mul_add(d, m);
877            *out_l.get_unchecked_mut(i) = (-devdn).mul_add(d, m);
878        }
879        i += 1;
880    }
881}
882
883#[derive(Debug, Clone)]
884pub struct BollingerBandsStream {
885    pub period: usize,
886    pub devup: f64,
887    pub devdn: f64,
888    pub matype: String,
889    pub devtype: usize,
890
891    buf: Vec<f64>,
892    idx: usize,
893    len: usize,
894    sum: f64,
895    sum_sq: f64,
896    inv_n: f64,
897
898    alpha: f64,
899    ema: f64,
900    ema_seeded: bool,
901
902    scratch: Vec<f64>,
903
904    fast_path: FastPath,
905}
906
907#[derive(Copy, Clone, Debug, PartialEq, Eq)]
908enum FastPath {
909    SmaStddev,
910    EmaStddev,
911    Generic,
912}
913
914impl BollingerBandsStream {
915    pub fn try_new(params: BollingerBandsParams) -> Result<Self, BollingerBandsError> {
916        let period = params.period.unwrap_or(20);
917        if period == 0 {
918            return Err(BollingerBandsError::InvalidPeriod {
919                period,
920                data_len: 0,
921            });
922        }
923        let devup = params.devup.unwrap_or(2.0);
924        let devdn = params.devdn.unwrap_or(2.0);
925        let matype = params.matype.unwrap_or_else(|| "sma".to_string());
926        let devtype = params.devtype.unwrap_or(0);
927
928        let mt_lc = matype.to_ascii_lowercase();
929        let fast_path = if devtype == 0 && (mt_lc == "sma") {
930            FastPath::SmaStddev
931        } else if devtype == 0 && (mt_lc == "ema") {
932            FastPath::EmaStddev
933        } else {
934            FastPath::Generic
935        };
936
937        let inv_n = 1.0 / (period as f64);
938        let alpha = 2.0 / ((period as f64) + 1.0);
939        Ok(Self {
940            period,
941            devup,
942            devdn,
943            matype,
944            devtype,
945            buf: vec![0.0; period],
946            idx: 0,
947            len: 0,
948            sum: 0.0,
949            sum_sq: 0.0,
950            inv_n,
951            alpha,
952            ema: 0.0,
953            ema_seeded: false,
954            scratch: Vec::with_capacity(period),
955            fast_path,
956        })
957    }
958
959    #[inline(always)]
960    fn reset(&mut self) {
961        self.idx = 0;
962        self.len = 0;
963        self.sum = 0.0;
964        self.sum_sq = 0.0;
965        self.ema = 0.0;
966        self.ema_seeded = false;
967    }
968
969    #[inline(always)]
970    fn window_contiguous<'a>(&'a mut self) -> &'a [f64] {
971        self.scratch.clear();
972        if self.len < self.period {
973            self.scratch.extend_from_slice(&self.buf[..self.len]);
974        } else {
975            self.scratch.extend_from_slice(&self.buf[self.idx..]);
976            if self.idx != 0 {
977                self.scratch.extend_from_slice(&self.buf[..self.idx]);
978            }
979        }
980        debug_assert_eq!(self.scratch.len(), self.len.min(self.period));
981        &self.scratch
982    }
983
984    #[inline(always)]
985    fn stddev_current(&self) -> f64 {
986        let mean = self.sum * self.inv_n;
987        let var = (self.sum_sq * self.inv_n) - mean * mean;
988
989        let v = if var > 0.0 { var } else { 0.0 };
990        v.sqrt()
991    }
992
993    #[inline(always)]
994    fn push(&mut self, x: f64) -> (f64, bool) {
995        if self.len < self.period {
996            self.buf[self.idx] = x;
997            self.idx += 1;
998            if self.idx == self.period {
999                self.idx = 0;
1000            }
1001            self.len += 1;
1002            self.sum += x;
1003            self.sum_sq = x.mul_add(x, self.sum_sq);
1004            (0.0, false)
1005        } else {
1006            let old = self.buf[self.idx];
1007            self.buf[self.idx] = x;
1008            self.idx += 1;
1009            if self.idx == self.period {
1010                self.idx = 0;
1011            }
1012
1013            self.sum += x - old;
1014
1015            let tmp = self.sum_sq - old * old;
1016            self.sum_sq = x.mul_add(x, tmp);
1017            (old, true)
1018        }
1019    }
1020
1021    #[inline(always)]
1022    pub fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
1023        if !value.is_finite() {
1024            self.reset();
1025            return None;
1026        }
1027
1028        let _ = self.push(value);
1029
1030        if self.len < self.period {
1031            return None;
1032        }
1033
1034        match self.fast_path {
1035            FastPath::SmaStddev => {
1036                let mean = self.sum * self.inv_n;
1037                let sd = self.stddev_current();
1038                let up = self.devup.mul_add(sd, mean);
1039                let lo = (-self.devdn).mul_add(sd, mean);
1040                Some((up, mean, lo))
1041            }
1042            FastPath::EmaStddev => {
1043                if !self.ema_seeded {
1044                    self.ema = self.sum * self.inv_n;
1045                    self.ema_seeded = true;
1046                } else {
1047                    self.ema = self.alpha.mul_add(value, (1.0 - self.alpha) * self.ema);
1048                }
1049                let sd = self.stddev_current();
1050                let up = self.devup.mul_add(sd, self.ema);
1051                let lo = (-self.devdn).mul_add(sd, self.ema);
1052                Some((up, self.ema, lo))
1053            }
1054            FastPath::Generic => {
1055                let matype = self.matype.clone();
1056                let period = self.period;
1057                let devtype = self.devtype;
1058                let devup = self.devup;
1059                let devdn = self.devdn;
1060                let win = self.window_contiguous();
1061
1062                let mid = ma(&matype, MaData::Slice(win), period)
1063                    .ok()
1064                    .and_then(|v| v.last().copied())
1065                    .unwrap_or(f64::NAN);
1066
1067                let dev = deviation(&DevInput::from_slice(
1068                    win,
1069                    DevParams {
1070                        period: Some(period),
1071                        devtype: Some(devtype),
1072                    },
1073                ))
1074                .ok()
1075                .and_then(|v| v.values.last().copied())
1076                .unwrap_or(f64::NAN);
1077
1078                let up = devup.mul_add(dev, mid);
1079                let lo = (-devdn).mul_add(dev, mid);
1080                Some((up, mid, lo))
1081            }
1082        }
1083    }
1084}
1085
1086#[derive(Clone, Debug)]
1087pub struct BollingerBandsBatchRange {
1088    pub period: (usize, usize, usize),
1089    pub devup: (f64, f64, f64),
1090    pub devdn: (f64, f64, f64),
1091    pub matype: (String, String, usize),
1092    pub devtype: (usize, usize, usize),
1093}
1094
1095impl Default for BollingerBandsBatchRange {
1096    fn default() -> Self {
1097        Self {
1098            period: (20, 269, 1),
1099            devup: (2.0, 2.0, 0.0),
1100            devdn: (2.0, 2.0, 0.0),
1101            matype: ("sma".to_string(), "sma".to_string(), 0),
1102            devtype: (0, 0, 0),
1103        }
1104    }
1105}
1106
1107#[derive(Clone, Debug, Default)]
1108pub struct BollingerBandsBatchBuilder {
1109    range: BollingerBandsBatchRange,
1110    kernel: Kernel,
1111}
1112
1113impl BollingerBandsBatchBuilder {
1114    pub fn new() -> Self {
1115        Self::default()
1116    }
1117    pub fn kernel(mut self, k: Kernel) -> Self {
1118        self.kernel = k;
1119        self
1120    }
1121    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1122        self.range.period = (start, end, step);
1123        self
1124    }
1125    pub fn period_static(mut self, p: usize) -> Self {
1126        self.range.period = (p, p, 0);
1127        self
1128    }
1129    pub fn devup_range(mut self, start: f64, end: f64, step: f64) -> Self {
1130        self.range.devup = (start, end, step);
1131        self
1132    }
1133    pub fn devup_static(mut self, x: f64) -> Self {
1134        self.range.devup = (x, x, 0.0);
1135        self
1136    }
1137    pub fn devdn_range(mut self, start: f64, end: f64, step: f64) -> Self {
1138        self.range.devdn = (start, end, step);
1139        self
1140    }
1141    pub fn devdn_static(mut self, x: f64) -> Self {
1142        self.range.devdn = (x, x, 0.0);
1143        self
1144    }
1145    pub fn matype_static(mut self, m: &str) -> Self {
1146        self.range.matype = (m.into(), m.into(), 0);
1147        self
1148    }
1149    pub fn devtype_static(mut self, d: usize) -> Self {
1150        self.range.devtype = (d, d, 0);
1151        self
1152    }
1153
1154    pub fn apply_slice(
1155        self,
1156        data: &[f64],
1157    ) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1158        bollinger_bands_batch_with_kernel(data, &self.range, self.kernel)
1159    }
1160    pub fn with_default_slice(
1161        data: &[f64],
1162        k: Kernel,
1163    ) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1164        Self::new().kernel(k).apply_slice(data)
1165    }
1166    pub fn apply_candles(
1167        self,
1168        c: &Candles,
1169        src: &str,
1170    ) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1171        let slice = source_type(c, src);
1172        self.apply_slice(slice)
1173    }
1174    pub fn with_default_candles(
1175        c: &Candles,
1176    ) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1177        Self::new().kernel(Kernel::Auto).apply_candles(c, "close")
1178    }
1179}
1180
1181#[derive(Clone, Debug)]
1182pub struct BollingerBandsBatchOutput {
1183    pub upper: Vec<f64>,
1184    pub middle: Vec<f64>,
1185    pub lower: Vec<f64>,
1186    pub combos: Vec<BollingerBandsParams>,
1187    pub rows: usize,
1188    pub cols: usize,
1189}
1190impl BollingerBandsBatchOutput {
1191    pub fn row_for_params(&self, p: &BollingerBandsParams) -> Option<usize> {
1192        self.combos.iter().position(|c| {
1193            c.period == p.period
1194                && (c.devup.unwrap_or(2.0) - p.devup.unwrap_or(2.0)).abs() < 1e-12
1195                && (c.devdn.unwrap_or(2.0) - p.devdn.unwrap_or(2.0)).abs() < 1e-12
1196                && c.matype == p.matype
1197                && c.devtype == p.devtype
1198        })
1199    }
1200    pub fn bands_for(&self, p: &BollingerBandsParams) -> Option<(&[f64], &[f64], &[f64])> {
1201        self.row_for_params(p).map(|row| {
1202            let start = row * self.cols;
1203            (
1204                &self.upper[start..start + self.cols],
1205                &self.middle[start..start + self.cols],
1206                &self.lower[start..start + self.cols],
1207            )
1208        })
1209    }
1210}
1211
1212fn expand_grid(
1213    r: &BollingerBandsBatchRange,
1214) -> Result<Vec<BollingerBandsParams>, BollingerBandsError> {
1215    fn axis_usize(
1216        (start, end, step): (usize, usize, usize),
1217    ) -> Result<Vec<usize>, BollingerBandsError> {
1218        if step == 0 || start == end {
1219            return Ok(vec![start]);
1220        }
1221        let mut v = Vec::new();
1222        if start < end {
1223            let mut cur = start;
1224            while cur <= end {
1225                v.push(cur);
1226                cur = cur.saturating_add(step);
1227                if cur == *v.last().unwrap() {
1228                    break;
1229                }
1230            }
1231        } else {
1232            let mut cur = start;
1233            while cur >= end {
1234                v.push(cur);
1235                let next = cur.saturating_sub(step);
1236                if next == cur {
1237                    break;
1238                }
1239                cur = next;
1240                if cur == 0 && end > 0 {
1241                    break;
1242                }
1243            }
1244        }
1245        if v.is_empty() {
1246            return Err(BollingerBandsError::InvalidRange { start, end, step });
1247        }
1248        Ok(v)
1249    }
1250    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, BollingerBandsError> {
1251        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
1252            return Ok(vec![start]);
1253        }
1254        let mut out = Vec::new();
1255        if start < end {
1256            let st = if step > 0.0 { step } else { -step };
1257            let mut x = start;
1258            while x <= end + 1e-12 {
1259                out.push(x);
1260                x += st;
1261            }
1262        } else {
1263            let st = if step > 0.0 { -step } else { step };
1264            if st.abs() < 1e-12 {
1265                return Ok(vec![start]);
1266            }
1267            let mut x = start;
1268            while x >= end - 1e-12 {
1269                out.push(x);
1270                x += st;
1271            }
1272        }
1273        if out.is_empty() {
1274            return Err(BollingerBandsError::InvalidRangeF64 { start, end, step });
1275        }
1276        Ok(out)
1277    }
1278    fn axis_str((start, end, _step): (String, String, usize)) -> Vec<String> {
1279        if start == end {
1280            vec![start.clone()]
1281        } else {
1282            vec![start, end]
1283        }
1284    }
1285
1286    let periods = axis_usize(r.period)?;
1287    let devups = axis_f64(r.devup)?;
1288    let devdns = axis_f64(r.devdn)?;
1289    let matypes = axis_str(r.matype.clone());
1290    let devtypes = axis_usize(r.devtype)?;
1291
1292    let mut out = Vec::with_capacity(
1293        periods
1294            .len()
1295            .saturating_mul(devups.len())
1296            .saturating_mul(devdns.len())
1297            .saturating_mul(matypes.len())
1298            .saturating_mul(devtypes.len()),
1299    );
1300    for &p in &periods {
1301        for &u in &devups {
1302            for &d in &devdns {
1303                for m in &matypes {
1304                    for &t in &devtypes {
1305                        out.push(BollingerBandsParams {
1306                            period: Some(p),
1307                            devup: Some(u),
1308                            devdn: Some(d),
1309                            matype: Some(m.clone()),
1310                            devtype: Some(t),
1311                        });
1312                    }
1313                }
1314            }
1315        }
1316    }
1317    Ok(out)
1318}
1319
1320pub fn bollinger_bands_batch_with_kernel(
1321    data: &[f64],
1322    sweep: &BollingerBandsBatchRange,
1323    k: Kernel,
1324) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1325    let kernel = match k {
1326        Kernel::Auto => detect_best_batch_kernel(),
1327        other if other.is_batch() => other,
1328        _ => {
1329            return Err(BollingerBandsError::InvalidKernelForBatch(k));
1330        }
1331    };
1332    let simd = match kernel {
1333        Kernel::Avx512Batch => Kernel::Avx512,
1334        Kernel::Avx2Batch => Kernel::Avx2,
1335        Kernel::ScalarBatch => Kernel::Scalar,
1336        _ => unreachable!(),
1337    };
1338    bollinger_bands_batch_par_slice(data, sweep, simd)
1339}
1340
1341pub fn bollinger_bands_batch_slice(
1342    data: &[f64],
1343    sweep: &BollingerBandsBatchRange,
1344    kern: Kernel,
1345) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1346    bollinger_bands_batch_inner(data, sweep, kern, false)
1347}
1348
1349pub fn bollinger_bands_batch_par_slice(
1350    data: &[f64],
1351    sweep: &BollingerBandsBatchRange,
1352    kern: Kernel,
1353) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1354    bollinger_bands_batch_inner(data, sweep, kern, true)
1355}
1356
1357fn bollinger_bands_batch_inner(
1358    data: &[f64],
1359    sweep: &BollingerBandsBatchRange,
1360    kern: Kernel,
1361    parallel: bool,
1362) -> Result<BollingerBandsBatchOutput, BollingerBandsError> {
1363    let combos = expand_grid(sweep)?;
1364    let first = data
1365        .iter()
1366        .position(|x| !x.is_nan())
1367        .ok_or(BollingerBandsError::AllValuesNaN)?;
1368    let mut max_p = 0usize;
1369    for c in &combos {
1370        let p = c.period.unwrap();
1371        if p == 0 {
1372            return Err(BollingerBandsError::InvalidPeriod {
1373                period: 0,
1374                data_len: data.len(),
1375            });
1376        }
1377        max_p = max_p.max(p);
1378    }
1379    if data.len() - first < max_p {
1380        return Err(BollingerBandsError::NotEnoughValidData {
1381            needed: max_p,
1382            valid: data.len() - first,
1383        });
1384    }
1385
1386    let rows = combos.len();
1387    let cols = data.len();
1388
1389    let _ = rows
1390        .checked_mul(cols)
1391        .ok_or_else(|| BollingerBandsError::InvalidInput("rows*cols overflow".into()))?;
1392
1393    let mut upper_mu = make_uninit_matrix(rows, cols);
1394    let mut middle_mu = make_uninit_matrix(rows, cols);
1395    let mut lower_mu = make_uninit_matrix(rows, cols);
1396
1397    let warm: Vec<usize> = combos
1398        .iter()
1399        .map(|c| first + c.period.unwrap() - 1)
1400        .collect();
1401
1402    init_matrix_prefixes(&mut upper_mu, cols, &warm);
1403    init_matrix_prefixes(&mut middle_mu, cols, &warm);
1404    init_matrix_prefixes(&mut lower_mu, cols, &warm);
1405
1406    let mut upper_guard = ManuallyDrop::new(upper_mu);
1407    let mut middle_guard = ManuallyDrop::new(middle_mu);
1408    let mut lower_guard = ManuallyDrop::new(lower_mu);
1409
1410    let upper: &mut [f64] = unsafe {
1411        core::slice::from_raw_parts_mut(upper_guard.as_mut_ptr() as *mut f64, upper_guard.len())
1412    };
1413    let middle: &mut [f64] = unsafe {
1414        core::slice::from_raw_parts_mut(middle_guard.as_mut_ptr() as *mut f64, middle_guard.len())
1415    };
1416    let lower: &mut [f64] = unsafe {
1417        core::slice::from_raw_parts_mut(lower_guard.as_mut_ptr() as *mut f64, lower_guard.len())
1418    };
1419
1420    let do_row = |row: usize, out_u: &mut [f64], out_m: &mut [f64], out_l: &mut [f64]| unsafe {
1421        let p = combos[row].period.unwrap();
1422        let du = combos[row].devup.unwrap();
1423        let dd = combos[row].devdn.unwrap();
1424        let mt = combos[row].matype.as_deref().unwrap_or("sma");
1425        let dt = combos[row].devtype.unwrap();
1426
1427        if mt.eq_ignore_ascii_case("sma") && dt == 0 {
1428            bb_row_scalar_classic_sma(data, p, dt, du, dd, first, out_u, out_m, out_l);
1429            return;
1430        }
1431
1432        match kern {
1433            Kernel::Scalar => bb_row_scalar(data, mt, p, dt, du, dd, first, out_u, out_m, out_l),
1434            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1435            Kernel::Avx2 => bb_row_avx2(data, mt, p, dt, du, dd, first, out_u, out_m, out_l),
1436            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1437            Kernel::Avx512 => bb_row_avx512(data, mt, p, dt, du, dd, first, out_u, out_m, out_l),
1438            _ => unreachable!(),
1439        };
1440    };
1441
1442    if parallel {
1443        #[cfg(not(target_arch = "wasm32"))]
1444        {
1445            upper
1446                .par_chunks_mut(cols)
1447                .zip(middle.par_chunks_mut(cols))
1448                .zip(lower.par_chunks_mut(cols))
1449                .enumerate()
1450                .for_each(|(row, ((u, m), l))| do_row(row, u, m, l));
1451        }
1452
1453        #[cfg(target_arch = "wasm32")]
1454        {
1455            for (row, ((u, m), l)) in upper
1456                .chunks_mut(cols)
1457                .zip(middle.chunks_mut(cols))
1458                .zip(lower.chunks_mut(cols))
1459                .enumerate()
1460            {
1461                do_row(row, u, m, l);
1462            }
1463        }
1464    } else {
1465        for (row, ((u, m), l)) in upper
1466            .chunks_mut(cols)
1467            .zip(middle.chunks_mut(cols))
1468            .zip(lower.chunks_mut(cols))
1469            .enumerate()
1470        {
1471            do_row(row, u, m, l);
1472        }
1473    }
1474
1475    let upper_vec = unsafe {
1476        Vec::from_raw_parts(
1477            upper_guard.as_mut_ptr() as *mut f64,
1478            upper_guard.len(),
1479            upper_guard.capacity(),
1480        )
1481    };
1482    let middle_vec = unsafe {
1483        Vec::from_raw_parts(
1484            middle_guard.as_mut_ptr() as *mut f64,
1485            middle_guard.len(),
1486            middle_guard.capacity(),
1487        )
1488    };
1489    let lower_vec = unsafe {
1490        Vec::from_raw_parts(
1491            lower_guard.as_mut_ptr() as *mut f64,
1492            lower_guard.len(),
1493            lower_guard.capacity(),
1494        )
1495    };
1496
1497    Ok(BollingerBandsBatchOutput {
1498        upper: upper_vec,
1499        middle: middle_vec,
1500        lower: lower_vec,
1501        combos,
1502        rows,
1503        cols,
1504    })
1505}
1506
1507#[inline(always)]
1508pub fn bollinger_bands_batch_inner_into(
1509    data: &[f64],
1510    sweep: &BollingerBandsBatchRange,
1511    kern: Kernel,
1512    parallel: bool,
1513    out_upper: &mut [f64],
1514    out_middle: &mut [f64],
1515    out_lower: &mut [f64],
1516) -> Result<Vec<BollingerBandsParams>, BollingerBandsError> {
1517    let combos = expand_grid(sweep)?;
1518
1519    let first = data
1520        .iter()
1521        .position(|x| !x.is_nan())
1522        .ok_or(BollingerBandsError::AllValuesNaN)?;
1523    let mut max_p = 0usize;
1524    for c in &combos {
1525        let p = c.period.unwrap();
1526        if p == 0 {
1527            return Err(BollingerBandsError::InvalidPeriod {
1528                period: 0,
1529                data_len: data.len(),
1530            });
1531        }
1532        max_p = max_p.max(p);
1533    }
1534    if data.len() - first < max_p {
1535        return Err(BollingerBandsError::NotEnoughValidData {
1536            needed: max_p,
1537            valid: data.len() - first,
1538        });
1539    }
1540
1541    let cols = data.len();
1542    let total = combos
1543        .len()
1544        .checked_mul(cols)
1545        .ok_or_else(|| BollingerBandsError::InvalidInput("rows*cols overflow".into()))?;
1546    if out_upper.len() != total || out_middle.len() != total || out_lower.len() != total {
1547        return Err(BollingerBandsError::OutputLengthMismatch {
1548            expected: total,
1549            got: out_upper.len().min(out_middle.len()).min(out_lower.len()),
1550        });
1551    }
1552
1553    let do_row = |row: usize,
1554                  out_u: &mut [f64],
1555                  out_m: &mut [f64],
1556                  out_l: &mut [f64]|
1557     -> Result<(), BollingerBandsError> {
1558        let p = combos[row].period.unwrap();
1559        let du = combos[row].devup.unwrap();
1560        let dd = combos[row].devdn.unwrap();
1561        let mt = combos[row].matype.as_deref().unwrap_or("sma");
1562        let dt = combos[row].devtype.unwrap();
1563
1564        if first + p > 0 {
1565            let nan_end = (first + p - 1).min(data.len());
1566            out_u[..nan_end].fill(f64::NAN);
1567            out_m[..nan_end].fill(f64::NAN);
1568            out_l[..nan_end].fill(f64::NAN);
1569        }
1570
1571        bollinger_bands_compute_into(data, p, du, dd, mt, dt, first, kern, out_u, out_m, out_l)
1572    };
1573
1574    if parallel {
1575        #[cfg(not(target_arch = "wasm32"))]
1576        {
1577            out_upper
1578                .par_chunks_mut(cols)
1579                .zip(out_middle.par_chunks_mut(cols))
1580                .zip(out_lower.par_chunks_mut(cols))
1581                .enumerate()
1582                .try_for_each(|(row, ((u, m), l))| do_row(row, u, m, l))?;
1583        }
1584
1585        #[cfg(target_arch = "wasm32")]
1586        {
1587            for (row, ((u, m), l)) in out_upper
1588                .chunks_mut(cols)
1589                .zip(out_middle.chunks_mut(cols))
1590                .zip(out_lower.chunks_mut(cols))
1591                .enumerate()
1592            {
1593                do_row(row, u, m, l)?;
1594            }
1595        }
1596    } else {
1597        for (row, ((u, m), l)) in out_upper
1598            .chunks_mut(cols)
1599            .zip(out_middle.chunks_mut(cols))
1600            .zip(out_lower.chunks_mut(cols))
1601            .enumerate()
1602        {
1603            do_row(row, u, m, l)?;
1604        }
1605    }
1606
1607    Ok(combos)
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612    use super::*;
1613    use crate::skip_if_unsupported;
1614    use crate::utilities::data_loader::read_candles_from_csv;
1615
1616    fn check_bb_partial_params(
1617        test_name: &str,
1618        kernel: Kernel,
1619    ) -> Result<(), Box<dyn std::error::Error>> {
1620        skip_if_unsupported!(kernel, test_name);
1621        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1622        let candles = read_candles_from_csv(file_path)?;
1623
1624        let partial_params = BollingerBandsParams {
1625            period: Some(22),
1626            devup: None,
1627            devdn: None,
1628            matype: Some("sma".to_string()),
1629            devtype: None,
1630        };
1631        let input_partial = BollingerBandsInput::from_candles(&candles, "close", partial_params);
1632        let output_partial = bollinger_bands_with_kernel(&input_partial, kernel)?;
1633        assert_eq!(output_partial.upper_band.len(), candles.close.len());
1634        assert_eq!(output_partial.middle_band.len(), candles.close.len());
1635        assert_eq!(output_partial.lower_band.len(), candles.close.len());
1636        Ok(())
1637    }
1638
1639    #[test]
1640    fn test_bollinger_bands_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
1641        let mut data = Vec::with_capacity(256);
1642        for i in 0..256u32 {
1643            let x = i as f64;
1644
1645            data.push((x * 0.25).sin() * 1000.0 + ((i % 7) as f64));
1646        }
1647        let input = BollingerBandsInput::from_slice(&data, BollingerBandsParams::default());
1648
1649        let base = bollinger_bands(&input)?;
1650
1651        let n = data.len();
1652        let mut up = vec![0.0; n];
1653        let mut mid = vec![0.0; n];
1654        let mut low = vec![0.0; n];
1655
1656        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1657        {
1658            bollinger_bands_into(&input, &mut up, &mut mid, &mut low)?;
1659        }
1660
1661        fn eq_or_both_nan(a: f64, b: f64) -> bool {
1662            (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
1663        }
1664
1665        assert_eq!(up.len(), base.upper_band.len());
1666        assert_eq!(mid.len(), base.middle_band.len());
1667        assert_eq!(low.len(), base.lower_band.len());
1668        for i in 0..n {
1669            assert!(
1670                eq_or_both_nan(up[i], base.upper_band[i]),
1671                "upper mismatch at {}: {} vs {}",
1672                i,
1673                up[i],
1674                base.upper_band[i]
1675            );
1676            assert!(
1677                eq_or_both_nan(mid[i], base.middle_band[i]),
1678                "middle mismatch at {}: {} vs {}",
1679                i,
1680                mid[i],
1681                base.middle_band[i]
1682            );
1683            assert!(
1684                eq_or_both_nan(low[i], base.lower_band[i]),
1685                "lower mismatch at {}: {} vs {}",
1686                i,
1687                low[i],
1688                base.lower_band[i]
1689            );
1690        }
1691
1692        Ok(())
1693    }
1694
1695    fn check_bb_accuracy(
1696        test_name: &str,
1697        kernel: Kernel,
1698    ) -> Result<(), Box<dyn std::error::Error>> {
1699        skip_if_unsupported!(kernel, test_name);
1700        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1701        let candles = read_candles_from_csv(file_path)?;
1702
1703        let input = BollingerBandsInput::with_default_candles(&candles);
1704        let result = bollinger_bands_with_kernel(&input, kernel)?;
1705        let expected_middle = [
1706            59403.199999999975,
1707            59423.24999999998,
1708            59370.49999999998,
1709            59371.39999999998,
1710            59351.299999999974,
1711        ];
1712        let expected_lower = [
1713            58299.51497247008,
1714            58351.47038179873,
1715            58332.65135978715,
1716            58334.33194052157,
1717            58275.767369163135,
1718        ];
1719        let expected_upper = [
1720            60506.88502752987,
1721            60495.029618201224,
1722            60408.348640212804,
1723            60408.468059478386,
1724            60426.83263083681,
1725        ];
1726
1727        let start_idx = result.middle_band.len() - 5;
1728        for i in 0..5 {
1729            let actual_mid = result.middle_band[start_idx + i];
1730            let actual_low = result.lower_band[start_idx + i];
1731            let actual_up = result.upper_band[start_idx + i];
1732            assert!(
1733                (actual_mid - expected_middle[i]).abs() < 1e-4,
1734                "[{}] BB middle mismatch at i={}: {} vs {}",
1735                test_name,
1736                i,
1737                actual_mid,
1738                expected_middle[i]
1739            );
1740            assert!(
1741                (actual_low - expected_lower[i]).abs() < 1e-4,
1742                "[{}] BB lower mismatch at i={}: {} vs {}",
1743                test_name,
1744                i,
1745                actual_low,
1746                expected_lower[i]
1747            );
1748            assert!(
1749                (actual_up - expected_upper[i]).abs() < 1e-4,
1750                "[{}] BB upper mismatch at i={}: {} vs {}",
1751                test_name,
1752                i,
1753                actual_up,
1754                expected_upper[i]
1755            );
1756        }
1757        Ok(())
1758    }
1759
1760    fn check_bb_default_candles(
1761        test_name: &str,
1762        kernel: Kernel,
1763    ) -> Result<(), Box<dyn std::error::Error>> {
1764        skip_if_unsupported!(kernel, test_name);
1765        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1766        let candles = read_candles_from_csv(file_path)?;
1767
1768        let input = BollingerBandsInput::with_default_candles(&candles);
1769        match input.data {
1770            BollingerBandsData::Candles { source, .. } => assert_eq!(source, "close"),
1771            _ => panic!("Expected BollingerBandsData::Candles"),
1772        }
1773        let output = bollinger_bands_with_kernel(&input, kernel)?;
1774        assert_eq!(output.middle_band.len(), candles.close.len());
1775        Ok(())
1776    }
1777
1778    fn check_bb_zero_period(
1779        test_name: &str,
1780        kernel: Kernel,
1781    ) -> Result<(), Box<dyn std::error::Error>> {
1782        skip_if_unsupported!(kernel, test_name);
1783        let data = [10.0, 20.0, 30.0];
1784        let params = BollingerBandsParams {
1785            period: Some(0),
1786            ..BollingerBandsParams::default()
1787        };
1788        let input = BollingerBandsInput::from_slice(&data, params);
1789        let res = bollinger_bands_with_kernel(&input, kernel);
1790        assert!(
1791            res.is_err(),
1792            "[{}] BB should fail with zero period",
1793            test_name
1794        );
1795        Ok(())
1796    }
1797
1798    fn check_bb_period_exceeds_length(
1799        test_name: &str,
1800        kernel: Kernel,
1801    ) -> Result<(), Box<dyn std::error::Error>> {
1802        skip_if_unsupported!(kernel, test_name);
1803        let data = [10.0, 20.0, 30.0];
1804        let params = BollingerBandsParams {
1805            period: Some(10),
1806            ..BollingerBandsParams::default()
1807        };
1808        let input = BollingerBandsInput::from_slice(&data, params);
1809        let res = bollinger_bands_with_kernel(&input, kernel);
1810        assert!(
1811            res.is_err(),
1812            "[{}] BB should fail with period exceeding length",
1813            test_name
1814        );
1815        Ok(())
1816    }
1817
1818    fn check_bb_very_small_dataset(
1819        test_name: &str,
1820        kernel: Kernel,
1821    ) -> Result<(), Box<dyn std::error::Error>> {
1822        skip_if_unsupported!(kernel, test_name);
1823        let data = [42.0];
1824        let input = BollingerBandsInput::from_slice(&data, BollingerBandsParams::default());
1825        let res = bollinger_bands_with_kernel(&input, kernel);
1826        assert!(
1827            res.is_err(),
1828            "[{}] BB should fail with insufficient data",
1829            test_name
1830        );
1831        Ok(())
1832    }
1833
1834    fn check_bb_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1835        skip_if_unsupported!(kernel, test_name);
1836        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1837        let candles = read_candles_from_csv(file_path)?;
1838
1839        let first_params = BollingerBandsParams {
1840            period: Some(20),
1841            ..BollingerBandsParams::default()
1842        };
1843        let first_input = BollingerBandsInput::from_candles(&candles, "close", first_params);
1844        let first_result = bollinger_bands_with_kernel(&first_input, kernel)?;
1845
1846        let second_params = BollingerBandsParams {
1847            period: Some(10),
1848            ..BollingerBandsParams::default()
1849        };
1850        let second_input =
1851            BollingerBandsInput::from_slice(&first_result.middle_band, second_params);
1852        let second_result = bollinger_bands_with_kernel(&second_input, kernel)?;
1853
1854        assert_eq!(
1855            second_result.middle_band.len(),
1856            first_result.middle_band.len()
1857        );
1858        Ok(())
1859    }
1860
1861    fn check_bb_nan_handling(
1862        test_name: &str,
1863        kernel: Kernel,
1864    ) -> Result<(), Box<dyn std::error::Error>> {
1865        skip_if_unsupported!(kernel, test_name);
1866        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1867        let candles = read_candles_from_csv(file_path)?;
1868
1869        let params = BollingerBandsParams {
1870            period: Some(20),
1871            ..BollingerBandsParams::default()
1872        };
1873        let input = BollingerBandsInput::from_candles(&candles, "close", params);
1874        let result = bollinger_bands_with_kernel(&input, kernel)?;
1875        let check_index = 240;
1876        if result.middle_band.len() > check_index {
1877            for i in check_index..result.middle_band.len() {
1878                assert!(
1879                    !result.middle_band[i].is_nan(),
1880                    "[{}] BB NaN middle idx {}",
1881                    test_name,
1882                    i
1883                );
1884                assert!(
1885                    !result.upper_band[i].is_nan(),
1886                    "[{}] BB NaN upper idx {}",
1887                    test_name,
1888                    i
1889                );
1890                assert!(
1891                    !result.lower_band[i].is_nan(),
1892                    "[{}] BB NaN lower idx {}",
1893                    test_name,
1894                    i
1895                );
1896            }
1897        }
1898        Ok(())
1899    }
1900
1901    fn check_bb_streaming(
1902        test_name: &str,
1903        kernel: Kernel,
1904    ) -> Result<(), Box<dyn std::error::Error>> {
1905        skip_if_unsupported!(kernel, test_name);
1906        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1907        let candles = read_candles_from_csv(file_path)?;
1908
1909        let params = BollingerBandsParams::default();
1910        let period = params.period.unwrap_or(20);
1911        let devup = params.devup.unwrap_or(2.0);
1912        let devdn = params.devdn.unwrap_or(2.0);
1913
1914        let input = BollingerBandsInput::from_candles(&candles, "close", params.clone());
1915        let batch_output = bollinger_bands_with_kernel(&input, kernel)?;
1916
1917        let mut stream = BollingerBandsStream::try_new(params)?;
1918        let mut stream_upper = Vec::with_capacity(candles.close.len());
1919        let mut stream_middle = Vec::with_capacity(candles.close.len());
1920        let mut stream_lower = Vec::with_capacity(candles.close.len());
1921        for &v in &candles.close {
1922            match stream.update(v) {
1923                Some((up, mid, low)) => {
1924                    stream_upper.push(up);
1925                    stream_middle.push(mid);
1926                    stream_lower.push(low);
1927                }
1928                None => {
1929                    stream_upper.push(f64::NAN);
1930                    stream_middle.push(f64::NAN);
1931                    stream_lower.push(f64::NAN);
1932                }
1933            }
1934        }
1935
1936        for (i, (bu, bm, bl)) in itertools::izip!(
1937            &batch_output.upper_band,
1938            &batch_output.middle_band,
1939            &batch_output.lower_band
1940        )
1941        .enumerate()
1942        {
1943            if bu.is_nan() && stream_upper[i].is_nan() {
1944                continue;
1945            }
1946            assert!(
1947                (*bu - stream_upper[i]).abs() < 1e-6,
1948                "[{}] BB stream/upper mismatch at idx {}",
1949                test_name,
1950                i
1951            );
1952            assert!(
1953                (*bm - stream_middle[i]).abs() < 1e-6,
1954                "[{}] BB stream/middle mismatch at idx {}",
1955                test_name,
1956                i
1957            );
1958            assert!(
1959                (*bl - stream_lower[i]).abs() < 1e-6,
1960                "[{}] BB stream/lower mismatch at idx {}",
1961                test_name,
1962                i
1963            );
1964        }
1965        Ok(())
1966    }
1967
1968    #[cfg(debug_assertions)]
1969    fn check_bb_no_poison(
1970        test_name: &str,
1971        kernel: Kernel,
1972    ) -> Result<(), Box<dyn std::error::Error>> {
1973        skip_if_unsupported!(kernel, test_name);
1974
1975        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1976        let candles = read_candles_from_csv(file_path)?;
1977
1978        let params_list = vec![
1979            BollingerBandsParams::default(),
1980            BollingerBandsParams {
1981                period: Some(10),
1982                devup: Some(1.5),
1983                devdn: Some(1.5),
1984                matype: Some("ema".to_string()),
1985                devtype: Some(1),
1986            },
1987            BollingerBandsParams {
1988                period: Some(30),
1989                devup: Some(3.0),
1990                devdn: Some(2.0),
1991                matype: Some("sma".to_string()),
1992                devtype: Some(2),
1993            },
1994        ];
1995
1996        for params in params_list {
1997            let input = BollingerBandsInput::from_candles(&candles, "close", params.clone());
1998            let output = bollinger_bands_with_kernel(&input, kernel)?;
1999
2000            for (band_name, band_data) in [
2001                ("upper", &output.upper_band),
2002                ("middle", &output.middle_band),
2003                ("lower", &output.lower_band),
2004            ] {
2005                for (i, &val) in band_data.iter().enumerate() {
2006                    if val.is_nan() {
2007                        continue;
2008                    }
2009
2010                    let bits = val.to_bits();
2011
2012                    if bits == 0x11111111_11111111 {
2013                        panic!(
2014                            "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} in {} band with params: period={}, devup={}, devdn={}, matype={}, devtype={}",
2015                            test_name, val, bits, i, band_name,
2016                            params.period.unwrap_or(20),
2017                            params.devup.unwrap_or(2.0),
2018                            params.devdn.unwrap_or(2.0),
2019                            params.matype.as_ref().unwrap_or(&"sma".to_string()),
2020                            params.devtype.unwrap_or(0)
2021                        );
2022                    }
2023
2024                    if bits == 0x22222222_22222222 {
2025                        panic!(
2026                            "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} in {} band with params: period={}, devup={}, devdn={}, matype={}, devtype={}",
2027                            test_name, val, bits, i, band_name,
2028                            params.period.unwrap_or(20),
2029                            params.devup.unwrap_or(2.0),
2030                            params.devdn.unwrap_or(2.0),
2031                            params.matype.as_ref().unwrap_or(&"sma".to_string()),
2032                            params.devtype.unwrap_or(0)
2033                        );
2034                    }
2035
2036                    if bits == 0x33333333_33333333 {
2037                        panic!(
2038                            "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} in {} band with params: period={}, devup={}, devdn={}, matype={}, devtype={}",
2039                            test_name, val, bits, i, band_name,
2040                            params.period.unwrap_or(20),
2041                            params.devup.unwrap_or(2.0),
2042                            params.devdn.unwrap_or(2.0),
2043                            params.matype.as_ref().unwrap_or(&"sma".to_string()),
2044                            params.devtype.unwrap_or(0)
2045                        );
2046                    }
2047                }
2048            }
2049        }
2050
2051        Ok(())
2052    }
2053
2054    #[cfg(not(debug_assertions))]
2055    fn check_bb_no_poison(
2056        _test_name: &str,
2057        _kernel: Kernel,
2058    ) -> Result<(), Box<dyn std::error::Error>> {
2059        Ok(())
2060    }
2061
2062    #[cfg(feature = "proptest")]
2063    #[allow(clippy::float_cmp)]
2064    fn check_bollinger_bands_property(
2065        test_name: &str,
2066        kernel: Kernel,
2067    ) -> Result<(), Box<dyn std::error::Error>> {
2068        use proptest::prelude::*;
2069        skip_if_unsupported!(kernel, test_name);
2070
2071        let strat = (1usize..=100).prop_flat_map(|period| {
2072            (
2073                prop::collection::vec(
2074                    (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
2075                    period..400,
2076                ),
2077                Just(period),
2078                0.0f64..5.0f64,
2079                0.0f64..5.0f64,
2080            )
2081        });
2082
2083        proptest::test_runner::TestRunner::default()
2084            .run(&strat, |(data, period, devup, devdn)| {
2085                let params = BollingerBandsParams {
2086                    period: Some(period),
2087                    devup: Some(devup),
2088                    devdn: Some(devdn),
2089                    matype: Some("sma".to_string()),
2090                    devtype: Some(0),
2091                };
2092                let input = BollingerBandsInput::from_slice(&data, params);
2093
2094                let result = bollinger_bands_with_kernel(&input, kernel).unwrap();
2095                let upper = &result.upper_band;
2096                let middle = &result.middle_band;
2097                let lower = &result.lower_band;
2098
2099                let ref_result = bollinger_bands_with_kernel(&input, Kernel::Scalar).unwrap();
2100                let ref_upper = &ref_result.upper_band;
2101                let ref_middle = &ref_result.middle_band;
2102                let ref_lower = &ref_result.lower_band;
2103
2104                let first_valid = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
2105                let warmup_end = first_valid + period - 1;
2106
2107                for i in 0..warmup_end.min(data.len()) {
2108                    prop_assert!(upper[i].is_nan(), "Upper band should be NaN at idx {}", i);
2109                    prop_assert!(middle[i].is_nan(), "Middle band should be NaN at idx {}", i);
2110                    prop_assert!(lower[i].is_nan(), "Lower band should be NaN at idx {}", i);
2111                }
2112
2113                for i in warmup_end..data.len() {
2114                    let u = upper[i];
2115                    let m = middle[i];
2116                    let l = lower[i];
2117
2118                    if u.is_finite()
2119                        && m.is_finite()
2120                        && l.is_finite()
2121                        && devup >= 0.0
2122                        && devdn >= 0.0
2123                    {
2124                        prop_assert!(
2125                            u >= m - 1e-9,
2126                            "Upper band {} should be >= middle band {} at idx {}",
2127                            u,
2128                            m,
2129                            i
2130                        );
2131                        prop_assert!(
2132                            m >= l - 1e-9,
2133                            "Middle band {} should be >= lower band {} at idx {}",
2134                            m,
2135                            l,
2136                            i
2137                        );
2138                    }
2139
2140                    if m.is_finite() {
2141                        let window_start = i.saturating_sub(period - 1);
2142                        let window = &data[window_start..=i];
2143                        let valid_values: Vec<f64> =
2144                            window.iter().filter(|x| x.is_finite()).copied().collect();
2145
2146                        if !valid_values.is_empty() {
2147                            let window_min =
2148                                valid_values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
2149                            let window_max = valid_values
2150                                .iter()
2151                                .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2152
2153                            let tolerance = if period == 1 { 1e-6 } else { 1e-9 };
2154
2155                            prop_assert!(
2156                                m >= window_min - tolerance && m <= window_max + tolerance,
2157                                "Middle band {} not in window range [{}, {}] at idx {}",
2158                                m,
2159                                window_min,
2160                                window_max,
2161                                i
2162                            );
2163                        }
2164                    }
2165
2166                    let ru = ref_upper[i];
2167                    let rm = ref_middle[i];
2168                    let rl = ref_lower[i];
2169
2170                    if u.is_finite() && ru.is_finite() {
2171                        let ulp_diff = u.to_bits().abs_diff(ru.to_bits());
2172                        prop_assert!(
2173                            (u - ru).abs() <= 1e-9 || ulp_diff <= 4,
2174                            "Upper band mismatch at idx {}: {} vs {} (ULP={})",
2175                            i,
2176                            u,
2177                            ru,
2178                            ulp_diff
2179                        );
2180                    } else {
2181                        prop_assert_eq!(u.is_nan(), ru.is_nan(), "Upper NaN mismatch at idx {}", i);
2182                    }
2183
2184                    if m.is_finite() && rm.is_finite() {
2185                        let ulp_diff = m.to_bits().abs_diff(rm.to_bits());
2186                        prop_assert!(
2187                            (m - rm).abs() <= 1e-9 || ulp_diff <= 4,
2188                            "Middle band mismatch at idx {}: {} vs {} (ULP={})",
2189                            i,
2190                            m,
2191                            rm,
2192                            ulp_diff
2193                        );
2194                    } else {
2195                        prop_assert_eq!(
2196                            m.is_nan(),
2197                            rm.is_nan(),
2198                            "Middle NaN mismatch at idx {}",
2199                            i
2200                        );
2201                    }
2202
2203                    if l.is_finite() && rl.is_finite() {
2204                        let ulp_diff = l.to_bits().abs_diff(rl.to_bits());
2205                        prop_assert!(
2206                            (l - rl).abs() <= 1e-9 || ulp_diff <= 4,
2207                            "Lower band mismatch at idx {}: {} vs {} (ULP={})",
2208                            i,
2209                            l,
2210                            rl,
2211                            ulp_diff
2212                        );
2213                    } else {
2214                        prop_assert_eq!(l.is_nan(), rl.is_nan(), "Lower NaN mismatch at idx {}", i);
2215                    }
2216                }
2217
2218                if period == 2 && warmup_end < data.len() {
2219                    for i in warmup_end..data.len() {
2220                        if upper[i].is_finite() && middle[i].is_finite() && lower[i].is_finite() {
2221                            prop_assert!(
2222                                upper[i] >= middle[i] - 1e-9,
2223                                "Period=2: upper band should be >= middle at idx {}",
2224                                i
2225                            );
2226                            prop_assert!(
2227                                middle[i] >= lower[i] - 1e-9,
2228                                "Period=2: middle band should be >= lower at idx {}",
2229                                i
2230                            );
2231                        }
2232                    }
2233                }
2234
2235                let all_same = data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-12);
2236                if all_same && data.len() > 0 && data[0].is_finite() {
2237                    let const_val = data[0];
2238                    for i in warmup_end..data.len() {
2239                        if upper[i].is_finite() {
2240                            prop_assert!(
2241                                (upper[i] - const_val).abs() <= 1e-9,
2242                                "Constant data: upper band should be {} at idx {}",
2243                                const_val,
2244                                i
2245                            );
2246                            prop_assert!(
2247                                (middle[i] - const_val).abs() <= 1e-9,
2248                                "Constant data: middle band should be {} at idx {}",
2249                                const_val,
2250                                i
2251                            );
2252                            prop_assert!(
2253                                (lower[i] - const_val).abs() <= 1e-9,
2254                                "Constant data: lower band should be {} at idx {}",
2255                                const_val,
2256                                i
2257                            );
2258                        }
2259                    }
2260                }
2261
2262                if devup > 0.0 && devdn > 0.0 && warmup_end < data.len() {
2263                    for i in warmup_end..data.len() {
2264                        if upper[i].is_finite() && middle[i].is_finite() && lower[i].is_finite() {
2265                            let upper_width = upper[i] - middle[i];
2266                            let lower_width = middle[i] - lower[i];
2267
2268                            if upper_width > 1e-12 && lower_width > 1e-12 {
2269                                let ratio = (upper_width / lower_width) / (devup / devdn);
2270                                prop_assert!(
2271                                    (ratio - 1.0).abs() <= 1e-4,
2272                                    "Band width ratio mismatch at idx {}: expected {}, got {}",
2273                                    i,
2274                                    devup / devdn,
2275                                    upper_width / lower_width
2276                                );
2277                            }
2278                        }
2279                    }
2280                }
2281
2282                if (devup - devdn).abs() < 1e-12 && devup > 0.0 {
2283                    for i in warmup_end..data.len() {
2284                        if upper[i].is_finite() && middle[i].is_finite() && lower[i].is_finite() {
2285                            let upper_dist = upper[i] - middle[i];
2286                            let lower_dist = middle[i] - lower[i];
2287                            prop_assert!(
2288                                (upper_dist - lower_dist).abs() <= 1e-9,
2289                                "Bands should be symmetric at idx {}: upper_dist={}, lower_dist={}",
2290                                i,
2291                                upper_dist,
2292                                lower_dist
2293                            );
2294                        }
2295                    }
2296                }
2297
2298                let all_finite = data.iter().all(|x| x.is_finite());
2299                if all_finite {
2300                    for i in warmup_end..data.len() {
2301                        prop_assert!(
2302                            upper[i].is_finite(),
2303                            "Upper band should be finite at idx {} with finite input",
2304                            i
2305                        );
2306                        prop_assert!(
2307                            middle[i].is_finite(),
2308                            "Middle band should be finite at idx {} with finite input",
2309                            i
2310                        );
2311                        prop_assert!(
2312                            lower[i].is_finite(),
2313                            "Lower band should be finite at idx {} with finite input",
2314                            i
2315                        );
2316                    }
2317                }
2318
2319                Ok(())
2320            })
2321            .unwrap();
2322
2323        Ok(())
2324    }
2325
2326    macro_rules! generate_all_bb_tests {
2327        ($($test_fn:ident),*) => {
2328            paste::paste! {
2329                $(
2330                    #[test]
2331                    fn [<$test_fn _scalar_f64>]() {
2332                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2333                    }
2334                )*
2335                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2336                $(
2337                    #[test]
2338                    fn [<$test_fn _avx2_f64>]() {
2339                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2340                    }
2341                    #[test]
2342                    fn [<$test_fn _avx512_f64>]() {
2343                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2344                    }
2345                )*
2346            }
2347        }
2348    }
2349
2350    generate_all_bb_tests!(
2351        check_bb_partial_params,
2352        check_bb_accuracy,
2353        check_bb_default_candles,
2354        check_bb_zero_period,
2355        check_bb_period_exceeds_length,
2356        check_bb_very_small_dataset,
2357        check_bb_reinput,
2358        check_bb_nan_handling,
2359        check_bb_streaming,
2360        check_bb_no_poison
2361    );
2362
2363    #[cfg(feature = "proptest")]
2364    generate_all_bb_tests!(check_bollinger_bands_property);
2365
2366    #[test]
2367    fn test_kernel_not_batch_error() {
2368        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
2369        let sweep = BollingerBandsBatchRange::default();
2370
2371        let result = bollinger_bands_batch_with_kernel(&data, &sweep, Kernel::Scalar);
2372        assert!(matches!(
2373            result,
2374            Err(BollingerBandsError::InvalidKernelForBatch(Kernel::Scalar))
2375        ));
2376
2377        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2378        {
2379            let result = bollinger_bands_batch_with_kernel(&data, &sweep, Kernel::Avx2);
2380            assert!(matches!(
2381                result,
2382                Err(BollingerBandsError::InvalidKernelForBatch(Kernel::Avx2))
2383            ));
2384        }
2385    }
2386
2387    fn check_batch_default_row(
2388        test: &str,
2389        kernel: Kernel,
2390    ) -> Result<(), Box<dyn std::error::Error>> {
2391        skip_if_unsupported!(kernel, test);
2392
2393        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2394        let c = read_candles_from_csv(file)?;
2395
2396        let output = BollingerBandsBatchBuilder::new()
2397            .kernel(kernel)
2398            .apply_candles(&c, "close")?;
2399
2400        let def = BollingerBandsParams::default();
2401        let (_up, mid, _low) = output.bands_for(&def).expect("default row missing");
2402
2403        assert_eq!(mid.len(), c.close.len());
2404        Ok(())
2405    }
2406
2407    #[cfg(debug_assertions)]
2408    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
2409        skip_if_unsupported!(kernel, test);
2410
2411        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2412        let c = read_candles_from_csv(file)?;
2413
2414        let output = BollingerBandsBatchBuilder::new()
2415            .kernel(kernel)
2416            .period_range(10, 30, 10)
2417            .devup_range(1.0, 3.0, 1.0)
2418            .devdn_range(1.0, 2.0, 0.5)
2419            .matype_static("sma")
2420            .devtype_static(0)
2421            .apply_candles(&c, "close")?;
2422
2423        for (band_name, band_data) in [
2424            ("upper", &output.upper),
2425            ("middle", &output.middle),
2426            ("lower", &output.lower),
2427        ] {
2428            for (idx, &val) in band_data.iter().enumerate() {
2429                if val.is_nan() {
2430                    continue;
2431                }
2432
2433                let bits = val.to_bits();
2434                let row = idx / output.cols;
2435                let col = idx % output.cols;
2436                let params = &output.combos[row];
2437
2438                if bits == 0x11111111_11111111 {
2439                    panic!(
2440                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (flat index {}) in {} band. Params: period={}, devup={}, devdn={}",
2441                        test, val, bits, row, col, idx, band_name,
2442                        params.period.unwrap_or(20),
2443                        params.devup.unwrap_or(2.0),
2444                        params.devdn.unwrap_or(2.0)
2445                    );
2446                }
2447
2448                if bits == 0x22222222_22222222 {
2449                    panic!(
2450                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (flat index {}) in {} band. Params: period={}, devup={}, devdn={}",
2451                        test, val, bits, row, col, idx, band_name,
2452                        params.period.unwrap_or(20),
2453                        params.devup.unwrap_or(2.0),
2454                        params.devdn.unwrap_or(2.0)
2455                    );
2456                }
2457
2458                if bits == 0x33333333_33333333 {
2459                    panic!(
2460                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (flat index {}) in {} band. Params: period={}, devup={}, devdn={}",
2461                        test, val, bits, row, col, idx, band_name,
2462                        params.period.unwrap_or(20),
2463                        params.devup.unwrap_or(2.0),
2464                        params.devdn.unwrap_or(2.0)
2465                    );
2466                }
2467            }
2468        }
2469
2470        Ok(())
2471    }
2472
2473    #[cfg(not(debug_assertions))]
2474    fn check_batch_no_poison(
2475        _test: &str,
2476        _kernel: Kernel,
2477    ) -> Result<(), Box<dyn std::error::Error>> {
2478        Ok(())
2479    }
2480
2481    macro_rules! gen_batch_tests {
2482        ($fn_name:ident) => {
2483            paste::paste! {
2484                #[test] fn [<$fn_name _scalar>]()      {
2485                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2486                }
2487                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2488                #[test] fn [<$fn_name _avx2>]()        {
2489                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2490                }
2491                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2492                #[test] fn [<$fn_name _avx512>]()      {
2493                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2494                }
2495                #[test] fn [<$fn_name _auto_detect>]() {
2496                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2497                }
2498            }
2499        };
2500    }
2501    gen_batch_tests!(check_batch_default_row);
2502    gen_batch_tests!(check_batch_no_poison);
2503}
2504
2505#[cfg(feature = "python")]
2506use crate::utilities::kernel_validation::validate_kernel;
2507#[cfg(feature = "python")]
2508use numpy::{IntoPyArray, PyArray1};
2509#[cfg(feature = "python")]
2510use pyo3::exceptions::PyValueError;
2511#[cfg(feature = "python")]
2512use pyo3::prelude::*;
2513#[cfg(feature = "python")]
2514use pyo3::types::{PyDict, PyList};
2515
2516#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2517use serde::{Deserialize, Serialize};
2518#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2519use wasm_bindgen::prelude::*;
2520
2521#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2522#[wasm_bindgen]
2523pub struct BollingerJsResult {
2524    values: Vec<f64>,
2525    rows: usize,
2526    cols: usize,
2527}
2528
2529#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2530#[wasm_bindgen]
2531impl BollingerJsResult {
2532    #[wasm_bindgen(getter)]
2533    pub fn values(&self) -> Vec<f64> {
2534        self.values.clone()
2535    }
2536
2537    #[wasm_bindgen(getter)]
2538    pub fn rows(&self) -> usize {
2539        self.rows
2540    }
2541
2542    #[wasm_bindgen(getter)]
2543    pub fn cols(&self) -> usize {
2544        self.cols
2545    }
2546}
2547
2548#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2549#[derive(serde::Serialize, serde::Deserialize)]
2550pub struct BollingerBatchJsOutput {
2551    pub upper: Vec<f64>,
2552    pub middle: Vec<f64>,
2553    pub lower: Vec<f64>,
2554    pub combos: Vec<BollingerBandsParams>,
2555    pub rows: usize,
2556    pub cols: usize,
2557}
2558
2559#[cfg(feature = "python")]
2560#[pyfunction(name = "bollinger_bands")]
2561#[pyo3(signature = (data, period=20, devup=2.0, devdn=2.0, matype="sma", devtype=0, kernel=None))]
2562pub fn bollinger_bands_py<'py>(
2563    py: Python<'py>,
2564    data: numpy::PyReadonlyArray1<'py, f64>,
2565    period: usize,
2566    devup: f64,
2567    devdn: f64,
2568    matype: &str,
2569    devtype: usize,
2570    kernel: Option<&str>,
2571) -> PyResult<(
2572    Bound<'py, numpy::PyArray1<f64>>,
2573    Bound<'py, numpy::PyArray1<f64>>,
2574    Bound<'py, numpy::PyArray1<f64>>,
2575)> {
2576    use numpy::PyArrayMethods;
2577
2578    let slice_in = data.as_slice()?;
2579    let n = slice_in.len();
2580
2581    let out_u = unsafe { numpy::PyArray1::<f64>::new(py, [n], false) };
2582    let out_m = unsafe { numpy::PyArray1::<f64>::new(py, [n], false) };
2583    let out_l = unsafe { numpy::PyArray1::<f64>::new(py, [n], false) };
2584
2585    let kern = validate_kernel(kernel, false)?;
2586    let input = BollingerBandsInput::from_slice(
2587        slice_in,
2588        BollingerBandsParams {
2589            period: Some(period),
2590            devup: Some(devup),
2591            devdn: Some(devdn),
2592            matype: Some(matype.to_string()),
2593            devtype: Some(devtype),
2594        },
2595    );
2596
2597    {
2598        let u = unsafe { out_u.as_slice_mut()? };
2599        let m = unsafe { out_m.as_slice_mut()? };
2600        let l = unsafe { out_l.as_slice_mut()? };
2601        py.allow_threads(|| bollinger_bands_into_slices(u, m, l, &input, kern))
2602            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2603    }
2604    Ok((out_u, out_m, out_l))
2605}
2606
2607#[cfg(feature = "python")]
2608#[pyclass(name = "BollingerBandsStream")]
2609pub struct BollingerBandsStreamPy {
2610    stream: BollingerBandsStream,
2611}
2612
2613#[cfg(feature = "python")]
2614#[pymethods]
2615impl BollingerBandsStreamPy {
2616    #[new]
2617    #[pyo3(signature = (period=20, devup=2.0, devdn=2.0, matype="sma", devtype=0))]
2618    fn new(period: usize, devup: f64, devdn: f64, matype: &str, devtype: usize) -> PyResult<Self> {
2619        let params = BollingerBandsParams {
2620            period: Some(period),
2621            devup: Some(devup),
2622            devdn: Some(devdn),
2623            matype: Some(matype.to_string()),
2624            devtype: Some(devtype),
2625        };
2626        let stream = BollingerBandsStream::try_new(params)
2627            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2628        Ok(BollingerBandsStreamPy { stream })
2629    }
2630
2631    fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
2632        self.stream.update(value)
2633    }
2634}
2635
2636#[cfg(feature = "python")]
2637#[pyfunction(name = "bollinger_bands_batch")]
2638#[pyo3(signature = (data, period_range=(20, 20, 0), devup_range=(2.0, 2.0, 0.0), devdn_range=(2.0, 2.0, 0.0), matype="sma", devtype_range=(0,0,0), kernel=None))]
2639pub fn bollinger_bands_batch_py<'py>(
2640    py: Python<'py>,
2641    data: numpy::PyReadonlyArray1<'py, f64>,
2642    period_range: (usize, usize, usize),
2643    devup_range: (f64, f64, f64),
2644    devdn_range: (f64, f64, f64),
2645    matype: &str,
2646    devtype_range: (usize, usize, usize),
2647    kernel: Option<&str>,
2648) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
2649    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2650
2651    let slice_in = data.as_slice()?;
2652    let sweep = BollingerBandsBatchRange {
2653        period: period_range,
2654        devup: devup_range,
2655        devdn: devdn_range,
2656        matype: (matype.to_string(), matype.to_string(), 0),
2657        devtype: devtype_range,
2658    };
2659
2660    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2661    let rows = combos.len();
2662    let cols = slice_in.len();
2663
2664    let upper_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2665    let middle_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2666    let lower_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2667
2668    let slice_upper = unsafe { upper_arr.as_slice_mut()? };
2669    let slice_middle = unsafe { middle_arr.as_slice_mut()? };
2670    let slice_lower = unsafe { lower_arr.as_slice_mut()? };
2671
2672    let kern = validate_kernel(kernel, true)?;
2673
2674    let combos_clone = py
2675        .allow_threads(
2676            || -> Result<Vec<BollingerBandsParams>, BollingerBandsError> {
2677                let kernel = match kern {
2678                    Kernel::Auto => detect_best_batch_kernel(),
2679                    k => k,
2680                };
2681                let simd = match kernel {
2682                    Kernel::Avx512Batch => Kernel::Avx512,
2683                    Kernel::Avx2Batch => Kernel::Avx2,
2684                    Kernel::ScalarBatch => Kernel::Scalar,
2685                    _ => unreachable!(),
2686                };
2687
2688                bollinger_bands_batch_inner_into(
2689                    slice_in,
2690                    &sweep,
2691                    simd,
2692                    true,
2693                    slice_upper,
2694                    slice_middle,
2695                    slice_lower,
2696                )
2697            },
2698        )
2699        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2700
2701    let dict = PyDict::new(py);
2702    dict.set_item("upper", upper_arr.reshape((rows, cols))?)?;
2703    dict.set_item("middle", middle_arr.reshape((rows, cols))?)?;
2704    dict.set_item("lower", lower_arr.reshape((rows, cols))?)?;
2705    dict.set_item(
2706        "periods",
2707        combos_clone
2708            .iter()
2709            .map(|p| p.period.unwrap() as u64)
2710            .collect::<Vec<_>>()
2711            .into_pyarray(py),
2712    )?;
2713    dict.set_item(
2714        "devups",
2715        combos_clone
2716            .iter()
2717            .map(|p| p.devup.unwrap())
2718            .collect::<Vec<_>>()
2719            .into_pyarray(py),
2720    )?;
2721    dict.set_item(
2722        "devdns",
2723        combos_clone
2724            .iter()
2725            .map(|p| p.devdn.unwrap())
2726            .collect::<Vec<_>>()
2727            .into_pyarray(py),
2728    )?;
2729    dict.set_item(
2730        "matypes",
2731        combos_clone
2732            .iter()
2733            .map(|p| p.matype.as_ref().unwrap().clone())
2734            .collect::<Vec<_>>(),
2735    )?;
2736    dict.set_item(
2737        "devtypes",
2738        combos_clone
2739            .iter()
2740            .map(|p| p.devtype.unwrap() as u64)
2741            .collect::<Vec<_>>()
2742            .into_pyarray(py),
2743    )?;
2744
2745    Ok(dict)
2746}
2747
2748#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2749#[wasm_bindgen]
2750pub fn bollinger_bands_js(
2751    data: &[f64],
2752    period: usize,
2753    devup: f64,
2754    devdn: f64,
2755    matype: String,
2756    devtype: usize,
2757) -> Result<BollingerJsResult, JsValue> {
2758    let input = BollingerBandsInput::from_slice(
2759        data,
2760        BollingerBandsParams {
2761            period: Some(period),
2762            devup: Some(devup),
2763            devdn: Some(devdn),
2764            matype: Some(matype),
2765            devtype: Some(devtype),
2766        },
2767    );
2768
2769    let mut u = vec![0.0; data.len()];
2770    let mut m = vec![0.0; data.len()];
2771    let mut l = vec![0.0; data.len()];
2772    bollinger_bands_into_slices(&mut u, &mut m, &mut l, &input, Kernel::Auto)
2773        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2774
2775    let mut values = u;
2776    values.extend_from_slice(&m);
2777    values.extend_from_slice(&l);
2778    Ok(BollingerJsResult {
2779        values,
2780        rows: 3,
2781        cols: data.len(),
2782    })
2783}
2784
2785#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2786#[wasm_bindgen]
2787pub fn bollinger_bands_batch_js(
2788    data: &[f64],
2789    period_start: usize,
2790    period_end: usize,
2791    period_step: usize,
2792    devup_start: f64,
2793    devup_end: f64,
2794    devup_step: f64,
2795    devdn_start: f64,
2796    devdn_end: f64,
2797    devdn_step: f64,
2798    matype: &str,
2799    devtype: usize,
2800) -> Result<Vec<f64>, JsValue> {
2801    let sweep = BollingerBandsBatchRange {
2802        period: (period_start, period_end, period_step),
2803        devup: (devup_start, devup_end, devup_step),
2804        devdn: (devdn_start, devdn_end, devdn_step),
2805        matype: (matype.to_string(), matype.to_string(), 0),
2806        devtype: (devtype, devtype, 0),
2807    };
2808
2809    let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2810    let rows = combos.len();
2811    let cols = data.len();
2812
2813    let first = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
2814
2815    let mut upper_mu = make_uninit_matrix(rows, cols);
2816    let mut middle_mu = make_uninit_matrix(rows, cols);
2817    let mut lower_mu = make_uninit_matrix(rows, cols);
2818
2819    let warm: Vec<usize> = combos
2820        .iter()
2821        .map(|c| first + c.period.unwrap() - 1)
2822        .collect();
2823
2824    init_matrix_prefixes(&mut upper_mu, cols, &warm);
2825    init_matrix_prefixes(&mut middle_mu, cols, &warm);
2826    init_matrix_prefixes(&mut lower_mu, cols, &warm);
2827
2828    let mut upper_guard = ManuallyDrop::new(upper_mu);
2829    let mut middle_guard = ManuallyDrop::new(middle_mu);
2830    let mut lower_guard = ManuallyDrop::new(lower_mu);
2831
2832    let upper_vec: &mut [f64] = unsafe {
2833        core::slice::from_raw_parts_mut(upper_guard.as_mut_ptr() as *mut f64, upper_guard.len())
2834    };
2835    let middle_vec: &mut [f64] = unsafe {
2836        core::slice::from_raw_parts_mut(middle_guard.as_mut_ptr() as *mut f64, middle_guard.len())
2837    };
2838    let lower_vec: &mut [f64] = unsafe {
2839        core::slice::from_raw_parts_mut(lower_guard.as_mut_ptr() as *mut f64, lower_guard.len())
2840    };
2841
2842    for (i, combo) in combos.iter().enumerate() {
2843        let row_start = i * cols;
2844        let out_u = &mut upper_vec[row_start..row_start + cols];
2845        let out_m = &mut middle_vec[row_start..row_start + cols];
2846        let out_l = &mut lower_vec[row_start..row_start + cols];
2847
2848        let p = combo.period.unwrap();
2849        let du = combo.devup.unwrap();
2850        let dd = combo.devdn.unwrap();
2851        let mt = combo.matype.as_ref().unwrap();
2852        let dt = combo.devtype.unwrap();
2853
2854        if first + p > 0 {
2855            let nan_end = (first + p - 1).min(cols);
2856            out_u[..nan_end].fill(f64::NAN);
2857            out_m[..nan_end].fill(f64::NAN);
2858            out_l[..nan_end].fill(f64::NAN);
2859        }
2860
2861        bollinger_bands_compute_into(
2862            data,
2863            p,
2864            du,
2865            dd,
2866            mt,
2867            dt,
2868            first,
2869            Kernel::Scalar,
2870            out_u,
2871            out_m,
2872            out_l,
2873        )
2874        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2875    }
2876
2877    let upper_vec = unsafe {
2878        Vec::from_raw_parts(
2879            upper_guard.as_mut_ptr() as *mut f64,
2880            upper_guard.len(),
2881            upper_guard.capacity(),
2882        )
2883    };
2884    let middle_vec = unsafe {
2885        Vec::from_raw_parts(
2886            middle_guard.as_mut_ptr() as *mut f64,
2887            middle_guard.len(),
2888            middle_guard.capacity(),
2889        )
2890    };
2891    let lower_vec = unsafe {
2892        Vec::from_raw_parts(
2893            lower_guard.as_mut_ptr() as *mut f64,
2894            lower_guard.len(),
2895            lower_guard.capacity(),
2896        )
2897    };
2898
2899    let mut result = upper_vec;
2900    result.extend(middle_vec);
2901    result.extend(lower_vec);
2902    Ok(result)
2903}
2904
2905#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2906#[wasm_bindgen]
2907pub fn bollinger_bands_batch_metadata_js(
2908    period_start: usize,
2909    period_end: usize,
2910    period_step: usize,
2911    devup_start: f64,
2912    devup_end: f64,
2913    devup_step: f64,
2914    devdn_start: f64,
2915    devdn_end: f64,
2916    devdn_step: f64,
2917    matype: &str,
2918    devtype: usize,
2919) -> Result<Vec<f64>, JsValue> {
2920    let sweep = BollingerBandsBatchRange {
2921        period: (period_start, period_end, period_step),
2922        devup: (devup_start, devup_end, devup_step),
2923        devdn: (devdn_start, devdn_end, devdn_step),
2924        matype: (matype.to_string(), matype.to_string(), 0),
2925        devtype: (devtype, devtype, 0),
2926    };
2927
2928    let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2929    let mut metadata = Vec::with_capacity(combos.len() * 4);
2930
2931    for combo in combos {
2932        metadata.push(combo.period.unwrap() as f64);
2933        metadata.push(combo.devup.unwrap());
2934        metadata.push(combo.devdn.unwrap());
2935        metadata.push(combo.devtype.unwrap() as f64);
2936    }
2937
2938    Ok(metadata)
2939}
2940
2941#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2942#[derive(Serialize, Deserialize)]
2943pub struct BollingerBandsBatchConfig {
2944    pub period_range: (usize, usize, usize),
2945    pub devup_range: (f64, f64, f64),
2946    pub devdn_range: (f64, f64, f64),
2947    pub matype: String,
2948    pub devtype: usize,
2949}
2950
2951#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2952#[derive(Serialize, Deserialize)]
2953pub struct BollingerBandsBatchJsOutput {
2954    pub upper: Vec<f64>,
2955    pub middle: Vec<f64>,
2956    pub lower: Vec<f64>,
2957    pub combos: Vec<BollingerBandsParams>,
2958    pub rows: usize,
2959    pub cols: usize,
2960}
2961
2962#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2963#[wasm_bindgen(js_name = bollinger_bands_batch)]
2964pub fn bollinger_bands_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2965    let config: BollingerBandsBatchConfig = serde_wasm_bindgen::from_value(config)
2966        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2967
2968    let sweep = BollingerBandsBatchRange {
2969        period: config.period_range,
2970        devup: config.devup_range,
2971        devdn: config.devdn_range,
2972        matype: (config.matype.clone(), config.matype, 0),
2973        devtype: (config.devtype, config.devtype, 0),
2974    };
2975
2976    let output = bollinger_bands_batch_inner(data, &sweep, Kernel::Scalar, false)
2977        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2978
2979    let js_output = BollingerBandsBatchJsOutput {
2980        upper: output.upper,
2981        middle: output.middle,
2982        lower: output.lower,
2983        combos: output.combos,
2984        rows: output.rows,
2985        cols: output.cols,
2986    };
2987
2988    serde_wasm_bindgen::to_value(&js_output)
2989        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2990}
2991
2992#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2993#[wasm_bindgen]
2994pub fn bollinger_bands_alloc(len: usize) -> *mut f64 {
2995    let mut v = Vec::<f64>::with_capacity(len);
2996    let p = v.as_mut_ptr();
2997    std::mem::forget(v);
2998    p
2999}
3000
3001#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3002#[wasm_bindgen]
3003pub fn bollinger_bands_free(ptr: *mut f64, len: usize) {
3004    unsafe {
3005        let _ = Vec::from_raw_parts(ptr, len, len);
3006    }
3007}
3008
3009#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3010#[wasm_bindgen]
3011pub fn bollinger_bands_into(
3012    in_ptr: *const f64,
3013    out_u: *mut f64,
3014    out_m: *mut f64,
3015    out_l: *mut f64,
3016    len: usize,
3017    period: usize,
3018    devup: f64,
3019    devdn: f64,
3020    matype: String,
3021    devtype: usize,
3022) -> Result<(), JsValue> {
3023    if in_ptr.is_null() || out_u.is_null() || out_m.is_null() || out_l.is_null() {
3024        return Err(JsValue::from_str("null pointer"));
3025    }
3026    unsafe {
3027        let data = std::slice::from_raw_parts(in_ptr, len);
3028        let mut u = std::slice::from_raw_parts_mut(out_u, len);
3029        let mut m = std::slice::from_raw_parts_mut(out_m, len);
3030        let mut l = std::slice::from_raw_parts_mut(out_l, len);
3031
3032        let input = BollingerBandsInput::from_slice(
3033            data,
3034            BollingerBandsParams {
3035                period: Some(period),
3036                devup: Some(devup),
3037                devdn: Some(devdn),
3038                matype: Some(matype),
3039                devtype: Some(devtype),
3040            },
3041        );
3042
3043        if in_ptr == out_u as *const f64
3044            || in_ptr == out_m as *const f64
3045            || in_ptr == out_l as *const f64
3046        {
3047            let mut tu = vec![0.0; len];
3048            let mut tm = vec![0.0; len];
3049            let mut tl = vec![0.0; len];
3050            bollinger_bands_into_slices(&mut tu, &mut tm, &mut tl, &input, Kernel::Auto)
3051                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3052            u.copy_from_slice(&tu);
3053            m.copy_from_slice(&tm);
3054            l.copy_from_slice(&tl);
3055        } else {
3056            bollinger_bands_into_slices(&mut u, &mut m, &mut l, &input, Kernel::Auto)
3057                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3058        }
3059    }
3060    Ok(())
3061}
3062
3063#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3064#[wasm_bindgen]
3065pub fn bollinger_bands_batch_into(
3066    in_ptr: *const f64,
3067    upper_ptr: *mut f64,
3068    middle_ptr: *mut f64,
3069    lower_ptr: *mut f64,
3070    len: usize,
3071    period_start: usize,
3072    period_end: usize,
3073    period_step: usize,
3074    devup_start: f64,
3075    devup_end: f64,
3076    devup_step: f64,
3077    devdn_start: f64,
3078    devdn_end: f64,
3079    devdn_step: f64,
3080    matype: &str,
3081    devtype: usize,
3082) -> Result<usize, JsValue> {
3083    if in_ptr.is_null() || upper_ptr.is_null() || middle_ptr.is_null() || lower_ptr.is_null() {
3084        return Err(JsValue::from_str("Null pointer provided"));
3085    }
3086
3087    unsafe {
3088        let data = std::slice::from_raw_parts(in_ptr, len);
3089
3090        let sweep = BollingerBandsBatchRange {
3091            period: (period_start, period_end, period_step),
3092            devup: (devup_start, devup_end, devup_step),
3093            devdn: (devdn_start, devdn_end, devdn_step),
3094            matype: (matype.to_string(), matype.to_string(), 0),
3095            devtype: (devtype, devtype, 0),
3096        };
3097
3098        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
3099        let rows = combos.len();
3100        let cols = len;
3101
3102        let upper_out = std::slice::from_raw_parts_mut(upper_ptr, rows * cols);
3103        let middle_out = std::slice::from_raw_parts_mut(middle_ptr, rows * cols);
3104        let lower_out = std::slice::from_raw_parts_mut(lower_ptr, rows * cols);
3105
3106        bollinger_bands_batch_inner_into(
3107            data,
3108            &sweep,
3109            Kernel::Auto,
3110            false,
3111            upper_out,
3112            middle_out,
3113            lower_out,
3114        )
3115        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3116
3117        Ok(rows)
3118    }
3119}
3120
3121#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3122#[wasm_bindgen(js_name = bb_batch_unified)]
3123pub fn bb_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
3124    #[derive(serde::Deserialize)]
3125    struct Cfg {
3126        period_range: (usize, usize, usize),
3127        devup_range: (f64, f64, f64),
3128        devdn_range: (f64, f64, f64),
3129        matype: String,
3130        devtype_range: (usize, usize, usize),
3131    }
3132    let c: Cfg = serde_wasm_bindgen::from_value(config)
3133        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
3134
3135    let sweep = BollingerBandsBatchRange {
3136        period: c.period_range,
3137        devup: c.devup_range,
3138        devdn: c.devdn_range,
3139        matype: (c.matype.clone(), c.matype, 0),
3140        devtype: c.devtype_range,
3141    };
3142    let out = bollinger_bands_batch_inner(data, &sweep, detect_best_kernel(), false)
3143        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3144
3145    let js = BollingerBatchJsOutput {
3146        upper: out.upper,
3147        middle: out.middle,
3148        lower: out.lower,
3149        combos: out.combos,
3150        rows: out.rows,
3151        cols: out.cols,
3152    };
3153    serde_wasm_bindgen::to_value(&js)
3154        .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
3155}
3156
3157#[cfg(all(feature = "python", feature = "cuda"))]
3158use crate::cuda::bollinger_bands_wrapper::DeviceArrayF32Bb;
3159#[cfg(all(feature = "python", feature = "cuda"))]
3160use crate::cuda::{cuda_available, CudaBollingerBands};
3161#[cfg(all(feature = "python", feature = "cuda"))]
3162use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
3163#[cfg(all(feature = "python", feature = "cuda"))]
3164use numpy::PyReadonlyArray1;
3165#[cfg(all(feature = "python", feature = "cuda"))]
3166use pyo3::{pyfunction, PyResult, Python};
3167#[cfg(all(feature = "python", feature = "cuda"))]
3168#[cfg(all(feature = "python", feature = "cuda"))]
3169#[pyclass(module = "ta_indicators.cuda", unsendable)]
3170pub struct BollingerDeviceArrayF32Py {
3171    pub(crate) inner: DeviceArrayF32Bb,
3172}
3173
3174#[cfg(all(feature = "python", feature = "cuda"))]
3175#[pymethods]
3176impl BollingerDeviceArrayF32Py {
3177    #[getter]
3178    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
3179        let d = PyDict::new(py);
3180        d.set_item("shape", (self.inner.rows, self.inner.cols))?;
3181        d.set_item("typestr", "<f4")?;
3182        d.set_item(
3183            "strides",
3184            (
3185                self.inner.cols * std::mem::size_of::<f32>(),
3186                std::mem::size_of::<f32>(),
3187            ),
3188        )?;
3189        d.set_item("data", (self.inner.device_ptr() as usize, false))?;
3190
3191        d.set_item("version", 3)?;
3192        Ok(d)
3193    }
3194
3195    fn __dlpack_device__(&self) -> (i32, i32) {
3196        (2, self.inner.device_id as i32)
3197    }
3198
3199    #[pyo3(signature=(stream=None, max_version=None, dl_device=None, copy=None))]
3200    fn __dlpack__<'py>(
3201        &mut self,
3202        py: Python<'py>,
3203        stream: Option<pyo3::PyObject>,
3204        max_version: Option<pyo3::PyObject>,
3205        dl_device: Option<pyo3::PyObject>,
3206        copy: Option<pyo3::PyObject>,
3207    ) -> PyResult<PyObject> {
3208        let (kdl, alloc_dev) = self.__dlpack_device__();
3209        if let Some(dev_obj) = dl_device.as_ref() {
3210            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
3211                if dev_ty != kdl || dev_id != alloc_dev {
3212                    let wants_copy = copy
3213                        .as_ref()
3214                        .and_then(|c| c.extract::<bool>(py).ok())
3215                        .unwrap_or(false);
3216                    if wants_copy {
3217                        return Err(PyValueError::new_err(
3218                            "device copy not implemented for __dlpack__",
3219                        ));
3220                    } else {
3221                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
3222                    }
3223                }
3224            }
3225        }
3226        let _ = stream;
3227
3228        let dummy = cust::memory::DeviceBuffer::from_slice(&[])
3229            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3230        let ctx_clone = self.inner.ctx.clone();
3231        let dev_id = self.inner.device_id;
3232        let inner = std::mem::replace(
3233            &mut self.inner,
3234            DeviceArrayF32Bb {
3235                buf: dummy,
3236                rows: 0,
3237                cols: 0,
3238                ctx: ctx_clone,
3239                device_id: dev_id,
3240            },
3241        );
3242
3243        let rows = inner.rows;
3244        let cols = inner.cols;
3245        let buf = inner.buf;
3246
3247        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
3248
3249        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
3250    }
3251}
3252
3253#[cfg(all(feature = "python", feature = "cuda"))]
3254#[pyfunction(name = "bollinger_bands_cuda_batch_dev")]
3255#[pyo3(signature = (data_f32, period_range, devup_range, devdn_range, device_id=0))]
3256pub fn bollinger_bands_cuda_batch_dev_py(
3257    py: Python<'_>,
3258    data_f32: PyReadonlyArray1<'_, f32>,
3259    period_range: (usize, usize, usize),
3260    devup_range: (f64, f64, f64),
3261    devdn_range: (f64, f64, f64),
3262    device_id: usize,
3263) -> PyResult<(
3264    BollingerDeviceArrayF32Py,
3265    BollingerDeviceArrayF32Py,
3266    BollingerDeviceArrayF32Py,
3267)> {
3268    if !cuda_available() {
3269        return Err(PyValueError::new_err("CUDA not available"));
3270    }
3271    let slice = data_f32.as_slice()?;
3272    let sweep = BollingerBandsBatchRange {
3273        period: period_range,
3274        devup: devup_range,
3275        devdn: devdn_range,
3276        matype: ("sma".to_string(), "sma".to_string(), 0),
3277        devtype: (0, 0, 0),
3278    };
3279    let (up, mid, lo) = py.allow_threads(|| {
3280        let cuda =
3281            CudaBollingerBands::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3282        cuda.bollinger_bands_batch_dev(slice, &sweep)
3283            .map_err(|e| PyValueError::new_err(e.to_string()))
3284    })?;
3285    Ok((
3286        BollingerDeviceArrayF32Py { inner: up },
3287        BollingerDeviceArrayF32Py { inner: mid },
3288        BollingerDeviceArrayF32Py { inner: lo },
3289    ))
3290}
3291
3292#[cfg(all(feature = "python", feature = "cuda"))]
3293#[pyfunction(name = "bollinger_bands_cuda_many_series_one_param_dev")]
3294#[pyo3(signature = (prices_tm_f32, cols, rows, period, devup, devdn, device_id=0))]
3295pub fn bollinger_bands_cuda_many_series_one_param_dev_py(
3296    py: Python<'_>,
3297    prices_tm_f32: PyReadonlyArray1<'_, f32>,
3298    cols: usize,
3299    rows: usize,
3300    period: usize,
3301    devup: f32,
3302    devdn: f32,
3303    device_id: usize,
3304) -> PyResult<(
3305    BollingerDeviceArrayF32Py,
3306    BollingerDeviceArrayF32Py,
3307    BollingerDeviceArrayF32Py,
3308)> {
3309    if !cuda_available() {
3310        return Err(PyValueError::new_err("CUDA not available"));
3311    }
3312    let tm = prices_tm_f32.as_slice()?;
3313    let (up, mid, lo) = py.allow_threads(|| {
3314        let cuda =
3315            CudaBollingerBands::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3316        cuda.bollinger_bands_many_series_one_param_time_major_dev(
3317            tm, cols, rows, period, devup, devdn,
3318        )
3319        .map_err(|e| PyValueError::new_err(e.to_string()))
3320    })?;
3321    Ok((
3322        BollingerDeviceArrayF32Py { inner: up },
3323        BollingerDeviceArrayF32Py { inner: mid },
3324        BollingerDeviceArrayF32Py { inner: lo },
3325    ))
3326}