diff --git a/CHANGELOG.md b/CHANGELOG.md
index abc4f404..3f8b38e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -67,6 +67,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Stalled Pattern** — 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 (`CDLSTALLEDPATTERN`).
- **Stick Sandwich** — a 3-bar bullish reversal: two black candles closing at the same level sandwich a white candle, marking a support floor; bullish +1 (`CDLSTICKSANDWICH`).
- **Takuri** — a single-bar bullish reversal, a strict Dragonfly Doji with a negligible upper shadow and very long lower shadow; bullish +1 (`CDLTAKURI`).
+ - **Closing Marubozu** — a single long-bodied candle with no shadow on the close end; bullish +1 (white, closes at the high) or bearish -1 (black, closes at the low) (`CDLCLOSINGMARUBOZU`).
+ - **Opening Marubozu** — a single long-bodied candle with no shadow on the open end; bullish +1 (white, opens at the low) or bearish -1 (black, opens at the high). No direct TA-Lib equivalent — completes the pair with the closing marubozu.
+ - **Tasuki Gap** — a 3-bar continuation: two same-coloured candles gap in the trend direction, then an opposite candle opens within the second body and closes back into the gap without filling it; upside +1, downside -1 (`CDLTASUKIGAP`).
+ - **Unique Three River** — a 3-bar bullish reversal: a long black candle, a black candle probing a new low with its body inside the first, then a small white candle held below it; bullish +1 (`CDLUNIQUE3RIVER`).
+ - **Concealing Baby Swallow** — a rare 4-bar bullish capitulation: two black marubozu, a black candle gapping down with an upper shadow into the second, then a large black candle engulfing it entirely; bullish +1 (`CDLCONCEALBABYSWALL`).
- **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 b6e389a0..e99785f1 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 284 indicators; start at the
+ every one of the 289 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
-284 streaming-first indicators across eighteen families. Every one passes the
+289 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, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri |
+| 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, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow |
| 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 284 indicators
+│ ├── wickra-core/ core engine + all 289 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 1ff591d5..d141fb41 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -275,6 +275,11 @@ const candleScalar = {
StalledPattern: { make: () => new wickra.StalledPattern(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
StickSandwich: { make: () => new wickra.StickSandwich(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
Takuri: { make: () => new wickra.Takuri(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ ClosingMarubozu: { make: () => new wickra.ClosingMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ OpeningMarubozu: { make: () => new wickra.OpeningMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ TasukiGap: { make: () => new wickra.TasukiGap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ UniqueThreeRiver: { make: () => new wickra.UniqueThreeRiver(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ ConcealingBabySwallow: { make: () => new wickra.ConcealingBabySwallow(), 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 38893431..5788f0a8 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -2570,6 +2570,51 @@ export declare class Takuri {
isReady(): boolean
warmupPeriod(): number
}
+export type ClosingMarubozuNode = ClosingMarubozu
+export declare class ClosingMarubozu {
+ 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 OpeningMarubozuNode = OpeningMarubozu
+export declare class OpeningMarubozu {
+ 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 TasukiGapNode = TasukiGap
+export declare class TasukiGap {
+ 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 UniqueThreeRiverNode = UniqueThreeRiver
+export declare class UniqueThreeRiver {
+ 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 ConcealingBabySwallowNode = ConcealingBabySwallow
+export declare class ConcealingBabySwallow {
+ 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 021ad9f3..1c26bead 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, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, 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, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, 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
@@ -555,6 +555,11 @@ module.exports.DownsideGapThreeMethods = DownsideGapThreeMethods
module.exports.StalledPattern = StalledPattern
module.exports.StickSandwich = StickSandwich
module.exports.Takuri = Takuri
+module.exports.ClosingMarubozu = ClosingMarubozu
+module.exports.OpeningMarubozu = OpeningMarubozu
+module.exports.TasukiGap = TasukiGap
+module.exports.UniqueThreeRiver = UniqueThreeRiver
+module.exports.ConcealingBabySwallow = ConcealingBabySwallow
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 626419c1..ce6d72bc 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -8825,6 +8825,19 @@ node_candle_pattern!(
node_candle_pattern!(StalledPatternNode, wc::StalledPattern, "StalledPattern");
node_candle_pattern!(StickSandwichNode, wc::StickSandwich, "StickSandwich");
node_candle_pattern!(TakuriNode, wc::Takuri, "Takuri");
+node_candle_pattern!(ClosingMarubozuNode, wc::ClosingMarubozu, "ClosingMarubozu");
+node_candle_pattern!(OpeningMarubozuNode, wc::OpeningMarubozu, "OpeningMarubozu");
+node_candle_pattern!(TasukiGapNode, wc::TasukiGap, "TasukiGap");
+node_candle_pattern!(
+ UniqueThreeRiverNode,
+ wc::UniqueThreeRiver,
+ "UniqueThreeRiver"
+);
+node_candle_pattern!(
+ ConcealingBabySwallowNode,
+ wc::ConcealingBabySwallow,
+ "ConcealingBabySwallow"
+);
// ============================== Microstructure: Order Book ==============================
//
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index b4613e7a..a95cd868 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -280,6 +280,11 @@ from ._wickra import (
StalledPattern,
StickSandwich,
Takuri,
+ ClosingMarubozu,
+ OpeningMarubozu,
+ TasukiGap,
+ UniqueThreeRiver,
+ ConcealingBabySwallow,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -587,6 +592,11 @@ __all__ = [
"StalledPattern",
"StickSandwich",
"Takuri",
+ "ClosingMarubozu",
+ "OpeningMarubozu",
+ "TasukiGap",
+ "UniqueThreeRiver",
+ "ConcealingBabySwallow",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index ff09855a..e97f8ee9 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -11688,6 +11688,15 @@ candle_pattern_no_param!(
candle_pattern_no_param!(PyStalledPattern, wc::StalledPattern, "StalledPattern");
candle_pattern_no_param!(PyStickSandwich, wc::StickSandwich, "StickSandwich");
candle_pattern_no_param!(PyTakuri, wc::Takuri, "Takuri");
+candle_pattern_no_param!(PyClosingMarubozu, wc::ClosingMarubozu, "ClosingMarubozu");
+candle_pattern_no_param!(PyOpeningMarubozu, wc::OpeningMarubozu, "OpeningMarubozu");
+candle_pattern_no_param!(PyTasukiGap, wc::TasukiGap, "TasukiGap");
+candle_pattern_no_param!(PyUniqueThreeRiver, wc::UniqueThreeRiver, "UniqueThreeRiver");
+candle_pattern_no_param!(
+ PyConcealingBabySwallow,
+ wc::ConcealingBabySwallow,
+ "ConcealingBabySwallow"
+);
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14224,6 +14233,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 e9280e6e..e662c548 100644
--- a/bindings/python/tests/test_new_indicators.py
+++ b/bindings/python/tests/test_new_indicators.py
@@ -683,6 +683,26 @@ CANDLE_SCALAR = {
lambda: ta.Takuri(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
+ "ClosingMarubozu": (
+ lambda: ta.ClosingMarubozu(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "OpeningMarubozu": (
+ lambda: ta.OpeningMarubozu(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "TasukiGap": (
+ lambda: ta.TasukiGap(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "UniqueThreeRiver": (
+ lambda: ta.UniqueThreeRiver(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "ConcealingBabySwallow": (
+ lambda: ta.ConcealingBabySwallow(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
}
@@ -2004,6 +2024,38 @@ 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)
+
+def test_closing_marubozu_reference():
+ t = ta.ClosingMarubozu()
+ assert t.update((10.5, 15.0, 10.0, 15.0, 1.0, 0)) == pytest.approx(1.0)
+
+
+def test_opening_marubozu_reference():
+ t = ta.OpeningMarubozu()
+ assert t.update((10.0, 15.0, 10.0, 14.5, 1.0, 0)) == pytest.approx(1.0)
+
+
+def test_tasuki_gap_reference():
+ t = ta.TasukiGap()
+ assert t.update((10.0, 11.2, 9.8, 11.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((12.0, 14.0, 11.9, 13.5, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((13.0, 13.1, 11.4, 11.5, 1.0, 2)) == pytest.approx(1.0)
+
+
+def test_unique_three_river_reference():
+ t = ta.UniqueThreeRiver()
+ assert t.update((15.0, 15.1, 10.0, 10.5, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((14.0, 14.1, 9.0, 11.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((10.2, 10.9, 9.5, 10.4, 1.0, 2)) == pytest.approx(1.0)
+
+
+def test_concealing_baby_swallow_reference():
+ t = ta.ConcealingBabySwallow()
+ assert t.update((20.0, 20.1, 14.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((16.0, 16.1, 11.9, 12.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((11.0, 13.0, 9.9, 10.0, 1.0, 2)) == pytest.approx(0.0)
+ assert t.update((14.0, 14.1, 8.9, 9.0, 1.0, 3)) == pytest.approx(1.0)
+
# --- Lifecycle ------------------------------------------------------------
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index dd73aa17..cd18638c 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -6402,6 +6402,15 @@ wasm_candle_pattern!(
wasm_candle_pattern!(WasmStalledPattern, wc::StalledPattern, StalledPattern);
wasm_candle_pattern!(WasmStickSandwich, wc::StickSandwich, StickSandwich);
wasm_candle_pattern!(WasmTakuri, wc::Takuri, Takuri);
+wasm_candle_pattern!(WasmClosingMarubozu, wc::ClosingMarubozu, ClosingMarubozu);
+wasm_candle_pattern!(WasmOpeningMarubozu, wc::OpeningMarubozu, OpeningMarubozu);
+wasm_candle_pattern!(WasmTasukiGap, wc::TasukiGap, TasukiGap);
+wasm_candle_pattern!(WasmUniqueThreeRiver, wc::UniqueThreeRiver, UniqueThreeRiver);
+wasm_candle_pattern!(
+ WasmConcealingBabySwallow,
+ wc::ConcealingBabySwallow,
+ ConcealingBabySwallow
+);
// ============================== Microstructure: Order Book ==============================
//
diff --git a/crates/wickra-core/src/indicators/closing_marubozu.rs b/crates/wickra-core/src/indicators/closing_marubozu.rs
new file mode 100644
index 00000000..f35ef255
--- /dev/null
+++ b/crates/wickra-core/src/indicators/closing_marubozu.rs
@@ -0,0 +1,177 @@
+//! Closing Marubozu candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Closing Marubozu — a single-bar strong-momentum candle with a long body and no
+/// shadow on the *close* end. A white closing marubozu closes right at the high
+/// (no upper shadow) and may carry an opening shadow below; a black one closes
+/// right at the low (no lower shadow) and may carry an opening shadow above. The
+/// shaved close end shows the move ran unopposed into the bell.
+///
+/// ```text
+/// range = high − low
+/// long body: |close − open| >= 0.7 * range
+/// white: close > open and high − close <= 0.05 * range (close at the high)
+/// black: close < open and close − low <= 0.05 * range (close at the low)
+/// ```
+///
+/// Output is `+1.0` for a white closing marubozu, `−1.0` for a black one, and
+/// `0.0` otherwise. Body and shadow thresholds follow the geometric house style
+/// rather than TA-Lib's rolling averages. The opposite shaved end is
+/// [`crate::OpeningMarubozu`]. 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, ClosingMarubozu, Indicator};
+///
+/// let mut indicator = ClosingMarubozu::new();
+/// // White: closes at the high, small opening shadow below.
+/// let candle = Candle::new(10.5, 15.0, 10.0, 15.0, 1.0, 0).unwrap();
+/// assert_eq!(indicator.update(candle), Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct ClosingMarubozu {
+ has_emitted: bool,
+}
+
+impl ClosingMarubozu {
+ /// Construct a new Closing Marubozu detector.
+ pub const fn new() -> Self {
+ Self { has_emitted: false }
+ }
+}
+
+impl Indicator for ClosingMarubozu {
+ 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.7 * range {
+ return Some(0.0);
+ }
+ let tol = 0.05 * range;
+ if body > 0.0 && candle.high - candle.close <= tol {
+ return Some(1.0);
+ }
+ if body < 0.0 && candle.close - candle.low <= 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 {
+ "ClosingMarubozu"
+ }
+}
+
+#[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 = ClosingMarubozu::new();
+ assert_eq!(t.name(), "ClosingMarubozu");
+ assert_eq!(t.warmup_period(), 1);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn white_closing_marubozu_is_plus_one() {
+ let mut t = ClosingMarubozu::new();
+ // Closes at the high, opening shadow below.
+ assert_eq!(t.update(c(10.5, 15.0, 10.0, 15.0, 0)), Some(1.0));
+ }
+
+ #[test]
+ fn black_closing_marubozu_is_minus_one() {
+ let mut t = ClosingMarubozu::new();
+ // Closes at the low, opening shadow above.
+ assert_eq!(t.update(c(14.5, 15.0, 10.0, 10.0, 0)), Some(-1.0));
+ }
+
+ #[test]
+ fn white_with_upper_shadow_yields_zero() {
+ let mut t = ClosingMarubozu::new();
+ // Long white body but a clear upper shadow -> close is not at the high.
+ assert_eq!(t.update(c(10.5, 16.0, 10.0, 15.0, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn black_with_lower_shadow_yields_zero() {
+ let mut t = ClosingMarubozu::new();
+ // Long black body but a clear lower shadow -> close is not at the low.
+ assert_eq!(t.update(c(14.5, 15.0, 9.0, 10.0, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn short_body_yields_zero() {
+ let mut t = ClosingMarubozu::new();
+ // Body is short relative to range.
+ assert_eq!(t.update(c(12.0, 15.0, 10.0, 12.5, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn zero_range_yields_zero() {
+ let mut t = ClosingMarubozu::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 + 0.5, base + 5.0, base, base + 5.0, i)
+ })
+ .collect();
+ let mut a = ClosingMarubozu::new();
+ let mut b = ClosingMarubozu::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = ClosingMarubozu::new();
+ t.update(c(10.5, 15.0, 10.0, 15.0, 0));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ }
+}
diff --git a/crates/wickra-core/src/indicators/concealing_baby_swallow.rs b/crates/wickra-core/src/indicators/concealing_baby_swallow.rs
new file mode 100644
index 00000000..f33b5f8d
--- /dev/null
+++ b/crates/wickra-core/src/indicators/concealing_baby_swallow.rs
@@ -0,0 +1,278 @@
+//! Concealing Baby Swallow candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Returns `true` when `candle` is a black marubozu: a down candle whose body fills
+/// the range with negligible shadows on both ends.
+fn black_marubozu(candle: Candle) -> bool {
+ let range = candle.high - candle.low;
+ if range <= 0.0 {
+ return false;
+ }
+ let upper = candle.high - candle.open;
+ let lower = candle.close - candle.low;
+ candle.open > candle.close && upper <= 0.05 * range && lower <= 0.05 * range
+}
+
+/// Concealing Baby Swallow — a rare 4-bar bullish reversal. Two black marubozu lead
+/// a steep decline; the third is a black candle that gaps down on the open yet
+/// throws a long upper shadow back up into the second body; the fourth is a large
+/// black candle that completely engulfs the third, shadows included. The relentless
+/// selling that can no longer make ground signals capitulation.
+///
+/// ```text
+/// bar1, bar2 black marubozu (body == range, negligible shadows)
+/// bar3 black, opens below bar2's body (open3 < close2) with an upper
+/// shadow into it (high3 > close2)
+/// bar4 black, engulfs bar3 including shadows: open4 > high3 and close4 < low3
+/// ```
+///
+/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Concealing Baby
+/// Swallow is a single-direction (bullish-only) reversal, so it never emits `−1.0`.
+/// The first three bars always return `0.0` because the four-bar window is not yet
+/// filled. Body and shadow thresholds follow the geometric house style rather than
+/// TA-Lib's rolling averages. 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, `0.0` no pattern — so it drops straight into
+/// a machine-learning feature matrix as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, ConcealingBabySwallow, Indicator};
+///
+/// let mut indicator = ConcealingBabySwallow::new();
+/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(16.0, 16.1, 11.9, 12.0, 1.0, 1).unwrap());
+/// indicator.update(Candle::new(11.0, 13.0, 9.9, 10.0, 1.0, 2).unwrap());
+/// let out = indicator
+/// .update(Candle::new(14.0, 14.1, 8.9, 9.0, 1.0, 3).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct ConcealingBabySwallow {
+ c1: Option,
+ c2: Option,
+ c3: Option,
+ has_emitted: bool,
+}
+
+impl ConcealingBabySwallow {
+ /// Construct a new Concealing Baby Swallow detector.
+ pub const fn new() -> Self {
+ Self {
+ c1: None,
+ c2: None,
+ c3: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for ConcealingBabySwallow {
+ 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;
+ self.c1 = self.c2;
+ self.c2 = self.c3;
+ self.c3 = Some(candle);
+ let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
+ return Some(0.0);
+ };
+ // bar1 and bar2 are black marubozu.
+ if !black_marubozu(bar1) || !black_marubozu(bar2) {
+ return Some(0.0);
+ }
+ // bar3 is black, gaps down on the open, throws an upper shadow into bar2.
+ if bar3.open <= bar3.close {
+ return Some(0.0);
+ }
+ if bar3.open >= bar2.close {
+ return Some(0.0); // no downside open gap
+ }
+ if bar3.high <= bar2.close {
+ return Some(0.0); // upper shadow does not reach into bar2's body
+ }
+ // bar4 is black and engulfs bar3 including its shadows.
+ if candle.open <= candle.close {
+ return Some(0.0);
+ }
+ if candle.open > bar3.high && candle.close < bar3.low {
+ return Some(1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = None;
+ self.c3 = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 4
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "ConcealingBabySwallow"
+ }
+}
+
+#[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 = ConcealingBabySwallow::new();
+ assert_eq!(t.name(), "ConcealingBabySwallow");
+ assert_eq!(t.warmup_period(), 4);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn concealing_baby_swallow_is_plus_one() {
+ let mut t = ConcealingBabySwallow::new();
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(1.0));
+ }
+
+ #[test]
+ fn warmup_returns_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_bar_not_marubozu_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ // bar1 white.
+ t.update(c(15.0, 20.1, 14.9, 20.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ t.update(c(11.0, 13.0, 9.9, 10.0, 2));
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn first_bar_zero_range_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ // bar1 zero range -> not a marubozu.
+ t.update(c(15.0, 15.0, 15.0, 15.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ t.update(c(11.0, 13.0, 9.9, 10.0, 2));
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn second_bar_not_marubozu_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ // bar2 white.
+ t.update(c(12.0, 16.1, 11.9, 16.0, 1));
+ t.update(c(11.0, 13.0, 9.9, 10.0, 2));
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn third_bar_not_black_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ // bar3 white.
+ t.update(c(11.0, 13.0, 9.9, 12.5, 2));
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn third_bar_no_gap_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ // bar3 black but opens at/above bar2's close -> no downside gap.
+ t.update(c(12.5, 13.0, 9.9, 10.0, 2));
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn third_bar_no_upper_shadow_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ // bar3 black, gaps down, but its high does not reach into bar2's body.
+ t.update(c(11.0, 11.5, 9.9, 10.0, 2));
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn fourth_bar_not_black_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ t.update(c(11.0, 13.0, 9.9, 10.0, 2));
+ // bar4 white.
+ assert_eq!(t.update(c(14.0, 14.1, 8.9, 14.05, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn fourth_bar_not_engulfing_yields_zero() {
+ let mut t = ConcealingBabySwallow::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ t.update(c(11.0, 13.0, 9.9, 10.0, 2));
+ // bar4 black but does not engulf bar3's high.
+ assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 200.0 - i as f64;
+ c(base, base + 0.05, base - 5.0, base - 5.0, i)
+ })
+ .collect();
+ let mut a = ConcealingBabySwallow::new();
+ let mut b = ConcealingBabySwallow::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = ConcealingBabySwallow::new();
+ t.update(c(20.0, 20.1, 14.9, 15.0, 0));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 1));
+ t.update(c(11.0, 13.0, 9.9, 10.0, 2));
+ t.update(c(14.0, 14.1, 8.9, 9.0, 3));
+ 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 11408f40..0b5ff5d4 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -45,10 +45,12 @@ mod chande_kroll_stop;
mod chandelier_exit;
mod choppiness_index;
mod classic_pivots;
+mod closing_marubozu;
mod cmf;
mod cmo;
mod coefficient_of_variation;
mod cointegration;
+mod concealing_baby_swallow;
mod conditional_value_at_risk;
mod connors_rsi;
mod coppock;
@@ -168,6 +170,7 @@ mod oi_price_divergence;
mod oi_weighted;
mod omega_ratio;
mod on_neck;
+mod opening_marubozu;
mod opening_range;
mod pain_index;
mod pair_spread_zscore;
@@ -226,6 +229,7 @@ mod super_trend;
mod t3;
mod taker_buy_sell_ratio;
mod takuri;
+mod tasuki_gap;
mod td_combo;
mod td_countdown;
mod td_demarker;
@@ -260,6 +264,7 @@ mod two_crows;
mod typical_price;
mod ulcer_index;
mod ultimate_oscillator;
+mod unique_three_river;
mod upside_gap_three_methods;
mod upside_gap_two_crows;
mod value_area;
@@ -329,10 +334,12 @@ pub use chande_kroll_stop::{ChandeKrollStop, ChandeKrollStopOutput};
pub use chandelier_exit::{ChandelierExit, ChandelierExitOutput};
pub use choppiness_index::ChoppinessIndex;
pub use classic_pivots::{ClassicPivots, ClassicPivotsOutput};
+pub use closing_marubozu::ClosingMarubozu;
pub use cmf::ChaikinMoneyFlow;
pub use cmo::Cmo;
pub use coefficient_of_variation::CoefficientOfVariation;
pub use cointegration::{Cointegration, CointegrationOutput};
+pub use concealing_baby_swallow::ConcealingBabySwallow;
pub use conditional_value_at_risk::ConditionalValueAtRisk;
pub use connors_rsi::ConnorsRsi;
pub use coppock::Coppock;
@@ -452,6 +459,7 @@ pub use oi_price_divergence::OIPriceDivergence;
pub use oi_weighted::OIWeighted;
pub use omega_ratio::OmegaRatio;
pub use on_neck::OnNeck;
+pub use opening_marubozu::OpeningMarubozu;
pub use opening_range::{OpeningRange, OpeningRangeOutput};
pub use pain_index::PainIndex;
pub use pair_spread_zscore::PairSpreadZScore;
@@ -510,6 +518,7 @@ pub use super_trend::{SuperTrend, SuperTrendOutput};
pub use t3::T3;
pub use taker_buy_sell_ratio::TakerBuySellRatio;
pub use takuri::Takuri;
+pub use tasuki_gap::TasukiGap;
pub use td_combo::TdCombo;
pub use td_countdown::TdCountdown;
pub use td_demarker::TdDeMarker;
@@ -544,6 +553,7 @@ pub use two_crows::TwoCrows;
pub use typical_price::TypicalPrice;
pub use ulcer_index::UlcerIndex;
pub use ultimate_oscillator::UltimateOscillator;
+pub use unique_three_river::UniqueThreeRiver;
pub use upside_gap_three_methods::UpsideGapThreeMethods;
pub use upside_gap_two_crows::UpsideGapTwoCrows;
pub use value_area::{ValueArea, ValueAreaOutput};
@@ -875,6 +885,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"StalledPattern",
"StickSandwich",
"Takuri",
+ "ClosingMarubozu",
+ "OpeningMarubozu",
+ "TasukiGap",
+ "UniqueThreeRiver",
+ "ConcealingBabySwallow",
],
),
(
@@ -966,6 +981,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, 279, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 284, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/indicators/opening_marubozu.rs b/crates/wickra-core/src/indicators/opening_marubozu.rs
new file mode 100644
index 00000000..90dea1de
--- /dev/null
+++ b/crates/wickra-core/src/indicators/opening_marubozu.rs
@@ -0,0 +1,178 @@
+//! Opening Marubozu candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Opening Marubozu — a single-bar strong-momentum candle with a long body and no
+/// shadow on the *open* end. A white opening marubozu opens right at the low (no
+/// lower shadow) and may carry a closing shadow above; a black one opens right at
+/// the high (no upper shadow) and may carry a closing shadow below. The shaved
+/// open end shows the move took off from the bell without hesitation.
+///
+/// ```text
+/// range = high − low
+/// long body: |close − open| >= 0.7 * range
+/// white: close > open and open − low <= 0.05 * range (open at the low)
+/// black: close < open and high − open <= 0.05 * range (open at the high)
+/// ```
+///
+/// Output is `+1.0` for a white opening marubozu, `−1.0` for a black one, and
+/// `0.0` otherwise. Body and shadow thresholds follow the geometric house style
+/// rather than TA-Lib's rolling averages. TA-Lib has no direct equivalent; this
+/// completes the pair with [`crate::ClosingMarubozu`], which shaves the close end.
+/// 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, Indicator, OpeningMarubozu};
+///
+/// let mut indicator = OpeningMarubozu::new();
+/// // White: opens at the low, small closing shadow above.
+/// let candle = Candle::new(10.0, 15.0, 10.0, 14.5, 1.0, 0).unwrap();
+/// assert_eq!(indicator.update(candle), Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct OpeningMarubozu {
+ has_emitted: bool,
+}
+
+impl OpeningMarubozu {
+ /// Construct a new Opening Marubozu detector.
+ pub const fn new() -> Self {
+ Self { has_emitted: false }
+ }
+}
+
+impl Indicator for OpeningMarubozu {
+ 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.7 * range {
+ return Some(0.0);
+ }
+ let tol = 0.05 * range;
+ if body > 0.0 && candle.open - candle.low <= tol {
+ return Some(1.0);
+ }
+ 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 {
+ "OpeningMarubozu"
+ }
+}
+
+#[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 = OpeningMarubozu::new();
+ assert_eq!(t.name(), "OpeningMarubozu");
+ assert_eq!(t.warmup_period(), 1);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn white_opening_marubozu_is_plus_one() {
+ let mut t = OpeningMarubozu::new();
+ // Opens at the low, closing shadow above.
+ assert_eq!(t.update(c(10.0, 15.0, 10.0, 14.5, 0)), Some(1.0));
+ }
+
+ #[test]
+ fn black_opening_marubozu_is_minus_one() {
+ let mut t = OpeningMarubozu::new();
+ // Opens at the high, closing shadow below.
+ assert_eq!(t.update(c(15.0, 15.0, 10.0, 10.5, 0)), Some(-1.0));
+ }
+
+ #[test]
+ fn white_with_lower_shadow_yields_zero() {
+ let mut t = OpeningMarubozu::new();
+ // Long white body but a clear lower shadow -> open is not at the low.
+ assert_eq!(t.update(c(11.0, 15.0, 10.0, 15.0, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn black_with_upper_shadow_yields_zero() {
+ let mut t = OpeningMarubozu::new();
+ // Long black body but a clear upper shadow -> open is not at the high.
+ assert_eq!(t.update(c(14.0, 16.0, 10.0, 10.5, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn short_body_yields_zero() {
+ let mut t = OpeningMarubozu::new();
+ // Body is short relative to range.
+ assert_eq!(t.update(c(10.0, 15.0, 10.0, 12.5, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn zero_range_yields_zero() {
+ let mut t = OpeningMarubozu::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 + 5.0, base, base + 4.5, i)
+ })
+ .collect();
+ let mut a = OpeningMarubozu::new();
+ let mut b = OpeningMarubozu::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = OpeningMarubozu::new();
+ t.update(c(10.0, 15.0, 10.0, 14.5, 0));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ }
+}
diff --git a/crates/wickra-core/src/indicators/tasuki_gap.rs b/crates/wickra-core/src/indicators/tasuki_gap.rs
new file mode 100644
index 00000000..47baea40
--- /dev/null
+++ b/crates/wickra-core/src/indicators/tasuki_gap.rs
@@ -0,0 +1,278 @@
+//! Tasuki Gap candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Tasuki Gap — a 3-bar continuation. Two same-coloured candles open a body gap in
+/// the trend direction, then an opposite-coloured candle opens inside the second
+/// body and closes back *into* the gap without filling it — the gap holds, so the
+/// trend is expected to continue.
+///
+/// ```text
+/// Upside (bullish, +1):
+/// bar1 white, bar2 white with an upside body gap (open2 > close1)
+/// bar3 black, opens within bar2's body, closes inside the gap
+/// (close1 < close3 < open2)
+/// Downside (bearish, −1): the mirror image with black candles and a downside gap
+/// ```
+///
+/// Output is `+1.0` for an upside Tasuki gap, `−1.0` for a downside one, and `0.0`
+/// otherwise. The first two bars always return `0.0` because the three-bar window
+/// is not yet filled. Thresholds follow the geometric house style rather than
+/// TA-Lib's rolling averages. 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, Indicator, TasukiGap};
+///
+/// let mut indicator = TasukiGap::new();
+/// indicator.update(Candle::new(10.0, 11.2, 9.8, 11.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(12.0, 14.0, 11.9, 13.5, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(13.0, 13.1, 11.4, 11.5, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct TasukiGap {
+ c1: Option,
+ c2: Option,
+ has_emitted: bool,
+}
+
+impl TasukiGap {
+ /// Construct a new Tasuki Gap detector.
+ pub const fn new() -> Self {
+ Self {
+ c1: None,
+ c2: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for TasukiGap {
+ 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;
+ self.c1 = self.c2;
+ self.c2 = Some(candle);
+ let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
+ return Some(0.0);
+ };
+
+ let up = bar1.close > bar1.open && bar2.close > bar2.open;
+ let down = bar1.close < bar1.open && bar2.close < bar2.open;
+ if up {
+ if bar2.open <= bar1.close {
+ return Some(0.0); // no upside body gap
+ }
+ if candle.close >= candle.open {
+ return Some(0.0); // bar3 must be black
+ }
+ if candle.open <= bar2.open || candle.open >= bar2.close {
+ return Some(0.0); // bar3 must open within bar2's body
+ }
+ if candle.close < bar2.open && candle.close > bar1.close {
+ return Some(1.0); // bar3 closes inside the gap
+ }
+ return Some(0.0);
+ }
+ if down {
+ if bar2.open >= bar1.close {
+ return Some(0.0); // no downside body gap
+ }
+ if candle.close <= candle.open {
+ return Some(0.0); // bar3 must be white
+ }
+ if candle.open >= bar2.open || candle.open <= bar2.close {
+ return Some(0.0); // bar3 must open within bar2's body
+ }
+ if candle.close > bar2.open && candle.close < bar1.close {
+ return Some(-1.0); // bar3 closes inside the gap
+ }
+ return Some(0.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 3
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "TasukiGap"
+ }
+}
+
+#[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 = TasukiGap::new();
+ assert_eq!(t.name(), "TasukiGap");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn upside_tasuki_gap_is_plus_one() {
+ let mut t = TasukiGap::new();
+ assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 2)), Some(1.0));
+ }
+
+ #[test]
+ fn downside_tasuki_gap_is_minus_one() {
+ let mut t = TasukiGap::new();
+ assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 11.1, 9.5, 10.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(10.5, 11.6, 10.4, 11.5, 2)), Some(-1.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = TasukiGap::new();
+ assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn up_no_gap_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(10.0, 11.2, 9.8, 11.0, 0));
+ // bar2 white but opens below bar1's close -> no upside gap.
+ t.update(c(10.5, 13.1, 10.4, 13.0, 1));
+ assert_eq!(t.update(c(12.5, 12.6, 10.9, 11.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn up_third_not_black_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(10.0, 11.2, 9.8, 11.0, 0));
+ t.update(c(12.0, 14.0, 11.9, 13.5, 1));
+ // bar3 white.
+ assert_eq!(t.update(c(12.5, 13.1, 12.4, 13.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn up_third_open_outside_body_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(10.0, 11.2, 9.8, 11.0, 0));
+ t.update(c(12.0, 14.0, 11.9, 13.5, 1));
+ // bar3 black but opens above bar2's body.
+ assert_eq!(t.update(c(14.0, 14.1, 11.4, 11.5, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn up_third_close_not_in_gap_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(10.0, 11.2, 9.8, 11.0, 0));
+ t.update(c(12.0, 14.0, 11.9, 13.5, 1));
+ // bar3 black, opens in body, but closes below the gap (under bar1's close).
+ assert_eq!(t.update(c(13.0, 13.1, 10.4, 10.5, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn down_no_gap_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(13.0, 13.2, 11.8, 12.0, 0));
+ // bar2 black but opens above bar1's close -> no downside gap.
+ t.update(c(12.5, 12.6, 10.4, 10.5, 1));
+ assert_eq!(t.update(c(11.0, 12.6, 10.9, 12.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn down_third_not_white_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(13.0, 13.2, 11.8, 12.0, 0));
+ t.update(c(11.0, 11.1, 9.5, 10.0, 1));
+ // bar3 black.
+ assert_eq!(t.update(c(11.5, 11.6, 10.4, 10.5, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn down_third_open_outside_body_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(13.0, 13.2, 11.8, 12.0, 0));
+ t.update(c(11.0, 11.1, 9.5, 10.0, 1));
+ // bar3 white but opens below bar2's body.
+ assert_eq!(t.update(c(9.5, 11.6, 9.4, 11.5, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn down_third_close_not_in_gap_yields_zero() {
+ let mut t = TasukiGap::new();
+ t.update(c(13.0, 13.2, 11.8, 12.0, 0));
+ t.update(c(11.0, 11.1, 9.5, 10.0, 1));
+ // bar3 white, opens in body, but closes above the gap (over bar1's close).
+ assert_eq!(t.update(c(10.5, 13.0, 10.4, 12.5, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn mixed_colours_yield_zero() {
+ let mut t = TasukiGap::new();
+ // bar1 white, bar2 black -> neither an upside nor downside setup.
+ t.update(c(10.0, 11.2, 9.8, 11.0, 0));
+ t.update(c(13.0, 13.2, 11.0, 11.5, 1));
+ assert_eq!(t.update(c(12.0, 12.6, 10.9, 11.0, 2)), 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 + 5.2, base - 0.1, base + 5.0, i)
+ })
+ .collect();
+ let mut a = TasukiGap::new();
+ let mut b = TasukiGap::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = TasukiGap::new();
+ t.update(c(10.0, 11.2, 9.8, 11.0, 0));
+ t.update(c(12.0, 14.0, 11.9, 13.5, 1));
+ t.update(c(13.0, 13.1, 11.4, 11.5, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/unique_three_river.rs b/crates/wickra-core/src/indicators/unique_three_river.rs
new file mode 100644
index 00000000..acdec060
--- /dev/null
+++ b/crates/wickra-core/src/indicators/unique_three_river.rs
@@ -0,0 +1,256 @@
+//! Unique Three River candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Unique Three River (Bottom) — a 3-bar bullish reversal. A long black candle is
+/// followed by a smaller black candle whose body sits inside the first but whose
+/// long lower shadow probes a new low, then a small white candle that stays below
+/// the second body. The fresh low that fails to hold marks an exhausted decline.
+///
+/// ```text
+/// bar1 long black: open1 − close1 >= 0.5 * (high1 − low1)
+/// bar2 black, body inside bar1's body, with a new low (low2 < low1)
+/// bar3 small white, contained below bar2's body (high3 <= close2)
+/// small body: close3 − open3 <= 0.3 * (high3 − low3)
+/// ```
+///
+/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Unique Three
+/// River is a single-direction (bullish-only) reversal, so it never emits `−1.0`.
+/// The first two bars always return `0.0` because the three-bar window is not yet
+/// filled. Body thresholds follow the geometric house style rather than TA-Lib's
+/// rolling averages. 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, `0.0` no pattern — so it drops straight into
+/// a machine-learning feature matrix as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Indicator, UniqueThreeRiver};
+///
+/// let mut indicator = UniqueThreeRiver::new();
+/// indicator.update(Candle::new(15.0, 15.1, 10.0, 10.5, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(14.0, 14.1, 9.0, 11.0, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(10.2, 10.9, 9.5, 10.4, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct UniqueThreeRiver {
+ c1: Option,
+ c2: Option,
+ has_emitted: bool,
+}
+
+impl UniqueThreeRiver {
+ /// Construct a new Unique Three River detector.
+ pub const fn new() -> Self {
+ Self {
+ c1: None,
+ c2: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for UniqueThreeRiver {
+ 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;
+ self.c1 = self.c2;
+ self.c2 = Some(candle);
+ let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
+ return Some(0.0);
+ };
+ // bar1 is a long black body.
+ if bar1.open <= bar1.close {
+ return Some(0.0);
+ }
+ let range1 = bar1.high - bar1.low;
+ if bar1.open - bar1.close < 0.5 * range1 {
+ return Some(0.0);
+ }
+ // bar2 is black, its body inside bar1's body, with a new low.
+ if bar2.open <= bar2.close {
+ return Some(0.0);
+ }
+ if bar2.open > bar1.open || bar2.close < bar1.close {
+ return Some(0.0);
+ }
+ if bar2.low >= bar1.low {
+ return Some(0.0);
+ }
+ // bar3 is a small white candle contained below bar2's body.
+ if candle.close <= candle.open {
+ return Some(0.0);
+ }
+ let range3 = candle.high - candle.low;
+ if candle.close - candle.open > 0.3 * range3 {
+ return Some(0.0);
+ }
+ if candle.high > bar2.close {
+ return Some(0.0);
+ }
+ Some(1.0)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 3
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "UniqueThreeRiver"
+ }
+}
+
+#[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 = UniqueThreeRiver::new();
+ assert_eq!(t.name(), "UniqueThreeRiver");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn unique_three_river_is_plus_one() {
+ let mut t = UniqueThreeRiver::new();
+ assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
+ assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(1.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = UniqueThreeRiver::new();
+ assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
+ assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn first_bar_not_black_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ // bar1 white.
+ t.update(c(10.5, 15.1, 10.0, 15.0, 0));
+ t.update(c(14.0, 14.1, 9.0, 11.0, 1));
+ assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_bar_short_body_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ // bar1 black but its body is short relative to range.
+ t.update(c(15.0, 15.1, 10.0, 14.5, 0));
+ t.update(c(14.0, 14.1, 9.0, 11.0, 1));
+ assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn second_bar_not_black_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ t.update(c(15.0, 15.1, 10.0, 10.5, 0));
+ // bar2 white.
+ t.update(c(11.0, 14.1, 9.0, 13.0, 1));
+ assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn second_bar_not_inside_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ t.update(c(15.0, 15.1, 10.0, 10.5, 0));
+ // bar2 black but opens above bar1's open -> body not inside.
+ t.update(c(16.0, 16.1, 9.0, 11.0, 1));
+ assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn second_bar_no_new_low_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ t.update(c(15.0, 15.1, 10.0, 10.5, 0));
+ // bar2 black, inside, but does not make a new low.
+ t.update(c(14.0, 14.1, 10.5, 11.0, 1));
+ assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn third_bar_not_white_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ t.update(c(15.0, 15.1, 10.0, 10.5, 0));
+ t.update(c(14.0, 14.1, 9.0, 11.0, 1));
+ // bar3 black.
+ assert_eq!(t.update(c(10.6, 10.9, 9.5, 10.2, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn third_bar_large_body_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ t.update(c(15.0, 15.1, 10.0, 10.5, 0));
+ t.update(c(14.0, 14.1, 9.0, 11.0, 1));
+ // bar3 white but with a large body.
+ assert_eq!(t.update(c(9.6, 10.9, 9.5, 10.8, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn third_bar_not_below_second_yields_zero() {
+ let mut t = UniqueThreeRiver::new();
+ t.update(c(15.0, 15.1, 10.0, 10.5, 0));
+ t.update(c(14.0, 14.1, 9.0, 11.0, 1));
+ // bar3 small white but pokes above bar2's close.
+ assert_eq!(t.update(c(10.5, 11.5, 10.4, 10.7, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 200.0 - i as f64;
+ c(base, base + 0.1, base - 5.2, base - 5.0, i)
+ })
+ .collect();
+ let mut a = UniqueThreeRiver::new();
+ let mut b = UniqueThreeRiver::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = UniqueThreeRiver::new();
+ t.update(c(15.0, 15.1, 10.0, 10.5, 0));
+ t.update(c(14.0, 14.1, 9.0, 11.0, 1));
+ t.update(c(10.2, 10.9, 9.5, 10.4, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs
index 66b78a29..f21c17df 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -61,31 +61,31 @@ pub use indicators::{
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, DojiStar, Donchian, DonchianOutput,
- DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
- DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, EaseOfMovement, EffectiveSpread,
- EhlersStochastic, ElderImpulse, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar,
- Evwma, FallingThreeMethods, Fama, FibonacciPivots, FibonacciPivotsOutput, FisherTransform,
- Footprint, FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama,
- FundingBasis, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
- GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami,
- HeikinAshi, HeikinAshiOutput, HiLoActivator, HighWave, Hikkake, HikkakeModified,
- HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HurstChannel,
- HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
- Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
- InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput,
- Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
- LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
+ ClassicPivotsOutput, ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration,
+ CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock,
+ Counterattack, CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema,
+ DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar,
+ Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
+ DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration,
+ EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
+ EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
+ FibonacciPivots, FibonacciPivotsOutput, FisherTransform, Footprint, FootprintOutput,
+ ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
+ FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
+ GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator,
+ HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
+ HomingPigeon, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
+ IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
+ InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion,
+ Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda,
+ LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, LiquidationFeatures,
LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown, McGinleyDynamic,
MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom, MorningDojiStar,
MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck,
- OpenInterestDelta, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
+ OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
@@ -96,18 +96,18 @@ pub use indicators::{
SpinningTop, StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput,
StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi,
Stochastic, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio,
- Takuri, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen,
- TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput,
- TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside,
- ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tii,
- TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
- Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UpsideGapThreeMethods,
- UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter,
- Vidya, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap,
- VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose,
- WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput,
- YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput,
- Zlema, FAMILIES, T3,
+ Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput,
+ TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
+ TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis,
+ ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting,
+ Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze,
+ TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
+ UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
+ ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop, VolumeOscillator,
+ VolumePriceTrend, Vortex, VortexOutput, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma,
+ Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals, WilliamsFractalsOutput,
+ WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore,
+ ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
};
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
// line so the indicator-count tooling (which scans the braced block above and
diff --git a/docs/README.md b/docs/README.md
index 8580af70..2bcc2388 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 **284 indicators** across
+- A per-indicator deep dive for every one of the **289 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 fe55c592..f8af3f23 100644
--- a/fuzz/fuzz_targets/indicator_update_candle.rs
+++ b/fuzz/fuzz_targets/indicator_update_candle.rs
@@ -23,7 +23,7 @@
use libfuzzer_sys::fuzz_target;
use wickra_core::{
-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, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UpsideGapThreeMethods, 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, ClosingMarubozu, ConcealingBabySwallow, Counterattack, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, 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
@@ -278,6 +278,11 @@ fuzz_target!(|data: Vec| {
}
// --- Candlestick Patterns (family 14) ---
+ drive(ConcealingBabySwallow::new, &candles);
+ drive(UniqueThreeRiver::new, &candles);
+ drive(TasukiGap::new, &candles);
+ drive(OpeningMarubozu::new, &candles);
+ drive(ClosingMarubozu::new, &candles);
drive(Takuri::new, &candles);
drive(StickSandwich::new, &candles);
drive(StalledPattern::new, &candles);