Files
wickra/crates/wickra-core/src/indicators/shark.rs
T
kingchencandGitHub 4250ed99f4 feat(patterns): add the Harmonic Patterns family (8 XABCD detectors) (#169)
## Summary

Adds a new **Harmonic Patterns** indicator family (counter 359 → 367, families 22 → 23) — the second half of the A4 roadmap item, following the Chart Patterns family in #166.

Eight Fibonacci-ratio detectors built on the shared swing-pivot tracker (`indicators::pattern_swing`) plus two new helpers there — `xabcd` (reads the last five pivots as X-A-B-C-D) and `ratios_in` (checks a list of `(value, low, high)` Fibonacci windows in one expression, no multi-line `&&` coverage gaps). Each consumes candles and emits the uniform pattern sign convention — `+1.0` bullish (terminal point D a swing low), `-1.0` bearish (D a swing high), `0.0` otherwise, never `None`. Parameter-free, with the Fibonacci windows documented as constants per detector.

## Detectors

| Indicator | Defining ratio |
|-----------|----------------|
| `Abcd` | four-point AB=CD (BC retraces AB, CD ≈ AB) |
| `Gartley` | AD/XA ≈ 0.786 |
| `Butterfly` | AD/XA ∈ 1.27–1.618 (extended D) |
| `Bat` | AD/XA ≈ 0.886, shallow B |
| `Crab` | AD/XA ≈ 1.618 (deepest D) |
| `Shark` | expansion AB, AD/XA 0.886–1.13 |
| `Cypher` | BC on XA, CD/XC ≈ 0.786 |
| `ThreeDrives` | two symmetric extension drives |

## Touchpoints

Core modules + `FAMILIES` group/assert, crate root re-exports, Python/Node/WASM bindings via the candle-pattern macros (Node `index.d.ts`/`index.js` regenerated), the candle fuzz target (`// --- Harmonic Patterns ---` section), Python reference + `CANDLE_SCALAR` registry tests and the Node candle-scalar factory, README catalogue counter + banner cache-buster + family table row + family-count word, `docs/README.md` counter, and the changelog.

## Verification

- `cargo test -p wickra-core --lib` — 2966 passed
- `cargo test -p wickra-core --doc` — 335 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean
- Node `npm run build && npm test` — 444 passed
- Python `maturin develop --release` + `pytest` — 748 passed

Every detector branch is unit-tested, including a bullish and a bearish match per pattern to cover both output arms, plus an out-of-ratio non-match. Fibonacci windows use standard harmonic-trading ranges with documented tolerance bands.
2026-06-03 23:24:25 +02:00

156 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Shark harmonic pattern.
use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Shark — a 5-point (X-A-B-C-D) harmonic pattern characterised by an
/// **expansion** leg (AB longer than XA) and a `0.886``1.13` D completion:
///
/// ```text
/// AB / XA ∈ [1.13, 1.618] (expansion — B overshoots X)
/// BC / AB ∈ [1.618, 2.24]
/// CD / BC ∈ [0.382, 0.886]
/// AD / XA ∈ [0.886, 1.13] (the defining D completion near A)
/// ```
///
/// This is the 5-point reading of the Shark; output is `+1.0` (bullish, D a
/// swing low), `-1.0` (bearish, D a swing high), or `0.0`; never `None`. See
/// `crates/wickra-core/src/indicators/shark.rs`.
#[derive(Debug, Clone)]
pub struct Shark {
swing: SwingTracker,
has_emitted: bool,
}
impl Shark {
/// Construct a new Shark detector.
pub const fn new() -> Self {
Self {
swing: SwingTracker::new(SWING_THRESHOLD, 5),
has_emitted: false,
}
}
}
impl Default for Shark {
fn default() -> Self {
Self::new()
}
}
impl Indicator for Shark {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
if !self.swing.update(candle) {
return Some(0.0);
}
let pivots = self.swing.pivots();
if pivots.len() < 5 {
return Some(0.0);
}
let p = xabcd(pivots);
let xa = (p.a - p.x).abs();
let ab = (p.b - p.a).abs();
let bc = (p.c - p.b).abs();
let cd = (p.d - p.c).abs();
let ad = (p.d - p.a).abs();
let matched = ratios_in(&[
(ab / xa, 1.13, 1.618),
(bc / ab, 1.618, 2.24),
(cd / bc, 0.382, 0.886),
(ad / xa, 0.886, 1.13),
]);
if matched {
return Some(if p.bullish { 1.0 } else { -1.0 });
}
Some(0.0)
}
fn reset(&mut self) {
self.swing.reset();
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
6
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"Shark"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::pattern_swing::candles_for_pivots;
use crate::traits::BatchExt;
fn run(pivots: &[f64]) -> Vec<f64> {
let mut indicator = Shark::new();
candles_for_pivots(pivots)
.into_iter()
.map(|c| indicator.update(c).unwrap())
.collect()
}
#[test]
fn accessors_and_metadata() {
let indicator = Shark::new();
assert_eq!(indicator.name(), "Shark");
assert_eq!(indicator.warmup_period(), 6);
assert!(!indicator.is_ready());
assert!(!Shark::default().is_ready());
}
#[test]
fn bullish_shark_is_plus_one() {
let out = run(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]);
assert_eq!(*out.last().unwrap(), 1.0);
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
}
#[test]
fn bearish_shark_is_minus_one() {
let out = run(&[150.0, 110.0, 162.0, 60.2, 150.0]);
assert_eq!(*out.last().unwrap(), -1.0);
}
#[test]
fn out_of_ratio_does_not_trigger() {
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
assert_eq!(*out.last().unwrap(), 0.0);
}
#[test]
fn reset_clears_state() {
let mut indicator = Shark::new();
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
let _ = indicator.update(c);
}
indicator.reset();
assert!(!indicator.is_ready());
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
assert_eq!(indicator.update(c), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]);
let mut a = Shark::new();
let mut b = Shark::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}