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 fn update(&mut self, input: f64) -> Option<f64> {
74 let o = self.bands.update(input)?;
75 let bandwidth = if o.middle == 0.0 {
76 0.0
78 } else {
79 (o.upper - o.lower) / o.middle
80 };
81 self.last = Some(bandwidth);
82 Some(bandwidth)
83 }
84
85 fn reset(&mut self) {
86 self.bands.reset();
87 self.last = None;
88 }
89
90 fn warmup_period(&self) -> usize {
91 self.bands.warmup_period()
92 }
93
94 fn is_ready(&self) -> bool {
95 self.last.is_some()
96 }
97
98 fn name(&self) -> &'static str {
99 "BollingerBandwidth"
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::traits::BatchExt;
107 use approx::assert_relative_eq;
108
109 #[test]
110 fn new_rejects_invalid_parameters() {
111 assert!(BollingerBandwidth::new(0, 2.0).is_err());
112 assert!(BollingerBandwidth::new(20, 0.0).is_err());
113 assert!(BollingerBandwidth::new(20, -1.0).is_err());
114 }
115
116 #[test]
122 fn accessors_and_metadata() {
123 let mut bbw = BollingerBandwidth::new(20, 2.0).unwrap();
124 assert_eq!(bbw.period(), 20);
125 assert_relative_eq!(bbw.multiplier(), 2.0, epsilon = 1e-12);
126 assert_eq!(bbw.value(), None);
128 assert_eq!(bbw.warmup_period(), 20);
129 assert_eq!(bbw.name(), "BollingerBandwidth");
130 for i in 1..=20 {
132 bbw.update(f64::from(i));
133 }
134 assert!(bbw.value().is_some());
135 }
136
137 #[test]
138 fn constant_series_yields_zero() {
139 let mut bbw = BollingerBandwidth::new(5, 2.0).unwrap();
141 let out = bbw.batch(&[100.0; 20]);
142 for v in out.iter().skip(4).flatten() {
143 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
144 }
145 }
146
147 #[test]
153 fn zero_middle_band_yields_zero_bandwidth() {
154 let mut bbw = BollingerBandwidth::new(5, 2.0).unwrap();
155 let out = bbw.batch(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
159 assert_eq!(out[..4], [None, None, None, None]);
160 let v = out[4].expect("warmed up");
161 assert_eq!(v, 0.0, "zero-middle fallback must emit exactly 0.0");
162 }
163
164 #[test]
165 fn matches_bands_definition() {
166 let prices: Vec<f64> = (1..=60)
168 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
169 .collect();
170 let bbw_out = BollingerBandwidth::new(20, 2.0).unwrap().batch(&prices);
171 let bands_out = BollingerBands::new(20, 2.0).unwrap().batch(&prices);
172 for (i, (w, b)) in bbw_out.iter().zip(bands_out.iter()).enumerate() {
173 assert_eq!(w.is_some(), b.is_some(), "warmup mismatch at index {i}");
175 if let (Some(wv), Some(bv)) = (w, b) {
176 assert_relative_eq!(*wv, (bv.upper - bv.lower) / bv.middle, epsilon = 1e-12);
177 }
178 }
179 }
180
181 #[test]
182 fn output_is_non_negative() {
183 let mut bbw = BollingerBandwidth::new(20, 2.0).unwrap();
184 let prices: Vec<f64> = (1..=120)
185 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 12.0)
186 .collect();
187 for v in bbw.batch(&prices).into_iter().flatten() {
188 assert!(v >= 0.0, "bandwidth must be non-negative, got {v}");
189 }
190 }
191
192 #[test]
193 fn reset_clears_state() {
194 let mut bbw = BollingerBandwidth::new(5, 2.0).unwrap();
195 bbw.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
196 assert!(bbw.is_ready());
197 bbw.reset();
198 assert!(!bbw.is_ready());
199 assert_eq!(bbw.update(1.0), None);
200 }
201
202 #[test]
203 fn batch_equals_streaming() {
204 let prices: Vec<f64> = (1..=80)
205 .map(|i| 100.0 + (f64::from(i) * 0.3).cos() * 7.0)
206 .collect();
207 let batch = BollingerBandwidth::new(20, 2.0).unwrap().batch(&prices);
208 let mut b = BollingerBandwidth::new(20, 2.0).unwrap();
209 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
210 assert_eq!(batch, streamed);
211 }
212}