Skip to main content

quantwave_core/indicators/incremental/
bbands.rs

1//! Native O(1) Bollinger Bands (SMA middle) — TA-Lib parity.
2
3use crate::indicators::incremental::utils::RingBuffer;
4use crate::traits::Next;
5use talib_rs::MaType;
6
7const NAN_TRIPLE: (f64, f64, f64) = (f64::NAN, f64::NAN, f64::NAN);
8
9/// Bollinger Bands — matches `talib_rs::overlap::bbands` (SMA path O(1); other `matype` via batch on history).
10#[derive(Debug, Clone)]
11#[allow(non_camel_case_types)]
12pub struct BBANDS {
13    pub timeperiod: usize,
14    pub nbdevup: f64,
15    pub nbdevdn: f64,
16    pub matype: MaType,
17    window: RingBuffer<f64>,
18    sum: f64,
19    sum_sq: f64,
20    history: Vec<f64>,
21}
22
23impl BBANDS {
24    pub fn new(timeperiod: usize, nbdevup: f64, nbdevdn: f64, matype: MaType) -> Self {
25        Self {
26            timeperiod,
27            nbdevup,
28            nbdevdn,
29            matype,
30            window: RingBuffer::with_capacity(timeperiod.max(1)),
31            sum: 0.0,
32            sum_sq: 0.0,
33            history: Vec::new(),
34        }
35    }
36
37    #[inline]
38    fn bands_from_sums(&self) -> (f64, f64, f64) {
39        let n = self.timeperiod as f64;
40        let inv_n = 1.0 / n;
41        let ma_val = self.sum * inv_n;
42        let variance = self.sum_sq * inv_n - ma_val * ma_val;
43        let stddev = variance.max(0.0).sqrt();
44        let upper = ma_val + self.nbdevup * stddev;
45        let lower = ma_val - self.nbdevdn * stddev;
46        (upper, ma_val, lower)
47    }
48
49    fn next_sma(&mut self, input: f64) -> (f64, f64, f64) {
50        let tp = self.timeperiod;
51        if tp == 0 {
52            return NAN_TRIPLE;
53        }
54
55        if self.window.len() == tp {
56            if let Some(old) = self.window.pop_front() {
57                self.sum -= old;
58                self.sum_sq -= old * old;
59            }
60        }
61
62        self.window.push_back(input);
63        self.sum += input;
64        self.sum_sq += input * input;
65
66        if self.window.len() < tp {
67            return NAN_TRIPLE;
68        }
69
70        self.bands_from_sums()
71    }
72
73    fn next_fallback(&mut self, input: f64) -> (f64, f64, f64) {
74        self.history.push(input);
75        let (u, m, l) = talib_rs::overlap::bbands(
76            &self.history,
77            self.timeperiod,
78            self.nbdevup,
79            self.nbdevdn,
80            self.matype,
81        )
82        .unwrap_or_else(|_| {
83            let n = self.history.len();
84            (vec![f64::NAN; n], vec![f64::NAN; n], vec![f64::NAN; n])
85        });
86        (
87            *u.last().unwrap_or(&f64::NAN),
88            *m.last().unwrap_or(&f64::NAN),
89            *l.last().unwrap_or(&f64::NAN),
90        )
91    }
92}
93
94impl Next<f64> for BBANDS {
95    type Output = (f64, f64, f64);
96
97    fn next(&mut self, input: f64) -> Self::Output {
98        if self.matype == MaType::Sma {
99            self.next_sma(input)
100        } else {
101            self.next_fallback(input)
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use proptest::prelude::*;
110
111    proptest! {
112        #[test]
113        fn test_bbands_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
114            let period = 10;
115            let nbdevup = 2.0;
116            let nbdevdn = 2.0;
117            let matype = MaType::Sma;
118            let mut bbands = BBANDS::new(period, nbdevup, nbdevdn, matype);
119            let streaming_results: Vec<(f64, f64, f64)> =
120                input.iter().map(|&x| bbands.next(x)).collect();
121            let (b_upper, b_middle, b_lower) = talib_rs::overlap::bbands(
122                &input,
123                period,
124                nbdevup,
125                nbdevdn,
126                matype,
127            )
128            .unwrap_or_else(|_| {
129                (
130                    vec![f64::NAN; input.len()],
131                    vec![f64::NAN; input.len()],
132                    vec![f64::NAN; input.len()],
133                )
134            });
135
136            for (i, (s_upper, s_middle, s_lower)) in streaming_results.into_iter().enumerate() {
137                if s_upper.is_nan() {
138                    assert!(b_upper[i].is_nan());
139                } else {
140                    approx::assert_relative_eq!(s_upper, b_upper[i], epsilon = 1e-6);
141                }
142                if s_middle.is_nan() {
143                    assert!(b_middle[i].is_nan());
144                } else {
145                    approx::assert_relative_eq!(s_middle, b_middle[i], epsilon = 1e-6);
146                }
147                if s_lower.is_nan() {
148                    assert!(b_lower[i].is_nan());
149                } else {
150                    approx::assert_relative_eq!(s_lower, b_lower[i], epsilon = 1e-6);
151                }
152            }
153        }
154    }
155}