1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct SpreadBollingerBandsOutput {
11 pub middle: f64,
13 pub upper: f64,
15 pub lower: f64,
17 pub percent_b: f64,
21}
22
23#[derive(Debug, Clone)]
64pub struct SpreadBollingerBands {
65 period: usize,
66 num_std: f64,
67 window: VecDeque<f64>,
68 sum: f64,
69 sum_sq: f64,
70}
71
72impl SpreadBollingerBands {
73 pub fn new(period: usize, num_std: f64) -> Result<Self> {
83 if period < 2 {
84 return Err(Error::InvalidPeriod {
85 message: "spread bollinger bands needs period >= 2",
86 });
87 }
88 if !num_std.is_finite() || num_std <= 0.0 {
89 return Err(Error::InvalidParameter {
90 message: "spread bollinger bands needs num_std > 0",
91 });
92 }
93 Ok(Self {
94 period,
95 num_std,
96 window: VecDeque::with_capacity(period),
97 sum: 0.0,
98 sum_sq: 0.0,
99 })
100 }
101
102 pub const fn period(&self) -> usize {
104 self.period
105 }
106
107 pub const fn num_std(&self) -> f64 {
109 self.num_std
110 }
111}
112
113impl Indicator for SpreadBollingerBands {
114 type Input = (f64, f64);
115 type Output = SpreadBollingerBandsOutput;
116
117 fn update(&mut self, input: (f64, f64)) -> Option<SpreadBollingerBandsOutput> {
118 let (a, b) = input;
119 let spread = a - b;
120 if self.window.len() == self.period {
121 let old = self.window.pop_front().expect("non-empty");
122 self.sum -= old;
123 self.sum_sq -= old * old;
124 }
125 self.window.push_back(spread);
126 self.sum += spread;
127 self.sum_sq += spread * spread;
128 if self.window.len() < self.period {
129 return None;
130 }
131 let n = self.period as f64;
132 let middle = self.sum / n;
133 let variance = (self.sum_sq / n - middle * middle).max(0.0);
134 let sigma = variance.sqrt();
135 let half_width = self.num_std * sigma;
136 let upper = middle + half_width;
137 let lower = middle - half_width;
138 let percent_b = if half_width == 0.0 {
139 0.5
140 } else {
141 (spread - lower) / (upper - lower)
142 };
143 Some(SpreadBollingerBandsOutput {
144 middle,
145 upper,
146 lower,
147 percent_b,
148 })
149 }
150
151 fn reset(&mut self) {
152 self.window.clear();
153 self.sum = 0.0;
154 self.sum_sq = 0.0;
155 }
156
157 fn warmup_period(&self) -> usize {
158 self.period
159 }
160
161 fn is_ready(&self) -> bool {
162 self.window.len() == self.period
163 }
164
165 fn name(&self) -> &'static str {
166 "SpreadBollingerBands"
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::traits::BatchExt;
174 use approx::assert_relative_eq;
175
176 #[test]
177 fn rejects_bad_parameters() {
178 assert!(SpreadBollingerBands::new(1, 2.0).is_err());
179 assert!(SpreadBollingerBands::new(20, 0.0).is_err());
180 assert!(SpreadBollingerBands::new(20, -1.0).is_err());
181 assert!(SpreadBollingerBands::new(20, f64::NAN).is_err());
182 assert!(SpreadBollingerBands::new(2, 2.0).is_ok());
183 }
184
185 #[test]
186 fn accessors_and_metadata() {
187 let bb = SpreadBollingerBands::new(20, 2.5).unwrap();
188 assert_eq!(bb.period(), 20);
189 assert_eq!(bb.num_std(), 2.5);
190 assert_eq!(bb.warmup_period(), 20);
191 assert_eq!(bb.name(), "SpreadBollingerBands");
192 assert!(!bb.is_ready());
193 }
194
195 #[test]
196 fn warmup_returns_none() {
197 let mut bb = SpreadBollingerBands::new(3, 2.0).unwrap();
198 assert_eq!(bb.update((1.0, 0.0)), None);
199 assert_eq!(bb.update((2.0, 0.0)), None);
200 assert!(bb.update((3.0, 0.0)).is_some());
201 assert!(bb.is_ready());
202 }
203
204 #[test]
205 fn hand_computed_value() {
206 let pairs = [(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)];
210 let out = SpreadBollingerBands::new(4, 2.0)
211 .unwrap()
212 .batch(&pairs)
213 .into_iter()
214 .flatten()
215 .last()
216 .unwrap();
217 assert_relative_eq!(out.middle, 2.5, epsilon = 1e-9);
218 assert_relative_eq!(out.upper, 4.736_067_977_499_79, epsilon = 1e-9);
219 assert_relative_eq!(out.lower, 0.263_932_022_500_21, epsilon = 1e-9);
220 assert_relative_eq!(out.percent_b, 0.835_410_196_624_97, epsilon = 1e-9);
221 }
222
223 #[test]
224 fn flat_spread_collapses_band() {
225 let pairs: Vec<(f64, f64)> = (0..10)
227 .map(|t| (5.0 + f64::from(t), f64::from(t)))
228 .collect();
229 let out = SpreadBollingerBands::new(5, 2.0)
230 .unwrap()
231 .batch(&pairs)
232 .into_iter()
233 .flatten()
234 .last()
235 .unwrap();
236 assert_relative_eq!(out.upper, out.middle, epsilon = 1e-12);
237 assert_relative_eq!(out.lower, out.middle, epsilon = 1e-12);
238 assert_relative_eq!(out.percent_b, 0.5, epsilon = 1e-12);
239 }
240
241 #[test]
242 fn bands_are_ordered() {
243 let pairs: Vec<(f64, f64)> = (0..80)
244 .map(|t| {
245 let b = 100.0 + f64::from(t);
246 (b + 3.0 * (f64::from(t) * 0.4).sin(), b)
247 })
248 .collect();
249 let mut bb = SpreadBollingerBands::new(20, 2.0).unwrap();
250 for out in bb.batch(&pairs).into_iter().flatten() {
251 assert!(out.lower <= out.middle && out.middle <= out.upper);
252 }
253 }
254
255 #[test]
256 fn reset_clears_state() {
257 let mut bb = SpreadBollingerBands::new(4, 2.0).unwrap();
258 bb.batch(&[(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0), (5.0, 0.0)]);
259 assert!(bb.is_ready());
260 bb.reset();
261 assert!(!bb.is_ready());
262 assert_eq!(bb.update((1.0, 0.0)), None);
263 }
264
265 #[test]
266 fn batch_equals_streaming() {
267 let pairs: Vec<(f64, f64)> = (0..60)
268 .map(|t| {
269 let b = 30.0 + 0.7 * f64::from(t);
270 (b + (f64::from(t) * 0.4).sin() * 1.5, b)
271 })
272 .collect();
273 let batch = SpreadBollingerBands::new(15, 2.0).unwrap().batch(&pairs);
274 let mut bb = SpreadBollingerBands::new(15, 2.0).unwrap();
275 let streamed: Vec<_> = pairs.iter().map(|p| bb.update(*p)).collect();
276 assert_eq!(batch, streamed);
277 }
278}