1use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::indicators::rolling_quantile::quantile_sorted;
8use crate::indicators::sorted_window;
9use crate::traits::Indicator;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct BomarBandsOutput {
14 pub upper: f64,
16 pub middle: f64,
18 pub lower: f64,
20}
21
22#[derive(Debug, Clone)]
61pub struct BomarBands {
62 period: usize,
63 coverage: f64,
64 window: VecDeque<f64>,
65 sorted: Vec<f64>,
68 scratch: Vec<f64>,
70}
71
72const MERGE_FROM: usize = 32;
77
78impl BomarBands {
79 pub fn new(period: usize, coverage: f64) -> Result<Self> {
88 if period == 0 {
89 return Err(Error::PeriodZero);
90 }
91 if period > crate::error::MAX_PERIOD {
92 return Err(Error::InvalidPeriod {
93 message: crate::error::PERIOD_ABOVE_MAX,
94 });
95 }
96 if !coverage.is_finite() || coverage <= 0.0 || coverage > 1.0 {
97 return Err(Error::InvalidParameter {
98 message: "bomar bands coverage must be a finite value in (0.0, 1.0]",
99 });
100 }
101 Ok(Self {
102 period,
103 coverage,
104 window: VecDeque::with_capacity(period),
105 sorted: Vec::with_capacity(period),
106 scratch: Vec::with_capacity(period),
107 })
108 }
109
110 pub const fn period(&self) -> usize {
112 self.period
113 }
114
115 pub const fn coverage(&self) -> f64 {
117 self.coverage
118 }
119}
120
121impl Indicator for BomarBands {
122 type Input = f64;
123 type Output = BomarBandsOutput;
124
125 #[inline]
126 fn update(&mut self, value: f64) -> Option<BomarBandsOutput> {
127 if !value.is_finite() {
128 return None;
129 }
130 let merge = self.period >= MERGE_FROM;
131 if self.window.len() == self.period {
132 let oldest = self.window.pop_front().expect("window is full");
133 if merge {
134 sorted_window::remove(&mut self.sorted, oldest);
135 }
136 }
137 self.window.push_back(value);
138 if merge {
139 sorted_window::insert(&mut self.sorted, value);
140 }
141 if self.window.len() < self.period {
142 return None;
143 }
144 let sum: f64 = self.window.iter().sum();
145 let middle = sum / (self.period as f64);
146 let denom = middle.abs();
147
148 if denom == 0.0 {
149 self.scratch.clear();
150 self.scratch.resize(self.period, 0.0);
151 } else if merge {
152 sorted_window::distances(
155 &self.sorted,
156 middle,
157 |v| ((v - middle) / denom).abs(),
158 &mut self.scratch,
159 );
160 } else {
161 self.scratch.clear();
162 let devs = self.window.iter().map(|&v| ((v - middle) / denom).abs());
163 self.scratch.extend(devs);
164 self.scratch.sort_by(f64::total_cmp);
165 }
166 let p = quantile_sorted(&self.scratch, self.coverage);
167 let offset = denom * p;
168
169 Some(BomarBandsOutput {
170 upper: middle + offset,
171 middle,
172 lower: middle - offset,
173 })
174 }
175
176 fn reset(&mut self) {
177 self.window.clear();
178 self.sorted.clear();
179 self.scratch.clear();
180 }
181
182 #[inline]
183 fn warmup_period(&self) -> usize {
184 self.period
185 }
186
187 #[inline]
188 fn is_ready(&self) -> bool {
189 self.window.len() == self.period
190 }
191
192 #[inline]
193 fn name(&self) -> &'static str {
194 "BomarBands"
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::traits::BatchExt;
202 use approx::assert_relative_eq;
203
204 fn from_scratch(window: &[f64], coverage: f64) -> [f64; 3] {
207 let middle = window.iter().sum::<f64>() / window.len() as f64;
208 let denom = middle.abs();
209 let mut devs: Vec<f64> = window
210 .iter()
211 .map(|&v| {
212 if denom == 0.0 {
213 0.0
214 } else {
215 ((v - middle) / denom).abs()
216 }
217 })
218 .collect();
219 devs.sort_by(f64::total_cmp);
220 let offset = denom * quantile_sorted(&devs, coverage);
221 [middle + offset, middle, middle - offset]
222 }
223
224 #[test]
225 fn short_and_long_periods_match_a_fresh_sort() {
226 let prices: Vec<f64> = (0..400)
227 .map(|i| {
228 let t = f64::from(i);
229 100.0 + ((t * 0.07).sin() * 4.0).round() / 2.0 + (t * 0.31).cos()
230 })
231 .collect();
232 for period in [5, MERGE_FROM - 1, MERGE_FROM, 60] {
233 let mut bands = BomarBands::new(period, 0.85).unwrap();
234 for (i, &price) in prices.iter().enumerate() {
235 let got = bands.update(price);
236 if i + 1 >= period {
237 let o = got.unwrap();
238 let want = from_scratch(&prices[i + 1 - period..=i], 0.85);
239 assert_eq!(
240 [o.upper.to_bits(), o.middle.to_bits(), o.lower.to_bits()],
241 want.map(f64::to_bits),
242 "period {period} at {i}"
243 );
244 }
245 }
246 }
247 }
248
249 #[test]
250 fn rejects_zero_period() {
251 assert!(matches!(BomarBands::new(0, 0.85), Err(Error::PeriodZero)));
252 assert!(BomarBands::new(1, 0.85).is_ok());
253 }
254
255 #[test]
256 fn rejects_out_of_range_coverage() {
257 assert!(matches!(
258 BomarBands::new(20, 0.0),
259 Err(Error::InvalidParameter { .. })
260 ));
261 assert!(matches!(
262 BomarBands::new(20, 1.1),
263 Err(Error::InvalidParameter { .. })
264 ));
265 assert!(matches!(
266 BomarBands::new(20, -0.5),
267 Err(Error::InvalidParameter { .. })
268 ));
269 assert!(matches!(
270 BomarBands::new(20, f64::NAN),
271 Err(Error::InvalidParameter { .. })
272 ));
273 }
274
275 #[test]
276 fn accessors_and_metadata() {
277 let bb = BomarBands::new(20, 0.85).unwrap();
278 assert_eq!(bb.period(), 20);
279 assert_relative_eq!(bb.coverage(), 0.85, epsilon = 1e-12);
280 assert_eq!(bb.warmup_period(), 20);
281 assert_eq!(bb.name(), "BomarBands");
282 assert!(!bb.is_ready());
283 }
284
285 #[test]
286 fn warms_up_then_emits() {
287 let mut bb = BomarBands::new(4, 0.85).unwrap();
288 assert!(bb.update(100.0).is_none());
289 assert!(bb.update(102.0).is_none());
290 assert!(bb.update(98.0).is_none());
291 assert!(bb.update(104.0).is_some());
292 assert!(bb.is_ready());
293 }
294
295 #[test]
296 fn known_bands() {
297 let mut bb = BomarBands::new(4, 0.85).unwrap();
300 let out = bb.batch(&[100.0, 102.0, 98.0, 104.0]);
301 let last = out[3].unwrap();
302 assert_relative_eq!(last.middle, 101.0, epsilon = 1e-9);
303 assert_relative_eq!(last.upper, 104.0, epsilon = 1e-9);
304 assert_relative_eq!(last.lower, 98.0, epsilon = 1e-9);
305 }
306
307 #[test]
308 fn zero_midline_collapses_bands() {
309 let mut bb = BomarBands::new(2, 0.85).unwrap();
311 let out = bb.batch(&[3.0, -3.0]);
312 let last = out[1].unwrap();
313 assert_relative_eq!(last.middle, 0.0, epsilon = 1e-12);
314 assert_relative_eq!(last.upper, 0.0, epsilon = 1e-12);
315 assert_relative_eq!(last.lower, 0.0, epsilon = 1e-12);
316 }
317
318 #[test]
319 fn rolling_window_evicts_oldest() {
320 let mut bb = BomarBands::new(4, 0.85).unwrap();
323 let out = bb.batch(&[50.0, 50.0, 50.0, 50.0, 100.0, 102.0, 98.0, 104.0]);
324 let last = out[7].unwrap();
325 assert_relative_eq!(last.middle, 101.0, epsilon = 1e-9);
326 assert_relative_eq!(last.upper, 104.0, epsilon = 1e-9);
327 assert_relative_eq!(last.lower, 98.0, epsilon = 1e-9);
328 }
329
330 #[test]
331 fn reset_clears_state() {
332 let mut bb = BomarBands::new(4, 0.85).unwrap();
333 for v in [100.0, 102.0, 98.0, 104.0] {
334 bb.update(v);
335 }
336 assert!(bb.is_ready());
337 bb.reset();
338 assert!(!bb.is_ready());
339 assert!(bb.update(100.0).is_none());
340 }
341}