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 if !a.is_finite() || !b.is_finite() {
120 return None;
121 }
122 let spread = a - b;
123 if self.window.len() == self.period {
124 let old = self.window.pop_front().expect("non-empty");
125 self.sum -= old;
126 self.sum_sq -= old * old;
127 }
128 self.window.push_back(spread);
129 self.sum += spread;
130 self.sum_sq += spread * spread;
131 if self.window.len() < self.period {
132 return None;
133 }
134 let n = self.period as f64;
135 let middle = self.sum / n;
136 let variance = (self.sum_sq / n - middle * middle).max(0.0);
137 let sigma = variance.sqrt();
138 let half_width = self.num_std * sigma;
139 let upper = middle + half_width;
140 let lower = middle - half_width;
141 let percent_b = if half_width == 0.0 {
142 0.5
143 } else {
144 (spread - lower) / (upper - lower)
145 };
146 Some(SpreadBollingerBandsOutput {
147 middle,
148 upper,
149 lower,
150 percent_b,
151 })
152 }
153
154 fn reset(&mut self) {
155 self.window.clear();
156 self.sum = 0.0;
157 self.sum_sq = 0.0;
158 }
159
160 fn warmup_period(&self) -> usize {
161 self.period
162 }
163
164 fn is_ready(&self) -> bool {
165 self.window.len() == self.period
166 }
167
168 fn name(&self) -> &'static str {
169 "SpreadBollingerBands"
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::traits::BatchExt;
177 use approx::assert_relative_eq;
178
179 #[test]
180 fn rejects_bad_parameters() {
181 assert!(SpreadBollingerBands::new(1, 2.0).is_err());
182 assert!(SpreadBollingerBands::new(20, 0.0).is_err());
183 assert!(SpreadBollingerBands::new(20, -1.0).is_err());
184 assert!(SpreadBollingerBands::new(20, f64::NAN).is_err());
185 assert!(SpreadBollingerBands::new(2, 2.0).is_ok());
186 }
187
188 #[test]
189 fn accessors_and_metadata() {
190 let bb = SpreadBollingerBands::new(20, 2.5).unwrap();
191 assert_eq!(bb.period(), 20);
192 assert_eq!(bb.num_std(), 2.5);
193 assert_eq!(bb.warmup_period(), 20);
194 assert_eq!(bb.name(), "SpreadBollingerBands");
195 assert!(!bb.is_ready());
196 }
197
198 #[test]
199 fn warmup_returns_none() {
200 let mut bb = SpreadBollingerBands::new(3, 2.0).unwrap();
201 assert_eq!(bb.update((1.0, 0.0)), None);
202 assert_eq!(bb.update((2.0, 0.0)), None);
203 assert!(bb.update((3.0, 0.0)).is_some());
204 assert!(bb.is_ready());
205 }
206
207 #[test]
208 fn hand_computed_value() {
209 let pairs = [(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)];
213 let out = SpreadBollingerBands::new(4, 2.0)
214 .unwrap()
215 .batch(&pairs)
216 .into_iter()
217 .flatten()
218 .last()
219 .unwrap();
220 assert_relative_eq!(out.middle, 2.5, epsilon = 1e-9);
221 assert_relative_eq!(out.upper, 4.736_067_977_499_79, epsilon = 1e-9);
222 assert_relative_eq!(out.lower, 0.263_932_022_500_21, epsilon = 1e-9);
223 assert_relative_eq!(out.percent_b, 0.835_410_196_624_97, epsilon = 1e-9);
224 }
225
226 #[test]
227 fn flat_spread_collapses_band() {
228 let pairs: Vec<(f64, f64)> = (0..10)
230 .map(|t| (5.0 + f64::from(t), f64::from(t)))
231 .collect();
232 let out = SpreadBollingerBands::new(5, 2.0)
233 .unwrap()
234 .batch(&pairs)
235 .into_iter()
236 .flatten()
237 .last()
238 .unwrap();
239 assert_relative_eq!(out.upper, out.middle, epsilon = 1e-12);
240 assert_relative_eq!(out.lower, out.middle, epsilon = 1e-12);
241 assert_relative_eq!(out.percent_b, 0.5, epsilon = 1e-12);
242 }
243
244 #[test]
245 fn bands_are_ordered() {
246 let pairs: Vec<(f64, f64)> = (0..80)
247 .map(|t| {
248 let b = 100.0 + f64::from(t);
249 (b + 3.0 * (f64::from(t) * 0.4).sin(), b)
250 })
251 .collect();
252 let mut bb = SpreadBollingerBands::new(20, 2.0).unwrap();
253 for out in bb.batch(&pairs).into_iter().flatten() {
254 assert!(out.lower <= out.middle && out.middle <= out.upper);
255 }
256 }
257
258 #[test]
259 fn reset_clears_state() {
260 let mut bb = SpreadBollingerBands::new(4, 2.0).unwrap();
261 bb.batch(&[(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0), (5.0, 0.0)]);
262 assert!(bb.is_ready());
263 bb.reset();
264 assert!(!bb.is_ready());
265 assert_eq!(bb.update((1.0, 0.0)), None);
266 }
267
268 #[test]
269 fn batch_equals_streaming() {
270 let pairs: Vec<(f64, f64)> = (0..60)
271 .map(|t| {
272 let b = 30.0 + 0.7 * f64::from(t);
273 (b + (f64::from(t) * 0.4).sin() * 1.5, b)
274 })
275 .collect();
276 let batch = SpreadBollingerBands::new(15, 2.0).unwrap().batch(&pairs);
277 let mut bb = SpreadBollingerBands::new(15, 2.0).unwrap();
278 let streamed: Vec<_> = pairs.iter().map(|p| bb.update(*p)).collect();
279 assert_eq!(batch, streamed);
280 }
281}