wickra_core/indicators/
bollinger_bandwidth.rs1use crate::error::Result;
4use crate::traits::Indicator;
5
6use super::BollingerBands;
7
8#[derive(Debug, Clone)]
34pub struct BollingerBandwidth {
35 bands: BollingerBands,
36 last: Option<f64>,
37}
38
39impl BollingerBandwidth {
40 pub fn new(period: usize, multiplier: f64) -> Result<Self> {
47 Ok(Self {
48 bands: BollingerBands::new(period, multiplier)?,
49 last: None,
50 })
51 }
52
53 pub const fn period(&self) -> usize {
55 self.bands.period()
56 }
57
58 pub const fn multiplier(&self) -> f64 {
60 self.bands.multiplier()
61 }
62
63 pub const fn value(&self) -> Option<f64> {
65 self.last
66 }
67}
68
69impl Indicator for BollingerBandwidth {
70 type Input = f64;
71 type Output = f64;
72
73 #[inline]
74 fn update(&mut self, input: f64) -> Option<f64> {
75 let o = self.bands.update(input)?;
76 let bandwidth = if o.middle == 0.0 {
77 0.0
79 } else {
80 (o.upper - o.lower) / o.middle
81 };
82 self.last = Some(bandwidth);
83 Some(bandwidth)
84 }
85
86 fn reset(&mut self) {
87 self.bands.reset();
88 self.last = None;
89 }
90
91 #[inline]
92 fn warmup_period(&self) -> usize {
93 self.bands.warmup_period()
94 }
95
96 #[inline]
97 fn is_ready(&self) -> bool {
98 self.last.is_some()
99 }
100
101 #[inline]
102 fn name(&self) -> &'static str {
103 "BollingerBandwidth"
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::traits::BatchExt;
111 use approx::assert_relative_eq;
112
113 #[test]
114 fn new_rejects_invalid_parameters() {
115 assert!(BollingerBandwidth::new(0, 2.0).is_err());
116 assert!(BollingerBandwidth::new(20, 0.0).is_err());
117 assert!(BollingerBandwidth::new(20, -1.0).is_err());
118 }
119
120 #[test]
126 fn accessors_and_metadata() {
127 let mut bbw = BollingerBandwidth::new(20, 2.0).unwrap();
128 assert_eq!(bbw.period(), 20);
129 assert_relative_eq!(bbw.multiplier(), 2.0, epsilon = 1e-12);
130 assert_eq!(bbw.value(), None);
132 assert_eq!(bbw.warmup_period(), 20);
133 assert_eq!(bbw.name(), "BollingerBandwidth");
134 for i in 1..=20 {
136 bbw.update(f64::from(i));
137 }
138 assert!(bbw.value().is_some());
139 }
140
141 #[test]
142 fn constant_series_yields_zero() {
143 let mut bbw = BollingerBandwidth::new(5, 2.0).unwrap();
145 let out = bbw.batch(&[100.0; 20]);
146 for v in out.iter().skip(4).flatten() {
147 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
148 }
149 }
150
151 #[test]
157 fn zero_middle_band_yields_zero_bandwidth() {
158 let mut bbw = BollingerBandwidth::new(5, 2.0).unwrap();
159 let out = bbw.batch(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
163 assert_eq!(out[..4], [None, None, None, None]);
164 let v = out[4].expect("warmed up");
165 assert_eq!(v, 0.0, "zero-middle fallback must emit exactly 0.0");
166 }
167
168 #[test]
169 fn matches_bands_definition() {
170 let prices: Vec<f64> = (1..=60)
172 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
173 .collect();
174 let bbw_out = BollingerBandwidth::new(20, 2.0).unwrap().batch(&prices);
175 let bands_out = BollingerBands::new(20, 2.0).unwrap().batch(&prices);
176 for (i, (w, b)) in bbw_out.iter().zip(bands_out.iter()).enumerate() {
177 assert_eq!(w.is_some(), b.is_some(), "warmup mismatch at index {i}");
179 if let (Some(wv), Some(bv)) = (w, b) {
180 assert_relative_eq!(*wv, (bv.upper - bv.lower) / bv.middle, epsilon = 1e-12);
181 }
182 }
183 }
184
185 #[test]
186 fn output_is_non_negative() {
187 let mut bbw = BollingerBandwidth::new(20, 2.0).unwrap();
188 let prices: Vec<f64> = (1..=120)
189 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 12.0)
190 .collect();
191 for v in bbw.batch(&prices).into_iter().flatten() {
192 assert!(v >= 0.0, "bandwidth must be non-negative, got {v}");
193 }
194 }
195
196 #[test]
197 fn reset_clears_state() {
198 let mut bbw = BollingerBandwidth::new(5, 2.0).unwrap();
199 bbw.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
200 assert!(bbw.is_ready());
201 bbw.reset();
202 assert!(!bbw.is_ready());
203 assert_eq!(bbw.update(1.0), None);
204 }
205
206 #[test]
207 fn batch_equals_streaming() {
208 let prices: Vec<f64> = (1..=80)
209 .map(|i| 100.0 + (f64::from(i) * 0.3).cos() * 7.0)
210 .collect();
211 let batch = BollingerBandwidth::new(20, 2.0).unwrap().batch(&prices);
212 let mut b = BollingerBandwidth::new(20, 2.0).unwrap();
213 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
214 assert_eq!(batch, streamed);
215 }
216}