finance_solution/stocks/ta/
bollinger.rs1use crate::stocks::ta::common::{opt_cell, validate_series, window_stdev, StdevKind};
71use crate::stocks::ta::moving_average::sma;
72use crate::util::error::{require_finite, FinanceError, FinanceResult};
73use crate::util::primitives::PeriodLength;
74use crate::{columns_with_strings, print_table_locale_opt};
75
76#[derive(Clone, Copy, Debug, PartialEq)]
78pub struct BollingerParams {
79 pub period: usize,
80 pub num_std: f64,
82 pub stdev: StdevKind,
84}
85
86impl BollingerParams {
87 pub const fn standard() -> Self {
89 Self {
90 period: 20,
91 num_std: 2.0,
92 stdev: StdevKind::Sample,
93 }
94 }
95
96 pub const fn new(period: usize, num_std: f64) -> Self {
98 Self {
99 period,
100 num_std,
101 stdev: StdevKind::Sample,
102 }
103 }
104
105 pub const fn with_stdev(period: usize, num_std: f64, stdev: StdevKind) -> Self {
106 Self {
107 period,
108 num_std,
109 stdev,
110 }
111 }
112}
113
114#[derive(Clone, Copy, Debug, PartialEq)]
116pub struct ValidatedBollinger {
117 params: BollingerParams,
118}
119
120impl ValidatedBollinger {
121 pub fn new(params: BollingerParams) -> FinanceResult<Self> {
122 PeriodLength::new(params.period)?;
123 require_finite("num_std", params.num_std)?;
124 if params.num_std < 0.0 {
125 return Err(FinanceError::Unsolvable {
126 message: "Bollinger num_std must be non-negative",
127 });
128 }
129 match params.stdev {
130 StdevKind::Sample if params.period < 2 => {
131 return Err(FinanceError::Unsolvable {
132 message: "Bollinger sample stdev requires period >= 2",
133 });
134 }
135 StdevKind::Population if params.period < 1 => {
136 return Err(FinanceError::Unsolvable {
137 message: "Bollinger population stdev requires period >= 1",
138 });
139 }
140 _ => {}
141 }
142 Ok(Self { params })
143 }
144
145 pub fn params(self) -> BollingerParams {
146 self.params
147 }
148
149 pub fn compute(self, closes: &[f64]) -> FinanceResult<BollingerSeries> {
150 bollinger_validated(closes, self)
151 }
152}
153
154#[derive(Clone, Debug, PartialEq)]
156pub struct BollingerSeries {
157 pub middle: Vec<Option<f64>>,
158 pub upper: Vec<Option<f64>>,
159 pub lower: Vec<Option<f64>>,
160 pub pct_b: Vec<Option<f64>>,
162 pub params: BollingerParams,
163}
164
165#[derive(Clone, Debug)]
167pub struct BollingerSolution {
168 series: BollingerSeries,
169 closes: Vec<f64>,
170 formula: String,
171 symbolic_formula: String,
172}
173
174impl BollingerSolution {
175 pub fn series(&self) -> &BollingerSeries {
176 &self.series
177 }
178 pub fn formula(&self) -> &str {
179 &self.formula
180 }
181 pub fn symbolic_formula(&self) -> &str {
182 &self.symbolic_formula
183 }
184
185 pub fn print_table(&self) {
192 self.print_table_locale_opt(None, None);
193 }
194
195 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
196 self.print_table_locale_opt(Some(locale), Some(precision));
197 }
198
199 fn print_table_locale_opt(
200 &self,
201 locale: Option<&num_format::Locale>,
202 precision: Option<usize>,
203 ) {
204 let columns = columns_with_strings(&[
205 ("period", "i", true),
206 ("close", "f", true),
207 ("middle", "f", true),
208 ("upper", "f", true),
209 ("lower", "f", true),
210 ("pct_b", "f", true),
211 ]);
212 let data = self
213 .closes
214 .iter()
215 .enumerate()
216 .map(|(i, c)| {
217 vec![
218 i.to_string(),
219 c.to_string(),
220 opt_cell(self.series.middle[i]),
221 opt_cell(self.series.upper[i]),
222 opt_cell(self.series.lower[i]),
223 opt_cell(self.series.pct_b[i]),
224 ]
225 })
226 .collect();
227 print_table_locale_opt(&columns, data, locale, precision);
228 }
229}
230
231pub fn bollinger(closes: &[f64], params: BollingerParams) -> FinanceResult<BollingerSeries> {
232 ValidatedBollinger::new(params)?.compute(closes)
233}
234
235pub fn bollinger_solution(
251 closes: &[f64],
252 params: BollingerParams,
253) -> FinanceResult<BollingerSolution> {
254 let series = bollinger(closes, params)?;
255 let stdev_label = match params.stdev {
256 StdevKind::Sample => "sample_stdev(n-1)",
257 StdevKind::Population => "population_stdev(n)",
258 };
259 let formula = format!(
260 "mid = SMA({}); upper/lower = mid ± {} * {}; %B = (close-lower)/(upper-lower)",
261 params.period, params.num_std, stdev_label
262 );
263 let symbolic = format!(
264 "mid = sma(close,n); band = k * stdev_{:?}(window); upper = mid+band; lower = mid-band",
265 params.stdev
266 );
267 Ok(BollingerSolution {
268 series,
269 closes: closes.to_vec(),
270 formula,
271 symbolic_formula: symbolic,
272 })
273}
274
275fn bollinger_validated(closes: &[f64], v: ValidatedBollinger) -> FinanceResult<BollingerSeries> {
276 validate_series("close", closes)?;
277 let p = v.params;
278 let mid = sma(closes, p.period)?;
279 let n = closes.len();
280 let mut upper = vec![None; n];
281 let mut lower = vec![None; n];
282 let mut pct_b = vec![None; n];
283 for i in 0..n {
284 if let Some(m) = mid[i] {
285 let start = i + 1 - p.period;
286 let window = &closes[start..=i];
287 let sd = window_stdev(window, p.stdev).unwrap_or(0.0);
288 let band = p.num_std * sd;
289 let u = m + band;
290 let l = m - band;
291 upper[i] = Some(u);
292 lower[i] = Some(l);
293 let width = u - l;
294 if width > 0.0 {
295 pct_b[i] = Some((closes[i] - l) / width);
296 }
297 }
298 }
299 Ok(BollingerSeries {
300 middle: mid,
301 upper,
302 lower,
303 pct_b,
304 params: p,
305 })
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn constant_series_zero_width() {
314 let c = vec![10.0; 25];
315 let s = bollinger(&c, BollingerParams::standard()).unwrap();
316 assert_eq!(s.middle[19], Some(10.0));
317 assert_eq!(s.upper[19], Some(10.0));
318 assert_eq!(s.pct_b[19], None);
319 }
320
321 #[test]
322 fn pct_b_midpoint_on_middle() {
323 let closes: Vec<f64> = (0..40).map(|i| 100.0 + (i as f64 - 20.0) * 0.1).collect();
325 let s = bollinger(&closes, BollingerParams::standard()).unwrap();
326 let i = 30;
327 let mid = s.middle[i].unwrap();
328 let u = s.upper[i].unwrap();
329 let l = s.lower[i].unwrap();
330 let pct = (mid - l) / (u - l);
332 assert!((pct - 0.5).abs() < 1e-9);
333 }
334
335 #[test]
336 fn sample_wider_than_population() {
337 let closes: Vec<f64> = (1..=30).map(|x| x as f64).collect();
338 let s = bollinger(&closes, BollingerParams::standard()).unwrap();
339 let p = bollinger(
340 &closes,
341 BollingerParams::with_stdev(20, 2.0, StdevKind::Population),
342 )
343 .unwrap();
344 let su = s.upper[29].unwrap();
345 let pu = p.upper[29].unwrap();
346 assert!(su + 1e-12 >= pu);
347 }
348}