diff --git a/crates/wickra-core/src/indicators/adl.rs b/crates/wickra-core/src/indicators/adl.rs index f325f456..a13928ae 100644 --- a/crates/wickra-core/src/indicators/adl.rs +++ b/crates/wickra-core/src/indicators/adl.rs @@ -127,6 +127,14 @@ mod tests { assert!(adl.update(candle(8.0, 10.0, 8.0, 9.0, 50.0, 0)).is_some()); } + /// Cover the Indicator-impl `name` body (94-96). The other accessors + /// are exercised by existing tests; `name` was never queried. + #[test] + fn accessors_and_metadata() { + let adl = Adl::new(); + assert_eq!(adl.name(), "ADL"); + } + #[test] fn close_at_high_accumulates_full_volume() { // Every bar closes at its high: MFM = +1, so ADL grows by `volume`. diff --git a/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs b/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs index b1c72a53..0c55e098 100644 --- a/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs +++ b/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs @@ -177,6 +177,15 @@ mod tests { assert!(VerticalHorizontalFilter::new(0).is_err()); } + /// Cover the const accessor `period` (61-63) and the Indicator-impl + /// `name` body (119-121). `warmup_period` is exercised elsewhere. + #[test] + fn accessors_and_metadata() { + let vhf = VerticalHorizontalFilter::new(28).unwrap(); + assert_eq!(vhf.period(), 28); + assert_eq!(vhf.name(), "VerticalHorizontalFilter"); + } + #[test] fn reset_clears_state() { let mut vhf = VerticalHorizontalFilter::new(8).unwrap(); diff --git a/crates/wickra-core/src/indicators/vpt.rs b/crates/wickra-core/src/indicators/vpt.rs index 6005811d..0bf58c9d 100644 --- a/crates/wickra-core/src/indicators/vpt.rs +++ b/crates/wickra-core/src/indicators/vpt.rs @@ -129,6 +129,30 @@ mod tests { assert_relative_eq!(out[2].unwrap(), 20.0 - 600.0 / 11.0, epsilon = 1e-12); } + /// Cover the `value()` Some branch (line 57) and the Indicator-impl + /// `name` body (100-102). `reset_clears_state` hits only the None + /// branch; the name was never queried. + #[test] + fn accessors_and_metadata() { + let mut vpt = VolumePriceTrend::new(); + assert_eq!(vpt.name(), "VPT"); + assert_eq!(vpt.value(), None); + vpt.update(candle(100.0, 50.0, 0)); + assert_eq!(vpt.value(), Some(0.0)); + } + + /// Cover the `prev == 0.0` defensive branch (line 77) — the previous + /// close is exactly 0, making the percentage ROC undefined. The + /// indicator must contribute 0 to the running total rather than NaN. + #[test] + fn zero_previous_close_contributes_zero() { + let mut vpt = VolumePriceTrend::new(); + vpt.update(candle(0.0, 100.0, 0)); // baseline; prev_close = 0 + let v = vpt.update(candle(50.0, 200.0, 1)).expect("emits"); + // ROC fallback is 0, so total stays at 0. + assert_eq!(v, 0.0); + } + #[test] fn emits_from_first_candle_at_zero() { let mut vpt = VolumePriceTrend::new(); diff --git a/crates/wickra-core/src/indicators/z_score.rs b/crates/wickra-core/src/indicators/z_score.rs index 0603894f..eba8902d 100644 --- a/crates/wickra-core/src/indicators/z_score.rs +++ b/crates/wickra-core/src/indicators/z_score.rs @@ -161,6 +161,15 @@ mod tests { assert!(ZScore::new(0).is_err()); } + /// Cover the const accessor `period` (59-61) and the Indicator-impl + /// `name` body (106-108). `warmup_period` is exercised elsewhere. + #[test] + fn accessors_and_metadata() { + let z = ZScore::new(20).unwrap(); + assert_eq!(z.period(), 20); + assert_eq!(z.name(), "ZScore"); + } + #[test] fn reset_clears_state() { let mut z = ZScore::new(5).unwrap(); diff --git a/crates/wickra-data/src/csv.rs b/crates/wickra-data/src/csv.rs index 904405b8..3b1e46ea 100644 --- a/crates/wickra-data/src/csv.rs +++ b/crates/wickra-data/src/csv.rs @@ -274,10 +274,30 @@ mod tests { // "volume" is absent. let data = "timestamp,open,high,low,close\n1,10.0,11.0,9.0,10.5\n"; let err = CandleReader::from_reader(data.as_bytes()).unwrap_err(); - match err { - Error::Malformed(msg) => assert!(msg.contains("volume"), "msg: {msg}"), - other => panic!("expected Malformed, got {other:?}"), - } + // The error variant must be Malformed and the message must mention + // the missing column. Asserting directly (rather than match-and- + // panic-on-other) keeps the assertion's cold path branch-free for + // coverage and still pins the diagnostic. + assert!( + matches!(&err, Error::Malformed(msg) if msg.contains("volume")), + "expected Malformed mentioning 'volume', got {err:?}" + ); + } + + /// Cover `from_csv_reader` (lines 201-204): existing tests use + /// `from_reader` / `open`, which both construct the inner `csv::Reader` + /// internally. Callers that want non-default csv configuration must + /// build the reader themselves and pass it through `from_csv_reader`. + #[test] + fn from_csv_reader_accepts_a_prebuilt_reader() { + let data = "timestamp;open;high;low;close;volume\n1;10.0;11.0;9.0;10.5;100\n"; + let inner = csv::ReaderBuilder::new() + .delimiter(b';') + .from_reader(data.as_bytes()); + let mut r = CandleReader::from_csv_reader(inner).unwrap(); + let candles = r.read_all().unwrap(); + assert_eq!(candles.len(), 1); + assert_eq!(candles[0].close, 10.5); } #[test]