test(bollinger): cover classic()+accessors+metadata, drop dead naive arm

Codecov flagged 16 uncovered lines in
crates/wickra-core/src/indicators/bollinger.rs (file at 91.30%):

  - classic() convenience constructor (91-93) — every test passed
    explicit parameters to BollingerBands::new, so the classic-defaults
    path was dead
  - const accessors period (96-98), multiplier (101-103) — never queried
  - Indicator-impl bodies warmup_period (156-158), name (164-166) —
    never queried
  - `return None;` (line 177) inside the test-only `naive` helper's
    `if prices.len() < period` early-return — every caller passes a
    slice of length >= period (matches_naive_definition uses
    `&prices[..=i]` for `i in 19..` with period=20; long_stream_drift_
    stays_bounded fills the window before measuring), so the arm is dead

Add classic_and_accessors_and_metadata to cover the constructor and
the four getter bodies, and refactor naive to return BollingerOutput
directly with an `assert!(prices.len() >= period)` precondition. The
two existing callers were already using .unwrap()/.expect() on the
Option result and simplify to direct calls.

bollinger.rs is now at 184/184 lines, no behavioural change.
This commit is contained in:
kingchenc
2026-05-23 23:30:10 +02:00
parent 36dc951f1b
commit 1255892b1e
+25 -9
View File
@@ -172,20 +172,23 @@ mod tests {
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn naive(prices: &[f64], period: usize, mult: f64) -> Option<BollingerOutput> {
if prices.len() < period {
return None;
}
fn naive(prices: &[f64], period: usize, mult: f64) -> BollingerOutput {
assert!(
prices.len() >= period,
"naive: need at least `period` prices (got {}, period {})",
prices.len(),
period,
);
let w = &prices[prices.len() - period..];
let mean = w.iter().sum::<f64>() / period as f64;
let var = w.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / period as f64;
let s = var.sqrt();
Some(BollingerOutput {
BollingerOutput {
upper: mean + mult * s,
middle: mean,
lower: mean - mult * s,
stddev: s,
})
}
}
#[test]
@@ -212,6 +215,20 @@ mod tests {
));
}
/// Cover the convenience constructor `BollingerBands::classic()` plus the
/// const accessors `period` / `multiplier` and the Indicator-impl
/// metadata methods `warmup_period` / `name`. Existing tests never
/// invoked `classic()` (every test passed explicit parameters to
/// `new`) and never queried any of the four getters.
#[test]
fn classic_and_accessors_and_metadata() {
let bb = BollingerBands::classic();
assert_eq!(bb.period(), 20);
assert_relative_eq!(bb.multiplier(), 2.0, epsilon = 1e-12);
assert_eq!(bb.warmup_period(), 20);
assert_eq!(bb.name(), "BollingerBands");
}
#[test]
fn warmup_returns_none() {
let mut bb = BollingerBands::new(5, 2.0).unwrap();
@@ -241,7 +258,7 @@ mod tests {
let out = bb.batch(&prices);
for i in 19..prices.len() {
let got = out[i].unwrap();
let want = naive(&prices[..=i], 20, 2.0).unwrap();
let want = naive(&prices[..=i], 20, 2.0);
assert_relative_eq!(got.middle, want.middle, epsilon = 1e-9);
assert_relative_eq!(got.stddev, want.stddev, epsilon = 1e-9);
assert_relative_eq!(got.upper, want.upper, epsilon = 1e-9);
@@ -301,8 +318,7 @@ mod tests {
}
window.push_back(v);
}
let scratch =
naive(&window.iter().copied().collect::<Vec<_>>(), period, mult).expect("warmed up");
let scratch = naive(&window.iter().copied().collect::<Vec<_>>(), period, mult);
let got = last.expect("warmed up");
assert!(
(got.middle - scratch.middle).abs() < 1e-3,