diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ace5a2d..6473e971 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,6 +57,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Kicking by Length** — a kicking pattern signalled by the colour of the longer marubozu; +1 / -1 (`CDLKICKINGBYLENGTH`).
- **Ladder Bottom** — three descending black candles, a fourth with an upper shadow, then a white candle gapping up, a bullish reversal; +1 (`CDLLADDERBOTTOM`).
- **Mat Hold** — a long white candle, a holding three-bar pullback, then a new-high white candle, a bullish continuation; +1 (`CDLMATHOLD`).
+ - **Matching Low** — a 2-bar bullish reversal where two black candles in a decline share the same close, signalling selling pressure is exhausting; bullish +1 (`CDLMATCHINGLOW`).
+ - **Long Line** — a single long-bodied candle with short shadows; bullish +1 (white) or bearish -1 (black) by colour (`CDLLONGLINE`).
+ - **Short Line** — a single short-bodied candle with short shadows; bullish +1 (white) or bearish -1 (black) by colour (`CDLSHORTLINE`).
+ - **Rising Three Methods** — a 5-bar bullish continuation: a long white candle, three small pullback bars holding within its range, then a white breakout to new highs; bullish +1 (`CDLRISEFALL3METHODS`).
+ - **Falling Three Methods** — the bearish mirror of rising three methods: a long black candle, three small bars holding within its range, then a black breakdown to new lows; bearish -1 (`CDLRISEFALL3METHODS`).
- **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 32b8fd1a..516780e8 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 274 indicators; start at the
+ every one of the 279 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
-274 streaming-first indicators across eighteen families. Every one passes the
+279 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 |
+| 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 |
| 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 274 indicators
+│ ├── wickra-core/ core engine + all 279 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 1c6df223..0f100e47 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -265,6 +265,11 @@ const candleScalar = {
KickingByLength: { make: () => new wickra.KickingByLength(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
LadderBottom: { make: () => new wickra.LadderBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
MatHold: { make: () => new wickra.MatHold(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ MatchingLow: { make: () => new wickra.MatchingLow(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ LongLine: { make: () => new wickra.LongLine(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ ShortLine: { make: () => new wickra.ShortLine(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ RisingThreeMethods: { make: () => new wickra.RisingThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ FallingThreeMethods: { make: () => new wickra.FallingThreeMethods(), 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 d4cb12c0..55370577 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -2480,6 +2480,51 @@ export declare class MatHold {
isReady(): boolean
warmupPeriod(): number
}
+export type MatchingLowNode = MatchingLow
+export declare class MatchingLow {
+ 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 LongLineNode = LongLine
+export declare class LongLine {
+ 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 ShortLineNode = ShortLine
+export declare class ShortLine {
+ 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 RisingThreeMethodsNode = RisingThreeMethods
+export declare class RisingThreeMethods {
+ 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 FallingThreeMethodsNode = FallingThreeMethods
+export declare class FallingThreeMethods {
+ 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 0a357cdc..112ac7f8 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, 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, 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
@@ -545,6 +545,11 @@ module.exports.Kicking = Kicking
module.exports.KickingByLength = KickingByLength
module.exports.LadderBottom = LadderBottom
module.exports.MatHold = MatHold
+module.exports.MatchingLow = MatchingLow
+module.exports.LongLine = LongLine
+module.exports.ShortLine = ShortLine
+module.exports.RisingThreeMethods = RisingThreeMethods
+module.exports.FallingThreeMethods = FallingThreeMethods
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 962385ee..6e94a06c 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -8799,6 +8799,19 @@ node_candle_pattern!(KickingNode, wc::Kicking, "Kicking");
node_candle_pattern!(KickingByLengthNode, wc::KickingByLength, "KickingByLength");
node_candle_pattern!(LadderBottomNode, wc::LadderBottom, "LadderBottom");
node_candle_pattern!(MatHoldNode, wc::MatHold, "MatHold");
+node_candle_pattern!(MatchingLowNode, wc::MatchingLow, "MatchingLow");
+node_candle_pattern!(LongLineNode, wc::LongLine, "LongLine");
+node_candle_pattern!(ShortLineNode, wc::ShortLine, "ShortLine");
+node_candle_pattern!(
+ RisingThreeMethodsNode,
+ wc::RisingThreeMethods,
+ "RisingThreeMethods"
+);
+node_candle_pattern!(
+ FallingThreeMethodsNode,
+ wc::FallingThreeMethods,
+ "FallingThreeMethods"
+);
// ============================== Microstructure: Order Book ==============================
//
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index 0bf7be39..430771d4 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -270,6 +270,11 @@ from ._wickra import (
KickingByLength,
LadderBottom,
MatHold,
+ MatchingLow,
+ LongLine,
+ ShortLine,
+ RisingThreeMethods,
+ FallingThreeMethods,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -567,6 +572,11 @@ __all__ = [
"KickingByLength",
"LadderBottom",
"MatHold",
+ "MatchingLow",
+ "LongLine",
+ "ShortLine",
+ "RisingThreeMethods",
+ "FallingThreeMethods",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 0194bd1c..6d461da2 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -11662,6 +11662,19 @@ candle_pattern_no_param!(PyKicking, wc::Kicking, "Kicking");
candle_pattern_no_param!(PyKickingByLength, wc::KickingByLength, "KickingByLength");
candle_pattern_no_param!(PyLadderBottom, wc::LadderBottom, "LadderBottom");
candle_pattern_no_param!(PyMatHold, wc::MatHold, "MatHold");
+candle_pattern_no_param!(PyMatchingLow, wc::MatchingLow, "MatchingLow");
+candle_pattern_no_param!(PyLongLine, wc::LongLine, "LongLine");
+candle_pattern_no_param!(PyShortLine, wc::ShortLine, "ShortLine");
+candle_pattern_no_param!(
+ PyRisingThreeMethods,
+ wc::RisingThreeMethods,
+ "RisingThreeMethods"
+);
+candle_pattern_no_param!(
+ PyFallingThreeMethods,
+ wc::FallingThreeMethods,
+ "FallingThreeMethods"
+);
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14188,6 +14201,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 56d9f76d..1898dfb0 100644
--- a/bindings/python/tests/test_new_indicators.py
+++ b/bindings/python/tests/test_new_indicators.py
@@ -643,6 +643,26 @@ CANDLE_SCALAR = {
lambda: ta.MatHold(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
+ "MatchingLow": (
+ lambda: ta.MatchingLow(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "LongLine": (
+ lambda: ta.LongLine(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "ShortLine": (
+ lambda: ta.ShortLine(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "RisingThreeMethods": (
+ lambda: ta.RisingThreeMethods(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "FallingThreeMethods": (
+ lambda: ta.FallingThreeMethods(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
}
@@ -1891,6 +1911,46 @@ def test_mat_hold_reference():
assert t.update((15.0, 15.1, 14.4, 14.5, 1.0, 3)) == pytest.approx(0.0)
assert t.update((14.5, 17.1, 14.4, 17.0, 1.0, 4)) == pytest.approx(1.0)
+
+def test_matching_low_reference():
+ t = ta.MatchingLow()
+ assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((13.0, 13.1, 9.9, 10.0, 1.0, 1)) == pytest.approx(1.0)
+
+
+def test_long_line_reference():
+ t = ta.LongLine()
+ # Five quiet bars fill the rolling range average, then a wide solid white bar.
+ for ts in range(5):
+ assert t.update((10.0, 10.5, 9.5, 10.2, 1.0, ts)) == pytest.approx(0.0)
+ assert t.update((10.0, 13.0, 9.9, 12.9, 1.0, 5)) == pytest.approx(1.0)
+
+
+def test_short_line_reference():
+ t = ta.ShortLine()
+ # Five wide bars fill the rolling range average, then a compact solid white bar.
+ for ts in range(5):
+ assert t.update((10.0, 13.0, 9.5, 12.9, 1.0, ts)) == pytest.approx(0.0)
+ assert t.update((10.0, 11.0, 9.9, 10.9, 1.0, 5)) == pytest.approx(1.0)
+
+
+def test_rising_three_methods_reference():
+ t = ta.RisingThreeMethods()
+ assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((14.0, 14.1, 12.9, 13.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((13.5, 13.6, 12.4, 12.5, 1.0, 2)) == pytest.approx(0.0)
+ assert t.update((13.0, 13.1, 11.9, 12.0, 1.0, 3)) == pytest.approx(0.0)
+ assert t.update((12.5, 16.1, 12.4, 16.0, 1.0, 4)) == pytest.approx(1.0)
+
+
+def test_falling_three_methods_reference():
+ t = ta.FallingThreeMethods()
+ assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((11.0, 12.1, 10.9, 12.0, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((11.5, 12.6, 11.4, 12.5, 1.0, 2)) == pytest.approx(0.0)
+ assert t.update((12.0, 13.1, 11.9, 13.0, 1.0, 3)) == pytest.approx(0.0)
+ assert t.update((12.5, 12.6, 8.9, 9.0, 1.0, 4)) == pytest.approx(-1.0)
+
# --- Lifecycle ------------------------------------------------------------
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 13227a94..b02ff37b 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -6376,6 +6376,19 @@ wasm_candle_pattern!(WasmKicking, wc::Kicking, Kicking);
wasm_candle_pattern!(WasmKickingByLength, wc::KickingByLength, KickingByLength);
wasm_candle_pattern!(WasmLadderBottom, wc::LadderBottom, LadderBottom);
wasm_candle_pattern!(WasmMatHold, wc::MatHold, MatHold);
+wasm_candle_pattern!(WasmMatchingLow, wc::MatchingLow, MatchingLow);
+wasm_candle_pattern!(WasmLongLine, wc::LongLine, LongLine);
+wasm_candle_pattern!(WasmShortLine, wc::ShortLine, ShortLine);
+wasm_candle_pattern!(
+ WasmRisingThreeMethods,
+ wc::RisingThreeMethods,
+ RisingThreeMethods
+);
+wasm_candle_pattern!(
+ WasmFallingThreeMethods,
+ wc::FallingThreeMethods,
+ FallingThreeMethods
+);
// ============================== Microstructure: Order Book ==============================
//
diff --git a/crates/wickra-core/src/indicators/falling_three_methods.rs b/crates/wickra-core/src/indicators/falling_three_methods.rs
new file mode 100644
index 00000000..19a0e0c5
--- /dev/null
+++ b/crates/wickra-core/src/indicators/falling_three_methods.rs
@@ -0,0 +1,213 @@
+//! Falling Three Methods candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Falling Three Methods — a 5-bar bearish continuation. A long black candle is
+/// followed by three small bars that drift up but stay inside its range (a brief
+/// rest), then a second long black candle closes below the first, resuming the
+/// decline.
+///
+/// ```text
+/// long body = |close − open| >= 0.5 * (high − low)
+/// bar1 black & long
+/// bar2, bar3, bar4 small bodies, each contained within bar1's high/low range
+/// bar5 black, closing below bar1's close
+/// ```
+///
+/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Falling Three
+/// Methods is a single-direction (bearish-only) continuation, so it never emits
+/// `+1.0`. The first four bars always return `0.0` because the five-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` bearish, `0.0` no pattern — so it drops straight into
+/// a machine-learning feature matrix as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, FallingThreeMethods, Indicator};
+///
+/// let mut indicator = FallingThreeMethods::new();
+/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(11.0, 12.1, 10.9, 12.0, 1.0, 1).unwrap());
+/// indicator.update(Candle::new(11.5, 12.6, 11.4, 12.5, 1.0, 2).unwrap());
+/// indicator.update(Candle::new(12.0, 13.1, 11.9, 13.0, 1.0, 3).unwrap());
+/// let out = indicator
+/// .update(Candle::new(12.5, 12.6, 8.9, 9.0, 1.0, 4).unwrap());
+/// assert_eq!(out, Some(-1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct FallingThreeMethods {
+ c1: Option,
+ c2: Option,
+ c3: Option,
+ c4: Option,
+ has_emitted: bool,
+}
+
+impl FallingThreeMethods {
+ /// Construct a new Falling Three Methods detector.
+ pub const fn new() -> Self {
+ Self {
+ c1: None,
+ c2: None,
+ c3: None,
+ c4: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for FallingThreeMethods {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let bar1 = self.c1;
+ let bar2 = self.c2;
+ let bar3 = self.c3;
+ let bar4 = self.c4;
+ self.c1 = self.c2;
+ self.c2 = self.c3;
+ self.c3 = self.c4;
+ self.c4 = Some(candle);
+ let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
+ return Some(0.0);
+ };
+ let range1 = bar1.high - bar1.low;
+ if range1 <= 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
+ }
+ // The three middle bars stay within bar1's range with smaller bodies.
+ for mid in [bar2, bar3, bar4] {
+ if (mid.close - mid.open).abs() >= body1 || mid.high > bar1.high || mid.low < bar1.low {
+ return Some(0.0);
+ }
+ }
+ // bar5 is a black candle closing below bar1's close.
+ if candle.close < candle.open && candle.close < bar1.close {
+ return Some(-1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = None;
+ self.c3 = None;
+ self.c4 = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 5
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "FallingThreeMethods"
+ }
+}
+
+#[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 = FallingThreeMethods::new();
+ assert_eq!(t.name(), "FallingThreeMethods");
+ assert_eq!(t.warmup_period(), 5);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn falling_three_methods_is_minus_one() {
+ let mut t = FallingThreeMethods::new();
+ assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 12.1, 10.9, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.5, 2)), Some(0.0));
+ assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 3)), Some(0.0));
+ assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(-1.0));
+ }
+
+ #[test]
+ fn middle_bar_breaks_range_yields_zero() {
+ let mut t = FallingThreeMethods::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ t.update(c(11.0, 12.1, 10.9, 12.0, 1));
+ // bar3 pokes below bar1's low.
+ t.update(c(11.5, 12.6, 9.0, 12.5, 2));
+ t.update(c(12.0, 13.1, 11.9, 13.0, 3));
+ assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(0.0));
+ }
+
+ #[test]
+ fn bar5_not_new_low_yields_zero() {
+ let mut t = FallingThreeMethods::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ t.update(c(11.0, 12.1, 10.9, 12.0, 1));
+ t.update(c(11.5, 12.6, 11.4, 12.5, 2));
+ t.update(c(12.0, 13.1, 11.9, 13.0, 3));
+ // bar5 black but closes above bar1's close.
+ assert_eq!(t.update(c(12.5, 12.6, 10.4, 10.5, 4)), Some(0.0));
+ }
+
+ #[test]
+ fn first_four_bars_return_zero() {
+ let mut t = FallingThreeMethods::new();
+ assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(11.0, 12.1, 10.9, 12.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.5, 2)), Some(0.0));
+ assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.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 + 5.0, base + 5.1, base - 0.1, base, i)
+ })
+ .collect();
+ let mut a = FallingThreeMethods::new();
+ let mut b = FallingThreeMethods::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = FallingThreeMethods::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ t.update(c(11.0, 12.1, 10.9, 12.0, 1));
+ t.update(c(11.5, 12.6, 11.4, 12.5, 2));
+ t.update(c(12.0, 13.1, 11.9, 13.0, 3));
+ t.update(c(12.5, 12.6, 8.9, 9.0, 4));
+ 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/indicators/long_line.rs b/crates/wickra-core/src/indicators/long_line.rs
new file mode 100644
index 00000000..39ebad74
--- /dev/null
+++ b/crates/wickra-core/src/indicators/long_line.rs
@@ -0,0 +1,229 @@
+//! Long Line candlestick pattern.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+use std::collections::VecDeque;
+
+/// Long Line — a single candle whose range is *longer* than the recent average and
+/// whose body dominates that range (a solid directional bar). Because "long" only
+/// has meaning relative to recent activity, the detector compares each candle's
+/// range against a rolling average of the previous `period` ranges.
+///
+/// ```text
+/// avg = mean range of the previous `period` candles
+/// long line = range > avg AND |close − open| >= 0.5 * range
+/// white -> +1.0, black -> −1.0
+/// ```
+///
+/// Output is `+1.0` (long white line), `−1.0` (long black line), or `0.0`
+/// otherwise. The first `period` candles return `0.0` while the rolling average
+/// fills. `period` defaults to `5` and must be at least `1`. This rolling baseline
+/// is the one place the family departs from a purely intra-candle rule, since a
+/// short/long classification is inherently scale-relative. 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 as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Indicator, LongLine};
+///
+/// let mut indicator = LongLine::new();
+/// // Five quiet bars fill the rolling average.
+/// for ts in 0..5 {
+/// indicator.update(Candle::new(10.0, 10.5, 9.5, 10.2, 1.0, ts).unwrap());
+/// }
+/// // A wide solid white bar is a long white line.
+/// let out = indicator
+/// .update(Candle::new(10.0, 13.0, 9.9, 12.9, 1.0, 5).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct LongLine {
+ period: usize,
+ ranges: VecDeque,
+}
+
+impl Default for LongLine {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl LongLine {
+ /// Construct a Long Line detector with the default 5-candle rolling average.
+ pub const fn new() -> Self {
+ Self {
+ period: 5,
+ ranges: VecDeque::new(),
+ }
+ }
+
+ /// Construct a Long Line detector with a custom averaging period.
+ ///
+ /// `period` must be at least `1`.
+ pub fn with_period(period: usize) -> Result {
+ if period == 0 {
+ return Err(Error::PeriodZero);
+ }
+ Ok(Self {
+ period,
+ ranges: VecDeque::new(),
+ })
+ }
+
+ /// Configured averaging period.
+ pub fn period(&self) -> usize {
+ self.period
+ }
+}
+
+impl Indicator for LongLine {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ let range = candle.high - candle.low;
+ let body = candle.close - candle.open;
+ if self.ranges.len() < self.period {
+ self.ranges.push_back(range);
+ return Some(0.0);
+ }
+ let avg = self.ranges.iter().sum::() / self.period as f64;
+ self.ranges.push_back(range);
+ self.ranges.pop_front();
+ if range > avg && body.abs() >= 0.5 * range {
+ return Some(if body > 0.0 { 1.0 } else { -1.0 });
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.ranges.clear();
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.period
+ }
+
+ fn is_ready(&self) -> bool {
+ self.ranges.len() >= self.period
+ }
+
+ fn name(&self) -> &'static str {
+ "LongLine"
+ }
+}
+
+#[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()
+ }
+
+ fn warm(t: &mut LongLine) {
+ for ts in 0..5 {
+ assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, ts)), Some(0.0));
+ }
+ }
+
+ #[test]
+ fn rejects_zero_period() {
+ assert!(LongLine::with_period(0).is_err());
+ }
+
+ #[test]
+ fn accepts_valid_period() {
+ let t = LongLine::with_period(10).unwrap();
+ assert_eq!(t.period(), 10);
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = LongLine::new();
+ assert_eq!(t.name(), "LongLine");
+ assert_eq!(t.warmup_period(), 5);
+ assert!(!t.is_ready());
+ assert_eq!(t.period(), 5);
+ }
+
+ #[test]
+ fn long_white_line_is_plus_one() {
+ let mut t = LongLine::new();
+ warm(&mut t);
+ assert!(t.is_ready());
+ assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 5)), Some(1.0));
+ }
+
+ #[test]
+ fn long_black_line_is_minus_one() {
+ let mut t = LongLine::new();
+ warm(&mut t);
+ assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 5)), Some(-1.0));
+ }
+
+ #[test]
+ fn short_range_yields_zero() {
+ let mut t = LongLine::new();
+ warm(&mut t);
+ // Range no bigger than the average -> not a long line.
+ assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, 5)), Some(0.0));
+ }
+
+ #[test]
+ fn wide_range_small_body_yields_zero() {
+ let mut t = LongLine::new();
+ warm(&mut t);
+ // Wide range but a tiny body -> a spinning top, not a long line.
+ assert_eq!(t.update(c(10.5, 13.0, 9.9, 10.6, 5)), Some(0.0));
+ }
+
+ #[test]
+ fn warmup_returns_zero() {
+ let mut t = LongLine::new();
+ for ts in 0..5 {
+ assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, ts)), Some(0.0));
+ }
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ if i % 7 == 0 {
+ c(base, base + 4.0, base - 0.1, base + 3.9, i)
+ } else {
+ c(base, base + 0.5, base - 0.5, base + 0.2, i)
+ }
+ })
+ .collect();
+ let mut a = LongLine::new();
+ let mut b = LongLine::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = LongLine::new();
+ warm(&mut t);
+ t.update(c(10.0, 13.0, 9.9, 12.9, 5));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/indicators/matching_low.rs b/crates/wickra-core/src/indicators/matching_low.rs
new file mode 100644
index 00000000..4cc46cf2
--- /dev/null
+++ b/crates/wickra-core/src/indicators/matching_low.rs
@@ -0,0 +1,166 @@
+//! Matching Low candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Matching Low — a 2-bar bullish reversal. Two black candles in a decline close
+/// at the *same* level: the second sell-off cannot push price any lower, so the
+/// matching closes mark a support floor.
+///
+/// ```text
+/// bar1, bar2 both black
+/// equal closes = |close2 − close1| <= 0.05 · mean(range1, range2)
+/// ```
+///
+/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Matching Low
+/// is a single-direction (bullish-only) reversal, so it never emits `−1.0`. The
+/// first bar always returns `0.0` because the two-bar window is not yet filled.
+/// The close-equality tolerance follows 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, MatchingLow};
+///
+/// let mut indicator = MatchingLow::new();
+/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
+/// let out = indicator
+/// .update(Candle::new(13.0, 13.1, 9.9, 10.0, 1.0, 1).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct MatchingLow {
+ prev: Option,
+ has_emitted: bool,
+}
+
+impl MatchingLow {
+ /// Construct a new Matching Low detector.
+ pub const fn new() -> Self {
+ Self {
+ prev: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for MatchingLow {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let prev = self.prev;
+ self.prev = Some(candle);
+ let Some(bar1) = prev else {
+ return Some(0.0);
+ };
+ let mean_range = 0.5 * ((bar1.high - bar1.low) + (candle.high - candle.low));
+ let tol = 0.05 * mean_range;
+ if bar1.close < bar1.open
+ && candle.close < candle.open
+ && (candle.close - bar1.close).abs() <= tol
+ {
+ return Some(1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.prev = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 2
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "MatchingLow"
+ }
+}
+
+#[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 = MatchingLow::new();
+ assert_eq!(t.name(), "MatchingLow");
+ assert_eq!(t.warmup_period(), 2);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn matching_low_is_plus_one() {
+ let mut t = MatchingLow::new();
+ assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 1)), Some(1.0));
+ }
+
+ #[test]
+ fn different_close_yields_zero() {
+ let mut t = MatchingLow::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ // Second close well away from the first.
+ assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn second_bar_white_yields_zero() {
+ let mut t = MatchingLow::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ assert_eq!(t.update(c(9.0, 10.1, 8.9, 10.0, 1)), Some(0.0));
+ }
+
+ #[test]
+ fn first_bar_returns_zero() {
+ let mut t = MatchingLow::new();
+ assert_eq!(t.update(c(15.0, 15.1, 9.9, 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 + 2.0, base + 2.1, base - 0.1, base, i)
+ })
+ .collect();
+ let mut a = MatchingLow::new();
+ let mut b = MatchingLow::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = MatchingLow::new();
+ t.update(c(15.0, 15.1, 9.9, 10.0, 0));
+ t.update(c(13.0, 13.1, 9.9, 10.0, 1));
+ 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/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs
index b5acdd77..b91c1223 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -79,6 +79,7 @@ mod empirical_mode_decomposition;
mod engulfing;
mod evening_doji_star;
mod evwma;
+mod falling_three_methods;
mod fama;
mod fibonacci_pivots;
mod fisher_transform;
@@ -136,6 +137,7 @@ mod linreg_channel;
mod linreg_slope;
mod liquidation_features;
mod long_legged_doji;
+mod long_line;
mod long_short_ratio;
mod ma_envelope;
mod macd;
@@ -144,6 +146,7 @@ mod market_facilitation_index;
mod marubozu;
mod mass_index;
mod mat_hold;
+mod matching_low;
mod max_drawdown;
mod mcginley_dynamic;
mod median_absolute_deviation;
@@ -186,6 +189,7 @@ mod recovery_factor;
mod relative_strength_ab;
mod renko_trailing_stop;
mod rickshaw_man;
+mod rising_three_methods;
mod roc;
mod rogers_satchell;
mod roofing_filter;
@@ -196,6 +200,7 @@ mod rwi;
mod separating_lines;
mod sharpe_ratio;
mod shooting_star;
+mod short_line;
mod signed_volume;
mod sine_wave;
mod skewness;
@@ -353,6 +358,7 @@ pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
pub use engulfing::Engulfing;
pub use evening_doji_star::EveningDojiStar;
pub use evwma::Evwma;
+pub use falling_three_methods::FallingThreeMethods;
pub use fama::Fama;
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
pub use fisher_transform::FisherTransform;
@@ -410,6 +416,7 @@ pub use linreg_channel::{LinRegChannel, LinRegChannelOutput};
pub use linreg_slope::LinRegSlope;
pub use liquidation_features::{LiquidationFeatures, LiquidationFeaturesOutput};
pub use long_legged_doji::LongLeggedDoji;
+pub use long_line::LongLine;
pub use long_short_ratio::LongShortRatio;
pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
pub use macd::{MacdIndicator, MacdOutput};
@@ -418,6 +425,7 @@ pub use market_facilitation_index::MarketFacilitationIndex;
pub use marubozu::Marubozu;
pub use mass_index::MassIndex;
pub use mat_hold::MatHold;
+pub use matching_low::MatchingLow;
pub use max_drawdown::MaxDrawdown;
pub use mcginley_dynamic::McGinleyDynamic;
pub use median_absolute_deviation::MedianAbsoluteDeviation;
@@ -460,6 +468,7 @@ pub use recovery_factor::RecoveryFactor;
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
pub use renko_trailing_stop::RenkoTrailingStop;
pub use rickshaw_man::RickshawMan;
+pub use rising_three_methods::RisingThreeMethods;
pub use roc::Roc;
pub use rogers_satchell::RogersSatchellVolatility;
pub use roofing_filter::RoofingFilter;
@@ -470,6 +479,7 @@ pub use rwi::{Rwi, RwiOutput};
pub use separating_lines::SeparatingLines;
pub use sharpe_ratio::SharpeRatio;
pub use shooting_star::ShootingStar;
+pub use short_line::ShortLine;
pub use signed_volume::SignedVolume;
pub use sine_wave::SineWave;
pub use skewness::Skewness;
@@ -845,6 +855,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"KickingByLength",
"LadderBottom",
"MatHold",
+ "MatchingLow",
+ "LongLine",
+ "ShortLine",
+ "RisingThreeMethods",
+ "FallingThreeMethods",
],
),
(
@@ -936,6 +951,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, 269, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 274, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/indicators/rising_three_methods.rs b/crates/wickra-core/src/indicators/rising_three_methods.rs
new file mode 100644
index 00000000..2c8bec5e
--- /dev/null
+++ b/crates/wickra-core/src/indicators/rising_three_methods.rs
@@ -0,0 +1,213 @@
+//! Rising Three Methods candlestick pattern.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Rising Three Methods — a 5-bar bullish continuation. A long white candle is
+/// followed by three small bars that drift back but stay inside its range (a brief
+/// rest), then a second long white candle closes above the first, resuming the
+/// advance.
+///
+/// ```text
+/// long body = |close − open| >= 0.5 * (high − low)
+/// bar1 white & long
+/// bar2, bar3, bar4 small bodies, each contained within bar1's high/low range
+/// bar5 white, closing above bar1's close
+/// ```
+///
+/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Rising Three
+/// Methods is a single-direction (bullish-only) continuation, so it never emits
+/// `−1.0`. The first four bars always return `0.0` because the five-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, RisingThreeMethods};
+///
+/// let mut indicator = RisingThreeMethods::new();
+/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
+/// indicator.update(Candle::new(14.0, 14.1, 12.9, 13.0, 1.0, 1).unwrap());
+/// indicator.update(Candle::new(13.5, 13.6, 12.4, 12.5, 1.0, 2).unwrap());
+/// indicator.update(Candle::new(13.0, 13.1, 11.9, 12.0, 1.0, 3).unwrap());
+/// let out = indicator
+/// .update(Candle::new(12.5, 16.1, 12.4, 16.0, 1.0, 4).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct RisingThreeMethods {
+ c1: Option,
+ c2: Option,
+ c3: Option,
+ c4: Option,
+ has_emitted: bool,
+}
+
+impl RisingThreeMethods {
+ /// Construct a new Rising Three Methods detector.
+ pub const fn new() -> Self {
+ Self {
+ c1: None,
+ c2: None,
+ c3: None,
+ c4: None,
+ has_emitted: false,
+ }
+ }
+}
+
+impl Indicator for RisingThreeMethods {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ self.has_emitted = true;
+ let bar1 = self.c1;
+ let bar2 = self.c2;
+ let bar3 = self.c3;
+ let bar4 = self.c4;
+ self.c1 = self.c2;
+ self.c2 = self.c3;
+ self.c3 = self.c4;
+ self.c4 = Some(candle);
+ let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
+ return Some(0.0);
+ };
+ let range1 = bar1.high - bar1.low;
+ if range1 <= 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
+ }
+ // The three middle bars stay within bar1's range with smaller bodies.
+ for mid in [bar2, bar3, bar4] {
+ if (mid.close - mid.open).abs() >= body1 || mid.high > bar1.high || mid.low < bar1.low {
+ return Some(0.0);
+ }
+ }
+ // bar5 is a white candle closing above bar1's close.
+ if candle.close > candle.open && candle.close > bar1.close {
+ return Some(1.0);
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = None;
+ self.c3 = None;
+ self.c4 = None;
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 5
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "RisingThreeMethods"
+ }
+}
+
+#[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 = RisingThreeMethods::new();
+ assert_eq!(t.name(), "RisingThreeMethods");
+ assert_eq!(t.warmup_period(), 5);
+ assert!(!t.is_ready());
+ }
+
+ #[test]
+ fn rising_three_methods_is_plus_one() {
+ let mut t = RisingThreeMethods::new();
+ assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(13.5, 13.6, 12.4, 12.5, 2)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 3)), Some(0.0));
+ assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(1.0));
+ }
+
+ #[test]
+ fn middle_bar_breaks_range_yields_zero() {
+ let mut t = RisingThreeMethods::new();
+ t.update(c(10.0, 15.1, 9.9, 15.0, 0));
+ t.update(c(14.0, 14.1, 12.9, 13.0, 1));
+ // bar3 pokes above bar1's high.
+ t.update(c(13.5, 16.0, 12.4, 12.5, 2));
+ t.update(c(13.0, 13.1, 11.9, 12.0, 3));
+ assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(0.0));
+ }
+
+ #[test]
+ fn bar5_not_new_high_yields_zero() {
+ let mut t = RisingThreeMethods::new();
+ t.update(c(10.0, 15.1, 9.9, 15.0, 0));
+ t.update(c(14.0, 14.1, 12.9, 13.0, 1));
+ t.update(c(13.5, 13.6, 12.4, 12.5, 2));
+ t.update(c(13.0, 13.1, 11.9, 12.0, 3));
+ // bar5 white but closes below bar1's close.
+ assert_eq!(t.update(c(12.5, 14.6, 12.4, 14.5, 4)), Some(0.0));
+ }
+
+ #[test]
+ fn first_four_bars_return_zero() {
+ let mut t = RisingThreeMethods::new();
+ assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
+ assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 1)), Some(0.0));
+ assert_eq!(t.update(c(13.5, 13.6, 12.4, 12.5, 2)), Some(0.0));
+ assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 3)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ c(base, base + 5.2, base - 0.1, base + 5.0, i)
+ })
+ .collect();
+ let mut a = RisingThreeMethods::new();
+ let mut b = RisingThreeMethods::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = RisingThreeMethods::new();
+ t.update(c(10.0, 15.1, 9.9, 15.0, 0));
+ t.update(c(14.0, 14.1, 12.9, 13.0, 1));
+ t.update(c(13.5, 13.6, 12.4, 12.5, 2));
+ t.update(c(13.0, 13.1, 11.9, 12.0, 3));
+ t.update(c(12.5, 16.1, 12.4, 16.0, 4));
+ 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/short_line.rs b/crates/wickra-core/src/indicators/short_line.rs
new file mode 100644
index 00000000..6dd3e906
--- /dev/null
+++ b/crates/wickra-core/src/indicators/short_line.rs
@@ -0,0 +1,228 @@
+//! Short Line candlestick pattern.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+use std::collections::VecDeque;
+
+/// Short Line — a single candle whose range is *shorter* than the recent average
+/// while its body still dominates that (small) range: a compact directional bar.
+/// As with [`LongLine`](crate::LongLine), "short" only has meaning relative to
+/// recent activity, so the detector compares each candle's range against a rolling
+/// average of the previous `period` ranges.
+///
+/// ```text
+/// avg = mean range of the previous `period` candles
+/// short line = range < avg AND |close − open| >= 0.5 * range
+/// white -> +1.0, black -> −1.0
+/// ```
+///
+/// Output is `+1.0` (short white line), `−1.0` (short black line), or `0.0`
+/// otherwise. The first `period` candles return `0.0` while the rolling average
+/// fills. `period` defaults to `5` and must be at least `1`. Pattern-shape check
+/// only — no trend filter is applied; combine with a trend indicator for
+/// actionable signals.
+///
+/// # Signed ±1 encoding
+///
+/// This detector emits the uniform candlestick sign convention shared across the
+/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
+/// drops straight into a machine-learning feature matrix as a single dimension.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Indicator, ShortLine};
+///
+/// let mut indicator = ShortLine::new();
+/// // Five wide bars fill the rolling average.
+/// for ts in 0..5 {
+/// indicator.update(Candle::new(10.0, 13.0, 9.5, 12.9, 1.0, ts).unwrap());
+/// }
+/// // A compact solid white bar is a short white line.
+/// let out = indicator
+/// .update(Candle::new(10.0, 11.0, 9.9, 10.9, 1.0, 5).unwrap());
+/// assert_eq!(out, Some(1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct ShortLine {
+ period: usize,
+ ranges: VecDeque,
+}
+
+impl Default for ShortLine {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl ShortLine {
+ /// Construct a Short Line detector with the default 5-candle rolling average.
+ pub const fn new() -> Self {
+ Self {
+ period: 5,
+ ranges: VecDeque::new(),
+ }
+ }
+
+ /// Construct a Short Line detector with a custom averaging period.
+ ///
+ /// `period` must be at least `1`.
+ pub fn with_period(period: usize) -> Result {
+ if period == 0 {
+ return Err(Error::PeriodZero);
+ }
+ Ok(Self {
+ period,
+ ranges: VecDeque::new(),
+ })
+ }
+
+ /// Configured averaging period.
+ pub fn period(&self) -> usize {
+ self.period
+ }
+}
+
+impl Indicator for ShortLine {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ let range = candle.high - candle.low;
+ let body = candle.close - candle.open;
+ if self.ranges.len() < self.period {
+ self.ranges.push_back(range);
+ return Some(0.0);
+ }
+ let avg = self.ranges.iter().sum::() / self.period as f64;
+ self.ranges.push_back(range);
+ self.ranges.pop_front();
+ if range < avg && body.abs() >= 0.5 * range {
+ return Some(if body > 0.0 { 1.0 } else { -1.0 });
+ }
+ Some(0.0)
+ }
+
+ fn reset(&mut self) {
+ self.ranges.clear();
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.period
+ }
+
+ fn is_ready(&self) -> bool {
+ self.ranges.len() >= self.period
+ }
+
+ fn name(&self) -> &'static str {
+ "ShortLine"
+ }
+}
+
+#[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()
+ }
+
+ fn warm(t: &mut ShortLine) {
+ for ts in 0..5 {
+ assert_eq!(t.update(c(10.0, 13.0, 9.5, 12.9, ts)), Some(0.0));
+ }
+ }
+
+ #[test]
+ fn rejects_zero_period() {
+ assert!(ShortLine::with_period(0).is_err());
+ }
+
+ #[test]
+ fn accepts_valid_period() {
+ let t = ShortLine::with_period(10).unwrap();
+ assert_eq!(t.period(), 10);
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = ShortLine::new();
+ assert_eq!(t.name(), "ShortLine");
+ assert_eq!(t.warmup_period(), 5);
+ assert!(!t.is_ready());
+ assert_eq!(t.period(), 5);
+ }
+
+ #[test]
+ fn short_white_line_is_plus_one() {
+ let mut t = ShortLine::new();
+ warm(&mut t);
+ assert!(t.is_ready());
+ assert_eq!(t.update(c(10.0, 11.0, 9.9, 10.9, 5)), Some(1.0));
+ }
+
+ #[test]
+ fn short_black_line_is_minus_one() {
+ let mut t = ShortLine::new();
+ warm(&mut t);
+ assert_eq!(t.update(c(10.9, 11.0, 9.9, 10.0, 5)), Some(-1.0));
+ }
+
+ #[test]
+ fn wide_range_yields_zero() {
+ let mut t = ShortLine::new();
+ warm(&mut t);
+ // Range as wide as the average -> not a short line.
+ assert_eq!(t.update(c(10.0, 13.0, 9.5, 12.9, 5)), Some(0.0));
+ }
+
+ #[test]
+ fn short_range_small_body_yields_zero() {
+ let mut t = ShortLine::new();
+ warm(&mut t);
+ // Compact range but a tiny body -> not a solid short line.
+ assert_eq!(t.update(c(10.4, 11.0, 9.9, 10.5, 5)), Some(0.0));
+ }
+
+ #[test]
+ fn warmup_returns_zero() {
+ let mut t = ShortLine::new();
+ for ts in 0..5 {
+ assert_eq!(t.update(c(10.0, 11.0, 9.9, 10.9, ts)), Some(0.0));
+ }
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ let base = 100.0 + i as f64;
+ if i % 7 == 0 {
+ c(base, base + 0.6, base - 0.1, base + 0.5, i)
+ } else {
+ c(base, base + 3.0, base - 1.0, base + 2.8, i)
+ }
+ })
+ .collect();
+ let mut a = ShortLine::new();
+ let mut b = ShortLine::new();
+ assert_eq!(
+ a.batch(&candles),
+ candles.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = ShortLine::new();
+ warm(&mut t);
+ t.update(c(10.0, 11.0, 9.9, 10.9, 5));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(c(10.0, 11.0, 9.9, 10.9, 0)), Some(0.0));
+ }
+}
diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs
index dec21b93..5ad22098 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -67,39 +67,40 @@ pub use indicators::{
DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar, Donchian, DonchianOutput,
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo, DragonflyDoji,
DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
- 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,
- 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,
+ 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, LongShortRatio, MaEnvelope, MaEnvelopeOutput,
- MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
- MatHold, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice,
- Mom, MorningDojiStar, MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv,
- OmegaRatio, OnNeck, 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, SeparatingLines,
- 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, Thrusting, Tii,
- TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
- Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UpsideGapTwoCrows, ValueArea,
+ 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,
+ OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
+ ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
+ PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
+ RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoTrailingStop, RickshawMan,
+ RisingThreeMethods, Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi,
+ RviVolatility, Rwi, RwiOutput, SeparatingLines, SharpeRatio, ShootingStar, ShortLine,
+ 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, Thrusting, 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,
diff --git a/docs/README.md b/docs/README.md
index 6986159c..7c05b264 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 **274 indicators** across
+- A per-indicator deep dive for every one of the **279 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 b6216ea5..98ede9f6 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, EveningDojiStar, Evwma, 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, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, Smi, SpinningTop, StarcBands, Stochastic, SuperTrend, 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, 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, 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, StarcBands, Stochastic, SuperTrend, 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, 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(FallingThreeMethods::new, &candles);
+ drive(RisingThreeMethods::new, &candles);
+ drive(ShortLine::new, &candles);
+ drive(LongLine::new, &candles);
+ drive(MatchingLow::new, &candles);
drive(MatHold::new, &candles);
drive(LadderBottom::new, &candles);
drive(KickingByLength::new, &candles);