nautilus_indicators/momentum/
bb.rs1use std::{
17 collections::VecDeque,
18 fmt::{Debug, Display},
19};
20
21use nautilus_core::correctness::FAILED;
22use nautilus_model::{
23 data::{Bar, QuoteTick, TradeTick},
24 enums::PriceType,
25};
26
27pub use crate::support::MAX_PERIOD;
28use crate::{
29 average::{MovingAverageFactory, MovingAverageType},
30 indicator::{Indicator, MovingAverage},
31};
32
33#[repr(C)]
35#[derive(Debug)]
36#[cfg_attr(
37 feature = "python",
38 pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
39)]
40#[cfg_attr(
41 feature = "python",
42 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
43)]
44pub struct BollingerBands {
45 pub period: usize,
46 pub k: f64,
47 pub ma_type: MovingAverageType,
48 pub upper: f64,
49 pub middle: f64,
50 pub lower: f64,
51 pub stddev: f64,
52 pub initialized: bool,
53 ma: Box<dyn MovingAverage + Send + 'static>,
54 prices: VecDeque<f64>,
55 has_inputs: bool,
56}
57
58impl Display for BollingerBands {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(
61 f,
62 "{}({},{},{})",
63 self.name(),
64 self.period,
65 self.k,
66 self.ma_type,
67 )
68 }
69}
70
71impl Indicator for BollingerBands {
72 fn name(&self) -> String {
73 stringify!(BollingerBands).into()
74 }
75
76 fn has_inputs(&self) -> bool {
77 self.has_inputs
78 }
79
80 fn initialized(&self) -> bool {
81 self.initialized
82 }
83
84 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
85 self.update_raw(quote.extract_price(PriceType::Mid)?.into());
86 Ok(())
87 }
88
89 fn handle_trade(&mut self, trade: &TradeTick) {
90 self.update_raw((&trade.price).into());
91 }
92
93 fn handle_bar(&mut self, bar: &Bar) {
94 self.update_raw((&bar.close).into());
95 }
96
97 fn reset(&mut self) {
98 self.ma.reset();
99 self.prices.clear();
100 self.upper = 0.0;
101 self.middle = 0.0;
102 self.lower = 0.0;
103 self.stddev = 0.0;
104 self.has_inputs = false;
105 self.initialized = false;
106 }
107}
108
109impl BollingerBands {
110 #[must_use]
118 pub fn new(period: usize, k: f64, ma_type: Option<MovingAverageType>) -> Self {
119 Self::new_checked(period, k, ma_type).expect(FAILED)
120 }
121
122 pub(crate) fn new_checked(
123 period: usize,
124 k: f64,
125 ma_type: Option<MovingAverageType>,
126 ) -> anyhow::Result<Self> {
127 anyhow::ensure!(
128 (1..=MAX_PERIOD).contains(&period),
129 "BollingerBands: period {period} out of range (1..={MAX_PERIOD})"
130 );
131 anyhow::ensure!(
132 k.is_finite() && k > 0.0,
133 "BollingerBands: k must be positive and finite (received {k})"
134 );
135
136 Ok(Self {
137 period,
138 k,
139 ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
140 ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period),
141 prices: VecDeque::with_capacity(period),
142 has_inputs: false,
143 initialized: false,
144 upper: 0.0,
145 middle: 0.0,
146 lower: 0.0,
147 stddev: 0.0,
148 })
149 }
150
151 pub fn update_raw(&mut self, value: f64) {
152 if !value.is_finite() {
153 return;
154 }
155
156 if self.prices.len() == self.period {
157 let _ = self.prices.pop_front();
158 }
159 self.prices.push_back(value);
160 self.ma.update_raw(value);
161 self.has_inputs = true;
162
163 if !self.ma.initialized() {
164 return;
165 }
166
167 self.middle = self.ma.value();
168 self.stddev = fast_std_with_mean(self.prices.iter().copied(), self.middle);
169 self.upper = self.k.mul_add(self.stddev, self.middle);
170 self.lower = self.k.mul_add(-self.stddev, self.middle);
171 self.initialized = true;
172 }
173}
174
175#[must_use]
176pub fn fast_std_with_mean<I>(values: I, mean: f64) -> f64
177where
178 I: IntoIterator<Item = f64>,
179{
180 let mut var_acc = 0.0_f64;
181 let mut count = 0_usize;
182
183 for v in values {
184 let diff = v - mean;
185 var_acc += diff * diff;
186 count += 1;
187 }
188
189 if count == 0 {
190 return 0.0;
191 }
192
193 let variance = var_acc / count as f64;
194 variance.sqrt()
195}
196
197#[cfg(test)]
198mod tests {
199 use rstest::rstest;
200
201 use super::*;
202 use crate::{stubs::bb_10, testing::assert_approx_equal};
203
204 #[rstest]
205 fn test_name_returns_expected_string(bb_10: BollingerBands) {
206 assert_eq!(bb_10.name(), "BollingerBands");
207 }
208
209 #[rstest]
210 fn test_str_repr_returns_expected_string(bb_10: BollingerBands) {
211 assert_eq!(format!("{bb_10}"), "BollingerBands(10,0.1,SIMPLE)");
212 }
213
214 #[rstest]
215 fn test_period_returns_expected_value(bb_10: BollingerBands) {
216 assert_eq!(bb_10.period, 10);
217 assert_eq!(bb_10.k, 0.1);
218 }
219
220 #[rstest]
221 fn test_initialized_without_inputs_returns_false(bb_10: BollingerBands) {
222 assert!(!bb_10.initialized());
223 }
224
225 #[rstest]
226 fn test_value_with_all_higher_inputs_returns_expected_value(mut bb_10: BollingerBands) {
227 let values = [
228 0.95, 1.95, 2.95, 3.95, 4.95, 5.95, 6.95, 7.95, 8.95, 9.95, 10.05, 10.15, 10.25, 11.05,
229 11.45,
230 ];
231
232 for value in values {
233 bb_10.update_raw(value);
234 }
235
236 let window = &values[5..];
237 let expected_middle = window.iter().sum::<f64>() / window.len() as f64;
238 let expected_variance = window
239 .iter()
240 .map(|value| (value - expected_middle).powi(2))
241 .sum::<f64>()
242 / window.len() as f64;
243 let expected_stddev = expected_variance.sqrt();
244
245 assert!(bb_10.initialized());
246 assert_approx_equal(bb_10.middle, expected_middle);
247 assert_approx_equal(bb_10.stddev, expected_stddev);
248 assert_approx_equal(bb_10.upper, expected_middle + bb_10.k * expected_stddev);
249 assert_approx_equal(bb_10.lower, expected_middle - bb_10.k * expected_stddev);
250 }
251
252 #[rstest]
253 fn test_reset_successfully_returns_indicator_to_fresh_state(mut bb_10: BollingerBands) {
254 for value in [1.00020, 1.00030, 1.00070] {
255 bb_10.update_raw(value);
256 }
257 bb_10.reset();
258
259 assert!(!bb_10.initialized());
260 assert_eq!(bb_10.upper, 0.0);
261 assert_eq!(bb_10.middle, 0.0);
262 assert_eq!(bb_10.lower, 0.0);
263 assert_eq!(bb_10.stddev, 0.0);
264 assert_eq!(bb_10.prices.len(), 0);
265 }
266
267 #[rstest]
268 #[should_panic(expected = "k must be positive")]
269 fn test_new_panics_on_zero_k() {
270 let _ = BollingerBands::new(10, 0.0, None);
271 }
272
273 #[rstest]
274 #[should_panic(expected = "k must be positive")]
275 fn test_new_panics_on_negative_k() {
276 let _ = BollingerBands::new(10, -2.0, None);
277 }
278
279 #[rstest]
280 #[should_panic(expected = "k must be positive")]
281 fn test_new_panics_on_nan_k() {
282 let _ = BollingerBands::new(10, f64::NAN, None);
283 }
284
285 #[rstest]
286 fn test_std_dev_uses_sliding_window() {
287 let mut bb = BollingerBands::new(3, 1.0, None);
288
289 for v in 1..=6 {
290 bb.update_raw(f64::from(v));
291 }
292
293 let expected_mid: f64 = (4.0 + 5.0 + 6.0) / 3.0;
294 let variance = (6.0 - expected_mid).mul_add(
295 6.0 - expected_mid,
296 (4.0 - expected_mid).mul_add(
297 4.0 - expected_mid,
298 (5.0 - expected_mid) * (5.0 - expected_mid),
299 ),
300 ) / 3.0;
301 let expected_std = variance.sqrt();
302
303 assert!((bb.middle - expected_mid).abs() < 1e-12);
304 assert!((bb.upper - (expected_mid + expected_std)).abs() < 1e-12);
305 assert!((bb.lower - (expected_mid - expected_std)).abs() < 1e-12);
306 }
307}