feat(patterns): add the Chart Patterns family (8 swing-based detectors) (#166)
## Summary Adds a new **Chart Patterns** indicator family (counter 351 → 359, families 21 → 22), the first half of the A4 roadmap item (the harmonic patterns follow in a second PR). All eight detectors are built on a shared, non-repainting swing-pivot tracker — the internal, **uncounted** `indicators::pattern_swing` module (declared `pub(crate) mod`, re-exported nowhere). Each consumes candles and emits the uniform pattern sign convention already used by the candlestick family — `+1.0` bullish / `-1.0` bearish / `0.0` otherwise, never `None`. They are parameter-free, baking the swing threshold (5%) and level tolerance (3%) in as documented constants, mirroring how candlestick patterns bake in their geometric thresholds. ## Detectors | Indicator | Signal | |-----------|--------| | `DoubleTopBottom` | twin-peak / twin-trough reversal | | `TripleTopBottom` | three matching extremes (stronger reversal) | | `HeadAndShoulders` | central head + matching shoulders + flat neckline (and inverse) | | `Triangle` | ascending (+1) / descending (-1) / symmetrical | | `Wedge` | rising wedge (-1) / falling wedge (+1) | | `FlagPennant` | shallow consolidation against a pole → continuation | | `RectangleRange` | flat support/resistance mean-reversion | | `CupAndHandle` | rounded base + shallow handle (and inverse) | ## Touchpoints Core modules + `FAMILIES` group and 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, 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` — 2915 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` — 436 passed - Python `maturin develop --release` + `pytest` — 732 passed Every detector branch is unit-tested; multi-condition predicates were flattened to single-line precomputed booleans to keep patch coverage at 100%.
This commit is contained in:
@@ -321,6 +321,15 @@ from ._wickra import (
|
||||
TasukiGap,
|
||||
UniqueThreeRiver,
|
||||
ConcealingBabySwallow,
|
||||
# Chart patterns
|
||||
CupAndHandle,
|
||||
RectangleRange,
|
||||
FlagPennant,
|
||||
Wedge,
|
||||
Triangle,
|
||||
HeadAndShoulders,
|
||||
TripleTopBottom,
|
||||
DoubleTopBottom,
|
||||
# Microstructure: order book
|
||||
OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN,
|
||||
@@ -698,6 +707,15 @@ __all__ = [
|
||||
"TasukiGap",
|
||||
"UniqueThreeRiver",
|
||||
"ConcealingBabySwallow",
|
||||
# Chart patterns
|
||||
"CupAndHandle",
|
||||
"RectangleRange",
|
||||
"FlagPennant",
|
||||
"Wedge",
|
||||
"Triangle",
|
||||
"HeadAndShoulders",
|
||||
"TripleTopBottom",
|
||||
"DoubleTopBottom",
|
||||
# Microstructure: order book
|
||||
"OrderBookImbalanceTop1",
|
||||
"OrderBookImbalanceTopN",
|
||||
|
||||
@@ -13675,6 +13675,14 @@ candle_pattern_no_param!(
|
||||
wc::ConcealingBabySwallow,
|
||||
"ConcealingBabySwallow"
|
||||
);
|
||||
candle_pattern_no_param!(PyDoubleTopBottom, wc::DoubleTopBottom, "DoubleTopBottom");
|
||||
candle_pattern_no_param!(PyTripleTopBottom, wc::TripleTopBottom, "TripleTopBottom");
|
||||
candle_pattern_no_param!(PyHeadAndShoulders, wc::HeadAndShoulders, "HeadAndShoulders");
|
||||
candle_pattern_no_param!(PyTriangle, wc::Triangle, "Triangle");
|
||||
candle_pattern_no_param!(PyWedge, wc::Wedge, "Wedge");
|
||||
candle_pattern_no_param!(PyFlagPennant, wc::FlagPennant, "FlagPennant");
|
||||
candle_pattern_no_param!(PyRectangleRange, wc::RectangleRange, "RectangleRange");
|
||||
candle_pattern_no_param!(PyCupAndHandle, wc::CupAndHandle, "CupAndHandle");
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
|
||||
@@ -18196,5 +18204,13 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyDayOfWeekProfile>()?;
|
||||
m.add_class::<PyIntradayVolatilityProfile>()?;
|
||||
m.add_class::<PyVolumeByTimeProfile>()?;
|
||||
m.add_class::<PyDoubleTopBottom>()?;
|
||||
m.add_class::<PyTripleTopBottom>()?;
|
||||
m.add_class::<PyHeadAndShoulders>()?;
|
||||
m.add_class::<PyTriangle>()?;
|
||||
m.add_class::<PyWedge>()?;
|
||||
m.add_class::<PyFlagPennant>()?;
|
||||
m.add_class::<PyRectangleRange>()?;
|
||||
m.add_class::<PyCupAndHandle>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -330,6 +330,38 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"CupAndHandle": (
|
||||
lambda: ta.CupAndHandle(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"RectangleRange": (
|
||||
lambda: ta.RectangleRange(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"FlagPennant": (
|
||||
lambda: ta.FlagPennant(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Wedge": (
|
||||
lambda: ta.Wedge(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Triangle": (
|
||||
lambda: ta.Triangle(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"HeadAndShoulders": (
|
||||
lambda: ta.HeadAndShoulders(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"TripleTopBottom": (
|
||||
lambda: ta.TripleTopBottom(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"DoubleTopBottom": (
|
||||
lambda: ta.DoubleTopBottom(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"MIDPRICE": (lambda: ta.MIDPRICE(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"AVGPRICE": (lambda: ta.AVGPRICE(), lambda ind, h, l, c, v: ind.batch(c, h, l, c)),
|
||||
"DX": (lambda: ta.DX(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
@@ -2354,6 +2386,81 @@ def test_granger_causality_reference():
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_double_top_bottom_reference():
|
||||
t = ta.DoubleTopBottom()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 120.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((108.0, 118.8, 108.0, 108.0, 1.0, 3)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_triple_top_bottom_reference():
|
||||
t = ta.TripleTopBottom()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 121.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((99.0, 119.79, 99.0, 99.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((99.99, 119.0, 99.99, 99.99, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((107.1, 117.81, 107.1, 107.1, 1.0, 5)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_head_and_shoulders_reference():
|
||||
t = ta.HeadAndShoulders()
|
||||
assert t.update((99.9, 100.0, 99.9, 99.9, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((90.0, 99.0, 90.0, 90.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 120.0, 90.9, 90.9, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((92.0, 118.8, 92.0, 92.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((92.92, 101.0, 92.92, 92.92, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 99.99, 90.9, 90.9, 1.0, 5)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_triangle_reference():
|
||||
t = ta.Triangle()
|
||||
assert t.update((129.87, 130.0, 129.87, 129.87, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 128.7, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 120.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((110.0, 118.8, 110.0, 110.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((111.1, 120.0, 111.1, 111.1, 1.0, 4)) == pytest.approx(1.0)
|
||||
assert t.update((108.0, 118.8, 108.0, 108.0, 1.0, 5)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_wedge_reference():
|
||||
t = ta.Wedge()
|
||||
assert t.update((109.89, 110.0, 109.89, 109.89, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((90.0, 108.9, 90.0, 90.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 100.0, 90.9, 90.9, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((94.0, 99.0, 94.0, 94.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((94.94, 103.0, 94.94, 94.94, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((92.7, 101.97, 92.7, 92.7, 1.0, 5)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_flag_pennant_reference():
|
||||
t = ta.FlagPennant()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((130.0, 138.6, 130.0, 130.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((131.3, 143.0, 131.3, 131.3, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_rectangle_range_reference():
|
||||
t = ta.RectangleRange()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 121.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((99.0, 119.79, 99.0, 99.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((99.99, 108.9, 99.99, 99.99, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_cup_and_handle_reference():
|
||||
t = ta.CupAndHandle()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((90.0, 118.8, 90.0, 90.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 121.0, 90.9, 90.9, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((110.0, 119.79, 110.0, 110.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((111.1, 121.0, 111.1, 111.1, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user