mantis_ta/indicators/volatility/
bollinger.rs1use crate::indicators::Indicator;
2use crate::types::{BollingerOutput, Candle};
3use crate::utils::ringbuf::RingBuf;
4
5#[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}