Skip to main content

mantis_ta/indicators/volatility/
bollinger.rs

1use crate::indicators::Indicator;
2use crate::types::{BollingerOutput, Candle};
3use crate::utils::ringbuf::RingBuf;
4
5/// Bollinger Bands over closing prices.
6///
7/// # Examples
8/// ```rust
9/// use mantis_ta::indicators::{Indicator, BollingerBands};
10/// use mantis_ta::types::Candle;
11///
12/// let prices = [1.0, 2.0, 3.0, 4.0];
13/// let candles: Vec<Candle> = prices
14///     .iter()
15///     .enumerate()
16///     .map(|(i, p)| Candle {
17///         timestamp: i as i64,
18///         open: *p,
19///         high: *p,
20///         low: *p,
21///         close: *p,
22///         volume: 0.0,
23///     })
24///     .collect();
25///
26/// let out = BollingerBands::new(3, 2.0).calculate(&candles);
27/// assert!(out.iter().take(2).all(|v| v.is_none()));
28/// assert!(out[2].is_some());
29/// ```
30#[derive(Debug, Clone)]
31pub struct BollingerBands {
32    period: usize,
33    std_mult: f64,
34    window: RingBuf<f64>,
35    sum: f64,
36    sum_sq: f64,
37}
38
39impl BollingerBands {
40    pub fn new(period: usize, std_mult: f64) -> Self {
41        assert!(period > 0, "period must be > 0");
42        assert!(std_mult >= 0.0, "std_mult must be >= 0");
43        Self {
44            period,
45            std_mult,
46            window: RingBuf::new(period, 0.0),
47            sum: 0.0,
48            sum_sq: 0.0,
49        }
50    }
51
52    #[inline]
53    fn update(&mut self, close: f64) -> Option<BollingerOutput> {
54        if let Some(old) = self.window.push(close) {
55            self.sum -= old;
56            self.sum_sq -= old * old;
57        }
58        self.sum += close;
59        self.sum_sq += close * close;
60
61        if self.window.len() < self.period {
62            return None;
63        }
64
65        let mean = self.sum / self.period as f64;
66        let variance = (self.sum_sq / self.period as f64) - mean * mean;
67        let std = variance.max(0.0).sqrt();
68        Some(BollingerOutput {
69            upper: mean + self.std_mult * std,
70            middle: mean,
71            lower: mean - self.std_mult * std,
72        })
73    }
74}
75
76impl Indicator for BollingerBands {
77    type Output = BollingerOutput;
78
79    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
80        self.update(candle.close)
81    }
82
83    fn reset(&mut self) {
84        self.window = RingBuf::new(self.period, 0.0);
85        self.sum = 0.0;
86        self.sum_sq = 0.0;
87    }
88
89    fn warmup_period(&self) -> usize {
90        self.period
91    }
92
93    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
94        Box::new(self.clone())
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn bollinger_emits_after_warmup() {
104        let mut bb = BollingerBands::new(3, 2.0);
105        let prices = [1.0, 2.0, 3.0, 4.0];
106        let candles: Vec<Candle> = prices
107            .iter()
108            .map(|p| Candle {
109                timestamp: 0,
110                open: *p,
111                high: *p,
112                low: *p,
113                close: *p,
114                volume: 0.0,
115            })
116            .collect();
117
118        let outputs: Vec<_> = candles.iter().map(|c| bb.next(c)).collect();
119        assert!(
120            outputs
121                .iter()
122                .take(bb.warmup_period() - 1)
123                .all(|o| o.is_none())
124        );
125        assert!(
126            outputs
127                .iter()
128                .skip(bb.warmup_period() - 1)
129                .any(|o| o.is_some())
130        );
131    }
132}