test: cover candlestick pattern rejection guards (#142)

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.
This commit is contained in:
kingchenc
2026-06-02 17:52:04 +02:00
committed by GitHub
parent 124efb4432
commit 3d98592461
13 changed files with 190 additions and 0 deletions
@@ -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));
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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
}
}
@@ -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));
}
}
@@ -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());
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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());
}
}
@@ -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));
}
}