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