From 3d98592461bbdbfb2e635aa99e5d7601da8ee2dd Mon Sep 17 00:00:00 2001 From: kingchenc Date: Tue, 2 Jun 2026 17:52:04 +0200 Subject: [PATCH] test: cover candlestick pattern rejection guards (#142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the coverage gaps in the candlestick-pattern family that landed across PRs #132–#141. Codecov flagged 25 uncovered lines on `main` (99.93%) — all in the new candlestick files, and all early-return rejection guards or unused `Default` impls that the accept-path unit tests never exercised. This PR adds focused white-box unit tests (one or two per affected file) that drive each rejection branch through the public `update()` API: - **Zero-range guards** — a flat bar (`high == low`) at the relevant window position: `DojiStar`, `InNeck`, `OnNeck`, `Thrusting`, `SeparatingLines`, `EveningDojiStar`, `MorningDojiStar`, `GapSideBySideWhite`, `FallingThreeMethods`, `RisingThreeMethods`, `MatHold`. - **Too-short trigger body** — a wide-range bar with a tiny body that fails the "long body" check: the three-bar stars, the three-methods pair, `MatHold`, `SeparatingLines`. - **Shape-specific guards** — `GapSideBySideWhite` body-size mismatch, `MatHold` bar-2 fails to gap up. - **`Default` impls** — `LongLine` / `ShortLine` (`default()` was never called). No production code changes; tests only. `cargo test -p wickra-core` and `cargo clippy -p wickra-core --all-targets -- -D warnings` pass locally. --- .../wickra-core/src/indicators/doji_star.rs | 8 +++++ .../src/indicators/evening_doji_star.rs | 18 ++++++++++ .../src/indicators/falling_three_methods.rs | 22 ++++++++++++ .../src/indicators/gap_side_by_side_white.rs | 18 ++++++++++ crates/wickra-core/src/indicators/in_neck.rs | 8 +++++ .../wickra-core/src/indicators/long_line.rs | 5 +++ crates/wickra-core/src/indicators/mat_hold.rs | 34 +++++++++++++++++++ .../src/indicators/morning_doji_star.rs | 18 ++++++++++ crates/wickra-core/src/indicators/on_neck.rs | 8 +++++ .../src/indicators/rising_three_methods.rs | 22 ++++++++++++ .../src/indicators/separating_lines.rs | 16 +++++++++ .../wickra-core/src/indicators/short_line.rs | 5 +++ .../wickra-core/src/indicators/thrusting.rs | 8 +++++ 13 files changed, 190 insertions(+) diff --git a/crates/wickra-core/src/indicators/doji_star.rs b/crates/wickra-core/src/indicators/doji_star.rs index 85b2d22e..75336fb7 100644 --- a/crates/wickra-core/src/indicators/doji_star.rs +++ b/crates/wickra-core/src/indicators/doji_star.rs @@ -202,4 +202,12 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_yields_zero() { + let mut t = DojiStar::new(); + t.update(c(20.0, 20.2, 14.8, 15.0, 0)); + // Flat second bar (high == low) -> zero-range guard. + assert_eq!(t.update(c(13.0, 13.0, 13.0, 13.0, 1)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/evening_doji_star.rs b/crates/wickra-core/src/indicators/evening_doji_star.rs index 65d09053..639b5fe1 100644 --- a/crates/wickra-core/src/indicators/evening_doji_star.rs +++ b/crates/wickra-core/src/indicators/evening_doji_star.rs @@ -238,4 +238,22 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_yields_zero() { + let mut t = EveningDojiStar::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + t.update(c(17.0, 17.1, 16.9, 17.0, 1)); + assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0)); + } + + #[test] + fn short_first_body_yields_zero() { + let mut t = EveningDojiStar::new(); + // bar1 has a wide range but a tiny body -> not a long white body. + t.update(c(10.0, 16.0, 9.0, 10.5, 0)); + t.update(c(17.0, 17.1, 16.9, 17.0, 1)); + assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/falling_three_methods.rs b/crates/wickra-core/src/indicators/falling_three_methods.rs index 19a0e0c5..6ac04d16 100644 --- a/crates/wickra-core/src/indicators/falling_three_methods.rs +++ b/crates/wickra-core/src/indicators/falling_three_methods.rs @@ -210,4 +210,26 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_first_bar_yields_zero() { + let mut t = FallingThreeMethods::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + t.update(c(11.0, 12.1, 10.9, 12.0, 1)); + t.update(c(11.5, 12.6, 11.4, 12.5, 2)); + t.update(c(12.0, 13.1, 11.9, 13.0, 3)); + assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(0.0)); + } + + #[test] + fn short_first_body_yields_zero() { + let mut t = FallingThreeMethods::new(); + // bar1 has a wide range but a tiny body -> not a long black body. + t.update(c(10.0, 16.0, 9.0, 10.2, 0)); + t.update(c(11.0, 12.1, 10.9, 12.0, 1)); + t.update(c(11.5, 12.6, 11.4, 12.5, 2)); + t.update(c(12.0, 13.1, 11.9, 13.0, 3)); + assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/gap_side_by_side_white.rs b/crates/wickra-core/src/indicators/gap_side_by_side_white.rs index f73fe98c..dd780ee2 100644 --- a/crates/wickra-core/src/indicators/gap_side_by_side_white.rs +++ b/crates/wickra-core/src/indicators/gap_side_by_side_white.rs @@ -217,4 +217,22 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_yields_zero() { + let mut t = GapSideBySideWhite::new(); + t.update(c(10.0, 11.1, 9.9, 11.0, 0)); + // Flat second bar (range2 == 0) -> rejected. + t.update(c(13.0, 13.0, 13.0, 13.0, 1)); + assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 2)), Some(0.0)); + } + + #[test] + fn body_size_mismatch_yields_zero() { + let mut t = GapSideBySideWhite::new(); + t.update(c(10.0, 11.1, 9.9, 11.0, 0)); + t.update(c(13.0, 16.0, 12.9, 15.0, 1)); // white, body 2.0 + // Level open, white, but its body is more than 2x smaller -> rejected. + assert_eq!(t.update(c(13.0, 13.7, 12.9, 13.5, 2)), Some(0.0)); // body 0.5 + } } diff --git a/crates/wickra-core/src/indicators/in_neck.rs b/crates/wickra-core/src/indicators/in_neck.rs index 16edc2c7..422ef1c1 100644 --- a/crates/wickra-core/src/indicators/in_neck.rs +++ b/crates/wickra-core/src/indicators/in_neck.rs @@ -180,4 +180,12 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_first_bar_yields_zero() { + let mut t = InNeck::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + assert_eq!(t.update(c(9.0, 10.0, 8.0, 9.5, 1)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/long_line.rs b/crates/wickra-core/src/indicators/long_line.rs index 39ebad74..f3e98fef 100644 --- a/crates/wickra-core/src/indicators/long_line.rs +++ b/crates/wickra-core/src/indicators/long_line.rs @@ -226,4 +226,9 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 0)), Some(0.0)); } + + #[test] + fn default_matches_new() { + assert_eq!(LongLine::default().period(), LongLine::new().period()); + } } diff --git a/crates/wickra-core/src/indicators/mat_hold.rs b/crates/wickra-core/src/indicators/mat_hold.rs index 19c99d1a..b9f0c090 100644 --- a/crates/wickra-core/src/indicators/mat_hold.rs +++ b/crates/wickra-core/src/indicators/mat_hold.rs @@ -271,4 +271,38 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_first_bar_yields_zero() { + let mut t = MatHold::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + t.update(c(16.0, 16.1, 15.4, 15.5, 1)); + t.update(c(15.5, 15.6, 14.9, 15.0, 2)); + t.update(c(15.0, 15.1, 14.4, 14.5, 3)); + assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0)); + } + + #[test] + fn short_first_body_yields_zero() { + let mut t = MatHold::new(); + // bar1 has a wide range but a tiny body -> not a long white body. + t.update(c(10.0, 16.0, 9.0, 10.5, 0)); + t.update(c(16.0, 16.1, 15.4, 15.5, 1)); + t.update(c(15.5, 15.6, 14.9, 15.0, 2)); + t.update(c(15.0, 15.1, 14.4, 14.5, 3)); + assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(0.0)); + } + + #[test] + fn no_gap_up_yields_zero() { + let mut t = MatHold::new(); + // Long white bar1 with small pullbacks, but bar2 fails to gap up above + // bar1's close. + t.update(c(10.0, 15.1, 9.9, 15.0, 0)); + t.update(c(14.5, 14.7, 14.3, 14.5, 1)); + t.update(c(14.5, 14.7, 14.3, 14.6, 2)); + t.update(c(14.6, 14.8, 14.4, 14.7, 3)); + assert_eq!(t.update(c(14.7, 17.1, 14.6, 17.0, 4)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/morning_doji_star.rs b/crates/wickra-core/src/indicators/morning_doji_star.rs index 9c368a21..d58a8c02 100644 --- a/crates/wickra-core/src/indicators/morning_doji_star.rs +++ b/crates/wickra-core/src/indicators/morning_doji_star.rs @@ -238,4 +238,22 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_yields_zero() { + let mut t = MorningDojiStar::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + t.update(c(8.0, 8.1, 7.9, 8.0, 1)); + assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(0.0)); + } + + #[test] + fn short_first_body_yields_zero() { + let mut t = MorningDojiStar::new(); + // bar1 has a wide range but a tiny body -> not a long black body. + t.update(c(10.0, 16.0, 9.0, 9.8, 0)); + t.update(c(8.0, 8.1, 7.9, 8.0, 1)); + assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/on_neck.rs b/crates/wickra-core/src/indicators/on_neck.rs index 2de38168..8864982a 100644 --- a/crates/wickra-core/src/indicators/on_neck.rs +++ b/crates/wickra-core/src/indicators/on_neck.rs @@ -178,4 +178,12 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_first_bar_yields_zero() { + let mut t = OnNeck::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + assert_eq!(t.update(c(9.0, 10.0, 8.0, 9.5, 1)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/rising_three_methods.rs b/crates/wickra-core/src/indicators/rising_three_methods.rs index 2c8bec5e..c1df8784 100644 --- a/crates/wickra-core/src/indicators/rising_three_methods.rs +++ b/crates/wickra-core/src/indicators/rising_three_methods.rs @@ -210,4 +210,26 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_first_bar_yields_zero() { + let mut t = RisingThreeMethods::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + t.update(c(14.0, 14.1, 12.9, 13.0, 1)); + t.update(c(13.5, 13.6, 12.4, 12.5, 2)); + t.update(c(13.0, 13.1, 11.9, 12.0, 3)); + assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(0.0)); + } + + #[test] + fn short_first_body_yields_zero() { + let mut t = RisingThreeMethods::new(); + // bar1 has a wide range but a tiny body -> not a long white body. + t.update(c(10.0, 16.0, 9.0, 10.2, 0)); + t.update(c(14.0, 14.1, 12.9, 13.0, 1)); + t.update(c(13.5, 13.6, 12.4, 12.5, 2)); + t.update(c(13.0, 13.1, 11.9, 12.0, 3)); + assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/separating_lines.rs b/crates/wickra-core/src/indicators/separating_lines.rs index 8fd73a22..1989723b 100644 --- a/crates/wickra-core/src/indicators/separating_lines.rs +++ b/crates/wickra-core/src/indicators/separating_lines.rs @@ -199,4 +199,20 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_yields_zero() { + let mut t = SeparatingLines::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + assert_eq!(t.update(c(10.0, 12.0, 9.0, 11.0, 1)), Some(0.0)); + } + + #[test] + fn short_second_body_yields_zero() { + let mut t = SeparatingLines::new(); + t.update(c(10.0, 12.0, 8.0, 9.0, 0)); + // Opens coincide but bar2's body is too short to be a separating line. + assert_eq!(t.update(c(10.0, 11.0, 9.0, 10.1, 1)), Some(0.0)); + } } diff --git a/crates/wickra-core/src/indicators/short_line.rs b/crates/wickra-core/src/indicators/short_line.rs index 6dd3e906..eabec8b6 100644 --- a/crates/wickra-core/src/indicators/short_line.rs +++ b/crates/wickra-core/src/indicators/short_line.rs @@ -225,4 +225,9 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(10.0, 11.0, 9.9, 10.9, 0)), Some(0.0)); } + + #[test] + fn default_matches_new() { + assert_eq!(ShortLine::default().period(), ShortLine::new().period()); + } } diff --git a/crates/wickra-core/src/indicators/thrusting.rs b/crates/wickra-core/src/indicators/thrusting.rs index dfc3afd3..aebab786 100644 --- a/crates/wickra-core/src/indicators/thrusting.rs +++ b/crates/wickra-core/src/indicators/thrusting.rs @@ -183,4 +183,12 @@ mod tests { assert!(!t.is_ready()); assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0)); } + + #[test] + fn zero_range_first_bar_yields_zero() { + let mut t = Thrusting::new(); + // Flat first bar (range1 == 0) -> rejected. + t.update(c(10.0, 10.0, 10.0, 10.0, 0)); + assert_eq!(t.update(c(9.0, 10.0, 8.0, 9.5, 1)), Some(0.0)); + } }