diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4bf0db8..18651a05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Three Stars in the South** — a rare three-bar bullish reversal
(`CDL3STARSINSOUTH`): three shrinking red candles each carving a higher low
and contracting toward a tiny black marubozu as selling exhausts.
+ - **Abandoned Baby** — a strong three-bar reversal (`CDLABANDONEDBABY`): a doji
+ isolated by price gaps on both sides; bullish `+1` after a decline, bearish
+ `−1` after an advance.
+ - **Advance Block** — a three-bar bearish warning (`CDLADVANCEBLOCK`): three
+ green candles to higher closes whose bodies shrink as their upper shadows
+ lengthen, signalling the advance is stalling.
+ - **Belt-hold** — a single-bar reversal that opens at one extreme of its range and runs the other way; bullish +1, bearish -1 (`CDLBELTHOLD`).
+ - **Breakaway** — a 5-bar reversal that gaps with the trend, drifts two more bars, then snaps back into the bar1/bar2 body gap; bullish +1, bearish -1 (`CDLBREAKAWAY`).
+ - **Counterattack** — a 2-bar reversal where an opposite-coloured second bar closes level with the first (the counterattack line); bullish +1, bearish -1 (`CDLCOUNTERATTACK`).
- **Derivatives family — funding & open interest (part 1).** A new family of
indicators that consume a perpetual / futures tick (`DerivativesTick`,
bundling funding rate, mark / index / futures price, open interest,
diff --git a/README.md b/README.md
index 4c41fa88..5bcd82e2 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM).
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
- every one of the 249 indicators; start at the
+ every one of the 254 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview).
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
## Indicators
-249 streaming-first indicators across eighteen families. Every one passes the
+254 streaming-first indicators across eighteen families. Every one passes the
`batch == streaming` equivalence test, reference-value tests, and reset
semantics tests. Each has a per-indicator deep dive (formula, parameters,
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
@@ -155,7 +155,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag |
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
-| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South |
+| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack |
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
| Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range |
@@ -238,7 +238,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
-│ ├── wickra-core/ core engine + all 249 indicators
+│ ├── wickra-core/ core engine + all 254 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js
index 4eda07ae..e2b99c7f 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -240,6 +240,11 @@ const candleScalar = {
IdenticalThreeCrows: { make: () => new wickra.IdenticalThreeCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ThreeLineStrike: { make: () => new wickra.ThreeLineStrike(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ThreeStarsInSouth: { make: () => new wickra.ThreeStarsInSouth(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ AbandonedBaby: { make: () => new wickra.AbandonedBaby(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ AdvanceBlock: { make: () => new wickra.AdvanceBlock(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ BeltHold: { make: () => new wickra.BeltHold(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ Breakaway: { make: () => new wickra.Breakaway(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ Counterattack: { make: () => new wickra.Counterattack(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts
index 234ba29b..2104720b 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -2255,6 +2255,51 @@ export declare class ThreeStarsInSouth {
isReady(): boolean
warmupPeriod(): number
}
+export type AbandonedBabyNode = AbandonedBaby
+export declare class AbandonedBaby {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type AdvanceBlockNode = AdvanceBlock
+export declare class AdvanceBlock {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type BeltHoldNode = BeltHold
+export declare class BeltHold {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type BreakawayNode = Breakaway
+export declare class Breakaway {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type CounterattackNode = Counterattack
+export declare class Counterattack {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
export declare class OrderBookImbalanceTop1 {
constructor()
diff --git a/bindings/node/index.js b/bindings/node/index.js
index eee2f986..f8b0a262 100644
--- a/bindings/node/index.js
+++ b/bindings/node/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
-const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
+const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
module.exports.version = version
module.exports.SMA = SMA
@@ -520,6 +520,11 @@ module.exports.UpsideGapTwoCrows = UpsideGapTwoCrows
module.exports.IdenticalThreeCrows = IdenticalThreeCrows
module.exports.ThreeLineStrike = ThreeLineStrike
module.exports.ThreeStarsInSouth = ThreeStarsInSouth
+module.exports.AbandonedBaby = AbandonedBaby
+module.exports.AdvanceBlock = AdvanceBlock
+module.exports.BeltHold = BeltHold
+module.exports.Breakaway = Breakaway
+module.exports.Counterattack = Counterattack
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
module.exports.Microprice = Microprice
diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs
index e5e9c605..8b105f8e 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -8770,6 +8770,11 @@ node_candle_pattern!(
wc::ThreeStarsInSouth,
"ThreeStarsInSouth"
);
+node_candle_pattern!(AbandonedBabyNode, wc::AbandonedBaby, "AbandonedBaby");
+node_candle_pattern!(AdvanceBlockNode, wc::AdvanceBlock, "AdvanceBlock");
+node_candle_pattern!(BeltHoldNode, wc::BeltHold, "BeltHold");
+node_candle_pattern!(BreakawayNode, wc::Breakaway, "Breakaway");
+node_candle_pattern!(CounterattackNode, wc::Counterattack, "Counterattack");
// ============================== Microstructure: Order Book ==============================
//
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index d5106e60..17122b2c 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -245,6 +245,11 @@ from ._wickra import (
IdenticalThreeCrows,
ThreeLineStrike,
ThreeStarsInSouth,
+ AbandonedBaby,
+ AdvanceBlock,
+ BeltHold,
+ Breakaway,
+ Counterattack,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -517,6 +522,11 @@ __all__ = [
"IdenticalThreeCrows",
"ThreeLineStrike",
"ThreeStarsInSouth",
+ "AbandonedBaby",
+ "AdvanceBlock",
+ "BeltHold",
+ "Breakaway",
+ "Counterattack",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index efb86e0a..f31acfa6 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -11632,7 +11632,12 @@ candle_pattern_no_param!(
wc::ThreeStarsInSouth,
"ThreeStarsInSouth"
);
+candle_pattern_no_param!(PyAbandonedBaby, wc::AbandonedBaby, "AbandonedBaby");
+candle_pattern_no_param!(PyAdvanceBlock, wc::AdvanceBlock, "AdvanceBlock");
+candle_pattern_no_param!(PyBeltHold, wc::BeltHold, "BeltHold");
+candle_pattern_no_param!(PyBreakaway, wc::Breakaway, "Breakaway");
+candle_pattern_no_param!(PyCounterattack, wc::Counterattack, "Counterattack");
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14134,6 +14139,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
// Microstructure: order book.
m.add_class::()?;
m.add_class::()?;
diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py
index e47995b9..f0501ad4 100644
--- a/bindings/python/tests/test_new_indicators.py
+++ b/bindings/python/tests/test_new_indicators.py
@@ -543,6 +543,26 @@ CANDLE_SCALAR = {
lambda: ta.ThreeStarsInSouth(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
+ "AbandonedBaby": (
+ lambda: ta.AbandonedBaby(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "AdvanceBlock": (
+ lambda: ta.AdvanceBlock(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "BeltHold": (
+ lambda: ta.BeltHold(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "Breakaway": (
+ lambda: ta.Breakaway(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "Counterattack": (
+ lambda: ta.Counterattack(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
}
@@ -1644,6 +1664,27 @@ def test_fractal_chaos_bands_detects_peak_and_trough():
assert out[5, 1] == pytest.approx(0.5)
+
+def test_belt_hold_reference():
+ t = ta.BeltHold()
+ assert t.update((10.0, 12.0, 10.0, 11.5, 1.0, 0)) == pytest.approx(1.0)
+ assert t.update((12.0, 12.0, 10.0, 10.5, 1.0, 1)) == pytest.approx(-1.0)
+
+
+def test_breakaway_reference():
+ t = ta.Breakaway()
+ assert t.update((20.0, 20.2, 14.8, 15.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((14.0, 14.1, 11.9, 12.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((12.5, 13.0, 10.5, 11.0, 1.0, 2)) == pytest.approx(0.0)
+ assert t.update((11.0, 11.5, 9.0, 9.5, 1.0, 3)) == pytest.approx(0.0)
+ assert t.update((9.5, 14.7, 9.4, 14.5, 1.0, 4)) == pytest.approx(1.0)
+
+
+def test_counterattack_reference():
+ t = ta.Counterattack()
+ assert t.update((20.0, 20.1, 14.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 1)) == pytest.approx(1.0)
+
# --- Lifecycle ------------------------------------------------------------
@@ -1907,6 +1948,20 @@ def test_three_stars_in_south_reference():
assert t.update((15.0, 15.0, 14.0, 14.0, 1.0, 2)) == pytest.approx(1.0)
+def test_abandoned_baby_reference():
+ t = ta.AbandonedBaby()
+ assert t.update((20.0, 20.1, 14.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((13.0, 13.1, 12.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((16.0, 18.1, 15.9, 18.0, 1.0, 2)) == pytest.approx(1.0)
+
+
+def test_advance_block_reference():
+ t = ta.AdvanceBlock()
+ assert t.update((10.0, 13.1, 9.9, 13.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((12.0, 14.3, 11.9, 14.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((13.5, 15.0, 13.4, 14.5, 1.0, 2)) == pytest.approx(-1.0)
+
+
# --- Lifecycle ------------------------------------------------------------
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 6f0429fe..b37c1f7b 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -6347,6 +6347,11 @@ wasm_candle_pattern!(
wc::ThreeStarsInSouth,
ThreeStarsInSouth
);
+wasm_candle_pattern!(WasmAbandonedBaby, wc::AbandonedBaby, AbandonedBaby);
+wasm_candle_pattern!(WasmAdvanceBlock, wc::AdvanceBlock, AdvanceBlock);
+wasm_candle_pattern!(WasmBeltHold, wc::BeltHold, BeltHold);
+wasm_candle_pattern!(WasmBreakaway, wc::Breakaway, Breakaway);
+wasm_candle_pattern!(WasmCounterattack, wc::Counterattack, Counterattack);
// ============================== Microstructure: Order Book ==============================
//
diff --git a/crates/wickra-core/src/indicators/abandoned_baby.rs b/crates/wickra-core/src/indicators/abandoned_baby.rs
new file mode 100644
index 00000000..a750b71b
--- /dev/null
+++ b/crates/wickra-core/src/indicators/abandoned_baby.rs
@@ -0,0 +1,247 @@
+//! Abandoned Baby candlestick pattern.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Abandoned Baby — a strong 3-bar reversal where a doji is "abandoned" by price
+/// gaps on both sides, isolating it from the candles before and after.
+///
+/// ```text
+/// tol = tolerance * max(|bar2.open|, |bar2.close|)
+/// bar2 doji (|bar2.close − bar2.open| <= tol)
+///
+/// bullish (+1.0): bar1 red, bar2 gaps fully below bar1 (bar2.high < bar1.low),
+/// bar3 green and gaps fully above bar2 (bar3.low > bar2.high)
+/// bearish (−1.0): bar1 green, bar2 gaps fully above bar1 (bar2.low > bar1.high),
+/// bar3 red and gaps fully below bar2 (bar3.high < bar2.low)
+/// ```
+///
+/// Output is `0.0` otherwise. The first two bars always return `0.0` because the
+/// three-bar window is not yet filled. `tolerance` defaults to `0.001` (10 bps
+/// relative) and bounds how flat the middle candle must be to count as a doji; it
+/// must lie in `[0, 1)`. Pattern-shape check only — no trend filter is applied;
+/// combine with a trend indicator for actionable signals.
+///
+/// # Signed ±1 encoding
+///
+/// This detector emits the uniform candlestick sign convention shared across the
+/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
+/// drops straight into a machine-learning feature matrix where the bullish and
+/// bearish variants occupy a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{AbandonedBaby, Candle, Indicator};
+///
+/// let mut indicator = AbandonedBaby::new();
+/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(13.0, 13.1, 12.9, 13.0, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(16.0, 18.1, 15.9, 18.0, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct AbandonedBaby {
+ tolerance: f64,
+ prev: Option,
+ prev_prev: Option,
+ has_emitted: bool,
+}
+
+impl Default for AbandonedBaby {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl AbandonedBaby {
+ /// Construct a detector with the default relative doji tolerance (1e-3).
+ pub const fn new() -> Self {
+ Self {
+ tolerance: 0.001,
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ }
+ }
+
+ /// Construct a detector with a custom relative doji tolerance.
+ ///
+ /// `tolerance` must lie in `[0, 1)`.
+ pub fn with_tolerance(tolerance: f64) -> Result {
+ if !(0.0..1.0).contains(&tolerance) {
+ return Err(Error::InvalidPeriod {
+ message: "abandoned baby tolerance must lie in [0, 1)",
+ });
+ }
+ Ok(Self {
+ tolerance,
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ })
+ }
+
+ /// Configured relative doji tolerance.
+ pub fn tolerance(&self) -> f64 {
+ self.tolerance
+ }
+}
+
+impl Indicator for AbandonedBaby {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let pp = self.prev_prev;
+ let p = self.prev;
+ self.prev_prev = self.prev;
+ self.prev = Some(candle);
+ let (Some(bar1), Some(bar2)) = (pp, p) else {
+ return Some(0.0);
+ };
+ let tol = self.tolerance * bar2.open.abs().max(bar2.close.abs());
+ let bar2_is_doji = (bar2.close - bar2.open).abs() <= tol;
+ if !bar2_is_doji {
+ return Some(0.0);
+ }
+ // Bullish: red bar1, doji gaps below, green bar3 gaps above.
+ if bar1.close < bar1.open
+ && bar2.high < bar1.low
+ && candle.close > candle.open
+ && candle.low > bar2.high
+ {
+ return Some(1.0);
+ }
+ // Bearish: green bar1, doji gaps above, red bar3 gaps below.
+ if bar1.close > bar1.open
+ && bar2.low > bar1.high
+ && candle.close < candle.open
+ && candle.high < bar2.low
+ {
+ return Some(-1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.prev = None;
+ self.prev_prev = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 3
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "AbandonedBaby"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
+ Candle::new(open, high, low, close, 1.0, ts).unwrap()
+ }
+
+ #[test]
+ fn rejects_invalid_tolerance() {
+ assert!(AbandonedBaby::with_tolerance(-0.01).is_err());
+ assert!(AbandonedBaby::with_tolerance(1.0).is_err());
+ }
+
+ #[test]
+ fn accepts_valid_tolerance() {
+ let t = AbandonedBaby::with_tolerance(0.0).unwrap();
+ assert!((t.tolerance() - 0.0).abs() < 1e-12);
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = AbandonedBaby::default();
+ assert_eq!(t.name(), "AbandonedBaby");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ assert!((t.tolerance() - 0.001).abs() < 1e-12);
+ }
+
+ #[test]
+ fn bullish_abandoned_baby_is_plus_one() {
+ let mut t = AbandonedBaby::new();
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(1.0));
+ }
+
+ #[test]
+ fn bearish_abandoned_baby_is_minus_one() {
+ let mut t = AbandonedBaby::new();
+ assert_eq!(t.update(c(15.0, 20.1, 14.9, 20.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(22.0, 22.1, 21.9, 22.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(19.0, 19.1, 16.9, 17.0, 2)), Some(-1.0));
+ }
+
+ #[test]
+ fn middle_not_doji_yields_zero() {
+ let mut t = AbandonedBaby::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ // Middle bar has a wide body -> not a doji.
+ assert_eq!(t.update(c(13.0, 14.0, 11.0, 11.5, 1)), Some(0.0));
+ assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn no_gap_yields_zero() {
+ let mut t = AbandonedBaby::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ // Doji overlaps bar1's range -> no gap.
+ assert_eq!(t.update(c(15.0, 15.1, 14.9, 15.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = AbandonedBaby::new();
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + (i as f64 * 0.3).sin() * 5.0;
+ c(base, base + 1.0, base - 1.0, base + 0.5, i)
+ })
+ .collect();
+ let mut a = AbandonedBaby::new();
+ let mut b = AbandonedBaby::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = AbandonedBaby::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(13.0, 13.1, 12.9, 13.0, 1));
+ t.update(c(16.0, 18.1, 15.9, 18.0, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/advance_block.rs b/crates/wickra-core/src/indicators/advance_block.rs
new file mode 100644
index 00000000..45d5faf0
--- /dev/null
+++ b/crates/wickra-core/src/indicators/advance_block.rs
@@ -0,0 +1,192 @@
+//! Advance Block candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Advance Block — a 3-bar bearish warning: three green candles still pushing to
+/// higher closes, but visibly running out of steam — each real body shrinks while
+/// the upper shadows lengthen, hinting the advance is about to stall.
+///
+/// ```text
+/// all three green & higher closes
+/// each opens inside the prior body
+/// shrinking bodies (body3 < body2 < body1)
+/// upper shadow of bar3 >= upper shadow of bar2 and bar3 has an upper shadow
+/// ```
+///
+/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Advance Block
+/// is a single-direction (bearish-only) warning, so it never emits `+1.0`. The
+/// first two bars always return `0.0` because the three-bar window is not yet
+/// filled. Pattern-shape check only — no trend filter is applied; combine with a
+/// trend indicator for actionable signals.
+///
+/// # Signed ±1 encoding
+///
+/// This detector emits the uniform candlestick sign convention shared across the
+/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
+/// a machine-learning feature matrix as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{AdvanceBlock, Candle, Indicator};
+///
+/// let mut indicator = AdvanceBlock::new();
+/// indicator.update(Candle::new(10.0, 13.1, 9.9, 13.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(12.0, 14.3, 11.9, 14.0, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(13.5, 15.0, 13.4, 14.5, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(-1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct AdvanceBlock {
+ prev: Option,
+ prev_prev: Option,
+ has_emitted: bool,
+}
+
+impl AdvanceBlock {
+ /// Construct a new Advance Block detector.
+ pub const fn new() -> Self {
+ Self {
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for AdvanceBlock {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let pp = self.prev_prev;
+ let p = self.prev;
+ self.prev_prev = self.prev;
+ self.prev = Some(candle);
+ let (Some(bar1), Some(bar2)) = (pp, p) else {
+ return Some(0.0);
+ };
+ let body1 = bar1.close - bar1.open;
+ let body2 = bar2.close - bar2.open;
+ let body3 = candle.close - candle.open;
+ let upper2 = bar2.high - bar2.close;
+ let upper3 = candle.high - candle.close;
+ if bar1.close > bar1.open
+ && bar2.close > bar2.open
+ && candle.close > candle.open
+ && bar2.close > bar1.close
+ && candle.close > bar2.close
+ && bar2.open >= bar1.open
+ && bar2.open <= bar1.close
+ && candle.open >= bar2.open
+ && candle.open <= bar2.close
+ && body2 < body1
+ && body3 < body2
+ && upper3 >= upper2
+ && upper3 > 0.0
+ {
+ return Some(-1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.prev = None;
+ self.prev_prev = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 3
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "AdvanceBlock"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
+ Candle::new(open, high, low, close, 1.0, ts).unwrap()
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = AdvanceBlock::new();
+ assert_eq!(t.name(), "AdvanceBlock");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn advance_block_is_minus_one() {
+ let mut t = AdvanceBlock::new();
+ assert_eq!(t.update(c(10.0, 13.1, 9.9, 13.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(12.0, 14.3, 11.9, 14.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(13.5, 15.0, 13.4, 14.5, 2)), Some(-1.0));
+ }
+
+ #[test]
+ fn strong_advance_yields_zero() {
+ let mut t = AdvanceBlock::new();
+ // Bodies grow instead of shrinking -> a strong advance, not blocked.
+ assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(10.5, 12.6, 10.4, 12.5, 1)), Some(0.0));
+ assert_eq!(t.update(c(11.5, 14.1, 11.4, 14.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn no_upper_shadow_growth_yields_zero() {
+ let mut t = AdvanceBlock::new();
+ t.update(c(10.0, 13.1, 9.9, 13.0, 0));
+ t.update(c(12.0, 14.3, 11.9, 14.0, 1));
+ // bar3 shrinking body but no upper shadow -> not blocked.
+ assert_eq!(t.update(c(13.5, 14.5, 13.4, 14.5, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = AdvanceBlock::new();
+ assert_eq!(t.update(c(10.0, 13.1, 9.9, 13.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(12.0, 14.3, 11.9, 14.0, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ c(base, base + 2.0, base - 0.2, base + 1.5, i)
+ })
+ .collect();
+ let mut a = AdvanceBlock::new();
+ let mut b = AdvanceBlock::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = AdvanceBlock::new();
+ t.update(c(10.0, 13.1, 9.9, 13.0, 0));
+ t.update(c(12.0, 14.3, 11.9, 14.0, 1));
+ t.update(c(13.5, 15.0, 13.4, 14.5, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(10.0, 13.1, 9.9, 13.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/belt_hold.rs b/crates/wickra-core/src/indicators/belt_hold.rs
new file mode 100644
index 00000000..4b868ebe
--- /dev/null
+++ b/crates/wickra-core/src/indicators/belt_hold.rs
@@ -0,0 +1,211 @@
+//! Belt-hold candlestick pattern.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Belt-hold — a single-bar reversal: a long candle that opens at one extreme of
+/// its range (an "opening marubozu") and runs the other way.
+///
+/// ```text
+/// range = high − low
+/// bullish (+1.0): green, opens at the low (open − low <= tol * range) & long body
+/// bearish (−1.0): red, opens at the high (high − open <= tol * range) & long body
+/// long body = |close − open| >= 0.5 * range
+/// ```
+///
+/// Output is `0.0` when the opening side carries a shadow, the body is short, or
+/// the range is degenerate. `shadow_tolerance` defaults to `0.05` (5 % of the bar
+/// range allowed on the opening side) and must lie in `[0, 1)`. Pattern-shape
+/// check only — no trend filter is applied; combine with a trend indicator for
+/// actionable signals.
+///
+/// # Signed ±1 encoding
+///
+/// This detector emits the uniform candlestick sign convention shared across the
+/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
+/// drops straight into a machine-learning feature matrix where the bullish and
+/// bearish variants occupy a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{BeltHold, Candle, Indicator};
+///
+/// let mut indicator = BeltHold::new();
+/// // Bullish belt-hold: opens at the low, closes near the high.
+/// let candle = Candle::new(10.0, 12.0, 10.0, 11.5, 1.0, 0).unwrap();
+/// assert_eq!(indicator.update(candle), Some(1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct BeltHold {
+ shadow_tolerance: f64,
+ has_emitted: bool,
+}
+
+impl Default for BeltHold {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl BeltHold {
+ /// Construct a Belt-hold detector with the default 5 % opening-shadow tolerance.
+ pub const fn new() -> Self {
+ Self {
+ shadow_tolerance: 0.05,
+ has_emitted: false,
+ }
+ }
+
+ /// Construct a Belt-hold detector with a custom opening-shadow tolerance.
+ ///
+ /// `shadow_tolerance` must lie in `[0, 1)`.
+ pub fn with_tolerance(shadow_tolerance: f64) -> Result {
+ if !(0.0..1.0).contains(&shadow_tolerance) {
+ return Err(Error::InvalidPeriod {
+ message: "belt-hold shadow tolerance must lie in [0, 1)",
+ });
+ }
+ Ok(Self {
+ shadow_tolerance,
+ has_emitted: false,
+ })
+ }
+
+ /// Configured opening-shadow tolerance.
+ pub fn shadow_tolerance(&self) -> f64 {
+ self.shadow_tolerance
+ }
+}
+
+impl Indicator for BeltHold {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let range = candle.high - candle.low;
+ if range <= 0.0 {
+ return Some(0.0);
+ }
+ let body = candle.close - candle.open;
+ if body.abs() < 0.5 * range {
+ return Some(0.0);
+ }
+ let tol = self.shadow_tolerance * range;
+ // Bullish: opens at the low (no lower shadow), green body.
+ if body > 0.0 && candle.open - candle.low <= tol {
+ return Some(1.0);
+ }
+ // Bearish: opens at the high (no upper shadow), red body.
+ if body < 0.0 && candle.high - candle.open <= tol {
+ return Some(-1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 1
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "BeltHold"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
+ Candle::new(open, high, low, close, 1.0, ts).unwrap()
+ }
+
+ #[test]
+ fn rejects_invalid_tolerance() {
+ assert!(BeltHold::with_tolerance(-0.01).is_err());
+ assert!(BeltHold::with_tolerance(1.0).is_err());
+ }
+
+ #[test]
+ fn accepts_valid_tolerance() {
+ let t = BeltHold::with_tolerance(0.0).unwrap();
+ assert!((t.shadow_tolerance() - 0.0).abs() < 1e-12);
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = BeltHold::default();
+ assert_eq!(t.name(), "BeltHold");
+ assert_eq!(t.warmup_period(), 1);
+ assert!(!t.is_ready());
+ assert!((t.shadow_tolerance() - 0.05).abs() < 1e-12);
+ }
+
+ #[test]
+ fn bullish_belt_hold_is_plus_one() {
+ let mut t = BeltHold::new();
+ assert_eq!(t.update(c(10.0, 12.0, 10.0, 11.5, 0)), Some(1.0));
+ }
+
+ #[test]
+ fn bearish_belt_hold_is_minus_one() {
+ let mut t = BeltHold::new();
+ assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.5, 0)), Some(-1.0));
+ }
+
+ #[test]
+ fn opening_shadow_yields_zero() {
+ let mut t = BeltHold::new();
+ // Opens 0.5 above the low -> lower shadow exceeds tolerance.
+ assert_eq!(t.update(c(10.5, 12.0, 10.0, 11.5, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn short_body_yields_zero() {
+ let mut t = BeltHold::new();
+ // Body 0.5 < half the range (1.0) -> not a long belt-hold.
+ assert_eq!(t.update(c(10.0, 12.0, 10.0, 10.5, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn zero_range_yields_zero() {
+ let mut t = BeltHold::new();
+ assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ c(base, base + 2.0, base, base + 1.8, i)
+ })
+ .collect();
+ let mut a = BeltHold::new();
+ let mut b = BeltHold::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = BeltHold::new();
+ t.update(c(10.0, 12.0, 10.0, 11.5, 0));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ }
+}
diff --git a/crates/wickra-core/src/indicators/breakaway.rs b/crates/wickra-core/src/indicators/breakaway.rs
new file mode 100644
index 00000000..e6e965c6
--- /dev/null
+++ b/crates/wickra-core/src/indicators/breakaway.rs
@@ -0,0 +1,250 @@
+//! Breakaway candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Breakaway — a 5-bar reversal that fades an exhausted run. A trend gaps away on
+/// the second bar, drifts two more bars in the same direction, then the fifth bar
+/// snaps the other way and closes back inside the body gap left between the first
+/// and second bars, signalling the move has broken away from the crowd and is
+/// turning.
+///
+/// ```text
+/// bullish (+1.0) — appears in a decline:
+/// bar1 black (close < open)
+/// bar2 black & its body gaps DOWN below bar1's body (bar2.open < bar1.close)
+/// bar3 extends lower (high & low below bar2)
+/// bar4 black & extends lower (high & low below bar3)
+/// bar5 green & closes inside the bar1/bar2 body gap (bar2.open < close < bar1.close)
+///
+/// bearish (−1.0) — the mirror in an advance:
+/// bar1 white (close > open)
+/// bar2 white & its body gaps UP above bar1's body (bar2.open > bar1.close)
+/// bar3 extends higher (high & low above bar2)
+/// bar4 white & extends higher (high & low above bar3)
+/// bar5 red & closes inside the bar1/bar2 body gap (bar1.close < close < bar2.open)
+/// ```
+///
+/// The middle bar (`bar3`) may be either colour — only its high/low must extend
+/// the run. Output is `+1.0` bullish, `−1.0` bearish, `0.0` otherwise. The first
+/// four bars always return `0.0` because the five-bar window is not yet filled.
+/// Pattern-shape check only — no trend filter is applied; combine with a trend
+/// indicator for actionable signals. Recognition uses TA-Lib's
+/// `CDLBREAKAWAY` body-gap and high/low ordering rules directly; it does not add
+/// TA-Lib's rolling body-length average, matching the geometric house style of
+/// the other multi-bar patterns in this family.
+///
+/// # Signed ±1 encoding
+///
+/// This detector emits the uniform candlestick sign convention shared across the
+/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
+/// drops straight into a machine-learning feature matrix where the bullish and
+/// bearish variants occupy a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Breakaway, Candle, Indicator};
+///
+/// let mut indicator = Breakaway::new();
+/// indicator.update(Candle::new(20.0, 20.2, 14.8, 15.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(14.0, 14.1, 11.9, 12.0, 1.0, 1).unwrap());
+/// indicator.update(Candle::new(12.5, 13.0, 10.5, 11.0, 1.0, 2).unwrap());
+/// indicator.update(Candle::new(11.0, 11.5, 9.0, 9.5, 1.0, 3).unwrap());
+/// let out = indicator
+/// .update(Candle::new(9.5, 14.7, 9.4, 14.5, 1.0, 4).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct Breakaway {
+ c1: Option,
+ c2: Option,
+ c3: Option,
+ c4: Option,
+ has_emitted: bool,
+}
+
+impl Breakaway {
+ /// Construct a new Breakaway detector.
+ pub const fn new() -> Self {
+ Self {
+ c1: None,
+ c2: None,
+ c3: None,
+ c4: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for Breakaway {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let bar1 = self.c1;
+ let bar2 = self.c2;
+ let bar3 = self.c3;
+ let bar4 = self.c4;
+ self.c1 = self.c2;
+ self.c2 = self.c3;
+ self.c3 = self.c4;
+ self.c4 = Some(candle);
+ let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
+ return Some(0.0);
+ };
+ // Bullish: a decline gaps lower, runs two more bars down, then a green
+ // bar5 closes back inside the bar1/bar2 body gap.
+ if bar1.close < bar1.open
+ && bar2.close < bar2.open
+ && bar2.open < bar1.close
+ && bar3.high < bar2.high
+ && bar3.low < bar2.low
+ && bar4.close < bar4.open
+ && bar4.high < bar3.high
+ && bar4.low < bar3.low
+ && candle.close > candle.open
+ && candle.close > bar2.open
+ && candle.close < bar1.close
+ {
+ return Some(1.0);
+ }
+ // Bearish: the mirror — an advance gaps higher, runs two more bars up,
+ // then a red bar5 closes back inside the bar1/bar2 body gap.
+ if bar1.close > bar1.open
+ && bar2.close > bar2.open
+ && bar2.open > bar1.close
+ && bar3.high > bar2.high
+ && bar3.low > bar2.low
+ && bar4.close > bar4.open
+ && bar4.high > bar3.high
+ && bar4.low > bar3.low
+ && candle.close < candle.open
+ && candle.close < bar2.open
+ && candle.close > bar1.close
+ {
+ return Some(-1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = None;
+ self.c3 = None;
+ self.c4 = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 5
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "Breakaway"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
+ Candle::new(open, high, low, close, 1.0, ts).unwrap()
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = Breakaway::new();
+ assert_eq!(t.name(), "Breakaway");
+ assert_eq!(t.warmup_period(), 5);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn bullish_breakaway_is_plus_one() {
+ let mut t = Breakaway::new();
+ assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), Some(0.0));
+ assert_eq!(t.update(c(9.5, 14.7, 9.4, 14.5, 4)), Some(1.0));
+ }
+
+ #[test]
+ fn bearish_breakaway_is_minus_one() {
+ let mut t = Breakaway::new();
+ assert_eq!(t.update(c(15.0, 20.2, 14.8, 20.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(21.0, 23.1, 20.9, 23.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(22.5, 24.5, 21.5, 24.0, 2)), Some(0.0));
+ assert_eq!(t.update(c(24.0, 26.5, 23.0, 26.0, 3)), Some(0.0));
+ assert_eq!(t.update(c(27.0, 27.2, 20.4, 20.5, 4)), Some(-1.0));
+ }
+
+ #[test]
+ fn no_body_gap_yields_zero() {
+ let mut t = Breakaway::new();
+ // bar2 does not gap below bar1's body (bar2.open >= bar1.close).
+ t.update(c(20.0, 20.2, 14.8, 15.0, 0));
+ t.update(c(16.0, 16.1, 13.9, 14.0, 1));
+ t.update(c(13.5, 14.0, 11.5, 12.0, 2));
+ t.update(c(12.0, 12.5, 10.0, 10.5, 3));
+ assert_eq!(t.update(c(10.5, 15.7, 10.4, 15.5, 4)), Some(0.0));
+ }
+
+ #[test]
+ fn bullish_close_outside_gap_yields_zero() {
+ let mut t = Breakaway::new();
+ t.update(c(20.0, 20.2, 14.8, 15.0, 0));
+ t.update(c(14.0, 14.1, 11.9, 12.0, 1));
+ t.update(c(12.5, 13.0, 10.5, 11.0, 2));
+ t.update(c(11.0, 11.5, 9.0, 9.5, 3));
+ // bar5 closes at 13.0 — below bar2.open (14), so outside the body gap.
+ assert_eq!(t.update(c(9.5, 13.2, 9.4, 13.0, 4)), Some(0.0));
+ }
+
+ #[test]
+ fn first_four_bars_return_zero() {
+ let mut t = Breakaway::new();
+ assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ c(base, base + 2.0, base - 0.5, base + 1.5, i)
+ })
+ .collect();
+ let mut a = Breakaway::new();
+ let mut b = Breakaway::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = Breakaway::new();
+ t.update(c(20.0, 20.2, 14.8, 15.0, 0));
+ t.update(c(14.0, 14.1, 11.9, 12.0, 1));
+ t.update(c(12.5, 13.0, 10.5, 11.0, 2));
+ t.update(c(11.0, 11.5, 9.0, 9.5, 3));
+ t.update(c(9.5, 14.7, 9.4, 14.5, 4));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/counterattack.rs b/crates/wickra-core/src/indicators/counterattack.rs
new file mode 100644
index 00000000..93afb83e
--- /dev/null
+++ b/crates/wickra-core/src/indicators/counterattack.rs
@@ -0,0 +1,245 @@
+//! Counterattack candlestick pattern.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Counterattack — a 2-bar reversal where the second bar storms back to close
+/// right where the first bar closed. A long candle runs with the trend, then an
+/// opposite-coloured long candle opens far in the trend direction and rallies (or
+/// sells off) all the way back to the prior close — the two closes meeting forms
+/// the "counterattack line".
+///
+/// ```text
+/// long bodies = |close − open| >= 0.5 * (high − low) (both bars)
+/// equal closes = |close2 − close1| <= tol * mean(range1, range2)
+/// bullish (+1.0): bar1 black (down), bar2 white (up), equal closes
+/// bearish (−1.0): bar1 white (up), bar2 black (down), equal closes
+/// ```
+///
+/// Output is `+1.0` bullish, `−1.0` bearish, and `0.0` when the bodies are short,
+/// the colours match, or the closes are not level. The first bar always returns
+/// `0.0` because the two-bar window is not yet filled. `equal_tolerance` defaults
+/// to `0.05` (TA-Lib's `CDLCOUNTERATTACK` "equal" factor — 5 % of the mean bar
+/// range) and must lie in `[0, 1)`. The body-length test uses a fixed half-range
+/// fraction rather than TA-Lib's rolling body average, matching the geometric
+/// house style of this pattern family. Pattern-shape check only — no trend filter
+/// is applied; combine with a trend indicator for actionable signals.
+///
+/// # Signed ±1 encoding
+///
+/// This detector emits the uniform candlestick sign convention shared across the
+/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
+/// drops straight into a machine-learning feature matrix where the bullish and
+/// bearish variants occupy a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Counterattack, Indicator};
+///
+/// let mut indicator = Counterattack::new();
+/// // Bullish: a long black bar, then a long white bar closing at the same level.
+/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
+/// let out = indicator
+/// .update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 1).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct Counterattack {
+ equal_tolerance: f64,
+ prev: Option,
+ has_emitted: bool,
+}
+
+impl Default for Counterattack {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Counterattack {
+ /// Construct a Counterattack detector with the default 5 % equal-close tolerance.
+ pub const fn new() -> Self {
+ Self {
+ equal_tolerance: 0.05,
+ prev: None,
+ has_emitted: false,
+ }
+ }
+
+ /// Construct a Counterattack detector with a custom equal-close tolerance.
+ ///
+ /// `equal_tolerance` is the fraction of the mean bar range within which the
+ /// two closes must agree and must lie in `[0, 1)`.
+ pub fn with_tolerance(equal_tolerance: f64) -> Result {
+ if !(0.0..1.0).contains(&equal_tolerance) {
+ return Err(Error::InvalidPeriod {
+ message: "counterattack equal tolerance must lie in [0, 1)",
+ });
+ }
+ Ok(Self {
+ equal_tolerance,
+ prev: None,
+ has_emitted: false,
+ })
+ }
+
+ /// Configured equal-close tolerance.
+ pub fn equal_tolerance(&self) -> f64 {
+ self.equal_tolerance
+ }
+}
+
+impl Indicator for Counterattack {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let prev = self.prev;
+ self.prev = Some(candle);
+ let Some(bar1) = prev else {
+ return Some(0.0);
+ };
+ let range1 = bar1.high - bar1.low;
+ let range2 = candle.high - candle.low;
+ let body1 = bar1.close - bar1.open;
+ let body2 = candle.close - candle.open;
+ let long1 = body1.abs() >= 0.5 * range1;
+ let long2 = body2.abs() >= 0.5 * range2;
+ let tol = self.equal_tolerance * 0.5 * (range1 + range2);
+ let equal_close = (candle.close - bar1.close).abs() <= tol;
+ if !(long1 && long2 && equal_close) {
+ return Some(0.0);
+ }
+ // Bullish: a long black bar met by a long white bar closing level.
+ if body1 < 0.0 && body2 > 0.0 {
+ return Some(1.0);
+ }
+ // Bearish: a long white bar met by a long black bar closing level.
+ if body1 > 0.0 && body2 < 0.0 {
+ return Some(-1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.prev = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 2
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "Counterattack"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
+ Candle::new(open, high, low, close, 1.0, ts).unwrap()
+ }
+
+ #[test]
+ fn rejects_invalid_tolerance() {
+ assert!(Counterattack::with_tolerance(-0.01).is_err());
+ assert!(Counterattack::with_tolerance(1.0).is_err());
+ }
+
+ #[test]
+ fn accepts_valid_tolerance() {
+ let t = Counterattack::with_tolerance(0.0).unwrap();
+ assert!((t.equal_tolerance() - 0.0).abs() < 1e-12);
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = Counterattack::default();
+ assert_eq!(t.name(), "Counterattack");
+ assert_eq!(t.warmup_period(), 2);
+ assert!(!t.is_ready());
+ assert!((t.equal_tolerance() - 0.05).abs() < 1e-12);
+ }
+
+ #[test]
+ fn bullish_counterattack_is_plus_one() {
+ let mut t = Counterattack::new();
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 1)), Some(1.0));
+ }
+
+ #[test]
+ fn bearish_counterattack_is_minus_one() {
+ let mut t = Counterattack::new();
+ assert_eq!(t.update(c(15.0, 20.1, 14.9, 20.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(25.0, 25.1, 19.9, 20.0, 1)), Some(-1.0));
+ }
+
+ #[test]
+ fn unequal_close_yields_zero() {
+ let mut t = Counterattack::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ // Second close at 17.0 is far from the first close (15.0) -> not level.
+ assert_eq!(t.update(c(10.0, 17.1, 9.9, 17.0, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn same_color_yields_zero() {
+ let mut t = Counterattack::new();
+ // Both bars black -> not opposite colours.
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn short_body_yields_zero() {
+ let mut t = Counterattack::new();
+ // Second bar has a tiny body relative to its range.
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ assert_eq!(t.update(c(14.8, 20.0, 9.9, 15.2, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn first_bar_returns_zero() {
+ let mut t = Counterattack::new();
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ c(base, base + 2.0, base - 2.0, base + 1.5, i)
+ })
+ .collect();
+ let mut a = Counterattack::new();
+ let mut b = Counterattack::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = Counterattack::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(10.0, 15.1, 9.9, 15.0, 1));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs
index 589d2b81..43a9f8b2 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -4,11 +4,13 @@
//! [`FAMILIES`]. Every public name is re-exported flat from this module and
//! from the crate root for convenience.
+mod abandoned_baby;
mod acceleration_bands;
mod accelerator_oscillator;
mod ad_oscillator;
mod adaptive_cycle;
mod adl;
+mod advance_block;
mod adx;
mod adxr;
mod alligator;
@@ -26,9 +28,11 @@ mod average_drawdown;
mod awesome_oscillator;
mod awesome_oscillator_histogram;
mod balance_of_power;
+mod belt_hold;
mod beta;
mod bollinger;
mod bollinger_bandwidth;
+mod breakaway;
mod calendar_spread;
mod calmar_ratio;
mod camarilla_pivots;
@@ -48,6 +52,7 @@ mod cointegration;
mod conditional_value_at_risk;
mod connors_rsi;
mod coppock;
+mod counterattack;
mod cvd;
mod cybernetic_cycle;
mod decycler;
@@ -253,11 +258,13 @@ mod zero_lag_macd;
mod zig_zag;
mod zlema;
+pub use abandoned_baby::AbandonedBaby;
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
pub use accelerator_oscillator::AcceleratorOscillator;
pub use ad_oscillator::AdOscillator;
pub use adaptive_cycle::AdaptiveCycle;
pub use adl::Adl;
+pub use advance_block::AdvanceBlock;
pub use adx::{Adx, AdxOutput};
pub use adxr::Adxr;
pub use alligator::{Alligator, AlligatorOutput};
@@ -275,9 +282,11 @@ pub use average_drawdown::AverageDrawdown;
pub use awesome_oscillator::AwesomeOscillator;
pub use awesome_oscillator_histogram::AwesomeOscillatorHistogram;
pub use balance_of_power::BalanceOfPower;
+pub use belt_hold::BeltHold;
pub use beta::Beta;
pub use bollinger::{BollingerBands, BollingerOutput};
pub use bollinger_bandwidth::BollingerBandwidth;
+pub use breakaway::Breakaway;
pub use calendar_spread::CalendarSpread;
pub use calmar_ratio::CalmarRatio;
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
@@ -297,6 +306,7 @@ pub use cointegration::{Cointegration, CointegrationOutput};
pub use conditional_value_at_risk::ConditionalValueAtRisk;
pub use connors_rsi::ConnorsRsi;
pub use coppock::Coppock;
+pub use counterattack::Counterattack;
pub use cvd::CumulativeVolumeDelta;
pub use cybernetic_cycle::CyberneticCycle;
pub use decycler::Decycler;
@@ -770,6 +780,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"IdenticalThreeCrows",
"ThreeLineStrike",
"ThreeStarsInSouth",
+ "AbandonedBaby",
+ "AdvanceBlock",
+ "BeltHold",
+ "Breakaway",
+ "Counterattack",
],
),
(
@@ -861,6 +876,6 @@ mod family_tests {
// the actual indicator count is the early-warning signal that an
// indicator was added without being assigned a family.
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
- assert_eq!(total, 244, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 249, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs
index 0a9b3edc..28a244d6 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -35,6 +35,12 @@
//! ```
#![cfg_attr(docsrs, feature(doc_cfg))]
+// The libtest harness collects every `#[test]` into a compiler-generated array
+// of test references. With 2000+ unit tests that array exceeds clippy's 16 KB
+// `large_stack_arrays` threshold; the diagnostic is spanless libtest scaffolding,
+// not our code, so it cannot be silenced at a call site. Suppress it only in test
+// builds — library code is still linted for genuinely large stack arrays.
+#![cfg_attr(test, allow(clippy::large_stack_arrays))]
mod derivatives;
mod error;
@@ -47,19 +53,20 @@ pub mod indicators;
pub use derivatives::DerivativesTick;
pub use error::{Error, Result};
pub use indicators::{
- AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator, AdaptiveCycle,
- Adl, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma, Alpha, AnchoredVwap, Apo, Aroon,
- AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, Autocorrelation,
- AverageDrawdown, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Beta,
- BollingerBands, BollingerBandwidth, BollingerOutput, CalendarSpread, CalmarRatio, Camarilla,
- CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator,
- ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
- ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, Cmo,
- CoefficientOfVariation, Cointegration, CointegrationOutput, ConditionalValueAtRisk, ConnorsRsi,
- Coppock, CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema,
- DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, Donchian,
- DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo,
- DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
+ AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator,
+ AdaptiveCycle, Adl, AdvanceBlock, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
+ Alpha, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput,
+ AtrTrailingStop, Autocorrelation, AverageDrawdown, AwesomeOscillator,
+ AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BollingerBands, BollingerBandwidth,
+ BollingerOutput, Breakaway, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci,
+ CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop,
+ ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots,
+ ClassicPivotsOutput, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
+ ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, CumulativeVolumeDelta,
+ CyberneticCycle, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
+ DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, Donchian, DonchianOutput, DonchianStop,
+ DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo, DrawdownDuration,
+ EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
EmpiricalModeDecomposition, Engulfing, Evwma, Fama, FibonacciPivots, FibonacciPivotsOutput,
FisherTransform, Footprint, FootprintOutput, ForceIndex, FractalChaosBands,
FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore,
diff --git a/docs/README.md b/docs/README.md
index 8065ea69..eea4a319 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,7 +8,7 @@ That includes:
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), and
[WASM](https://docs.wickra.org/Quickstart-WASM).
-- A per-indicator deep dive for every one of the **249 indicators** across
+- A per-indicator deep dive for every one of the **254 indicators** across
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
diff --git a/fuzz/fuzz_targets/indicator_update_candle.rs b/fuzz/fuzz_targets/indicator_update_candle.rs
index 039fb314..0abb9bc1 100644
--- a/fuzz/fuzz_targets/indicator_update_candle.rs
+++ b/fuzz/fuzz_targets/indicator_update_candle.rs
@@ -23,26 +23,7 @@
use libfuzzer_sys::fuzz_target;
use wickra_core::{
- AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, Adx, Adxr, Alligator,
- AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator,
- AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, Camarilla, Candle, Cci, ChaikinMoneyFlow,
- ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex,
- ClassicPivots, DemandIndex, DemarkPivots, Doji, Donchian, DonchianStop, EaseOfMovement,
- Engulfing, Evwma, FibonacciPivots, ForceIndex, FractalChaosBands, GarmanKlassVolatility,
- Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HurstChannel, Ichimoku,
- IdenticalThreeCrows, Indicator,
- Inertia, InitialBalance, InvertedHammer, Keltner, Kvo, MarketFacilitationIndex, Marubozu,
- MassIndex, MedianPrice, Mfi, MorningEveningStar, Natr, Nvi, Obv, OpeningRange,
- ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RogersSatchellVolatility, RollingVwap,
- Rvi, Rwi, ShootingStar, Smi, SpinningTop, StarcBands, Stochastic, SuperTrend, TdCombo,
- TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei,
- TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside,
- ThreeSoldiersOrCrows, ThreeStarsInSouth, TrueRange,
- Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UpsideGapTwoCrows,
- ValueArea, VoltyStop,
- VolumeOscillator, VolumePriceTrend, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend,
- WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit,
- ZigZag,
+AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, Counterattack, DemandIndex, DemarkPivots, Doji, Donchian, DonchianStop, EaseOfMovement, Engulfing, Evwma, FibonacciPivots, ForceIndex, FractalChaosBands, GarmanKlassVolatility, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HurstChannel, Ichimoku, IdenticalThreeCrows, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kvo, MarketFacilitationIndex, Marubozu, MassIndex, MedianPrice, Mfi, MorningEveningStar, Natr, Nvi, Obv, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, ShootingStar, Smi, SpinningTop, StarcBands, Stochastic, SuperTrend, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag
};
/// Convert a flat `f64` stream into a `Vec` by chunking it into
@@ -297,6 +278,11 @@ fuzz_target!(|data: Vec| {
}
// --- Candlestick Patterns (family 14) ---
+ drive(Counterattack::new, &candles);
+ drive(Breakaway::new, &candles);
+ drive(BeltHold::new, &candles);
+ drive(AbandonedBaby::new, &candles);
+ drive(AdvanceBlock::new, &candles);
drive(Doji::new, &candles);
drive(|| Doji::new().signed(), &candles);
drive(Hammer::new, &candles);