use std::{fmt::Display, num::NonZero};
use crate::{
Indicator, IndicatorConfig, IndicatorConfigBuilder, Ohlcv, Price, PriceSource, StdDev,
internals::{PriceWindow, PriceWindowWithSumOfSquares},
};
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct BbConfig {
length: usize,
source: PriceSource,
std_dev: StdDev,
}
impl IndicatorConfig for BbConfig {
type Builder = BbConfigBuilder;
fn builder() -> Self::Builder {
BbConfigBuilder::new()
}
fn source(&self) -> PriceSource {
self.source
}
fn convergence(&self) -> usize {
self.length
}
fn to_builder(&self) -> Self::Builder {
BbConfigBuilder {
length: Some(self.length),
source: self.source,
std_dev: self.std_dev,
}
}
}
impl BbConfig {
#[must_use]
pub fn length(&self) -> usize {
self.length
}
#[must_use]
pub fn std_dev(&self) -> StdDev {
self.std_dev
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn default_20() -> Self {
Self::builder().length(NonZero::new(20).unwrap()).build()
}
#[must_use]
pub fn close(length: NonZero<usize>) -> Self {
Self::builder().length(length).build()
}
}
impl Default for BbConfig {
fn default() -> Self {
Self {
length: 20,
source: PriceSource::Close,
std_dev: StdDev::new(2.0),
}
}
}
impl Display for BbConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"BbConfig({}, {}, {})",
self.length,
self.source,
self.std_dev.value()
)
}
}
pub struct BbConfigBuilder {
length: Option<usize>,
source: PriceSource,
std_dev: StdDev,
}
impl BbConfigBuilder {
fn new() -> Self {
Self {
length: None,
source: PriceSource::Close,
std_dev: StdDev::new(2.0),
}
}
#[must_use]
pub fn length(mut self, length: NonZero<usize>) -> Self {
self.length.replace(length.get());
self
}
#[must_use]
pub fn std_dev(mut self, std_dev: StdDev) -> Self {
self.std_dev = std_dev;
self
}
}
impl IndicatorConfigBuilder<BbConfig> for BbConfigBuilder {
fn source(mut self, source: PriceSource) -> Self {
self.source = source;
self
}
fn build(self) -> BbConfig {
BbConfig {
length: self.length.expect("length is required"),
source: self.source,
std_dev: self.std_dev,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BbValue {
upper: Price,
middle: Price,
lower: Price,
}
impl BbValue {
#[inline]
#[must_use]
pub fn upper(&self) -> Price {
self.upper
}
#[inline]
#[must_use]
pub fn middle(&self) -> Price {
self.middle
}
#[inline]
#[must_use]
pub fn lower(&self) -> Price {
self.lower
}
#[inline]
#[must_use]
pub fn width(&self) -> f64 {
self.upper - self.lower
}
}
impl Display for BbValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"BB(u: {}, m: {}, l: {})",
self.upper, self.middle, self.lower
)
}
}
#[derive(Clone, Debug)]
pub struct Bb {
config: BbConfig,
length_reciprocal: f64,
std_dev_multiplier: f64,
window: PriceWindowWithSumOfSquares,
current: Option<BbValue>,
}
impl Indicator for Bb {
type Config = BbConfig;
type Output = BbValue;
fn new(config: Self::Config) -> Self {
let window = PriceWindow::with_sum_of_squares(config.length, config.source);
Self {
config,
#[allow(clippy::cast_precision_loss)]
length_reciprocal: 1.0 / config.length as f64,
std_dev_multiplier: config.std_dev.value(),
window,
current: None,
}
}
fn compute(&mut self, ohlcv: &impl Ohlcv) -> Option<Self::Output> {
self.window.add(ohlcv);
self.current = match (self.window.sum(), self.window.sum_of_squares()) {
(Some(sum), Some(sum_of_squares)) => {
let mean = sum * self.length_reciprocal;
let variance = sum_of_squares.mul_add(self.length_reciprocal, -(mean * mean));
let std_dev = variance.max(0.0).sqrt() * self.std_dev_multiplier;
Some(Self::Output {
upper: mean + std_dev,
middle: mean,
lower: mean - std_dev,
})
}
_ => None,
};
self.current
}
#[inline]
fn value(&self) -> Option<Self::Output> {
self.current
}
}
impl Display for Bb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"BB({}, {}, {})",
self.config.length, self.config.source, self.std_dev_multiplier,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{Bar, nz};
fn bb(length: usize) -> Bb {
Bb::new(BbConfig::builder().length(nz(length)).build())
}
fn bb_with_std_dev(length: usize, std_dev: f64) -> Bb {
Bb::new(
BbConfig::builder()
.length(nz(length))
.std_dev(StdDev::new(std_dev))
.build(),
)
}
fn bar(close: f64, time: u64) -> Bar {
Bar::new(0.0, 0.0, 0.0, close).at(time)
}
fn assert_bb(value: Option<BbValue>, upper: f64, middle: f64, lower: f64) {
let v = value.expect("expected Some(BbValue)");
assert!(
(v.upper() - upper).abs() < 1e-10,
"upper: expected {upper}, got {}",
v.upper()
);
assert!(
(v.middle() - middle).abs() < 1e-10,
"middle: expected {middle}, got {}",
v.middle()
);
assert!(
(v.lower() - lower).abs() < 1e-10,
"lower: expected {lower}, got {}",
v.lower()
);
}
mod filling {
use super::*;
#[test]
fn none_until_window_full() {
let mut bb = bb(3);
assert!(bb.compute(&bar(10.0, 1)).is_none());
assert!(bb.compute(&bar(20.0, 2)).is_none());
}
#[test]
fn returns_value_when_full() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
assert!(bb.compute(&bar(5.0, 2)).is_some());
}
}
mod computation {
use super::*;
#[test]
fn basic_bands() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
assert_bb(bb.compute(&bar(5.0, 2)), 6.0, 4.0, 2.0);
}
#[test]
fn constant_input_zero_width() {
let mut bb = bb(3);
bb.compute(&bar(10.0, 1));
bb.compute(&bar(10.0, 2));
assert_bb(bb.compute(&bar(10.0, 3)), 10.0, 10.0, 10.0);
}
#[test]
fn bands_are_symmetric() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
let v = bb.compute(&bar(5.0, 2)).unwrap();
let upper_dist = v.upper() - v.middle();
let lower_dist = v.middle() - v.lower();
assert!((upper_dist - lower_dist).abs() < 1e-10);
}
}
mod sliding {
use super::*;
#[test]
fn updates_on_advance() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
bb.compute(&bar(5.0, 2));
assert_bb(bb.compute(&bar(7.0, 3)), 8.0, 6.0, 4.0);
}
}
mod repaint {
use super::*;
#[test]
fn replaces_current_bar() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
bb.compute(&bar(5.0, 2));
assert_bb(bb.compute(&bar(7.0, 2)), 9.0, 5.0, 1.0);
}
#[test]
fn repaint_during_filling() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
bb.compute(&bar(4.0, 1)); assert!(bb.compute(&bar(4.0, 1)).is_none()); assert_bb(bb.compute(&bar(6.0, 2)), 7.0, 5.0, 3.0);
}
}
mod std_dev_multiplier {
use super::*;
#[test]
fn multiplier_of_one() {
let mut bb = bb_with_std_dev(2, 1.0);
bb.compute(&bar(3.0, 1));
assert_bb(bb.compute(&bar(5.0, 2)), 5.0, 4.0, 3.0);
}
#[test]
fn fractional_multiplier() {
let mut bb = bb_with_std_dev(2, 1.5);
bb.compute(&bar(3.0, 1));
assert_bb(bb.compute(&bar(5.0, 2)), 5.5, 4.0, 2.5);
}
#[test]
fn wider_multiplier_wider_bands() {
let mut bb1 = bb_with_std_dev(2, 1.0);
let mut bb2 = bb_with_std_dev(2, 3.0);
bb1.compute(&bar(3.0, 1));
bb2.compute(&bar(3.0, 1));
let v1 = bb1.compute(&bar(5.0, 2)).unwrap();
let v2 = bb2.compute(&bar(5.0, 2)).unwrap();
assert!(v2.width() > v1.width());
}
}
mod width {
use super::*;
#[test]
fn equals_upper_minus_lower() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
let v = bb.compute(&bar(5.0, 2)).unwrap();
assert!((v.width() - (v.upper() - v.lower())).abs() < 1e-10);
}
#[test]
fn zero_for_constant_input() {
let mut bb = bb(2);
bb.compute(&bar(10.0, 1));
let v = bb.compute(&bar(10.0, 2)).unwrap();
assert!((v.width()).abs() < 1e-10);
}
}
mod value {
use super::*;
#[test]
fn returns_last_computed() {
let mut bb = bb(2);
bb.compute(&bar(3.0, 1));
bb.compute(&bar(5.0, 2));
assert_eq!(bb.value(), bb.compute(&bar(5.0, 2)));
}
#[test]
fn none_before_first_value() {
let bb = bb(2);
assert!(bb.value().is_none());
}
}
mod config {
use super::*;
#[test]
fn convergence_equals_length() {
let config = BbConfig::close(nz(20));
assert_eq!(config.convergence(), 20);
let config = BbConfig::close(nz(200));
assert_eq!(config.convergence(), 200);
}
#[test]
fn default_std_dev_is_two() {
let config = BbConfig::builder().length(nz(20)).build();
assert!((config.std_dev().value() - 2.0).abs() < f64::EPSILON);
}
#[test]
fn default_source_is_close() {
let config = BbConfig::builder().length(nz(20)).build();
assert_eq!(config.source(), PriceSource::Close);
}
#[test]
#[should_panic(expected = "length is required")]
fn panics_without_length() {
let _ = BbConfig::builder().build();
}
#[test]
#[should_panic(expected = "std_dev must be positive")]
fn std_dev_rejects_zero() {
let _ = StdDev::new(0.0);
}
#[test]
#[should_panic(expected = "std_dev must be positive")]
fn std_dev_rejects_negative() {
let _ = StdDev::new(-1.0);
}
#[test]
#[should_panic(expected = "std_dev must not be NaN")]
fn std_dev_rejects_nan() {
let _ = StdDev::new(f64::NAN);
}
#[test]
fn to_builder_roundtrip() {
let config = BbConfig::builder()
.length(nz(15))
.std_dev(StdDev::new(2.5))
.source(PriceSource::HLC3)
.build();
assert_eq!(config.to_builder().build(), config);
}
}
mod clone {
use super::*;
#[test]
fn produces_independent_state() {
let mut bb = bb(3);
bb.compute(&bar(10.0, 1));
bb.compute(&bar(20.0, 2));
let mut cloned = bb.clone();
assert!(bb.compute(&bar(30.0, 3)).is_some());
assert_eq!(cloned.value(), None);
assert!(cloned.compute(&bar(90.0, 3)).is_some());
assert!(
(bb.value().unwrap().middle() - cloned.value().unwrap().middle()).abs() > 1e-10
);
}
}
mod price_source {
use super::*;
#[test]
fn hl2_source() {
let mut bb = Bb::new(
BbConfig::builder()
.length(nz(2))
.source(PriceSource::HL2)
.build(),
);
bb.compute(&Bar::new(0.0, 20.0, 10.0, 0.0).at(1)); let v = bb.compute(&Bar::new(0.0, 30.0, 20.0, 0.0).at(2)).unwrap(); assert!((v.middle() - 20.0).abs() < 1e-10);
}
}
mod display {
use super::*;
#[test]
fn bb_formats_correctly() {
let bb = bb(20);
assert_eq!(bb.to_string(), "BB(20, Close, 2)");
}
#[test]
fn bb_value_formats_correctly() {
let v = BbValue {
upper: 6.0,
middle: 4.0,
lower: 2.0,
};
assert_eq!(v.to_string(), "BB(u: 6, m: 4, l: 2)");
}
#[test]
fn config_formats_correctly() {
let config = BbConfig::builder().length(nz(20)).build();
assert_eq!(config.to_string(), "BbConfig(20, Close, 2)");
}
}
mod eq_and_hash {
use super::*;
use std::collections::HashSet;
#[test]
fn identical_configs_match() {
let a = BbConfig::builder().length(nz(20)).build();
let b = BbConfig::builder().length(nz(20)).build();
let c = BbConfig::builder().length(nz(10)).build();
let mut set = HashSet::new();
set.insert(a);
assert!(set.contains(&b));
assert!(!set.contains(&c));
}
}
}