1use crate::error::{Error, Result};
4use crate::indicators::bollinger::BollingerBands;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct DoubleBollingerOutput {
11 pub upper_outer: f64,
13 pub upper_inner: f64,
15 pub middle: f64,
17 pub lower_inner: f64,
19 pub lower_outer: f64,
21}
22
23#[derive(Debug, Clone)]
57pub struct DoubleBollinger {
58 inner: BollingerBands,
59 k_inner: f64,
60 k_outer: f64,
61}
62
63impl DoubleBollinger {
64 pub fn new(period: usize, k_inner: f64, k_outer: f64) -> Result<Self> {
73 if !k_inner.is_finite() || k_inner <= 0.0 || !k_outer.is_finite() || k_outer <= 0.0 {
74 return Err(Error::NonPositiveMultiplier);
75 }
76 if k_outer <= k_inner {
77 return Err(Error::InvalidPeriod {
78 message: "double bollinger requires k_outer > k_inner",
79 });
80 }
81 Ok(Self {
85 inner: BollingerBands::new(period, k_outer)?,
86 k_inner,
87 k_outer,
88 })
89 }
90
91 pub fn classic() -> Self {
93 Self::new(20, 1.0, 2.0).expect("classic Double Bollinger parameters are valid")
94 }
95
96 pub const fn parameters(&self) -> (usize, f64, f64) {
98 (self.inner.period(), self.k_inner, self.k_outer)
99 }
100}
101
102impl Indicator for DoubleBollinger {
103 type Input = f64;
104 type Output = DoubleBollingerOutput;
105
106 fn update(&mut self, value: f64) -> Option<DoubleBollingerOutput> {
107 let o = self.inner.update(value)?;
108 Some(DoubleBollingerOutput {
109 upper_outer: o.upper,
110 upper_inner: o.middle + self.k_inner * o.stddev,
111 middle: o.middle,
112 lower_inner: o.middle - self.k_inner * o.stddev,
113 lower_outer: o.lower,
114 })
115 }
116
117 fn reset(&mut self) {
118 self.inner.reset();
119 }
120
121 fn warmup_period(&self) -> usize {
122 self.inner.warmup_period()
123 }
124
125 fn is_ready(&self) -> bool {
126 self.inner.is_ready()
127 }
128
129 fn name(&self) -> &'static str {
130 "DoubleBollinger"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 #[test]
141 fn rejects_zero_period() {
142 assert!(matches!(
143 DoubleBollinger::new(0, 1.0, 2.0),
144 Err(Error::PeriodZero)
145 ));
146 }
147
148 #[test]
149 fn rejects_non_positive_multiplier() {
150 assert!(matches!(
151 DoubleBollinger::new(20, 0.0, 2.0),
152 Err(Error::NonPositiveMultiplier)
153 ));
154 assert!(matches!(
155 DoubleBollinger::new(20, 1.0, -2.0),
156 Err(Error::NonPositiveMultiplier)
157 ));
158 assert!(matches!(
159 DoubleBollinger::new(20, f64::NAN, 2.0),
160 Err(Error::NonPositiveMultiplier)
161 ));
162 }
163
164 #[test]
165 fn rejects_outer_not_greater_than_inner() {
166 assert!(matches!(
167 DoubleBollinger::new(20, 2.0, 1.0),
168 Err(Error::InvalidPeriod { .. })
169 ));
170 assert!(matches!(
171 DoubleBollinger::new(20, 2.0, 2.0),
172 Err(Error::InvalidPeriod { .. })
173 ));
174 }
175
176 #[test]
177 fn accessors_and_metadata() {
178 let db = DoubleBollinger::classic();
179 let (p, ki, ko) = db.parameters();
180 assert_eq!(p, 20);
181 assert_relative_eq!(ki, 1.0, epsilon = 1e-12);
182 assert_relative_eq!(ko, 2.0, epsilon = 1e-12);
183 assert_eq!(db.warmup_period(), 20);
184 assert_eq!(db.name(), "DoubleBollinger");
185 }
186
187 #[test]
188 fn constant_series_collapses_all_bands() {
189 let mut db = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
190 let last = db
191 .batch(&[5.0_f64; 20])
192 .into_iter()
193 .flatten()
194 .last()
195 .unwrap();
196 assert_relative_eq!(last.middle, 5.0, epsilon = 1e-12);
197 assert_relative_eq!(last.upper_outer, 5.0, epsilon = 1e-12);
198 assert_relative_eq!(last.upper_inner, 5.0, epsilon = 1e-12);
199 assert_relative_eq!(last.lower_inner, 5.0, epsilon = 1e-12);
200 assert_relative_eq!(last.lower_outer, 5.0, epsilon = 1e-12);
201 }
202
203 #[test]
204 fn bands_strictly_ordered_with_dispersion() {
205 let prices: Vec<f64> = (0..80)
206 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
207 .collect();
208 let mut db = DoubleBollinger::classic();
209 for o in db.batch(&prices).into_iter().flatten() {
210 assert!(o.upper_outer >= o.upper_inner);
211 assert!(o.upper_inner >= o.middle);
212 assert!(o.middle >= o.lower_inner);
213 assert!(o.lower_inner >= o.lower_outer);
214 }
215 }
216
217 #[test]
218 fn batch_equals_streaming() {
219 let prices: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.7).collect();
220 let mut a = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
221 let mut b = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
222 assert_eq!(
223 a.batch(&prices),
224 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
225 );
226 }
227
228 #[test]
229 fn reset_clears_state() {
230 let mut db = DoubleBollinger::new(5, 1.0, 2.0).unwrap();
231 db.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
232 assert!(db.is_ready());
233 db.reset();
234 assert!(!db.is_ready());
235 assert_eq!(db.update(1.0), None);
236 }
237
238 #[test]
241 fn inner_band_matches_separate_bollinger() {
242 let prices: Vec<f64> = (0..80)
243 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
244 .collect();
245 let mut db = DoubleBollinger::new(20, 1.0, 2.0).unwrap();
246 let mut bb_inner = BollingerBands::new(20, 1.0).unwrap();
247 let mut bb_outer = BollingerBands::new(20, 2.0).unwrap();
248 for p in &prices {
249 let d = db.update(*p);
250 let i = bb_inner.update(*p);
251 let o = bb_outer.update(*p);
252 if let (Some(d), Some(i), Some(o)) = (d, i, o) {
253 assert_relative_eq!(d.middle, i.middle, epsilon = 1e-9);
254 assert_relative_eq!(d.upper_inner, i.upper, epsilon = 1e-9);
255 assert_relative_eq!(d.lower_inner, i.lower, epsilon = 1e-9);
256 assert_relative_eq!(d.upper_outer, o.upper, epsilon = 1e-9);
257 assert_relative_eq!(d.lower_outer, o.lower, epsilon = 1e-9);
258 }
259 }
260 }
261}