use crate::stocks::ta::common::{opt_cell, validate_series, window_stdev, StdevKind};
use crate::stocks::ta::moving_average::sma;
use crate::util::error::{require_finite, FinanceError, FinanceResult};
use crate::util::primitives::PeriodLength;
use crate::{columns_with_strings, print_table_locale_opt};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BollingerParams {
pub period: usize,
pub num_std: f64,
pub stdev: StdevKind,
}
impl BollingerParams {
pub const fn standard() -> Self {
Self {
period: 20,
num_std: 2.0,
stdev: StdevKind::Sample,
}
}
pub const fn new(period: usize, num_std: f64) -> Self {
Self {
period,
num_std,
stdev: StdevKind::Sample,
}
}
pub const fn with_stdev(period: usize, num_std: f64, stdev: StdevKind) -> Self {
Self {
period,
num_std,
stdev,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ValidatedBollinger {
params: BollingerParams,
}
impl ValidatedBollinger {
pub fn new(params: BollingerParams) -> FinanceResult<Self> {
PeriodLength::new(params.period)?;
require_finite("num_std", params.num_std)?;
if params.num_std < 0.0 {
return Err(FinanceError::Unsolvable {
message: "Bollinger num_std must be non-negative",
});
}
match params.stdev {
StdevKind::Sample if params.period < 2 => {
return Err(FinanceError::Unsolvable {
message: "Bollinger sample stdev requires period >= 2",
});
}
StdevKind::Population if params.period < 1 => {
return Err(FinanceError::Unsolvable {
message: "Bollinger population stdev requires period >= 1",
});
}
_ => {}
}
Ok(Self { params })
}
pub fn params(self) -> BollingerParams {
self.params
}
pub fn compute(self, closes: &[f64]) -> FinanceResult<BollingerSeries> {
bollinger_validated(closes, self)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BollingerSeries {
pub middle: Vec<Option<f64>>,
pub upper: Vec<Option<f64>>,
pub lower: Vec<Option<f64>>,
pub pct_b: Vec<Option<f64>>,
pub params: BollingerParams,
}
#[derive(Clone, Debug)]
pub struct BollingerSolution {
series: BollingerSeries,
closes: Vec<f64>,
formula: String,
symbolic_formula: String,
}
impl BollingerSolution {
pub fn series(&self) -> &BollingerSeries {
&self.series
}
pub fn formula(&self) -> &str {
&self.formula
}
pub fn symbolic_formula(&self) -> &str {
&self.symbolic_formula
}
pub fn print_table(&self) {
self.print_table_locale_opt(None, None);
}
pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
self.print_table_locale_opt(Some(locale), Some(precision));
}
fn print_table_locale_opt(
&self,
locale: Option<&num_format::Locale>,
precision: Option<usize>,
) {
let columns = columns_with_strings(&[
("period", "i", true),
("close", "f", true),
("middle", "f", true),
("upper", "f", true),
("lower", "f", true),
("pct_b", "f", true),
]);
let data = self
.closes
.iter()
.enumerate()
.map(|(i, c)| {
vec![
i.to_string(),
c.to_string(),
opt_cell(self.series.middle[i]),
opt_cell(self.series.upper[i]),
opt_cell(self.series.lower[i]),
opt_cell(self.series.pct_b[i]),
]
})
.collect();
print_table_locale_opt(&columns, data, locale, precision);
}
}
pub fn bollinger(closes: &[f64], params: BollingerParams) -> FinanceResult<BollingerSeries> {
ValidatedBollinger::new(params)?.compute(closes)
}
pub fn bollinger_solution(
closes: &[f64],
params: BollingerParams,
) -> FinanceResult<BollingerSolution> {
let series = bollinger(closes, params)?;
let stdev_label = match params.stdev {
StdevKind::Sample => "sample_stdev(n-1)",
StdevKind::Population => "population_stdev(n)",
};
let formula = format!(
"mid = SMA({}); upper/lower = mid ± {} * {}; %B = (close-lower)/(upper-lower)",
params.period, params.num_std, stdev_label
);
let symbolic = format!(
"mid = sma(close,n); band = k * stdev_{:?}(window); upper = mid+band; lower = mid-band",
params.stdev
);
Ok(BollingerSolution {
series,
closes: closes.to_vec(),
formula,
symbolic_formula: symbolic,
})
}
fn bollinger_validated(closes: &[f64], v: ValidatedBollinger) -> FinanceResult<BollingerSeries> {
validate_series("close", closes)?;
let p = v.params;
let mid = sma(closes, p.period)?;
let n = closes.len();
let mut upper = vec![None; n];
let mut lower = vec![None; n];
let mut pct_b = vec![None; n];
for i in 0..n {
if let Some(m) = mid[i] {
let start = i + 1 - p.period;
let window = &closes[start..=i];
let sd = window_stdev(window, p.stdev).unwrap_or(0.0);
let band = p.num_std * sd;
let u = m + band;
let l = m - band;
upper[i] = Some(u);
lower[i] = Some(l);
let width = u - l;
if width > 0.0 {
pct_b[i] = Some((closes[i] - l) / width);
}
}
}
Ok(BollingerSeries {
middle: mid,
upper,
lower,
pct_b,
params: p,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_series_zero_width() {
let c = vec![10.0; 25];
let s = bollinger(&c, BollingerParams::standard()).unwrap();
assert_eq!(s.middle[19], Some(10.0));
assert_eq!(s.upper[19], Some(10.0));
assert_eq!(s.pct_b[19], None);
}
#[test]
fn pct_b_midpoint_on_middle() {
let closes: Vec<f64> = (0..40).map(|i| 100.0 + (i as f64 - 20.0) * 0.1).collect();
let s = bollinger(&closes, BollingerParams::standard()).unwrap();
let i = 30;
let mid = s.middle[i].unwrap();
let u = s.upper[i].unwrap();
let l = s.lower[i].unwrap();
let pct = (mid - l) / (u - l);
assert!((pct - 0.5).abs() < 1e-9);
}
#[test]
fn sample_wider_than_population() {
let closes: Vec<f64> = (1..=30).map(|x| x as f64).collect();
let s = bollinger(&closes, BollingerParams::standard()).unwrap();
let p = bollinger(
&closes,
BollingerParams::with_stdev(20, 2.0, StdevKind::Population),
)
.unwrap();
let su = s.upper[29].unwrap();
let pu = p.upper[29].unwrap();
assert!(su + 1e-12 >= pu);
}
}