1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct BollingerOutput {
11 pub upper: f64,
13 pub middle: f64,
15 pub lower: f64,
17 pub stddev: f64,
20}
21
22#[derive(Debug, Clone)]
49pub struct BollingerBands {
50 period: usize,
51 multiplier: f64,
52 window: VecDeque<f64>,
53 sum: f64,
54 sum_sq: f64,
55 updates_since_recompute: usize,
58}
59
60const RECOMPUTE_EVERY: usize = 16;
65
66impl BollingerBands {
67 pub fn new(period: usize, multiplier: f64) -> Result<Self> {
74 if period == 0 {
75 return Err(Error::PeriodZero);
76 }
77 if !multiplier.is_finite() || multiplier <= 0.0 {
78 return Err(Error::NonPositiveMultiplier);
79 }
80 Ok(Self {
81 period,
82 multiplier,
83 window: VecDeque::with_capacity(period),
84 sum: 0.0,
85 sum_sq: 0.0,
86 updates_since_recompute: 0,
87 })
88 }
89
90 pub fn classic() -> Self {
92 Self::new(20, 2.0).expect("classic Bollinger parameters are valid")
93 }
94
95 pub const fn period(&self) -> usize {
97 self.period
98 }
99
100 pub const fn multiplier(&self) -> f64 {
102 self.multiplier
103 }
104
105 fn current(&self) -> Option<BollingerOutput> {
106 if self.window.len() != self.period {
107 return None;
108 }
109 let n = self.period as f64;
110 let mean = self.sum / n;
111 let var = (self.sum_sq / n - mean * mean).max(0.0);
114 let stddev = var.sqrt();
115 Some(BollingerOutput {
116 upper: mean + self.multiplier * stddev,
117 middle: mean,
118 lower: mean - self.multiplier * stddev,
119 stddev,
120 })
121 }
122}
123
124impl Indicator for BollingerBands {
125 type Input = f64;
126 type Output = BollingerOutput;
127
128 fn update(&mut self, input: f64) -> Option<BollingerOutput> {
129 if !input.is_finite() {
130 return self.current();
131 }
132 if self.window.len() == self.period {
133 let old = self.window.pop_front().expect("non-empty");
134 self.sum -= old;
135 self.sum_sq -= old * old;
136 }
137 self.window.push_back(input);
138 self.sum += input;
139 self.sum_sq += input * input;
140 self.updates_since_recompute += 1;
141 if self.updates_since_recompute >= RECOMPUTE_EVERY * self.period {
142 self.sum = self.window.iter().copied().sum();
143 self.sum_sq = self.window.iter().copied().map(|x| x * x).sum();
144 self.updates_since_recompute = 0;
145 }
146 self.current()
147 }
148
149 fn reset(&mut self) {
150 self.window.clear();
151 self.sum = 0.0;
152 self.sum_sq = 0.0;
153 self.updates_since_recompute = 0;
154 }
155
156 fn warmup_period(&self) -> usize {
157 self.period
158 }
159
160 fn is_ready(&self) -> bool {
161 self.window.len() == self.period
162 }
163
164 fn name(&self) -> &'static str {
165 "BollingerBands"
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::traits::BatchExt;
173 use approx::assert_relative_eq;
174
175 fn naive(prices: &[f64], period: usize, mult: f64) -> BollingerOutput {
176 assert!(
177 prices.len() >= period,
178 "naive requires at least `period` prices"
179 );
180 let w = &prices[prices.len() - period..];
181 let mean = w.iter().sum::<f64>() / period as f64;
182 let var = w.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / period as f64;
183 let s = var.sqrt();
184 BollingerOutput {
185 upper: mean + mult * s,
186 middle: mean,
187 lower: mean - mult * s,
188 stddev: s,
189 }
190 }
191
192 #[test]
193 fn rejects_zero_period() {
194 assert!(matches!(
195 BollingerBands::new(0, 2.0),
196 Err(Error::PeriodZero)
197 ));
198 }
199
200 #[test]
201 fn rejects_non_positive_multiplier() {
202 assert!(matches!(
203 BollingerBands::new(20, 0.0),
204 Err(Error::NonPositiveMultiplier)
205 ));
206 assert!(matches!(
207 BollingerBands::new(20, -1.0),
208 Err(Error::NonPositiveMultiplier)
209 ));
210 assert!(matches!(
211 BollingerBands::new(20, f64::NAN),
212 Err(Error::NonPositiveMultiplier)
213 ));
214 }
215
216 #[test]
222 fn classic_and_accessors_and_metadata() {
223 let bb = BollingerBands::classic();
224 assert_eq!(bb.period(), 20);
225 assert_relative_eq!(bb.multiplier(), 2.0, epsilon = 1e-12);
226 assert_eq!(bb.warmup_period(), 20);
227 assert_eq!(bb.name(), "BollingerBands");
228 }
229
230 #[test]
231 fn warmup_returns_none() {
232 let mut bb = BollingerBands::new(5, 2.0).unwrap();
233 for v in [1.0, 2.0, 3.0, 4.0] {
234 assert!(bb.update(v).is_none());
235 }
236 assert!(bb.update(5.0).is_some());
237 }
238
239 #[test]
240 fn constant_series_yields_zero_stddev() {
241 let mut bb = BollingerBands::new(10, 2.0).unwrap();
242 let out = bb.batch(&[5.0_f64; 30]);
243 let last = out.iter().rev().flatten().next().unwrap();
244 assert_relative_eq!(last.middle, 5.0, epsilon = 1e-12);
245 assert_relative_eq!(last.stddev, 0.0, epsilon = 1e-12);
246 assert_relative_eq!(last.upper, 5.0, epsilon = 1e-12);
247 assert_relative_eq!(last.lower, 5.0, epsilon = 1e-12);
248 }
249
250 #[test]
251 fn matches_naive_definition() {
252 let prices: Vec<f64> = (1..=60)
253 .map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
254 .collect();
255 let mut bb = BollingerBands::new(20, 2.0).unwrap();
256 let out = bb.batch(&prices);
257 for i in 19..prices.len() {
258 let got = out[i].unwrap();
259 let want = naive(&prices[..=i], 20, 2.0);
260 assert_relative_eq!(got.middle, want.middle, epsilon = 1e-9);
261 assert_relative_eq!(got.stddev, want.stddev, epsilon = 1e-9);
262 assert_relative_eq!(got.upper, want.upper, epsilon = 1e-9);
263 assert_relative_eq!(got.lower, want.lower, epsilon = 1e-9);
264 }
265 }
266
267 #[test]
268 fn upper_above_middle_above_lower() {
269 let prices: Vec<f64> = (1..=100).map(f64::from).collect();
270 let mut bb = BollingerBands::new(20, 2.0).unwrap();
271 for o in bb.batch(&prices).into_iter().flatten() {
272 assert!(o.upper >= o.middle);
273 assert!(o.middle >= o.lower);
274 }
275 }
276
277 #[test]
278 fn batch_equals_streaming() {
279 let prices: Vec<f64> = (1..=50).map(|i| f64::from(i) * 0.7).collect();
280 let mut a = BollingerBands::new(10, 2.0).unwrap();
281 let mut b = BollingerBands::new(10, 2.0).unwrap();
282 assert_eq!(
283 a.batch(&prices),
284 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
285 );
286 }
287
288 #[test]
289 fn reset_clears_state() {
290 let mut bb = BollingerBands::new(5, 2.0).unwrap();
291 bb.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
292 assert!(bb.is_ready());
293 bb.reset();
294 assert!(!bb.is_ready());
295 }
296
297 #[test]
303 fn long_stream_drift_stays_bounded() {
304 let period = 20;
305 let mult = 2.0;
306 let mut bb = BollingerBands::new(period, mult).unwrap();
307 let mut window: VecDeque<f64> = VecDeque::with_capacity(period);
308 let n_updates = 16 * period * 5;
310 let mut last = None;
311 for i in 0..n_updates {
312 let v = if i % 2 == 0 { 1e6 } else { 1.0 };
313 last = bb.update(v);
314 if window.len() == period {
315 window.pop_front();
316 }
317 window.push_back(v);
318 }
319 let scratch = naive(&window.iter().copied().collect::<Vec<_>>(), period, mult);
320 let got = last.expect("warmed up");
321 assert!(
322 (got.middle - scratch.middle).abs() < 1e-3,
323 "middle drift: got={}, scratch={}",
324 got.middle,
325 scratch.middle,
326 );
327 assert!(
328 (got.stddev - scratch.stddev).abs() < 1e-3,
329 "stddev drift: got={}, scratch={}",
330 got.stddev,
331 scratch.stddev,
332 );
333 }
334
335 #[test]
336 fn ignores_non_finite_input() {
337 let mut bb = BollingerBands::new(5, 2.0).unwrap();
338 let ready = bb.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
339 let last = ready.last().unwrap().unwrap();
340 assert_eq!(bb.update(f64::NAN).unwrap(), last);
342 assert_eq!(bb.update(f64::INFINITY).unwrap(), last);
343 let after = bb.update(6.0).unwrap();
345 assert_relative_eq!(
346 after.middle,
347 (2.0 + 3.0 + 4.0 + 5.0 + 6.0) / 5.0,
348 epsilon = 1e-12
349 );
350 }
351}