From e85334a2e90d5137ac9acc977898abbfd581d723 Mon Sep 17 00:00:00 2001 From: kingchenc Date: Tue, 26 May 2026 21:08:50 +0200 Subject: [PATCH] test(cold-paths): cover 3 lines in mama/rsi/sine_wave (#58) * test(cold-paths): cover phase fallback in mama/sine_wave and rsi naive helper saturation * fix(rsi): drop redundant closure in test helper --- crates/wickra-core/src/indicators/mama.rs | 12 ++++++------ crates/wickra-core/src/indicators/rsi.rs | 14 ++++++++++++++ crates/wickra-core/src/indicators/sine_wave.rs | 10 ++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/crates/wickra-core/src/indicators/mama.rs b/crates/wickra-core/src/indicators/mama.rs index bcd32594..e870d3c2 100644 --- a/crates/wickra-core/src/indicators/mama.rs +++ b/crates/wickra-core/src/indicators/mama.rs @@ -369,13 +369,13 @@ mod tests { #[test] fn flat_input_uses_phase_fallback() { - // A perfectly constant series leaves every smooth/detrender slot at - // the same value, so `i1` collapses to zero and the phase calc takes - // the `self.prev_phase` fallback rather than `atan(q1/i1)`. Stretch - // the run long enough to clear the 50-bar warmup with comfortable - // margin and confirm the indicator still emits. + // Zero inputs make every smooth/detrender term arithmetically exact + // zero, so `i1 == 0.0` and the phase calculation takes the + // `self.prev_phase` fallback rather than `atan(q1/i1)`. A non-zero + // constant like `50.0` leaves a sub-EPSILON cancellation residue + // that flips the branch back to the `atan` path on real hardware. let mut mama = Mama::classic(); - let out = mama.batch(&[50.0_f64; 200]); + let out = mama.batch(&[0.0_f64; 200]); assert!(out.iter().flatten().count() > 100); } } diff --git a/crates/wickra-core/src/indicators/rsi.rs b/crates/wickra-core/src/indicators/rsi.rs index e3ccdcff..a8f77f50 100644 --- a/crates/wickra-core/src/indicators/rsi.rs +++ b/crates/wickra-core/src/indicators/rsi.rs @@ -230,6 +230,20 @@ mod tests { } } + /// Cover the `100.0` branch (line 169) of the test-helper `rsi_naive`: + /// strictly increasing prices give `avg_loss == 0` while `avg_gain > 0`, + /// the textbook overbought saturation case. Random proptest inputs + /// virtually never satisfy `al == 0 && ag != 0`, so this needs an + /// explicit monotone series. + #[test] + fn naive_helper_monotone_up_yields_100() { + let prices: Vec = (1..=20).map(f64::from).collect(); + let ks = rsi_naive(&prices, 5); + for r in ks.into_iter().skip(5) { + assert_eq!(r.expect("ready after period+1 inputs"), 100.0); + } + } + #[test] fn warmup_period_is_period_plus_one() { let rsi = Rsi::new(14).unwrap(); diff --git a/crates/wickra-core/src/indicators/sine_wave.rs b/crates/wickra-core/src/indicators/sine_wave.rs index 3ef5ed24..e06b8e4c 100644 --- a/crates/wickra-core/src/indicators/sine_wave.rs +++ b/crates/wickra-core/src/indicators/sine_wave.rs @@ -218,11 +218,13 @@ mod tests { #[test] fn flat_input_uses_phase_fallback() { - // A constant series leaves the detrender chain at zero, so the `i1` - // arm is `i1.abs() <= EPSILON` for every bar and the phase calculation - // takes the `self.last_phase` fallback rather than `atan(q1/i1)`. + // Zero inputs make every smooth/detrender term arithmetically exact + // zero (no IEEE-754 cancellation residue), so `i1 == 0.0` and the + // phase calculation deterministically takes the `self.last_phase` + // fallback rather than `atan(q1/i1)`. A non-zero constant like + // `100.0` leaves a sub-EPSILON residue that flips the branch back. let mut sw = SineWave::new(); - let _ = sw.batch(&[100.0_f64; 120]); + let _ = sw.batch(&[0.0_f64; 120]); assert!(sw.value().is_some()); } }