use crate::error::{Error, Result};
use crate::indicators::bollinger::BollingerBands;
use crate::traits::Indicator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DoubleBollingerOutput {
pub upper_outer: f64,
pub upper_inner: f64,
pub middle: f64,
pub lower_inner: f64,
pub lower_outer: f64,
}
#[derive(Debug, Clone)]
pub struct DoubleBollinger {
inner: BollingerBands,
k_inner: f64,
k_outer: f64,
}
impl DoubleBollinger {
pub fn new(period: usize, k_inner: f64, k_outer: f64) -> Result<Self> {
if !k_inner.is_finite() || k_inner <= 0.0 || !k_outer.is_finite() || k_outer <= 0.0 {
return Err(Error::NonPositiveMultiplier);
}
if k_outer <= k_inner {
return Err(Error::InvalidPeriod {
message: "double bollinger requires k_outer > k_inner",
});
}
Ok(Self {
inner: BollingerBands::new(period, k_outer)?,
k_inner,
k_outer,
})
}
pub fn classic() -> Self {
Self::new(20, 1.0, 2.0).expect("classic Double Bollinger parameters are valid")
}
pub const fn parameters(&self) -> (usize, f64, f64) {
(self.inner.period(), self.k_inner, self.k_outer)
}
}
impl Indicator for DoubleBollinger {
type Input = f64;
type Output = DoubleBollingerOutput;
fn update(&mut self, value: f64) -> Option<DoubleBollingerOutput> {
let o = self.inner.update(value)?;
Some(DoubleBollingerOutput {
upper_outer: o.upper,
upper_inner: o.middle + self.k_inner * o.stddev,
middle: o.middle,
lower_inner: o.middle - self.k_inner * o.stddev,
lower_outer: o.lower,
})
}
fn reset(&mut self) {
self.inner.reset();
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn name(&self) -> &'static str {
"DoubleBollinger"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_zero_period() {
assert!(matches!(
DoubleBollinger::new(0, 1.0, 2.0),
Err(Error::PeriodZero)
));
}
#[test]
fn rejects_non_positive_multiplier() {
assert!(matches!(
DoubleBollinger::new(20, 0.0, 2.0),
Err(Error::NonPositiveMultiplier)
));
assert!(matches!(
DoubleBollinger::new(20, 1.0, -2.0),
Err(Error::NonPositiveMultiplier)
));
assert!(matches!(
DoubleBollinger::new(20, f64::NAN, 2.0),
Err(Error::NonPositiveMultiplier)
));
}
#[test]
fn rejects_outer_not_greater_than_inner() {
assert!(matches!(
DoubleBollinger::new(20, 2.0, 1.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
DoubleBollinger::new(20, 2.0, 2.0),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let db = DoubleBollinger::classic();
let (p, ki, ko) = db.parameters();
assert_eq!(p, 20);
assert_relative_eq!(ki, 1.0, epsilon = 1e-12);
assert_relative_eq!(ko, 2.0, epsilon = 1e-12);
assert_eq!(db.warmup_period(), 20);
assert_eq!(db.name(), "DoubleBollinger");
}
#[test]
fn constant_series_collapses_all_bands() {
let mut db = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
let last = db
.batch(&[5.0_f64; 20])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last.middle, 5.0, epsilon = 1e-12);
assert_relative_eq!(last.upper_outer, 5.0, epsilon = 1e-12);
assert_relative_eq!(last.upper_inner, 5.0, epsilon = 1e-12);
assert_relative_eq!(last.lower_inner, 5.0, epsilon = 1e-12);
assert_relative_eq!(last.lower_outer, 5.0, epsilon = 1e-12);
}
#[test]
fn bands_strictly_ordered_with_dispersion() {
let prices: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
.collect();
let mut db = DoubleBollinger::classic();
for o in db.batch(&prices).into_iter().flatten() {
assert!(o.upper_outer >= o.upper_inner);
assert!(o.upper_inner >= o.middle);
assert!(o.middle >= o.lower_inner);
assert!(o.lower_inner >= o.lower_outer);
}
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.7).collect();
let mut a = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
let mut b = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut db = DoubleBollinger::new(5, 1.0, 2.0).unwrap();
db.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
assert!(db.is_ready());
db.reset();
assert!(!db.is_ready());
assert_eq!(db.update(1.0), None);
}
#[test]
fn inner_band_matches_separate_bollinger() {
let prices: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
.collect();
let mut db = DoubleBollinger::new(20, 1.0, 2.0).unwrap();
let mut bb_inner = BollingerBands::new(20, 1.0).unwrap();
let mut bb_outer = BollingerBands::new(20, 2.0).unwrap();
for p in &prices {
let d = db.update(*p);
let i = bb_inner.update(*p);
let o = bb_outer.update(*p);
if let (Some(d), Some(i), Some(o)) = (d, i, o) {
assert_relative_eq!(d.middle, i.middle, epsilon = 1e-9);
assert_relative_eq!(d.upper_inner, i.upper, epsilon = 1e-9);
assert_relative_eq!(d.lower_inner, i.lower, epsilon = 1e-9);
assert_relative_eq!(d.upper_outer, o.upper, epsilon = 1e-9);
assert_relative_eq!(d.lower_outer, o.lower, epsilon = 1e-9);
}
}
}
}