Skip to main content

quantedge_ta/
bb.rs

1use std::{
2    fmt::Display,
3    hash::{Hash, Hasher},
4    num::NonZero,
5};
6
7use crate::{
8    Indicator, IndicatorConfig, IndicatorConfigBuilder, Ohlcv, Price, PriceSource,
9    price_window::PriceWindow,
10};
11
12/// Standard deviation multiplier for Bollinger Bands.
13///
14/// Wraps a positive, non-NaN `f64`. The constructor panics if the value is
15/// zero, negative, or NaN.
16///
17/// Defaults to `2.0` (the standard Bollinger Bands setting).
18///
19/// Implements `Eq` and `Hash` via bit-level comparison, which is safe because
20/// NaN is rejected at construction.
21#[derive(Clone, Copy, Debug)]
22pub struct StdDev(f64);
23
24impl StdDev {
25    /// Creates a new standard deviation multiplier.
26    ///
27    /// # Panics
28    ///
29    /// Panics if `value` is zero, negative, or NaN.
30    #[must_use]
31    pub fn new(value: f64) -> Self {
32        assert!(!value.is_nan(), "std_dev must not be NaN");
33        assert!(value > 0.0, "std_dev must be positive");
34        Self(value)
35    }
36
37    #[must_use]
38    pub fn value(self) -> f64 {
39        self.0
40    }
41}
42
43impl PartialEq for StdDev {
44    fn eq(&self, other: &Self) -> bool {
45        self.0.to_bits() == other.0.to_bits()
46    }
47}
48
49impl Eq for StdDev {}
50
51impl Hash for StdDev {
52    fn hash<H: Hasher>(&self, state: &mut H) {
53        self.0.to_bits().hash(state);
54    }
55}
56
57impl Default for StdDev {
58    fn default() -> Self {
59        Self(2.0)
60    }
61}
62
63/// Configuration for Bollinger Bands ([`Bb`]).
64///
65/// # Convergence
66///
67/// Bollinger Bands use an SMA for the middle band. Like SMA, values are exact
68/// once the window is full, there is no warm-up bias to suppress.
69///
70/// # Example
71///
72/// ```
73/// use quantedge_ta::{BbConfig, IndicatorConfig, IndicatorConfigBuilder};
74/// use std::num::NonZero;
75///
76/// // Default: length 20, close, 2.0 std devs
77/// let config = BbConfig::builder()
78///     .length(NonZero::new(20).unwrap())
79///     .build();
80///
81/// assert_eq!(config.length(), 20);
82/// ```
83#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
84pub struct BbConfig {
85    length: usize,
86    source: PriceSource,
87    std_dev: StdDev,
88}
89
90impl IndicatorConfig for BbConfig {
91    type Builder = BbConfigBuilder;
92
93    #[inline]
94    fn builder() -> Self::Builder {
95        BbConfigBuilder::new()
96    }
97
98    #[inline]
99    fn length(&self) -> usize {
100        self.length
101    }
102
103    #[inline]
104    fn source(&self) -> &PriceSource {
105        &self.source
106    }
107}
108
109impl BbConfig {
110    #[inline]
111    #[must_use]
112    pub fn std_dev(&self) -> StdDev {
113        self.std_dev
114    }
115
116    /// BB(20, Close, 2σ) — the standard Bollinger Bands setting.
117    #[allow(clippy::missing_panics_doc)]
118    #[must_use]
119    pub fn default_20() -> Self {
120        Self::builder().length(NonZero::new(20).unwrap()).build()
121    }
122
123    /// BB with custom length, close price, 2σ.
124    #[must_use]
125    pub fn close(length: NonZero<usize>) -> Self {
126        Self::builder().length(length).build()
127    }
128}
129
130impl Display for BbConfig {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(
133            f,
134            "BbConfig({}, {}, {})",
135            self.length,
136            self.source,
137            self.std_dev.value()
138        )
139    }
140}
141
142/// Builder for [`BbConfig`].
143///
144/// Defaults: source = [`PriceSource::Close`],
145/// `std_dev` = `2.0`.
146/// Length must be set before calling
147/// [`build`](IndicatorConfigBuilder::build).
148pub struct BbConfigBuilder {
149    length: Option<usize>,
150    source: PriceSource,
151    std_dev: StdDev,
152}
153
154impl BbConfigBuilder {
155    fn new() -> Self {
156        Self {
157            length: None,
158            source: PriceSource::Close,
159            std_dev: StdDev(2.0),
160        }
161    }
162
163    #[inline]
164    #[must_use]
165    pub fn std_dev(mut self, std_dev: StdDev) -> Self {
166        self.std_dev = std_dev;
167        self
168    }
169}
170
171impl IndicatorConfigBuilder<BbConfig> for BbConfigBuilder {
172    #[inline]
173    fn length(mut self, length: NonZero<usize>) -> Self {
174        self.length.replace(length.get());
175        self
176    }
177
178    #[inline]
179    fn source(mut self, source: PriceSource) -> Self {
180        self.source = source;
181        self
182    }
183
184    #[inline]
185    fn build(self) -> BbConfig {
186        BbConfig {
187            length: self.length.expect("length is required"),
188            source: self.source,
189            std_dev: self.std_dev,
190        }
191    }
192}
193
194/// Bollinger Bands output: upper, middle, and lower bands.
195///
196/// The middle band is the SMA. Upper and lower bands are offset by
197/// `std_dev × σ`, where `σ` is the population standard deviation of the window.
198///
199/// ```text
200/// upper  = SMA + k × σ
201/// middle = SMA
202/// lower  = SMA − k × σ
203/// ```
204#[derive(Debug, Clone, Copy, PartialEq)]
205pub struct BbValue {
206    upper: Price,
207    middle: Price,
208    lower: Price,
209}
210
211impl BbValue {
212    /// Upper band: `SMA + k × σ`.
213    #[inline]
214    #[must_use]
215    pub fn upper(&self) -> Price {
216        self.upper
217    }
218
219    /// Middle band: SMA of the window.
220    #[inline]
221    #[must_use]
222    pub fn middle(&self) -> Price {
223        self.middle
224    }
225
226    /// Lower band: `SMA − k × σ`.
227    #[inline]
228    #[must_use]
229    pub fn lower(&self) -> Price {
230        self.lower
231    }
232
233    /// Band width: `upper − lower`.
234    ///
235    /// Useful for measuring volatility. Narrow width indicates
236    /// consolidation (Bollinger squeeze); wide width indicates
237    /// high volatility.
238    #[inline]
239    #[must_use]
240    pub fn width(&self) -> f64 {
241        self.upper - self.lower
242    }
243}
244
245impl Display for BbValue {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        write!(
248            f,
249            "BB(u: {}, m: {}, l: {})",
250            self.upper, self.middle, self.lower
251        )
252    }
253}
254
255/// Bollinger Bands (BB).
256///
257/// A volatility indicator consisting of three bands: a simple moving average
258/// (middle) with upper and lower bands offset by a configurable number of
259/// standard deviations.
260///
261/// Uses a running sum and sum of squares for O(1) updates per tick. The only
262/// non-constant operation is `sqrt` for the standard deviation, which is
263/// unavoidable.
264///
265/// Supports live repainting: feeding a bar with the same `open_time` replaces
266/// the current value without advancing the window.
267///
268/// # Example
269///
270/// ```
271/// use quantedge_ta::{Bb, BbConfig, Indicator, IndicatorConfig,
272///     IndicatorConfigBuilder};
273/// use std::num::NonZero;
274/// # use quantedge_ta::{Ohlcv, Price, Timestamp};
275/// #
276/// # struct Bar(f64, u64);
277/// # impl Ohlcv for Bar {
278/// #     fn open(&self) -> Price { self.0 }
279/// #     fn high(&self) -> Price { self.0 }
280/// #     fn low(&self) -> Price { self.0 }
281/// #     fn close(&self) -> Price { self.0 }
282/// #     fn open_time(&self) -> Timestamp { self.1 }
283/// # }
284///
285/// let config = BbConfig::builder()
286///     .length(NonZero::new(20).unwrap())
287///     .build();
288/// let mut bb = Bb::new(config);
289///
290/// // Feed bars...
291/// # for i in 1..=19 { bb.compute(&Bar(100.0, i)); }
292///
293/// if let Some(value) = bb.compute(&Bar(100.0, 20)) {
294///     println!("upper: {}, middle: {}, lower: {}",
295///         value.upper(), value.middle(), value.lower());
296/// }
297/// ```
298#[derive(Clone, Debug)]
299pub struct Bb {
300    config: BbConfig,
301    length_reciprocal: f64,
302    std_dev_multiplier: f64,
303    window: PriceWindow,
304    current: Option<BbValue>,
305}
306
307impl Indicator for Bb {
308    type Config = BbConfig;
309    type Output = BbValue;
310
311    fn new(config: Self::Config) -> Self {
312        let window = PriceWindow::new(config.length, config.source);
313
314        Self {
315            config,
316            #[allow(clippy::cast_precision_loss)]
317            length_reciprocal: 1.0 / config.length as f64,
318            std_dev_multiplier: config.std_dev.0,
319            window,
320            current: None,
321        }
322    }
323
324    fn compute(&mut self, ohlcv: &impl Ohlcv) -> Option<Self::Output> {
325        self.window.add(ohlcv);
326
327        self.current = match (self.window.sum(), self.window.sum_of_squares()) {
328            (Some(sum), Some(sum_of_squares)) => {
329                let mean = sum * self.length_reciprocal;
330
331                // Variance = E[X^2] - (E[X])^2 = (sum_of_squares / n) - mean^2
332                let variance = sum_of_squares.mul_add(self.length_reciprocal, -(mean * mean));
333                let std_dev = variance.max(0.0).sqrt() * self.std_dev_multiplier;
334
335                Some(Self::Output {
336                    upper: mean + std_dev,
337                    middle: mean,
338                    lower: mean - std_dev,
339                })
340            }
341            _ => None,
342        };
343
344        self.current
345    }
346
347    fn value(&self) -> Option<Self::Output> {
348        self.current
349    }
350}
351
352impl Display for Bb {
353    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354        write!(
355            f,
356            "BB({}, {}, {})",
357            self.config.length, self.config.source, self.std_dev_multiplier,
358        )
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use crate::test_util::Bar;
366    use std::num::NonZero;
367
368    fn bb(length: usize) -> Bb {
369        Bb::new(
370            BbConfig::builder()
371                .length(NonZero::new(length).unwrap())
372                .build(),
373        )
374    }
375
376    fn bb_with_std_dev(length: usize, std_dev: f64) -> Bb {
377        Bb::new(
378            BbConfig::builder()
379                .length(NonZero::new(length).unwrap())
380                .std_dev(StdDev::new(std_dev))
381                .build(),
382        )
383    }
384
385    fn bar(close: f64, time: u64) -> Bar {
386        Bar::new(0.0, 0.0, 0.0, close).at(time)
387    }
388
389    fn assert_bb(value: Option<BbValue>, upper: f64, middle: f64, lower: f64) {
390        let v = value.expect("expected Some(BbValue)");
391        assert!(
392            (v.upper() - upper).abs() < 1e-10,
393            "upper: expected {upper}, got {}",
394            v.upper()
395        );
396        assert!(
397            (v.middle() - middle).abs() < 1e-10,
398            "middle: expected {middle}, got {}",
399            v.middle()
400        );
401        assert!(
402            (v.lower() - lower).abs() < 1e-10,
403            "lower: expected {lower}, got {}",
404            v.lower()
405        );
406    }
407
408    mod filling {
409        use super::*;
410
411        #[test]
412        fn none_until_window_full() {
413            let mut bb = bb(3);
414            assert!(bb.compute(&bar(10.0, 1)).is_none());
415            assert!(bb.compute(&bar(20.0, 2)).is_none());
416        }
417
418        #[test]
419        fn returns_value_when_full() {
420            let mut bb = bb(2);
421            bb.compute(&bar(3.0, 1));
422            assert!(bb.compute(&bar(5.0, 2)).is_some());
423        }
424    }
425
426    mod computation {
427        use super::*;
428
429        #[test]
430        fn basic_bands() {
431            // window [3, 5], std_dev=2
432            // mean=4, variance=1, σ=1
433            // upper=6, middle=4, lower=2
434            let mut bb = bb(2);
435            bb.compute(&bar(3.0, 1));
436            assert_bb(bb.compute(&bar(5.0, 2)), 6.0, 4.0, 2.0);
437        }
438
439        #[test]
440        fn constant_input_zero_width() {
441            // All values equal → variance=0 → bands collapse
442            let mut bb = bb(3);
443            bb.compute(&bar(10.0, 1));
444            bb.compute(&bar(10.0, 2));
445            assert_bb(bb.compute(&bar(10.0, 3)), 10.0, 10.0, 10.0);
446        }
447
448        #[test]
449        fn bands_are_symmetric() {
450            let mut bb = bb(2);
451            bb.compute(&bar(3.0, 1));
452            let v = bb.compute(&bar(5.0, 2)).unwrap();
453            let upper_dist = v.upper() - v.middle();
454            let lower_dist = v.middle() - v.lower();
455            assert!((upper_dist - lower_dist).abs() < 1e-10);
456        }
457    }
458
459    mod sliding {
460        use super::*;
461
462        #[test]
463        fn updates_on_advance() {
464            // [3, 5] → [5, 7]
465            // mean=6, variance=1, σ=1
466            // upper=8, middle=6, lower=4
467            let mut bb = bb(2);
468            bb.compute(&bar(3.0, 1));
469            bb.compute(&bar(5.0, 2));
470            assert_bb(bb.compute(&bar(7.0, 3)), 8.0, 6.0, 4.0);
471        }
472    }
473
474    mod repaint {
475        use super::*;
476
477        #[test]
478        fn replaces_current_bar() {
479            // [3, 5] then repaint → [3, 7]
480            // mean=5, variance=4, σ=2
481            // upper=9, middle=5, lower=1
482            let mut bb = bb(2);
483            bb.compute(&bar(3.0, 1));
484            bb.compute(&bar(5.0, 2));
485            assert_bb(bb.compute(&bar(7.0, 2)), 9.0, 5.0, 1.0);
486        }
487
488        #[test]
489        fn repaint_during_filling() {
490            let mut bb = bb(2);
491            bb.compute(&bar(3.0, 1));
492            bb.compute(&bar(4.0, 1)); // repaint
493            assert!(bb.compute(&bar(4.0, 1)).is_none()); // still filling
494            // Advance to convergence after repaint
495            // [4, 6], mean=5, var=1, σ=1, k=2 → (7, 5, 3)
496            assert_bb(bb.compute(&bar(6.0, 2)), 7.0, 5.0, 3.0);
497        }
498    }
499
500    mod std_dev_multiplier {
501        use super::*;
502
503        #[test]
504        fn multiplier_of_one() {
505            // [3, 5], std_dev=1 → σ=1
506            // upper=5, middle=4, lower=3
507            let mut bb = bb_with_std_dev(2, 1.0);
508            bb.compute(&bar(3.0, 1));
509            assert_bb(bb.compute(&bar(5.0, 2)), 5.0, 4.0, 3.0);
510        }
511
512        #[test]
513        fn fractional_multiplier() {
514            // [3, 5], std_dev=1.5 → σ=1
515            // upper=4+1.5=5.5, middle=4, lower=4-1.5=2.5
516            let mut bb = bb_with_std_dev(2, 1.5);
517            bb.compute(&bar(3.0, 1));
518            assert_bb(bb.compute(&bar(5.0, 2)), 5.5, 4.0, 2.5);
519        }
520
521        #[test]
522        fn wider_multiplier_wider_bands() {
523            let mut bb1 = bb_with_std_dev(2, 1.0);
524            let mut bb2 = bb_with_std_dev(2, 3.0);
525
526            bb1.compute(&bar(3.0, 1));
527            bb2.compute(&bar(3.0, 1));
528
529            let v1 = bb1.compute(&bar(5.0, 2)).unwrap();
530            let v2 = bb2.compute(&bar(5.0, 2)).unwrap();
531
532            assert!(v2.width() > v1.width());
533        }
534    }
535
536    mod width {
537        use super::*;
538
539        #[test]
540        fn equals_upper_minus_lower() {
541            let mut bb = bb(2);
542            bb.compute(&bar(3.0, 1));
543            let v = bb.compute(&bar(5.0, 2)).unwrap();
544            assert!((v.width() - (v.upper() - v.lower())).abs() < 1e-10);
545        }
546
547        #[test]
548        fn zero_for_constant_input() {
549            let mut bb = bb(2);
550            bb.compute(&bar(10.0, 1));
551            let v = bb.compute(&bar(10.0, 2)).unwrap();
552            assert!((v.width()).abs() < 1e-10);
553        }
554    }
555
556    mod value {
557        use super::*;
558
559        #[test]
560        fn returns_last_computed() {
561            let mut bb = bb(2);
562            bb.compute(&bar(3.0, 1));
563            bb.compute(&bar(5.0, 2));
564            assert_eq!(bb.value(), bb.compute(&bar(5.0, 2)));
565        }
566
567        #[test]
568        fn none_before_first_value() {
569            let bb = bb(2);
570            assert!(bb.value().is_none());
571        }
572    }
573
574    mod config {
575        use super::*;
576
577        #[test]
578        fn default_std_dev_is_two() {
579            let config = BbConfig::builder()
580                .length(NonZero::new(20).unwrap())
581                .build();
582            assert!((config.std_dev().value() - 2.0).abs() < f64::EPSILON);
583        }
584
585        #[test]
586        fn default_source_is_close() {
587            let config = BbConfig::builder()
588                .length(NonZero::new(20).unwrap())
589                .build();
590            assert_eq!(*config.source(), PriceSource::Close);
591        }
592
593        #[test]
594        #[should_panic(expected = "length is required")]
595        fn panics_without_length() {
596            let _ = BbConfig::builder().build();
597        }
598
599        #[test]
600        #[should_panic(expected = "std_dev must be positive")]
601        fn std_dev_rejects_zero() {
602            let _ = StdDev::new(0.0);
603        }
604
605        #[test]
606        #[should_panic(expected = "std_dev must be positive")]
607        fn std_dev_rejects_negative() {
608            let _ = StdDev::new(-1.0);
609        }
610
611        #[test]
612        #[should_panic(expected = "std_dev must not be NaN")]
613        fn std_dev_rejects_nan() {
614            let _ = StdDev::new(f64::NAN);
615        }
616    }
617
618    mod clone {
619        use super::*;
620
621        #[test]
622        fn produces_independent_state() {
623            let mut bb = bb(3);
624            bb.compute(&bar(10.0, 1));
625            bb.compute(&bar(20.0, 2));
626
627            let mut cloned = bb.clone();
628
629            // Advance original to convergence
630            // [10, 20, 30], mean=20, var=200/3, σ=√(200/3), k=2
631            assert!(bb.compute(&bar(30.0, 3)).is_some());
632
633            // Clone still has no value (only saw 2 bars)
634            assert_eq!(cloned.value(), None);
635
636            // Clone converges independently with different data
637            assert!(cloned.compute(&bar(90.0, 3)).is_some());
638            assert!(
639                (bb.value().unwrap().middle() - cloned.value().unwrap().middle()).abs() > 1e-10
640            );
641        }
642    }
643
644    mod price_source {
645        use super::*;
646
647        #[test]
648        fn hl2_source() {
649            let mut bb = Bb::new(
650                BbConfig::builder()
651                    .length(NonZero::new(2).unwrap())
652                    .source(PriceSource::HL2)
653                    .build(),
654            );
655            // HL2 = (high + low) / 2
656            bb.compute(&Bar::new(0.0, 20.0, 10.0, 0.0).at(1)); // HL2 = 15
657            let v = bb.compute(&Bar::new(0.0, 30.0, 20.0, 0.0).at(2)).unwrap(); // HL2 = 25
658            // [15, 25], mean=20
659            assert!((v.middle() - 20.0).abs() < 1e-10);
660        }
661    }
662
663    mod display {
664        use super::*;
665
666        #[test]
667        fn bb_formats_correctly() {
668            let bb = bb(20);
669            assert_eq!(bb.to_string(), "BB(20, Close, 2)");
670        }
671
672        #[test]
673        fn bb_value_formats_correctly() {
674            let v = BbValue {
675                upper: 6.0,
676                middle: 4.0,
677                lower: 2.0,
678            };
679            assert_eq!(v.to_string(), "BB(u: 6, m: 4, l: 2)");
680        }
681
682        #[test]
683        fn config_formats_correctly() {
684            let config = BbConfig::builder()
685                .length(NonZero::new(20).unwrap())
686                .build();
687            assert_eq!(config.to_string(), "BbConfig(20, Close, 2)");
688        }
689    }
690
691    mod eq_and_hash {
692        use super::*;
693        use std::collections::HashSet;
694
695        #[test]
696        fn identical_configs_match() {
697            let a = BbConfig::builder()
698                .length(NonZero::new(20).unwrap())
699                .build();
700            let b = BbConfig::builder()
701                .length(NonZero::new(20).unwrap())
702                .build();
703            let c = BbConfig::builder()
704                .length(NonZero::new(10).unwrap())
705                .build();
706
707            let mut set = HashSet::new();
708            set.insert(a);
709
710            assert!(set.contains(&b));
711            assert!(!set.contains(&c));
712        }
713    }
714}