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