stock-bot/apps/stock/engine/src/indicators/bollinger_bands.rs
2025-07-04 11:24:27 -04:00

256 lines
No EOL
7.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<f64>,
}
impl BollingerBands {
pub fn new(period: usize, std_dev_multiplier: f64) -> Result<Self, IndicatorError> {
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, IndicatorError> {
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::<f64>() / 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<f64>, Vec<f64>, Vec<f64>), 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<f64> {
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<f64> {
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<BollingerBandsValues> {
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<IndicatorResult, IndicatorError> {
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<Option<f64>, 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<f64> {
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<BollingerBandsValues> {
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);
}
}
}