feat: TA-Lib candlestick patterns — gap-three-methods/stalled/stick-sandwich/takuri (part 8 of 9) (#140)

Adds five TA-Lib candlestick patterns, each a streaming `Indicator<Input = Candle, Output = f64>` emitting the family's uniform `±1.0 / 0.0` sign convention, fully wired across the Rust core, Python / Node / WASM bindings, fuzz target and reference tests.

- **Upside Gap Three Methods** (`CDLXSIDEGAP3METHODS`) — a 3-bar bullish continuation: two white candles gap up, then a black candle opens within the second body and closes within the first; bullish +1.
- **Downside Gap Three Methods** (`CDLXSIDEGAP3METHODS`) — the bearish mirror: two black candles gap down, then a white candle opens within the second body and closes within the first; bearish -1.
- **Stalled Pattern** (`CDLSTALLEDPATTERN`) — a 3-bar bearish reversal warning: two long white candles then a small white candle riding the shoulder, signalling the rally is stalling; bearish -1.
- **Stick Sandwich** (`CDLSTICKSANDWICH`) — a 3-bar bullish reversal: two black candles closing at the same level sandwich a white candle, marking a support floor; bullish +1.
- **Takuri** (`CDLTAKURI`) — a single-bar bullish reversal, a strict Dragonfly Doji with a negligible upper shadow and very long lower shadow; bullish +1.

Body and shadow thresholds follow the geometric house style (fixed fractions of the bar range) rather than TA-Lib's rolling averages. Upside / Downside Gap Three Methods share the `CDLXSIDEGAP3METHODS` code, so the second carries a manual CHANGELOG entry (as with Rising / Falling Three Methods).

Counter 279 → 284 (mod-count == lib counted block; FAMILIES total 274 → 279).

Stacked on #139 (`feat/cdl-lines`); base retargets to `main` once the predecessor merges.
This commit is contained in:
kingchenc
2026-06-02 17:24:42 +02:00
committed by GitHub
parent c2c85c7ecf
commit 4d0bc08efd
19 changed files with 1262 additions and 36 deletions
+10
View File
@@ -275,6 +275,11 @@ from ._wickra import (
ShortLine,
RisingThreeMethods,
FallingThreeMethods,
UpsideGapThreeMethods,
DownsideGapThreeMethods,
StalledPattern,
StickSandwich,
Takuri,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -577,6 +582,11 @@ __all__ = [
"ShortLine",
"RisingThreeMethods",
"FallingThreeMethods",
"UpsideGapThreeMethods",
"DownsideGapThreeMethods",
"StalledPattern",
"StickSandwich",
"Takuri",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
+18
View File
@@ -11675,6 +11675,19 @@ candle_pattern_no_param!(
wc::FallingThreeMethods,
"FallingThreeMethods"
);
candle_pattern_no_param!(
PyUpsideGapThreeMethods,
wc::UpsideGapThreeMethods,
"UpsideGapThreeMethods"
);
candle_pattern_no_param!(
PyDownsideGapThreeMethods,
wc::DownsideGapThreeMethods,
"DownsideGapThreeMethods"
);
candle_pattern_no_param!(PyStalledPattern, wc::StalledPattern, "StalledPattern");
candle_pattern_no_param!(PyStickSandwich, wc::StickSandwich, "StickSandwich");
candle_pattern_no_param!(PyTakuri, wc::Takuri, "Takuri");
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14206,6 +14219,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyShortLine>()?;
m.add_class::<PyRisingThreeMethods>()?;
m.add_class::<PyFallingThreeMethods>()?;
m.add_class::<PyUpsideGapThreeMethods>()?;
m.add_class::<PyDownsideGapThreeMethods>()?;
m.add_class::<PyStalledPattern>()?;
m.add_class::<PyStickSandwich>()?;
m.add_class::<PyTakuri>()?;
// Microstructure: order book.
m.add_class::<PyOrderBookImbalanceTop1>()?;
m.add_class::<PyOrderBookImbalanceTopN>()?;
@@ -663,6 +663,26 @@ CANDLE_SCALAR = {
lambda: ta.FallingThreeMethods(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"UpsideGapThreeMethods": (
lambda: ta.UpsideGapThreeMethods(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"DownsideGapThreeMethods": (
lambda: ta.DownsideGapThreeMethods(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"StalledPattern": (
lambda: ta.StalledPattern(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"StickSandwich": (
lambda: ta.StickSandwich(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"Takuri": (
lambda: ta.Takuri(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
}
@@ -1951,6 +1971,39 @@ def test_falling_three_methods_reference():
assert t.update((12.0, 13.1, 11.9, 13.0, 1.0, 3)) == pytest.approx(0.0)
assert t.update((12.5, 12.6, 8.9, 9.0, 1.0, 4)) == pytest.approx(-1.0)
def test_upside_gap_three_methods_reference():
t = ta.UpsideGapThreeMethods()
assert t.update((10.0, 11.2, 9.8, 11.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((12.0, 13.2, 11.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((12.5, 12.6, 10.4, 10.5, 1.0, 2)) == pytest.approx(1.0)
def test_downside_gap_three_methods_reference():
t = ta.DownsideGapThreeMethods()
assert t.update((13.0, 13.2, 11.8, 12.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((11.0, 11.1, 9.8, 10.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((10.5, 12.6, 10.4, 12.5, 1.0, 2)) == pytest.approx(-1.0)
def test_stalled_pattern_reference():
t = ta.StalledPattern()
assert t.update((10.0, 12.05, 9.9, 12.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((11.0, 14.05, 10.9, 14.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((14.0, 14.6, 13.95, 14.15, 1.0, 2)) == pytest.approx(-1.0)
def test_stick_sandwich_reference():
t = ta.StickSandwich()
assert t.update((12.0, 12.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((10.5, 11.6, 10.4, 11.5, 1.0, 1)) == pytest.approx(0.0)
assert t.update((11.5, 11.6, 9.9, 10.0, 1.0, 2)) == pytest.approx(1.0)
def test_takuri_reference():
t = ta.Takuri()
assert t.update((10.0, 10.05, 7.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
# --- Lifecycle ------------------------------------------------------------