Skip to main content

nodedb_query/ts_functions/
bollinger.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Bollinger Bands for anomaly detection and volatility analysis.
4//!
5//! - `BOLLINGER_UPPER(period, k)` = SMA + k × σ
6//! - `BOLLINGER_LOWER(period, k)` = SMA − k × σ
7//! - `BOLLINGER_MID(period)` = SMA
8//! - `BOLLINGER_WIDTH(period, k)` = (upper − lower) / mid
9
10use super::stddev::TsStddevAccum;
11
12/// Complete Bollinger Band computation for a sliding window.
13///
14/// Returns `(upper, middle, lower, width)` vectors.
15/// - `period`: window size for SMA and stddev.
16/// - `num_std`: number of standard deviations (typically 2.0).
17/// - Positions with fewer than `period` samples return `None`.
18pub fn ts_bollinger(values: &[f64], period: usize, num_std: f64) -> BollingerResult {
19    let n = values.len();
20    if n == 0 || period == 0 {
21        return BollingerResult::empty(n);
22    }
23
24    let mut upper = Vec::with_capacity(n);
25    let mut middle = Vec::with_capacity(n);
26    let mut lower = Vec::with_capacity(n);
27    let mut width = Vec::with_capacity(n);
28
29    for i in 0..n {
30        if i + 1 < period {
31            upper.push(None);
32            middle.push(None);
33            lower.push(None);
34            width.push(None);
35            continue;
36        }
37
38        let start = i + 1 - period;
39        let window = &values[start..=i];
40
41        // Compute SMA.
42        let valid: Vec<f64> = window.iter().copied().filter(|v| !v.is_nan()).collect();
43        if valid.len() < 2 {
44            upper.push(None);
45            middle.push(None);
46            lower.push(None);
47            width.push(None);
48            continue;
49        }
50
51        let mean = valid.iter().sum::<f64>() / valid.len() as f64;
52
53        // Compute population stddev.
54        let mut accum = TsStddevAccum::new();
55        accum.update_batch(&valid);
56
57        match accum.evaluate_population() {
58            Some(stddev) => {
59                let u = mean + num_std * stddev;
60                let l = mean - num_std * stddev;
61                let w = if mean.abs() > f64::EPSILON {
62                    (u - l) / mean
63                } else {
64                    0.0
65                };
66                upper.push(Some(u));
67                middle.push(Some(mean));
68                lower.push(Some(l));
69                width.push(Some(w));
70            }
71            None => {
72                upper.push(None);
73                middle.push(None);
74                lower.push(None);
75                width.push(None);
76            }
77        }
78    }
79
80    BollingerResult {
81        upper,
82        middle,
83        lower,
84        width,
85    }
86}
87
88/// Individual Bollinger band accessors for SQL function registration.
89pub fn ts_bollinger_upper(values: &[f64], period: usize, num_std: f64) -> Vec<Option<f64>> {
90    ts_bollinger(values, period, num_std).upper
91}
92
93pub fn ts_bollinger_lower(values: &[f64], period: usize, num_std: f64) -> Vec<Option<f64>> {
94    ts_bollinger(values, period, num_std).lower
95}
96
97pub fn ts_bollinger_mid(values: &[f64], period: usize) -> Vec<Option<f64>> {
98    ts_bollinger(values, period, 0.0).middle
99}
100
101pub fn ts_bollinger_width(values: &[f64], period: usize, num_std: f64) -> Vec<Option<f64>> {
102    ts_bollinger(values, period, num_std).width
103}
104
105/// Result of a Bollinger Band computation.
106pub struct BollingerResult {
107    pub upper: Vec<Option<f64>>,
108    pub middle: Vec<Option<f64>>,
109    pub lower: Vec<Option<f64>>,
110    pub width: Vec<Option<f64>>,
111}
112
113impl BollingerResult {
114    fn empty(n: usize) -> Self {
115        Self {
116            upper: vec![None; n],
117            middle: vec![None; n],
118            lower: vec![None; n],
119            width: vec![None; n],
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn basic_bollinger() {
130        // 5 values, period=5, k=2
131        let vals = [2.0, 4.0, 4.0, 4.0, 5.0];
132        let b = ts_bollinger(&vals, 5, 2.0);
133
134        // First 4 should be None.
135        for i in 0..4 {
136            assert!(b.upper[i].is_none());
137        }
138
139        // At position 4: mean = 3.8, stddev ≈ 0.98
140        let mid = b.middle[4].unwrap();
141        let up = b.upper[4].unwrap();
142        let lo = b.lower[4].unwrap();
143        assert!((mid - 3.8).abs() < 1e-10);
144        assert!(up > mid);
145        assert!(lo < mid);
146        assert!((up - mid - (mid - lo)).abs() < 1e-10); // symmetric
147    }
148
149    #[test]
150    fn bollinger_width() {
151        let vals = [10.0, 12.0, 11.0, 13.0, 14.0];
152        let b = ts_bollinger(&vals, 5, 2.0);
153        let w = b.width[4].unwrap();
154        // Width = (upper - lower) / mid, should be positive.
155        assert!(w > 0.0);
156    }
157
158    #[test]
159    fn constant_values() {
160        let vals = [5.0, 5.0, 5.0, 5.0, 5.0];
161        let b = ts_bollinger(&vals, 3, 2.0);
162        // stddev = 0, so upper == lower == mid
163        let mid = b.middle[4].unwrap();
164        let up = b.upper[4].unwrap();
165        let lo = b.lower[4].unwrap();
166        assert!((up - mid).abs() < 1e-12);
167        assert!((lo - mid).abs() < 1e-12);
168    }
169
170    #[test]
171    fn individual_accessors() {
172        let vals = [1.0, 2.0, 3.0, 4.0, 5.0];
173        let up = ts_bollinger_upper(&vals, 3, 2.0);
174        let lo = ts_bollinger_lower(&vals, 3, 2.0);
175        let mid = ts_bollinger_mid(&vals, 3);
176        let w = ts_bollinger_width(&vals, 3, 2.0);
177        assert_eq!(up.len(), 5);
178        assert_eq!(lo.len(), 5);
179        assert_eq!(mid.len(), 5);
180        assert_eq!(w.len(), 5);
181        assert!(up[2].unwrap() > mid[2].unwrap());
182        assert!(lo[2].unwrap() < mid[2].unwrap());
183    }
184
185    #[test]
186    fn empty_input() {
187        let b = ts_bollinger(&[], 5, 2.0);
188        assert!(b.upper.is_empty());
189    }
190}