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 #[inline]
107 fn update(&mut self, value: f64) -> Option<DoubleBollingerOutput> {
108 let o = self.inner.update(value)?;
109 Some(DoubleBollingerOutput {
110 upper_outer: o.upper,
111 upper_inner: o.middle + self.k_inner * o.stddev,
112 middle: o.middle,
113 lower_inner: o.middle - self.k_inner * o.stddev,
114 lower_outer: o.lower,
115 })
116 }
117
118 fn reset(&mut self) {
119 self.inner.reset();
120 }
121
122 #[inline]
123 fn warmup_period(&self) -> usize {
124 self.inner.warmup_period()
125 }
126
127 #[inline]
128 fn is_ready(&self) -> bool {
129 self.inner.is_ready()
130 }
131
132 #[inline]
133 fn name(&self) -> &'static str {
134 "DoubleBollinger"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::BatchExt;
142 use approx::assert_relative_eq;
143
144 #[test]
145 fn rejects_zero_period() {
146 assert!(matches!(
147 DoubleBollinger::new(0, 1.0, 2.0),
148 Err(Error::PeriodZero)
149 ));
150 }
151
152 #[test]
153 fn rejects_non_positive_multiplier() {
154 assert!(matches!(
155 DoubleBollinger::new(20, 0.0, 2.0),
156 Err(Error::NonPositiveMultiplier)
157 ));
158 assert!(matches!(
159 DoubleBollinger::new(20, 1.0, -2.0),
160 Err(Error::NonPositiveMultiplier)
161 ));
162 assert!(matches!(
163 DoubleBollinger::new(20, f64::NAN, 2.0),
164 Err(Error::NonPositiveMultiplier)
165 ));
166 }
167
168 #[test]
169 fn rejects_outer_not_greater_than_inner() {
170 assert!(matches!(
171 DoubleBollinger::new(20, 2.0, 1.0),
172 Err(Error::InvalidPeriod { .. })
173 ));
174 assert!(matches!(
175 DoubleBollinger::new(20, 2.0, 2.0),
176 Err(Error::InvalidPeriod { .. })
177 ));
178 }
179
180 #[test]
181 fn accessors_and_metadata() {
182 let db = DoubleBollinger::classic();
183 let (p, ki, ko) = db.parameters();
184 assert_eq!(p, 20);
185 assert_relative_eq!(ki, 1.0, epsilon = 1e-12);
186 assert_relative_eq!(ko, 2.0, epsilon = 1e-12);
187 assert_eq!(db.warmup_period(), 20);
188 assert_eq!(db.name(), "DoubleBollinger");
189 }
190
191 #[test]
192 fn constant_series_collapses_all_bands() {
193 let mut db = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
194 let last = db
195 .batch(&[5.0_f64; 20])
196 .into_iter()
197 .flatten()
198 .last()
199 .unwrap();
200 assert_relative_eq!(last.middle, 5.0, epsilon = 1e-12);
201 assert_relative_eq!(last.upper_outer, 5.0, epsilon = 1e-12);
202 assert_relative_eq!(last.upper_inner, 5.0, epsilon = 1e-12);
203 assert_relative_eq!(last.lower_inner, 5.0, epsilon = 1e-12);
204 assert_relative_eq!(last.lower_outer, 5.0, epsilon = 1e-12);
205 }
206
207 #[test]
208 fn bands_strictly_ordered_with_dispersion() {
209 let prices: Vec<f64> = (0..80)
210 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
211 .collect();
212 let mut db = DoubleBollinger::classic();
213 for o in db.batch(&prices).into_iter().flatten() {
214 assert!(o.upper_outer >= o.upper_inner);
215 assert!(o.upper_inner >= o.middle);
216 assert!(o.middle >= o.lower_inner);
217 assert!(o.lower_inner >= o.lower_outer);
218 }
219 }
220
221 #[test]
222 fn batch_equals_streaming() {
223 let prices: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.7).collect();
224 let mut a = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
225 let mut b = DoubleBollinger::new(10, 1.0, 2.0).unwrap();
226 assert_eq!(
227 a.batch(&prices),
228 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
229 );
230 }
231
232 #[test]
233 fn reset_clears_state() {
234 let mut db = DoubleBollinger::new(5, 1.0, 2.0).unwrap();
235 db.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
236 assert!(db.is_ready());
237 db.reset();
238 assert!(!db.is_ready());
239 assert_eq!(db.update(1.0), None);
240 }
241
242 #[test]
245 fn inner_band_matches_separate_bollinger() {
246 let prices: Vec<f64> = (0..80)
247 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
248 .collect();
249 let mut db = DoubleBollinger::new(20, 1.0, 2.0).unwrap();
250 let mut bb_inner = BollingerBands::new(20, 1.0).unwrap();
251 let mut bb_outer = BollingerBands::new(20, 2.0).unwrap();
252 for p in &prices {
253 let d = db.update(*p);
254 let i = bb_inner.update(*p);
255 let o = bb_outer.update(*p);
256 if let (Some(d), Some(i), Some(o)) = (d, i, o) {
257 assert_relative_eq!(d.middle, i.middle, epsilon = 1e-9);
258 assert_relative_eq!(d.upper_inner, i.upper, epsilon = 1e-9);
259 assert_relative_eq!(d.lower_inner, i.lower, epsilon = 1e-9);
260 assert_relative_eq!(d.upper_outer, o.upper, epsilon = 1e-9);
261 assert_relative_eq!(d.lower_outer, o.lower, epsilon = 1e-9);
262 }
263 }
264 }
265}