use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData}; use super::sma::SMA; use super::common::RollingWindow; /// Bollinger Bands Indicator /// /// Middle Band = SMA(n) /// Upper Band = SMA(n) + (k × σ) /// Lower Band = SMA(n) - (k × σ) /// where σ is the standard deviation and k is typically 2 pub struct BollingerBands { period: usize, std_dev_multiplier: f64, sma: SMA, window: RollingWindow, } impl BollingerBands { pub fn new(period: usize, std_dev_multiplier: f64) -> Result { if period == 0 { return Err(IndicatorError::InvalidParameter( "Period must be greater than 0".to_string() )); } if std_dev_multiplier <= 0.0 { return Err(IndicatorError::InvalidParameter( "Standard deviation multiplier must be positive".to_string() )); } Ok(Self { period, std_dev_multiplier, sma: SMA::new(period)?, window: RollingWindow::new(period), }) } /// Standard Bollinger Bands with 20 period and 2 standard deviations pub fn standard() -> Result { Self::new(20, 2.0) } /// Calculate standard deviation fn calculate_std_dev(values: &[f64], mean: f64) -> f64 { if values.is_empty() { return 0.0; } let variance = values.iter() .map(|x| (*x - mean).powi(2)) .sum::() / values.len() as f64; variance.sqrt() } /// Calculate Bollinger Bands for a series of values pub fn calculate_series( values: &[f64], period: usize, std_dev_multiplier: f64 ) -> Result<(Vec, Vec, Vec), IndicatorError> { if period == 0 { return Err(IndicatorError::InvalidParameter( "Period must be greater than 0".to_string() )); } if values.len() < period { return Err(IndicatorError::InsufficientData { required: period, actual: values.len(), }); } // Calculate SMA (middle band) let middle_band = SMA::calculate_series(values, period)?; let mut upper_band = Vec::with_capacity(middle_band.len()); let mut lower_band = Vec::with_capacity(middle_band.len()); // Calculate bands for i in 0..middle_band.len() { // Get the window of values for this position let start_idx = i; let end_idx = i + period; let window = &values[start_idx..end_idx]; // Calculate standard deviation let std_dev = Self::calculate_std_dev(window, middle_band[i]); let band_width = std_dev * std_dev_multiplier; upper_band.push(middle_band[i] + band_width); lower_band.push(middle_band[i] - band_width); } Ok((middle_band, upper_band, lower_band)) } /// Calculate the current bandwidth (distance between upper and lower bands) pub fn bandwidth(&self) -> Option { if let Some(values) = self.get_current_bands() { Some(values.upper - values.lower) } else { None } } /// Calculate %B (percent b) - position of price relative to bands /// %B = (Price - Lower Band) / (Upper Band - Lower Band) pub fn percent_b(&self, price: f64) -> Option { if let Some(values) = self.get_current_bands() { let width = values.upper - values.lower; if width > 0.0 { Some((price - values.lower) / width) } else { None } } else { None } } fn get_current_bands(&self) -> Option { if let Some(middle) = self.sma.current() { if self.window.is_full() { let values = self.window.as_slice(); let std_dev = Self::calculate_std_dev(&values, middle); let band_width = std_dev * self.std_dev_multiplier; Some(BollingerBandsValues { middle, upper: middle + band_width, lower: middle - band_width, }) } else { None } } else { None } } } impl Indicator for BollingerBands { fn calculate(&mut self, data: &PriceData) -> Result { let values = &data.close; let (middle, upper, lower) = Self::calculate_series( values, self.period, self.std_dev_multiplier )?; Ok(IndicatorResult::BollingerBands { middle, upper, lower, }) } fn reset(&mut self) { self.sma.reset(); self.window.clear(); } fn is_ready(&self) -> bool { self.sma.is_ready() && self.window.is_full() } } impl IncrementalIndicator for BollingerBands { fn update(&mut self, value: f64) -> Result, IndicatorError> { // Update window self.window.push(value); // Update SMA let _sma_result = self.sma.update(value)?; // Return bandwidth if ready if self.is_ready() { Ok(self.bandwidth()) } else { Ok(None) } } fn current(&self) -> Option { self.bandwidth() } } /// Structure to hold all Bollinger Bands values pub struct BollingerBandsValues { pub middle: f64, pub upper: f64, pub lower: f64, } impl BollingerBands { /// Get all current band values pub fn current_values(&self) -> Option { self.get_current_bands() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_bollinger_bands_calculation() { let values = vec![ 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 24.0, 23.0, 22.0, 21.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 24.0, 23.0, 22.0, 21.0 ]; let (middle, upper, lower) = BollingerBands::calculate_series(&values, 5, 2.0).unwrap(); assert_eq!(middle.len(), 16); assert_eq!(upper.len(), 16); assert_eq!(lower.len(), 16); // Upper band should always be above middle for i in 0..middle.len() { assert!(upper[i] > middle[i]); assert!(lower[i] < middle[i]); } } #[test] fn test_percent_b() { let mut bb = BollingerBands::standard().unwrap(); // Create some test data for i in 0..25 { let _ = bb.update(20.0 + (i as f64 % 5.0)); } // Price at upper band should give %B ≈ 1.0 if let Some(bands) = bb.current_values() { let percent_b = bb.percent_b(bands.upper).unwrap(); assert!((percent_b - 1.0).abs() < 1e-10); // Price at lower band should give %B ≈ 0.0 let percent_b = bb.percent_b(bands.lower).unwrap(); assert!(percent_b.abs() < 1e-10); // Price at middle band should give %B ≈ 0.5 let percent_b = bb.percent_b(bands.middle).unwrap(); assert!((percent_b - 0.5).abs() < 0.1); } } }