A3: close core test-coverage gaps
Adds the reset tests the audit named as missing (aroon, awesome oscillator, donchian, keltner, williams_r, and both VWAP variants), non-finite-input tests for every scalar indicator that guards is_finite (WMA, RSI, MACD, Bollinger, KAMA), and naive-reference proptests for EMA, RSI and ATR. 189 core tests pass.
This commit is contained in:
@@ -153,4 +153,17 @@ mod tests {
|
||||
assert!((0.0..=100.0).contains(&o.down));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (1..=20)
|
||||
.map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
|
||||
.collect();
|
||||
let mut a = Aroon::new(14).unwrap();
|
||||
a.batch(&candles);
|
||||
assert!(a.is_ready());
|
||||
a.reset();
|
||||
assert!(!a.is_ready());
|
||||
assert_eq!(a.update(candles[0]), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,36 @@ mod tests {
|
||||
Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
/// Independent reference: Wilder ATR computed straight from the definition.
|
||||
fn atr_naive(hlc: &[(f64, f64, f64)], period: usize) -> Vec<Option<f64>> {
|
||||
let n = period as f64;
|
||||
let mut out = Vec::with_capacity(hlc.len());
|
||||
let mut trs: Vec<f64> = Vec::new();
|
||||
let mut avg: Option<f64> = None;
|
||||
let mut prev_close: Option<f64> = None;
|
||||
for &(h, l, cl) in hlc {
|
||||
let tr = match prev_close {
|
||||
None => h - l,
|
||||
Some(pc) => (h - l).max((h - pc).abs()).max((l - pc).abs()),
|
||||
};
|
||||
prev_close = Some(cl);
|
||||
if let Some(a) = avg {
|
||||
let na = (a * (n - 1.0) + tr) / n;
|
||||
avg = Some(na);
|
||||
out.push(Some(na));
|
||||
} else {
|
||||
trs.push(tr);
|
||||
if trs.len() == period {
|
||||
avg = Some(trs.iter().sum::<f64>() / n);
|
||||
out.push(avg);
|
||||
} else {
|
||||
out.push(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(Atr::new(0), Err(Error::PeriodZero)));
|
||||
@@ -187,4 +217,37 @@ mod tests {
|
||||
assert!(v >= 0.0, "ATR must be non-negative: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#![proptest_config(proptest::test_runner::Config::with_cases(48))]
|
||||
#[test]
|
||||
fn atr_matches_naive(
|
||||
period in 1usize..15,
|
||||
bars in proptest::collection::vec(
|
||||
(10.0_f64..1000.0, 0.0_f64..50.0, 0.0_f64..1.0),
|
||||
0..120,
|
||||
),
|
||||
) {
|
||||
// bars: (low, range, close_fraction) -> a valid OHLC candle.
|
||||
let hlc: Vec<(f64, f64, f64)> = bars
|
||||
.iter()
|
||||
.map(|&(low, range, frac)| (low + range, low, low + range * frac))
|
||||
.collect();
|
||||
let candles: Vec<Candle> = hlc.iter().map(|&(h, l, cl)| c(h, l, cl)).collect();
|
||||
let mut atr = Atr::new(period).unwrap();
|
||||
let got = atr.batch(&candles);
|
||||
let want = atr_naive(&hlc, period);
|
||||
proptest::prop_assert_eq!(got.len(), want.len());
|
||||
for (g, w) in got.iter().zip(want.iter()) {
|
||||
match (g, w) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => proptest::prop_assert!(
|
||||
(a - b).abs() <= 1e-9 * a.abs().max(1.0),
|
||||
"got={a} want={b}"
|
||||
),
|
||||
_ => proptest::prop_assert!(false, "warmup mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,4 +114,17 @@ mod tests {
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
|
||||
.collect();
|
||||
let mut ao = AwesomeOscillator::classic();
|
||||
ao.batch(&candles);
|
||||
assert!(ao.is_ready());
|
||||
ao.reset();
|
||||
assert!(!ao.is_ready());
|
||||
assert_eq!(ao.update(candles[0]), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,4 +240,17 @@ mod tests {
|
||||
bb.reset();
|
||||
assert!(!bb.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut bb = BollingerBands::new(5, 2.0).unwrap();
|
||||
let ready = bb.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let last = ready.last().unwrap().unwrap();
|
||||
// Non-finite inputs return the current bands without mutating the window.
|
||||
assert_eq!(bb.update(f64::NAN).unwrap(), last);
|
||||
assert_eq!(bb.update(f64::INFINITY).unwrap(), last);
|
||||
// The window still holds 1..=5, so a real input slides it to 2..=6.
|
||||
let after = bb.update(6.0).unwrap();
|
||||
assert_relative_eq!(after.middle, (2.0 + 3.0 + 4.0 + 5.0 + 6.0) / 5.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,4 +138,17 @@ mod tests {
|
||||
fn rejects_zero_period() {
|
||||
assert!(Donchian::new(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
|
||||
.collect();
|
||||
let mut d = Donchian::new(5).unwrap();
|
||||
d.batch(&candles);
|
||||
assert!(d.is_ready());
|
||||
d.reset();
|
||||
assert!(!d.is_ready());
|
||||
assert_eq!(d.update(candles[0]), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,27 @@ mod tests {
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Independent reference: SMA-seeded EMA computed straight from the definition.
|
||||
fn ema_naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
|
||||
let alpha = 2.0 / (period as f64 + 1.0);
|
||||
let mut out = Vec::with_capacity(prices.len());
|
||||
let mut state: Option<f64> = None;
|
||||
for (i, &p) in prices.iter().enumerate() {
|
||||
if let Some(prev) = state {
|
||||
let v = alpha * p + (1.0 - alpha) * prev;
|
||||
state = Some(v);
|
||||
out.push(Some(v));
|
||||
} else if i + 1 == period {
|
||||
let seed = prices[..period].iter().sum::<f64>() / period as f64;
|
||||
state = Some(seed);
|
||||
out.push(Some(seed));
|
||||
} else {
|
||||
out.push(None);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
assert!(matches!(Ema::new(0), Err(Error::PeriodZero)));
|
||||
@@ -221,4 +242,28 @@ mod tests {
|
||||
assert_eq!(ema.update(f64::NAN), before);
|
||||
assert_eq!(ema.update(f64::INFINITY), before);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#![proptest_config(proptest::test_runner::Config::with_cases(48))]
|
||||
#[test]
|
||||
fn ema_matches_naive(
|
||||
period in 1usize..20,
|
||||
prices in proptest::collection::vec(-1000.0_f64..1000.0, 0..150),
|
||||
) {
|
||||
let mut ema = Ema::new(period).unwrap();
|
||||
let got = ema.batch(&prices);
|
||||
let want = ema_naive(&prices, period);
|
||||
proptest::prop_assert_eq!(got.len(), want.len());
|
||||
for (g, w) in got.iter().zip(want.iter()) {
|
||||
match (g, w) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => proptest::prop_assert!(
|
||||
(a - b).abs() <= 1e-9 * a.abs().max(1.0),
|
||||
"got={a} want={b}"
|
||||
),
|
||||
_ => proptest::prop_assert!(false, "warmup mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,4 +154,15 @@ mod tests {
|
||||
k.reset();
|
||||
assert!(!k.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut k = Kama::classic();
|
||||
k.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
|
||||
let before = k.update(41.0);
|
||||
assert!(before.is_some());
|
||||
// Non-finite inputs return the last state without sliding the window.
|
||||
assert_eq!(k.update(f64::NAN), before);
|
||||
assert_eq!(k.update(f64::INFINITY), before);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +139,17 @@ mod tests {
|
||||
assert!(Keltner::new(20, 10, 0.0).is_err());
|
||||
assert!(Keltner::new(20, 10, -1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
|
||||
.collect();
|
||||
let mut k = Keltner::classic();
|
||||
k.batch(&candles);
|
||||
assert!(k.is_ready());
|
||||
k.reset();
|
||||
assert!(!k.is_ready());
|
||||
assert_eq!(k.update(candles[0]), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,4 +227,16 @@ mod tests {
|
||||
assert!(!macd.is_ready());
|
||||
assert_eq!(macd.update(1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut macd = MacdIndicator::classic();
|
||||
macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
|
||||
let before = macd.value();
|
||||
assert!(before.is_some());
|
||||
// Non-finite inputs return the last value without advancing any EMA.
|
||||
assert_eq!(macd.update(f64::NAN), before);
|
||||
assert_eq!(macd.update(f64::INFINITY), before);
|
||||
assert_eq!(macd.value(), before);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,50 @@ mod tests {
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
/// Independent reference: Wilder RSI computed straight from the definition.
|
||||
fn rsi_naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
|
||||
let n = period as f64;
|
||||
let mut out = vec![None; prices.len()];
|
||||
let mut gains: Vec<f64> = Vec::new();
|
||||
let mut losses: Vec<f64> = Vec::new();
|
||||
let mut avg_gain: Option<f64> = None;
|
||||
let mut avg_loss: Option<f64> = None;
|
||||
let rsi_val = |ag: f64, al: f64| -> f64 {
|
||||
if al == 0.0 {
|
||||
if ag == 0.0 {
|
||||
50.0
|
||||
} else {
|
||||
100.0
|
||||
}
|
||||
} else {
|
||||
100.0 - 100.0 / (1.0 + ag / al)
|
||||
}
|
||||
};
|
||||
for i in 1..prices.len() {
|
||||
let diff = prices[i] - prices[i - 1];
|
||||
let gain = if diff > 0.0 { diff } else { 0.0 };
|
||||
let loss = if diff < 0.0 { -diff } else { 0.0 };
|
||||
if let (Some(ag), Some(al)) = (avg_gain, avg_loss) {
|
||||
let nag = (ag * (n - 1.0) + gain) / n;
|
||||
let nal = (al * (n - 1.0) + loss) / n;
|
||||
avg_gain = Some(nag);
|
||||
avg_loss = Some(nal);
|
||||
out[i] = Some(rsi_val(nag, nal));
|
||||
} else {
|
||||
gains.push(gain);
|
||||
losses.push(loss);
|
||||
if gains.len() == period {
|
||||
let ag = gains.iter().sum::<f64>() / n;
|
||||
let al = losses.iter().sum::<f64>() / n;
|
||||
avg_gain = Some(ag);
|
||||
avg_loss = Some(al);
|
||||
out[i] = Some(rsi_val(ag, al));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_period() {
|
||||
assert!(matches!(Rsi::new(0), Err(Error::PeriodZero)));
|
||||
@@ -244,4 +288,39 @@ mod tests {
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut rsi = Rsi::new(3).unwrap();
|
||||
rsi.batch(&[1.0, 2.0, 3.0, 4.0]);
|
||||
let before = rsi.value();
|
||||
assert!(before.is_some());
|
||||
assert_eq!(rsi.update(f64::NAN), before);
|
||||
assert_eq!(rsi.update(f64::INFINITY), before);
|
||||
assert_eq!(rsi.value(), before);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#![proptest_config(proptest::test_runner::Config::with_cases(48))]
|
||||
#[test]
|
||||
fn rsi_matches_naive(
|
||||
period in 1usize..20,
|
||||
prices in proptest::collection::vec(1.0_f64..1000.0, 0..150),
|
||||
) {
|
||||
let mut rsi = Rsi::new(period).unwrap();
|
||||
let got = rsi.batch(&prices);
|
||||
let want = rsi_naive(&prices, period);
|
||||
proptest::prop_assert_eq!(got.len(), want.len());
|
||||
for (g, w) in got.iter().zip(want.iter()) {
|
||||
match (g, w) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => proptest::prop_assert!(
|
||||
(a - b).abs() < 1e-7,
|
||||
"got={a} want={b}"
|
||||
),
|
||||
_ => proptest::prop_assert!(false, "warmup mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,4 +210,26 @@ mod tests {
|
||||
fn rolling_rejects_zero_period() {
|
||||
assert!(RollingVwap::new(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cumulative_reset_clears_state() {
|
||||
let candles = vec![c(10.0, 1.0), c(20.0, 1.0), c(30.0, 1.0)];
|
||||
let mut v = Vwap::new();
|
||||
v.batch(&candles);
|
||||
assert!(v.is_ready());
|
||||
v.reset();
|
||||
assert!(!v.is_ready());
|
||||
assert_eq!(v.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_reset_clears_state() {
|
||||
let candles: Vec<Candle> = (1..=10).map(|i| c(f64::from(i), 1.0)).collect();
|
||||
let mut v = RollingVwap::new(5).unwrap();
|
||||
v.batch(&candles);
|
||||
assert!(v.is_ready());
|
||||
v.reset();
|
||||
assert!(!v.is_ready());
|
||||
assert_eq!(v.update(candles[0]), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,4 +138,17 @@ mod tests {
|
||||
fn rejects_zero_period() {
|
||||
assert!(WilliamsR::new(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
|
||||
.collect();
|
||||
let mut w = WilliamsR::new(5).unwrap();
|
||||
w.batch(&candles);
|
||||
assert!(w.is_ready());
|
||||
w.reset();
|
||||
assert!(!w.is_ready());
|
||||
assert_eq!(w.update(candles[0]), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input_but_keeps_state() {
|
||||
let mut wma = Wma::new(3).unwrap();
|
||||
wma.update(1.0);
|
||||
wma.update(2.0);
|
||||
let ready = wma.update(3.0).expect("WMA(3) ready after three inputs");
|
||||
// Non-finite inputs return the last value without mutating the window.
|
||||
assert_eq!(wma.update(f64::NAN), Some(ready));
|
||||
assert_eq!(wma.update(f64::INFINITY), Some(ready));
|
||||
// The window still holds 1, 2, 3 -> next real input slides it to 2, 3, 4.
|
||||
assert_relative_eq!(
|
||||
wma.update(4.0).unwrap(),
|
||||
(2.0 * 1.0 + 3.0 * 2.0 + 4.0 * 3.0) / 6.0,
|
||||
epsilon = 1e-12
|
||||
);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#![proptest_config(proptest::test_runner::Config::with_cases(48))]
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user