nodedb_query/ts_functions/
bollinger.rs1use super::stddev::TsStddevAccum;
11
12pub 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 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 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
88pub 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
105pub 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 let vals = [2.0, 4.0, 4.0, 4.0, 5.0];
132 let b = ts_bollinger(&vals, 5, 2.0);
133
134 for i in 0..4 {
136 assert!(b.upper[i].is_none());
137 }
138
139 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); }
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 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 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}