diff --git a/CHANGELOG.md b/CHANGELOG.md
index 359c958e..84d78867 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Gravestone Doji** — a doji opening and closing at the low with a long upper shadow, a bearish reversal; -1 (`CDLGRAVESTONEDOJI`).
- **Long-Legged Doji** — a doji with long shadows on both sides, an indecision signal; +1 detection (`CDLLONGLEGGEDDOJI`).
- **Rickshaw Man** — a long-legged doji with the body centred in the range, an indecision signal; +1 detection (`CDLRICKSHAWMAN`).
+ - **Evening Doji Star** — a bearish top reversal: long white bar, a doji gapping up, then a black bar closing deep into the first body; -1 (`CDLEVENINGDOJISTAR`).
+ - **Morning Doji Star** — a bullish bottom reversal: long black bar, a doji gapping down, then a white bar closing deep into the first body; +1 (`CDLMORNINGDOJISTAR`).
+ - **Gap Side-by-Side White** — two similar white candles opening side by side after a gap, a continuation; gap up +1, gap down -1 (`CDLGAPSIDESIDEWHITE`).
+ - **High-Wave** — a small body with very long shadows on both sides, an extreme indecision signal; +1 detection (`CDLHIGHWAVE`).
+ - **Hikkake** — an inside bar followed by a failed breakout, a trap; bullish +1, bearish -1 (`CDLHIKKAKE`).
- **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 41ca9c24..a5c81199 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 259 indicators; start at the
+ every one of the 264 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
-259 streaming-first indicators across eighteen families. Every one passes the
+264 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 |
+| 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 |
| 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 259 indicators
+│ ├── wickra-core/ core engine + all 264 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 0ef5335a..844d572c 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -250,6 +250,11 @@ const candleScalar = {
GravestoneDoji: { make: () => new wickra.GravestoneDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
LongLeggedDoji: { make: () => new wickra.LongLeggedDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
RickshawMan: { make: () => new wickra.RickshawMan(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ EveningDojiStar: { make: () => new wickra.EveningDojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ MorningDojiStar: { make: () => new wickra.MorningDojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ GapSideBySideWhite: { make: () => new wickra.GapSideBySideWhite(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ HighWave: { make: () => new wickra.HighWave(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ Hikkake: { make: () => new wickra.Hikkake(), 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 c602cd96..df226fd0 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -2345,6 +2345,51 @@ export declare class RickshawMan {
isReady(): boolean
warmupPeriod(): number
}
+export type EveningDojiStarNode = EveningDojiStar
+export declare class EveningDojiStar {
+ 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 MorningDojiStarNode = MorningDojiStar
+export declare class MorningDojiStar {
+ 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 GapSideBySideWhiteNode = GapSideBySideWhite
+export declare class GapSideBySideWhite {
+ 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 HighWaveNode = HighWave
+export declare class HighWave {
+ 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 HikkakeNode = Hikkake
+export declare class Hikkake {
+ 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 cb2921db..74ac4a94 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, 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, 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
@@ -530,6 +530,11 @@ module.exports.DragonflyDoji = DragonflyDoji
module.exports.GravestoneDoji = GravestoneDoji
module.exports.LongLeggedDoji = LongLeggedDoji
module.exports.RickshawMan = RickshawMan
+module.exports.EveningDojiStar = EveningDojiStar
+module.exports.MorningDojiStar = MorningDojiStar
+module.exports.GapSideBySideWhite = GapSideBySideWhite
+module.exports.HighWave = HighWave
+module.exports.Hikkake = Hikkake
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 e2c279a4..bd5d89c6 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -8780,6 +8780,15 @@ node_candle_pattern!(DragonflyDojiNode, wc::DragonflyDoji, "DragonflyDoji");
node_candle_pattern!(GravestoneDojiNode, wc::GravestoneDoji, "GravestoneDoji");
node_candle_pattern!(LongLeggedDojiNode, wc::LongLeggedDoji, "LongLeggedDoji");
node_candle_pattern!(RickshawManNode, wc::RickshawMan, "RickshawMan");
+node_candle_pattern!(EveningDojiStarNode, wc::EveningDojiStar, "EveningDojiStar");
+node_candle_pattern!(MorningDojiStarNode, wc::MorningDojiStar, "MorningDojiStar");
+node_candle_pattern!(
+ GapSideBySideWhiteNode,
+ wc::GapSideBySideWhite,
+ "GapSideBySideWhite"
+);
+node_candle_pattern!(HighWaveNode, wc::HighWave, "HighWave");
+node_candle_pattern!(HikkakeNode, wc::Hikkake, "Hikkake");
// ============================== Microstructure: Order Book ==============================
//
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index 636b9477..a43a4677 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -255,6 +255,11 @@ from ._wickra import (
GravestoneDoji,
LongLeggedDoji,
RickshawMan,
+ EveningDojiStar,
+ MorningDojiStar,
+ GapSideBySideWhite,
+ HighWave,
+ Hikkake,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -537,6 +542,11 @@ __all__ = [
"GravestoneDoji",
"LongLeggedDoji",
"RickshawMan",
+ "EveningDojiStar",
+ "MorningDojiStar",
+ "GapSideBySideWhite",
+ "HighWave",
+ "Hikkake",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 6e0aec9c..94e3b15a 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -11643,6 +11643,15 @@ candle_pattern_no_param!(PyDragonflyDoji, wc::DragonflyDoji, "DragonflyDoji");
candle_pattern_no_param!(PyGravestoneDoji, wc::GravestoneDoji, "GravestoneDoji");
candle_pattern_no_param!(PyLongLeggedDoji, wc::LongLeggedDoji, "LongLeggedDoji");
candle_pattern_no_param!(PyRickshawMan, wc::RickshawMan, "RickshawMan");
+candle_pattern_no_param!(PyEveningDojiStar, wc::EveningDojiStar, "EveningDojiStar");
+candle_pattern_no_param!(PyMorningDojiStar, wc::MorningDojiStar, "MorningDojiStar");
+candle_pattern_no_param!(
+ PyGapSideBySideWhite,
+ wc::GapSideBySideWhite,
+ "GapSideBySideWhite"
+);
+candle_pattern_no_param!(PyHighWave, wc::HighWave, "HighWave");
+candle_pattern_no_param!(PyHikkake, wc::Hikkake, "Hikkake");
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14154,6 +14163,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 f0d32ab0..8bf9c7bf 100644
--- a/bindings/python/tests/test_new_indicators.py
+++ b/bindings/python/tests/test_new_indicators.py
@@ -583,6 +583,26 @@ CANDLE_SCALAR = {
lambda: ta.RickshawMan(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
+ "EveningDojiStar": (
+ lambda: ta.EveningDojiStar(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "MorningDojiStar": (
+ lambda: ta.MorningDojiStar(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "GapSideBySideWhite": (
+ lambda: ta.GapSideBySideWhite(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "HighWave": (
+ lambda: ta.HighWave(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "Hikkake": (
+ lambda: ta.Hikkake(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
}
@@ -1731,6 +1751,39 @@ def test_rickshaw_man_reference():
t = ta.RickshawMan()
assert t.update((10.0, 12.0, 8.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
+
+def test_evening_doji_star_reference():
+ t = ta.EveningDojiStar()
+ assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((17.0, 17.1, 16.9, 17.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((16.0, 16.1, 11.9, 12.0, 1.0, 2)) == pytest.approx(-1.0)
+
+
+def test_morning_doji_star_reference():
+ t = ta.MorningDojiStar()
+ assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((8.0, 8.1, 7.9, 8.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((9.0, 13.1, 8.9, 13.0, 1.0, 2)) == pytest.approx(1.0)
+
+
+def test_gap_side_by_side_white_reference():
+ t = ta.GapSideBySideWhite()
+ assert t.update((10.0, 11.1, 9.9, 11.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((13.0, 14.1, 12.9, 14.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((13.0, 14.1, 12.9, 14.0, 1.0, 2)) == pytest.approx(1.0)
+
+
+def test_high_wave_reference():
+ t = ta.HighWave()
+ assert t.update((10.0, 12.0, 8.0, 10.3, 1.0, 0)) == pytest.approx(1.0)
+
+
+def test_hikkake_reference():
+ t = ta.Hikkake()
+ assert t.update((10.0, 15.0, 5.0, 12.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((11.0, 13.0, 8.0, 12.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((9.0, 12.0, 6.0, 7.0, 1.0, 2)) == pytest.approx(1.0)
+
# --- Lifecycle ------------------------------------------------------------
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 4d9dd599..b3626b10 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -6357,6 +6357,15 @@ wasm_candle_pattern!(WasmDragonflyDoji, wc::DragonflyDoji, DragonflyDoji);
wasm_candle_pattern!(WasmGravestoneDoji, wc::GravestoneDoji, GravestoneDoji);
wasm_candle_pattern!(WasmLongLeggedDoji, wc::LongLeggedDoji, LongLeggedDoji);
wasm_candle_pattern!(WasmRickshawMan, wc::RickshawMan, RickshawMan);
+wasm_candle_pattern!(WasmEveningDojiStar, wc::EveningDojiStar, EveningDojiStar);
+wasm_candle_pattern!(WasmMorningDojiStar, wc::MorningDojiStar, MorningDojiStar);
+wasm_candle_pattern!(
+ WasmGapSideBySideWhite,
+ wc::GapSideBySideWhite,
+ GapSideBySideWhite
+);
+wasm_candle_pattern!(WasmHighWave, wc::HighWave, HighWave);
+wasm_candle_pattern!(WasmHikkake, wc::Hikkake, Hikkake);
// ============================== Microstructure: Order Book ==============================
//
diff --git a/crates/wickra-core/src/indicators/evening_doji_star.rs b/crates/wickra-core/src/indicators/evening_doji_star.rs
new file mode 100644
index 00000000..65d09053
--- /dev/null
+++ b/crates/wickra-core/src/indicators/evening_doji_star.rs
@@ -0,0 +1,241 @@
+//! Evening Doji Star candlestick pattern.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Evening Doji Star — a 3-bar bearish top reversal. A long white bar extends the
+/// advance, a doji gaps up above it (the star of indecision), then a black bar
+/// gaps back down and closes deep into the first body, confirming the turn.
+///
+/// ```text
+/// long body = |close − open| >= 0.5 * (high − low)
+/// doji = |close − open| <= 0.1 * (high − low)
+/// bar1 white & long
+/// bar2 doji, body gaps UP above bar1 body (min(o2,c2) > close1)
+/// bar3 black, body gaps DOWN below the doji (max(o3,c3) < min(o2,c2))
+/// bar3 closes deep into bar1 body (close3 < close1 − penetration·body1)
+/// ```
+///
+/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Evening Doji
+/// Star is a single-direction (bearish-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. `penetration` is how far into the first body the third bar must close;
+/// it defaults to `0.3` (TA-Lib's `CDLEVENINGDOJISTAR` default) and must lie in
+/// `[0, 1)`. Body and doji 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` bearish, `0.0` no pattern — so it drops straight into
+/// a machine-learning feature matrix as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, EveningDojiStar, Indicator};
+///
+/// let mut indicator = EveningDojiStar::new();
+/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(17.0, 17.1, 16.9, 17.0, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(16.0, 16.1, 11.9, 12.0, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(-1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct EveningDojiStar {
+ penetration: f64,
+ prev: Option,
+ prev_prev: Option,
+ has_emitted: bool,
+}
+
+impl Default for EveningDojiStar {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl EveningDojiStar {
+ /// Construct an Evening Doji Star detector with the default 0.3 penetration.
+ pub const fn new() -> Self {
+ Self {
+ penetration: 0.3,
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ }
+ }
+
+ /// Construct an Evening Doji Star detector with a custom penetration fraction.
+ ///
+ /// `penetration` must lie in `[0, 1)`.
+ pub fn with_penetration(penetration: f64) -> Result {
+ if !(0.0..1.0).contains(&penetration) {
+ return Err(Error::InvalidPeriod {
+ message: "evening doji star penetration must lie in [0, 1)",
+ });
+ }
+ Ok(Self {
+ penetration,
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ })
+ }
+
+ /// Configured penetration fraction.
+ pub fn penetration(&self) -> f64 {
+ self.penetration
+ }
+}
+
+impl Indicator for EveningDojiStar {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let bar1 = self.prev_prev;
+ let bar2 = self.prev;
+ self.prev_prev = self.prev;
+ self.prev = Some(candle);
+ let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
+ return Some(0.0);
+ };
+ let range1 = bar1.high - bar1.low;
+ let range2 = bar2.high - bar2.low;
+ if range1 <= 0.0 || range2 <= 0.0 {
+ return Some(0.0);
+ }
+ let body1 = bar1.close - bar1.open;
+ if body1 < 0.5 * range1 {
+ return Some(0.0); // bar1 must be a long white body
+ }
+ if (bar2.close - bar2.open).abs() > 0.1 * range2 {
+ return Some(0.0); // bar2 must be a doji
+ }
+ let star_bottom = bar2.open.min(bar2.close);
+ let bar3_top = candle.open.max(candle.close);
+ if star_bottom > bar1.close
+ && candle.close < candle.open
+ && bar3_top < star_bottom
+ && candle.close < bar1.close - self.penetration * body1
+ {
+ 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 {
+ "EveningDojiStar"
+ }
+}
+
+#[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_penetration() {
+ assert!(EveningDojiStar::with_penetration(-0.01).is_err());
+ assert!(EveningDojiStar::with_penetration(1.0).is_err());
+ }
+
+ #[test]
+ fn accepts_valid_penetration() {
+ let t = EveningDojiStar::with_penetration(0.5).unwrap();
+ assert!((t.penetration() - 0.5).abs() < 1e-12);
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = EveningDojiStar::default();
+ assert_eq!(t.name(), "EveningDojiStar");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ assert!((t.penetration() - 0.3).abs() < 1e-12);
+ }
+
+ #[test]
+ fn evening_doji_star_is_minus_one() {
+ let mut t = EveningDojiStar::new();
+ assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(-1.0));
+ }
+
+ #[test]
+ fn middle_not_doji_yields_zero() {
+ let mut t = EveningDojiStar::new();
+ t.update(c(10.0, 15.1, 9.9, 15.0, 0));
+ // Wide-bodied star, not a doji.
+ t.update(c(16.0, 18.1, 15.9, 18.0, 1));
+ assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn shallow_close_yields_zero() {
+ let mut t = EveningDojiStar::new();
+ t.update(c(10.0, 15.1, 9.9, 15.0, 0));
+ t.update(c(17.0, 17.1, 16.9, 17.0, 1));
+ // bar3 black but closes at 14.0 -> only 1.0 into the 5.0 body (< 0.3·5).
+ assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = EveningDojiStar::new();
+ assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.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 + 5.2, base - 0.1, base + 5.0, i)
+ })
+ .collect();
+ let mut a = EveningDojiStar::new();
+ let mut b = EveningDojiStar::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = EveningDojiStar::new();
+ t.update(c(10.0, 15.1, 9.9, 15.0, 0));
+ t.update(c(17.0, 17.1, 16.9, 17.0, 1));
+ t.update(c(16.0, 16.1, 11.9, 12.0, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/gap_side_by_side_white.rs b/crates/wickra-core/src/indicators/gap_side_by_side_white.rs
new file mode 100644
index 00000000..f73fe98c
--- /dev/null
+++ b/crates/wickra-core/src/indicators/gap_side_by_side_white.rs
@@ -0,0 +1,220 @@
+//! Gap Side-by-Side White Lines candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Gap Side-by-Side White Lines — a 3-bar continuation. After a gap away from the
+/// first bar, two white candles of similar size open at roughly the same level
+/// (side by side) and hold the gap open, signalling the trend resumes in the gap
+/// direction.
+///
+/// ```text
+/// bar2, bar3 both white
+/// bar2 body gaps away from bar1 body (up or down)
+/// bar3 opens beside bar2 (|open3 − open2| <= 0.1 · range2)
+/// bar3 body is similar in size to bar2 (neither more than twice the other)
+/// gap up -> +1.0 (bullish continuation)
+/// gap down -> −1.0 (bearish continuation — "downside" gap side-by-side white)
+/// ```
+///
+/// Output is `+1.0` (gap up) or `−1.0` (gap down) when the pattern completes and
+/// `0.0` otherwise. The first two bars always return `0.0` because the three-bar
+/// window is not yet filled. Open-equality and body-similarity 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 two gap
+/// directions occupy a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, GapSideBySideWhite, Indicator};
+///
+/// let mut indicator = GapSideBySideWhite::new();
+/// indicator.update(Candle::new(10.0, 11.1, 9.9, 11.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(13.0, 14.1, 12.9, 14.0, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(13.0, 14.1, 12.9, 14.0, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct GapSideBySideWhite {
+ prev: Option,
+ prev_prev: Option,
+ has_emitted: bool,
+}
+
+impl GapSideBySideWhite {
+ /// Construct a new Gap Side-by-Side White Lines detector.
+ pub const fn new() -> Self {
+ Self {
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for GapSideBySideWhite {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let bar1 = self.prev_prev;
+ let bar2 = self.prev;
+ self.prev_prev = self.prev;
+ self.prev = Some(candle);
+ let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
+ return Some(0.0);
+ };
+ let range2 = bar2.high - bar2.low;
+ if range2 <= 0.0 {
+ return Some(0.0);
+ }
+ // Both of the side-by-side bars must be white.
+ if bar2.close <= bar2.open || candle.close <= candle.open {
+ return Some(0.0);
+ }
+ // Side by side: opens level and bodies of comparable size.
+ if (candle.open - bar2.open).abs() > 0.1 * range2 {
+ return Some(0.0);
+ }
+ let body2 = bar2.close - bar2.open;
+ let body3 = candle.close - candle.open;
+ if body2 > 2.0 * body3 || body3 > 2.0 * body2 {
+ return Some(0.0);
+ }
+ let bar1_top = bar1.open.max(bar1.close);
+ let bar1_bottom = bar1.open.min(bar1.close);
+ let bar2_bottom = bar2.open.min(bar2.close);
+ let bar2_top = bar2.open.max(bar2.close);
+ if bar2_bottom > bar1_top {
+ return Some(1.0); // gap up -> bullish continuation
+ }
+ if bar2_top < bar1_bottom {
+ return Some(-1.0); // gap down -> bearish continuation
+ }
+ 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 {
+ "GapSideBySideWhite"
+ }
+}
+
+#[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 = GapSideBySideWhite::new();
+ assert_eq!(t.name(), "GapSideBySideWhite");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn gap_up_is_plus_one() {
+ let mut t = GapSideBySideWhite::new();
+ assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 2)), Some(1.0));
+ }
+
+ #[test]
+ fn gap_down_is_minus_one() {
+ let mut t = GapSideBySideWhite::new();
+ assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 2)), Some(-1.0));
+ }
+
+ #[test]
+ fn second_bar_black_yields_zero() {
+ let mut t = GapSideBySideWhite::new();
+ t.update(c(10.0, 11.1, 9.9, 11.0, 0));
+ // bar3 is black -> not two white lines.
+ t.update(c(13.0, 14.1, 12.9, 14.0, 1));
+ assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn not_side_by_side_yields_zero() {
+ let mut t = GapSideBySideWhite::new();
+ t.update(c(10.0, 11.1, 9.9, 11.0, 0));
+ t.update(c(13.0, 14.1, 12.9, 14.0, 1));
+ // bar3 opens far from bar2's open -> not side by side.
+ assert_eq!(t.update(c(16.0, 17.1, 15.9, 17.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn no_gap_yields_zero() {
+ let mut t = GapSideBySideWhite::new();
+ t.update(c(10.0, 13.1, 9.9, 13.0, 0));
+ // bar2 overlaps bar1 (no gap).
+ t.update(c(12.0, 13.1, 11.9, 13.0, 1));
+ assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = GapSideBySideWhite::new();
+ assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 14.1, 12.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 * 3.0;
+ c(base, base + 1.1, base - 0.1, base + 1.0, i)
+ })
+ .collect();
+ let mut a = GapSideBySideWhite::new();
+ let mut b = GapSideBySideWhite::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = GapSideBySideWhite::new();
+ t.update(c(10.0, 11.1, 9.9, 11.0, 0));
+ t.update(c(13.0, 14.1, 12.9, 14.0, 1));
+ t.update(c(13.0, 14.1, 12.9, 14.0, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/high_wave.rs b/crates/wickra-core/src/indicators/high_wave.rs
new file mode 100644
index 00000000..c67594c6
--- /dev/null
+++ b/crates/wickra-core/src/indicators/high_wave.rs
@@ -0,0 +1,160 @@
+//! High-Wave candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// High-Wave — a single-bar extreme-indecision signal. A small body with very
+/// long shadows on *both* sides: price swung far up and far down yet finished
+/// near the open, a sign that trend conviction has evaporated.
+///
+/// ```text
+/// range = high − low
+/// long upper = high − max(open, close) >= 0.4 * range
+/// long lower = min(open, close) − low >= 0.4 * range
+/// ```
+///
+/// The two long-shadow conditions force the body below `0.2 * range`, so no
+/// separate body test is needed. Output is `+1.0` when the high-wave prints and
+/// `0.0` otherwise — a non-directional indecision flag, it never emits `−1.0`.
+/// 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` detected, `0.0` no pattern — so it drops straight into
+/// a machine-learning feature matrix as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, HighWave, Indicator};
+///
+/// let mut indicator = HighWave::new();
+/// // Small body, long shadows both sides.
+/// let candle = Candle::new(10.0, 12.0, 8.0, 10.3, 1.0, 0).unwrap();
+/// assert_eq!(indicator.update(candle), Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct HighWave {
+ has_emitted: bool,
+}
+
+impl HighWave {
+ /// Construct a new High-Wave detector.
+ pub const fn new() -> Self {
+ Self { has_emitted: false }
+ }
+}
+
+impl Indicator for HighWave {
+ 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 upper = candle.high - candle.open.max(candle.close);
+ let lower = candle.open.min(candle.close) - candle.low;
+ if upper >= 0.4 * range && lower >= 0.4 * range {
+ 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 {
+ "HighWave"
+ }
+}
+
+#[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 = HighWave::new();
+ assert_eq!(t.name(), "HighWave");
+ assert_eq!(t.warmup_period(), 1);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn high_wave_is_plus_one() {
+ let mut t = HighWave::new();
+ assert_eq!(t.update(c(10.0, 12.0, 8.0, 10.3, 0)), Some(1.0));
+ }
+
+ #[test]
+ fn short_upper_shadow_yields_zero() {
+ let mut t = HighWave::new();
+ // Long lower shadow but short upper -> not a high-wave.
+ assert_eq!(t.update(c(11.5, 12.0, 8.0, 11.7, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn short_lower_shadow_yields_zero() {
+ let mut t = HighWave::new();
+ // Long upper shadow but short lower -> not a high-wave.
+ assert_eq!(t.update(c(8.3, 12.0, 8.0, 8.5, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn big_body_yields_zero() {
+ let mut t = HighWave::new();
+ // A large body cannot leave both shadows long.
+ assert_eq!(t.update(c(8.5, 12.0, 8.0, 11.5, 0)), Some(0.0));
+ }
+
+ #[test]
+ fn zero_range_yields_zero() {
+ let mut t = HighWave::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 + 3.0, base - 3.0, base + 0.2, i)
+ })
+ .collect();
+ let mut a = HighWave::new();
+ let mut b = HighWave::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = HighWave::new();
+ t.update(c(10.0, 12.0, 8.0, 10.3, 0));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ }
+}
diff --git a/crates/wickra-core/src/indicators/hikkake.rs b/crates/wickra-core/src/indicators/hikkake.rs
new file mode 100644
index 00000000..3c78314c
--- /dev/null
+++ b/crates/wickra-core/src/indicators/hikkake.rs
@@ -0,0 +1,198 @@
+//! Hikkake candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Hikkake — a 3-bar trap. An inside bar (bar2 fully contained by bar1) sets up a
+/// breakout that immediately fails on bar3, trapping breakout traders and pointing
+/// the opposite way.
+///
+/// ```text
+/// inside bar : bar2.high < bar1.high && bar2.low > bar1.low
+/// bullish (+1.0): bar3 makes a LOWER high AND LOWER low than bar2
+/// (a false downside break -> expect a move up)
+/// bearish (−1.0): bar3 makes a HIGHER high AND HIGHER low than bar2
+/// (a false upside break -> expect a move down)
+/// ```
+///
+/// Output is `+1.0` (bullish setup), `−1.0` (bearish setup), or `0.0` otherwise.
+/// The detector fires when the three-bar setup completes on bar3; it does not
+/// separately flag the optional later confirmation bar. The first two bars always
+/// return `0.0` because the 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` bullish, `−1.0` bearish, `0.0` no pattern — so it
+/// drops straight into a machine-learning feature matrix where the bullish and
+/// bearish setups occupy a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Hikkake, Indicator};
+///
+/// let mut indicator = Hikkake::new();
+/// indicator.update(Candle::new(10.0, 15.0, 5.0, 12.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(11.0, 13.0, 8.0, 12.0, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(9.0, 12.0, 6.0, 7.0, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct Hikkake {
+ prev: Option,
+ prev_prev: Option,
+ has_emitted: bool,
+}
+
+impl Hikkake {
+ /// Construct a new Hikkake detector.
+ pub const fn new() -> Self {
+ Self {
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for Hikkake {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let bar1 = self.prev_prev;
+ let bar2 = self.prev;
+ self.prev_prev = self.prev;
+ self.prev = Some(candle);
+ let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
+ return Some(0.0);
+ };
+ // bar2 must be an inside bar of bar1.
+ if !(bar2.high < bar1.high && bar2.low > bar1.low) {
+ return Some(0.0);
+ }
+ // Bullish: bar3 breaks below the inside bar (lower high and lower low).
+ if candle.high < bar2.high && candle.low < bar2.low {
+ return Some(1.0);
+ }
+ // Bearish: bar3 breaks above the inside bar (higher high and higher low).
+ if candle.high > bar2.high && candle.low > 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 {
+ "Hikkake"
+ }
+}
+
+#[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 = Hikkake::new();
+ assert_eq!(t.name(), "Hikkake");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn bullish_hikkake_is_plus_one() {
+ let mut t = Hikkake::new();
+ assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(1.0));
+ }
+
+ #[test]
+ fn bearish_hikkake_is_minus_one() {
+ let mut t = Hikkake::new();
+ assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(12.0, 14.0, 9.0, 13.0, 2)), Some(-1.0));
+ }
+
+ #[test]
+ fn not_inside_bar_yields_zero() {
+ let mut t = Hikkake::new();
+ t.update(c(10.0, 15.0, 5.0, 12.0, 0));
+ // bar2 is not contained by bar1 (higher high).
+ t.update(c(11.0, 16.0, 8.0, 12.0, 1));
+ assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn outside_bar3_yields_zero() {
+ let mut t = Hikkake::new();
+ t.update(c(10.0, 15.0, 5.0, 12.0, 0));
+ t.update(c(11.0, 13.0, 8.0, 12.0, 1));
+ // bar3 engulfs bar2 (higher high and lower low) -> neither direction.
+ assert_eq!(t.update(c(11.0, 14.0, 7.0, 9.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = Hikkake::new();
+ assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ match i % 3 {
+ 0 => c(base, base + 6.0, base - 6.0, base, i),
+ 1 => c(base, base + 2.0, base - 2.0, base, i),
+ _ => c(base, base + 1.0, base - 5.0, base - 4.0, i),
+ }
+ })
+ .collect();
+ let mut a = Hikkake::new();
+ let mut b = Hikkake::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = Hikkake::new();
+ t.update(c(10.0, 15.0, 5.0, 12.0, 0));
+ t.update(c(11.0, 13.0, 8.0, 12.0, 1));
+ t.update(c(9.0, 12.0, 6.0, 7.0, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs
index d7e68c63..7bac8a64 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -77,6 +77,7 @@ mod elder_impulse;
mod ema;
mod empirical_mode_decomposition;
mod engulfing;
+mod evening_doji_star;
mod evwma;
mod fama;
mod fibonacci_pivots;
@@ -90,12 +91,15 @@ mod funding_rate;
mod funding_rate_mean;
mod funding_rate_zscore;
mod gain_loss_ratio;
+mod gap_side_by_side_white;
mod garman_klass;
mod gravestone_doji;
mod hammer;
mod hanging_man;
mod harami;
mod heikin_ashi;
+mod high_wave;
+mod hikkake;
mod hilbert_dominant_cycle;
mod hilo_activator;
mod historical_volatility;
@@ -140,6 +144,7 @@ mod median_price;
mod mfi;
mod microprice;
mod mom;
+mod morning_doji_star;
mod morning_evening_star;
mod natr;
mod nvi;
@@ -336,6 +341,7 @@ pub use elder_impulse::ElderImpulse;
pub use ema::Ema;
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
pub use engulfing::Engulfing;
+pub use evening_doji_star::EveningDojiStar;
pub use evwma::Evwma;
pub use fama::Fama;
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
@@ -349,12 +355,15 @@ pub use funding_rate::FundingRate;
pub use funding_rate_mean::FundingRateMean;
pub use funding_rate_zscore::FundingRateZScore;
pub use gain_loss_ratio::GainLossRatio;
+pub use gap_side_by_side_white::GapSideBySideWhite;
pub use garman_klass::GarmanKlassVolatility;
pub use gravestone_doji::GravestoneDoji;
pub use hammer::Hammer;
pub use hanging_man::HangingMan;
pub use harami::Harami;
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
+pub use high_wave::HighWave;
+pub use hikkake::Hikkake;
pub use hilbert_dominant_cycle::HilbertDominantCycle;
pub use hilo_activator::HiLoActivator;
pub use historical_volatility::HistoricalVolatility;
@@ -399,6 +408,7 @@ pub use median_price::MedianPrice;
pub use mfi::Mfi;
pub use microprice::Microprice;
pub use mom::Mom;
+pub use morning_doji_star::MorningDojiStar;
pub use morning_evening_star::MorningEveningStar;
pub use natr::Natr;
pub use nvi::Nvi;
@@ -800,6 +810,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"GravestoneDoji",
"LongLeggedDoji",
"RickshawMan",
+ "EveningDojiStar",
+ "MorningDojiStar",
+ "GapSideBySideWhite",
+ "HighWave",
+ "Hikkake",
],
),
(
@@ -891,6 +906,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, 254, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 259, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/indicators/morning_doji_star.rs b/crates/wickra-core/src/indicators/morning_doji_star.rs
new file mode 100644
index 00000000..9c368a21
--- /dev/null
+++ b/crates/wickra-core/src/indicators/morning_doji_star.rs
@@ -0,0 +1,241 @@
+//! Morning Doji Star candlestick pattern.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Morning Doji Star — a 3-bar bullish bottom reversal. A long black bar extends
+/// the decline, a doji gaps down below it (the star of indecision), then a white
+/// bar gaps back up and closes deep into the first body, confirming the turn.
+///
+/// ```text
+/// long body = |close − open| >= 0.5 * (high − low)
+/// doji = |close − open| <= 0.1 * (high − low)
+/// bar1 black & long
+/// bar2 doji, body gaps DOWN below bar1 body (max(o2,c2) < close1)
+/// bar3 white, body gaps UP above the doji (min(o3,c3) > max(o2,c2))
+/// bar3 closes deep into bar1 body (close3 > close1 + penetration·body1)
+/// ```
+///
+/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Morning Doji
+/// Star 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. `penetration` is how far into the first body the third bar must close;
+/// it defaults to `0.3` (TA-Lib's `CDLMORNINGDOJISTAR` default) and must lie in
+/// `[0, 1)`. Body and doji 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, MorningDojiStar};
+///
+/// let mut indicator = MorningDojiStar::new();
+/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(8.0, 8.1, 7.9, 8.0, 1.0, 1).unwrap());
+/// let out = indicator
+/// .update(Candle::new(9.0, 13.1, 8.9, 13.0, 1.0, 2).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct MorningDojiStar {
+ penetration: f64,
+ prev: Option,
+ prev_prev: Option,
+ has_emitted: bool,
+}
+
+impl Default for MorningDojiStar {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl MorningDojiStar {
+ /// Construct a Morning Doji Star detector with the default 0.3 penetration.
+ pub const fn new() -> Self {
+ Self {
+ penetration: 0.3,
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ }
+ }
+
+ /// Construct a Morning Doji Star detector with a custom penetration fraction.
+ ///
+ /// `penetration` must lie in `[0, 1)`.
+ pub fn with_penetration(penetration: f64) -> Result {
+ if !(0.0..1.0).contains(&penetration) {
+ return Err(Error::InvalidPeriod {
+ message: "morning doji star penetration must lie in [0, 1)",
+ });
+ }
+ Ok(Self {
+ penetration,
+ prev: None,
+ prev_prev: None,
+ has_emitted: false,
+ })
+ }
+
+ /// Configured penetration fraction.
+ pub fn penetration(&self) -> f64 {
+ self.penetration
+ }
+}
+
+impl Indicator for MorningDojiStar {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let bar1 = self.prev_prev;
+ let bar2 = self.prev;
+ self.prev_prev = self.prev;
+ self.prev = Some(candle);
+ let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
+ return Some(0.0);
+ };
+ let range1 = bar1.high - bar1.low;
+ let range2 = bar2.high - bar2.low;
+ if range1 <= 0.0 || range2 <= 0.0 {
+ return Some(0.0);
+ }
+ let body1 = bar1.open - bar1.close;
+ if body1 < 0.5 * range1 {
+ return Some(0.0); // bar1 must be a long black body
+ }
+ if (bar2.close - bar2.open).abs() > 0.1 * range2 {
+ return Some(0.0); // bar2 must be a doji
+ }
+ let star_top = bar2.open.max(bar2.close);
+ let bar3_bottom = candle.open.min(candle.close);
+ if star_top < bar1.close
+ && candle.close > candle.open
+ && bar3_bottom > star_top
+ && candle.close > bar1.close + self.penetration * body1
+ {
+ 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 {
+ "MorningDojiStar"
+ }
+}
+
+#[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_penetration() {
+ assert!(MorningDojiStar::with_penetration(-0.01).is_err());
+ assert!(MorningDojiStar::with_penetration(1.0).is_err());
+ }
+
+ #[test]
+ fn accepts_valid_penetration() {
+ let t = MorningDojiStar::with_penetration(0.5).unwrap();
+ assert!((t.penetration() - 0.5).abs() < 1e-12);
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = MorningDojiStar::default();
+ assert_eq!(t.name(), "MorningDojiStar");
+ assert_eq!(t.warmup_period(), 3);
+ assert!(!t.is_ready());
+ assert!((t.penetration() - 0.3).abs() < 1e-12);
+ }
+
+ #[test]
+ fn morning_doji_star_is_plus_one() {
+ let mut t = MorningDojiStar::new();
+ assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(8.0, 8.1, 7.9, 8.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(1.0));
+ }
+
+ #[test]
+ fn middle_not_doji_yields_zero() {
+ let mut t = MorningDojiStar::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ // Wide-bodied star, not a doji.
+ t.update(c(8.0, 10.1, 7.9, 10.0, 1));
+ assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn shallow_close_yields_zero() {
+ let mut t = MorningDojiStar::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ t.update(c(8.0, 8.1, 7.9, 8.0, 1));
+ // bar3 white but closes at 11.0 -> only 1.0 into the 5.0 body (< 0.3·5).
+ assert_eq!(t.update(c(9.0, 11.1, 8.9, 11.0, 2)), Some(0.0));
+ }
+
+ #[test]
+ fn first_two_bars_return_zero() {
+ let mut t = MorningDojiStar::new();
+ assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(8.0, 8.1, 7.9, 8.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 + 5.0, base + 5.1, base - 0.1, base, i)
+ })
+ .collect();
+ let mut a = MorningDojiStar::new();
+ let mut b = MorningDojiStar::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = MorningDojiStar::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ t.update(c(8.0, 8.1, 7.9, 8.0, 1));
+ t.update(c(9.0, 13.1, 8.9, 13.0, 2));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs
index 09a1b692..28fb4cc9 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -67,42 +67,42 @@ pub use indicators::{
DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar, Donchian, DonchianOutput,
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo, DragonflyDoji,
DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
- EmpiricalModeDecomposition, Engulfing, Evwma, Fama, FibonacciPivots, FibonacciPivotsOutput,
- FisherTransform, Footprint, FootprintOutput, ForceIndex, FractalChaosBands,
- FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore,
- GainLossRatio, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi,
- HeikinAshiOutput, HiLoActivator, HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel,
- HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, Inertia,
- InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
- InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput, Kst,
- KstOutput, Kurtosis, Kvo, KylesLambda, LaguerreRsi, LeadLagCrossCorrelation,
- LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegSlope,
- LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji,
- LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput,
- MarketFacilitationIndex, Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic,
- MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom, MorningEveningStar, Natr, Nvi,
- OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OpenInterestDelta, OpeningRange,
- OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
- PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
- PercentageTrailingStop, Pgo, PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi,
- QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
- RelativeStrengthOutput, RenkoTrailingStop, RickshawMan, Roc, RogersSatchellVolatility,
- RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SharpeRatio, ShootingStar,
- SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation,
- SpinningTop, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
- StarcBandsOutput, Stc, StdDev, StepTrailingStop, StochRsi, Stochastic, StochasticOutput,
- SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, TdCombo, TdCountdown,
- TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
- TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
- TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
- ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Tii, TradeImbalance, TreynorRatio,
- Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows,
- TypicalPrice, UlcerIndex, UltimateOscillator, 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,
+ EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, 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,
+ HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel, HurstChannelOutput,
+ HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, Inertia, InformationRatio,
+ InitialBalance, InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform,
+ InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis,
+ Kvo, KylesLambda, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput,
+ LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression,
+ LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongShortRatio, MaEnvelope,
+ MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
+ Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
+ Microprice, Mom, MorningDojiStar, MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted,
+ Obv, OmegaRatio, OpenInterestDelta, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
+ OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
+ ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
+ PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
+ RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoTrailingStop, RickshawMan,
+ Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi,
+ RwiOutput, SharpeRatio, ShootingStar, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
+ SortinoRatio, SpearmanCorrelation, SpinningTop, StandardError, StandardErrorBands,
+ StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
+ StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
+ TakerBuySellRatio, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput,
+ TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
+ TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis,
+ ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Tii,
+ TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
+ Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, 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 925f521e..d5e8e0bb 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 **259 indicators** across
+- A per-indicator deep dive for every one of the **264 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 106bd37d..e92e41f4 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, DragonflyDoji, EaseOfMovement, Engulfing, Evwma, FibonacciPivots, ForceIndex, FractalChaosBands, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HurstChannel, Ichimoku, IdenticalThreeCrows, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kvo, LongLeggedDoji, MarketFacilitationIndex, Marubozu, MassIndex, MedianPrice, Mfi, MorningEveningStar, Natr, Nvi, Obv, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, 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, DojiStar, Donchian, DonchianStop, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HurstChannel, Ichimoku, IdenticalThreeCrows, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kvo, LongLeggedDoji, MarketFacilitationIndex, Marubozu, MassIndex, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, 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
@@ -278,6 +278,11 @@ fuzz_target!(|data: Vec| {
}
// --- Candlestick Patterns (family 14) ---
+ drive(Hikkake::new, &candles);
+ drive(HighWave::new, &candles);
+ drive(GapSideBySideWhite::new, &candles);
+ drive(MorningDojiStar::new, &candles);
+ drive(EveningDojiStar::new, &candles);
drive(RickshawMan::new, &candles);
drive(LongLeggedDoji::new, &candles);
drive(GravestoneDoji::new, &candles);