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}