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