feat: TA-Lib candlestick patterns — separating/kicking/ladder/mat-hold (part 6 of 9) (#138)

This commit is contained in:
kingchenc
2026-06-02 17:06:40 +02:00
committed by GitHub
parent e4ca9c3f8f
commit 04ae145126
19 changed files with 1274 additions and 39 deletions
+5
View File
@@ -52,6 +52,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **On-Neck** — a long black candle then a white candle closing at its low (the neckline), a bearish continuation; -1 (`CDLONNECK`).
- **In-Neck** — a long black candle then a white candle closing just into its body, a bearish continuation; -1 (`CDLINNECK`).
- **Thrusting** — a long black candle then a white candle closing well into but below the midpoint of its body, a bearish continuation; -1 (`CDLTHRUSTING`).
- **Separating Lines** — opposite-coloured candles sharing the same open, the second an opening marubozu resuming the trend; bullish +1, bearish -1 (`CDLSEPARATINGLINES`).
- **Kicking** — two opposite-coloured marubozu separated by a gap; bullish +1, bearish -1 (`CDLKICKING`).
- **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`).
- **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,
+5 -5
View File
@@ -1,5 +1,5 @@
<p align="center">
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=269" alt="Wickra — streaming-first technical indicators" width="100%"></a>
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=274" alt="Wickra — streaming-first technical indicators" width="100%"></a>
</p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](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 269 indicators; start at the
every one of the 274 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
269 streaming-first indicators across eighteen families. Every one passes the
274 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 |
| 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 |
| 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 269 indicators
│ ├── wickra-core/ core engine + all 274 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
@@ -260,6 +260,11 @@ const candleScalar = {
OnNeck: { make: () => new wickra.OnNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
InNeck: { make: () => new wickra.InNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
Thrusting: { make: () => new wickra.Thrusting(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
SeparatingLines: { make: () => new wickra.SeparatingLines(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
Kicking: { make: () => new wickra.Kicking(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
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) },
};
for (const [name, d] of Object.entries(candleScalar)) {
+45
View File
@@ -2435,6 +2435,51 @@ export declare class Thrusting {
isReady(): boolean
warmupPeriod(): number
}
export type SeparatingLinesNode = SeparatingLines
export declare class SeparatingLines {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type KickingNode = Kicking
export declare class Kicking {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type KickingByLengthNode = KickingByLength
export declare class KickingByLength {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type LadderBottomNode = LadderBottom
export declare class LadderBottom {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type MatHoldNode = MatHold
export declare class MatHold {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
export declare class OrderBookImbalanceTop1 {
constructor()
+6 -1
View File
@@ -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, 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, 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
@@ -540,6 +540,11 @@ module.exports.HomingPigeon = HomingPigeon
module.exports.OnNeck = OnNeck
module.exports.InNeck = InNeck
module.exports.Thrusting = Thrusting
module.exports.SeparatingLines = SeparatingLines
module.exports.Kicking = Kicking
module.exports.KickingByLength = KickingByLength
module.exports.LadderBottom = LadderBottom
module.exports.MatHold = MatHold
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
module.exports.Microprice = Microprice
+5
View File
@@ -8794,6 +8794,11 @@ node_candle_pattern!(HomingPigeonNode, wc::HomingPigeon, "HomingPigeon");
node_candle_pattern!(OnNeckNode, wc::OnNeck, "OnNeck");
node_candle_pattern!(InNeckNode, wc::InNeck, "InNeck");
node_candle_pattern!(ThrustingNode, wc::Thrusting, "Thrusting");
node_candle_pattern!(SeparatingLinesNode, wc::SeparatingLines, "SeparatingLines");
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");
// ============================== Microstructure: Order Book ==============================
//
+10
View File
@@ -265,6 +265,11 @@ from ._wickra import (
OnNeck,
InNeck,
Thrusting,
SeparatingLines,
Kicking,
KickingByLength,
LadderBottom,
MatHold,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -557,6 +562,11 @@ __all__ = [
"OnNeck",
"InNeck",
"Thrusting",
"SeparatingLines",
"Kicking",
"KickingByLength",
"LadderBottom",
"MatHold",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
+10
View File
@@ -11657,6 +11657,11 @@ candle_pattern_no_param!(PyHomingPigeon, wc::HomingPigeon, "HomingPigeon");
candle_pattern_no_param!(PyOnNeck, wc::OnNeck, "OnNeck");
candle_pattern_no_param!(PyInNeck, wc::InNeck, "InNeck");
candle_pattern_no_param!(PyThrusting, wc::Thrusting, "Thrusting");
candle_pattern_no_param!(PySeparatingLines, wc::SeparatingLines, "SeparatingLines");
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");
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14178,6 +14183,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyOnNeck>()?;
m.add_class::<PyInNeck>()?;
m.add_class::<PyThrusting>()?;
m.add_class::<PySeparatingLines>()?;
m.add_class::<PyKicking>()?;
m.add_class::<PyKickingByLength>()?;
m.add_class::<PyLadderBottom>()?;
m.add_class::<PyMatHold>()?;
// Microstructure: order book.
m.add_class::<PyOrderBookImbalanceTop1>()?;
m.add_class::<PyOrderBookImbalanceTopN>()?;
@@ -623,6 +623,26 @@ CANDLE_SCALAR = {
lambda: ta.Thrusting(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"SeparatingLines": (
lambda: ta.SeparatingLines(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"Kicking": (
lambda: ta.Kicking(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"KickingByLength": (
lambda: ta.KickingByLength(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"LadderBottom": (
lambda: ta.LadderBottom(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"MatHold": (
lambda: ta.MatHold(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
}
@@ -1835,6 +1855,42 @@ def test_thrusting_reference():
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((7.0, 11.6, 6.9, 11.5, 1.0, 1)) == pytest.approx(-1.0)
def test_separating_lines_reference():
t = ta.SeparatingLines()
assert t.update((12.0, 12.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((12.0, 14.1, 12.0, 14.0, 1.0, 1)) == pytest.approx(1.0)
def test_kicking_reference():
t = ta.Kicking()
assert t.update((12.0, 12.0, 10.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((14.0, 16.0, 14.0, 16.0, 1.0, 1)) == pytest.approx(1.0)
def test_kicking_by_length_reference():
t = ta.KickingByLength()
assert t.update((12.0, 12.0, 10.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((14.0, 20.0, 14.0, 20.0, 1.0, 1)) == pytest.approx(1.0)
def test_ladder_bottom_reference():
t = ta.LadderBottom()
assert t.update((20.0, 20.1, 17.9, 18.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((18.0, 18.1, 15.9, 16.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((16.0, 16.1, 13.9, 14.0, 1.0, 2)) == pytest.approx(0.0)
assert t.update((14.0, 15.0, 12.4, 12.5, 1.0, 3)) == pytest.approx(0.0)
assert t.update((15.0, 17.1, 14.9, 17.0, 1.0, 4)) == pytest.approx(1.0)
def test_mat_hold_reference():
t = ta.MatHold()
assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((16.0, 16.1, 15.4, 15.5, 1.0, 1)) == pytest.approx(0.0)
assert t.update((15.5, 15.6, 14.9, 15.0, 1.0, 2)) == pytest.approx(0.0)
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)
# --- Lifecycle ------------------------------------------------------------
+5
View File
@@ -6371,6 +6371,11 @@ wasm_candle_pattern!(WasmHomingPigeon, wc::HomingPigeon, HomingPigeon);
wasm_candle_pattern!(WasmOnNeck, wc::OnNeck, OnNeck);
wasm_candle_pattern!(WasmInNeck, wc::InNeck, InNeck);
wasm_candle_pattern!(WasmThrusting, wc::Thrusting, Thrusting);
wasm_candle_pattern!(WasmSeparatingLines, wc::SeparatingLines, SeparatingLines);
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);
// ============================== Microstructure: Order Book ==============================
//
@@ -0,0 +1,189 @@
//! Kicking candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Kicking — a 2-bar reversal of two opposite-coloured marubozu separated by a
/// gap. A shadowless candle is "kicked" the other way by a shadowless candle of
/// the opposite colour that gaps clear of it — a violent change of control. It is
/// trend-agnostic: the gap direction alone defines the signal.
///
/// ```text
/// marubozu = |close open| >= 0.95 * (high low) (no meaningful shadows)
/// bullish (+1.0): black marubozu, then a white marubozu gapping UP (low2 > high1)
/// bearish (1.0): white marubozu, then a black marubozu gapping DOWN (high2 < low1)
/// ```
///
/// Output is `+1.0` (bullish) or `1.0` (bearish) when the pattern completes and
/// `0.0` otherwise. The first bar always returns `0.0` because the two-bar window
/// is not yet filled. The marubozu threshold 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, `1.0` bearish, `0.0` no pattern — so it
/// drops straight into a machine-learning feature matrix where the two directions
/// occupy a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, Kicking};
///
/// let mut indicator = Kicking::new();
/// indicator.update(Candle::new(12.0, 12.0, 10.0, 10.0, 1.0, 0).unwrap());
/// let out = indicator
/// .update(Candle::new(14.0, 16.0, 14.0, 16.0, 1.0, 1).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct Kicking {
prev: Option<Candle>,
has_emitted: bool,
}
impl Kicking {
/// Construct a new Kicking detector.
pub const fn new() -> Self {
Self {
prev: None,
has_emitted: false,
}
}
}
/// Whether `candle` is a marubozu (body fills at least 95 % of its range).
fn is_marubozu(candle: &Candle) -> bool {
let range = candle.high - candle.low;
range > 0.0 && (candle.close - candle.open).abs() >= 0.95 * range
}
impl Indicator for Kicking {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let prev = self.prev;
self.prev = Some(candle);
let Some(bar1) = prev else {
return Some(0.0);
};
if !is_marubozu(&bar1) || !is_marubozu(&candle) {
return Some(0.0);
}
// Bullish: black marubozu kicked up by a white marubozu gapping above it.
if bar1.close < bar1.open && candle.close > candle.open && candle.low > bar1.high {
return Some(1.0);
}
// Bearish: white marubozu kicked down by a black marubozu gapping below it.
if bar1.close > bar1.open && candle.close < candle.open && candle.high < bar1.low {
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 {
"Kicking"
}
}
#[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 = Kicking::new();
assert_eq!(t.name(), "Kicking");
assert_eq!(t.warmup_period(), 2);
assert!(!t.is_ready());
}
#[test]
fn bullish_kicking_is_plus_one() {
let mut t = Kicking::new();
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(1.0));
}
#[test]
fn bearish_kicking_is_minus_one() {
let mut t = Kicking::new();
assert_eq!(t.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(8.0, 8.0, 6.0, 6.0, 1)), Some(-1.0));
}
#[test]
fn not_marubozu_yields_zero() {
let mut t = Kicking::new();
// bar1 has long shadows -> not a marubozu.
t.update(c(12.0, 14.0, 8.0, 10.0, 0));
assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(0.0));
}
#[test]
fn no_gap_yields_zero() {
let mut t = Kicking::new();
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
// White marubozu but it overlaps bar1 (no gap up).
assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
}
#[test]
fn first_bar_returns_zero() {
let mut t = Kicking::new();
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64 * 5.0;
if i % 2 == 0 {
c(base + 2.0, base + 2.0, base, base, i)
} else {
c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
}
})
.collect();
let mut a = Kicking::new();
let mut b = Kicking::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = Kicking::new();
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
t.update(c(14.0, 16.0, 14.0, 16.0, 1));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,197 @@
//! Kicking-by-Length candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Kicking-by-Length — the [`Kicking`](crate::Kicking) pattern with the signal
/// taken from the *longer* of the two marubozu rather than from the gap direction.
/// When the two shadowless candles differ in size, the bigger one is treated as
/// the dominant force.
///
/// ```text
/// marubozu = |close open| >= 0.95 * (high low)
/// setup: two opposite-coloured marubozu separated by a gap
/// black then white gapping UP, or white then black gapping DOWN
/// signal = colour of the LONGER marubozu (white -> +1.0, black -> 1.0)
/// ```
///
/// Output is `+1.0` or `1.0` when the kicking setup is present and `0.0`
/// otherwise. Note this can disagree with [`Kicking`](crate::Kicking): a black
/// marubozu kicked up by a *shorter* white marubozu reports `1.0` here. The first
/// bar always returns `0.0` because the two-bar window is not yet filled. The
/// marubozu threshold 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, `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, KickingByLength};
///
/// let mut indicator = KickingByLength::new();
/// indicator.update(Candle::new(12.0, 12.0, 10.0, 10.0, 1.0, 0).unwrap());
/// // White marubozu gaps up and is the longer body -> +1.
/// let out = indicator
/// .update(Candle::new(14.0, 20.0, 14.0, 20.0, 1.0, 1).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct KickingByLength {
prev: Option<Candle>,
has_emitted: bool,
}
impl KickingByLength {
/// Construct a new Kicking-by-Length detector.
pub const fn new() -> Self {
Self {
prev: None,
has_emitted: false,
}
}
}
fn is_marubozu(candle: &Candle) -> bool {
let range = candle.high - candle.low;
range > 0.0 && (candle.close - candle.open).abs() >= 0.95 * range
}
impl Indicator for KickingByLength {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let prev = self.prev;
self.prev = Some(candle);
let Some(bar1) = prev else {
return Some(0.0);
};
if !is_marubozu(&bar1) || !is_marubozu(&candle) {
return Some(0.0);
}
let body1 = bar1.close - bar1.open;
let body2 = candle.close - candle.open;
let bullish_setup = body1 < 0.0 && body2 > 0.0 && candle.low > bar1.high;
let bearish_setup = body1 > 0.0 && body2 < 0.0 && candle.high < bar1.low;
if !(bullish_setup || bearish_setup) {
return Some(0.0);
}
// The longer marubozu's colour is the signal.
let longer_is_white = if body1.abs() >= body2.abs() {
body1 > 0.0
} else {
body2 > 0.0
};
Some(if longer_is_white { 1.0 } else { -1.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 {
"KickingByLength"
}
}
#[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 = KickingByLength::new();
assert_eq!(t.name(), "KickingByLength");
assert_eq!(t.warmup_period(), 2);
assert!(!t.is_ready());
}
#[test]
fn longer_white_is_plus_one() {
let mut t = KickingByLength::new();
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
// White marubozu (length 6) longer than the black one (length 2).
assert_eq!(t.update(c(14.0, 20.0, 14.0, 20.0, 1)), Some(1.0));
}
#[test]
fn longer_black_is_minus_one() {
let mut t = KickingByLength::new();
// Black marubozu (length 6), then a shorter white marubozu (length 2)
// gapping up -> the longer black body wins, so -1.
assert_eq!(t.update(c(16.0, 16.0, 10.0, 10.0, 0)), Some(0.0));
assert_eq!(t.update(c(18.0, 20.0, 18.0, 20.0, 1)), Some(-1.0));
}
#[test]
fn not_marubozu_yields_zero() {
let mut t = KickingByLength::new();
t.update(c(12.0, 14.0, 8.0, 10.0, 0));
assert_eq!(t.update(c(14.0, 20.0, 14.0, 20.0, 1)), Some(0.0));
}
#[test]
fn no_gap_yields_zero() {
let mut t = KickingByLength::new();
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
}
#[test]
fn first_bar_returns_zero() {
let mut t = KickingByLength::new();
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64 * 5.0;
if i % 2 == 0 {
c(base + 2.0, base + 2.0, base, base, i)
} else {
c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
}
})
.collect();
let mut a = KickingByLength::new();
let mut b = KickingByLength::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = KickingByLength::new();
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
t.update(c(14.0, 20.0, 14.0, 20.0, 1));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,207 @@
//! Ladder Bottom candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Ladder Bottom — a 5-bar bullish reversal. Three long black candles step the
/// market down like rungs of a ladder, a fourth black candle finally shows an
/// upper shadow (the first sign of buying), and a white candle then gaps up into
/// its body to confirm the turn.
///
/// ```text
/// bar1, bar2, bar3 black, with consecutively lower opens AND closes
/// bar4 black with an upper shadow (high4 > open4)
/// bar5 white, opens above bar4's body (open5 > open4) and closes up
/// ```
///
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Ladder Bottom
/// is a single-direction (bullish-only) reversal, so it never emits `1.0`. The
/// first four bars always return `0.0` because the five-bar window is not yet
/// filled. Pattern-shape check only — no trend filter is applied; combine with a
/// trend indicator for actionable signals.
///
/// # 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, LadderBottom};
///
/// let mut indicator = LadderBottom::new();
/// indicator.update(Candle::new(20.0, 20.1, 17.9, 18.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(18.0, 18.1, 15.9, 16.0, 1.0, 1).unwrap());
/// indicator.update(Candle::new(16.0, 16.1, 13.9, 14.0, 1.0, 2).unwrap());
/// indicator.update(Candle::new(14.0, 15.0, 12.4, 12.5, 1.0, 3).unwrap());
/// let out = indicator
/// .update(Candle::new(15.0, 17.1, 14.9, 17.0, 1.0, 4).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct LadderBottom {
c1: Option<Candle>,
c2: Option<Candle>,
c3: Option<Candle>,
c4: Option<Candle>,
has_emitted: bool,
}
impl LadderBottom {
/// Construct a new Ladder Bottom detector.
pub const fn new() -> Self {
Self {
c1: None,
c2: None,
c3: None,
c4: None,
has_emitted: false,
}
}
}
impl Indicator for LadderBottom {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
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);
};
if bar1.close < bar1.open
&& bar2.close < bar2.open
&& bar3.close < bar3.open
&& bar2.open < bar1.open
&& bar2.close < bar1.close
&& bar3.open < bar2.open
&& bar3.close < bar2.close
&& bar4.close < bar4.open
&& bar4.high > bar4.open
&& candle.close > candle.open
&& candle.open > bar4.open
{
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 {
"LadderBottom"
}
}
#[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 = LadderBottom::new();
assert_eq!(t.name(), "LadderBottom");
assert_eq!(t.warmup_period(), 5);
assert!(!t.is_ready());
}
#[test]
fn ladder_bottom_is_plus_one() {
let mut t = LadderBottom::new();
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(1.0));
}
#[test]
fn fourth_bar_without_upper_shadow_yields_zero() {
let mut t = LadderBottom::new();
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
t.update(c(18.0, 18.1, 15.9, 16.0, 1));
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
// bar4 opens at its high -> no upper shadow.
t.update(c(14.0, 14.0, 12.4, 12.5, 3));
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
}
#[test]
fn not_three_descending_blacks_yields_zero() {
let mut t = LadderBottom::new();
// bar2 is not lower than bar1.
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
t.update(c(21.0, 21.1, 18.9, 19.0, 1));
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
t.update(c(14.0, 15.0, 12.4, 12.5, 3));
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
}
#[test]
fn first_four_bars_return_zero() {
let mut t = LadderBottom::new();
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 200.0 - i as f64;
c(base, base + 0.1, base - 2.1, base - 2.0, i)
})
.collect();
let mut a = LadderBottom::new();
let mut b = LadderBottom::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = LadderBottom::new();
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
t.update(c(18.0, 18.1, 15.9, 16.0, 1));
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
t.update(c(14.0, 15.0, 12.4, 12.5, 3));
t.update(c(15.0, 17.1, 14.9, 17.0, 4));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,274 @@
//! Mat Hold candlestick pattern.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Mat Hold — a 5-bar bullish continuation. A long white candle is followed by a
/// brief three-bar pullback that gaps up and then drifts on small bodies *without*
/// surrendering much ground, after which a white candle breaks to a new high and
/// the uptrend resumes.
///
/// ```text
/// long body = |close open| >= 0.5 * (high low)
/// bar1 white & long
/// bar2 small body gapping up above bar1 (min(o2,c2) > close1)
/// bar2, bar3, bar4 each small (|body| <= 0.5 · body1)
/// the pullback holds (min low of bars 2..4 > close1 penetration·body1)
/// bar5 white, closing at a new high (close5 > max high of bars 1..4)
/// ```
///
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Mat Hold 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. `penetration` is how far the pullback may retrace into the first body;
/// it defaults to `0.5` (TA-Lib's `CDLMATHOLD` default) and must lie in `[0, 1)`.
/// 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, MatHold};
///
/// let mut indicator = MatHold::new();
/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(16.0, 16.1, 15.4, 15.5, 1.0, 1).unwrap());
/// indicator.update(Candle::new(15.5, 15.6, 14.9, 15.0, 1.0, 2).unwrap());
/// indicator.update(Candle::new(15.0, 15.1, 14.4, 14.5, 1.0, 3).unwrap());
/// let out = indicator
/// .update(Candle::new(14.5, 17.1, 14.4, 17.0, 1.0, 4).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone)]
pub struct MatHold {
penetration: f64,
c1: Option<Candle>,
c2: Option<Candle>,
c3: Option<Candle>,
c4: Option<Candle>,
has_emitted: bool,
}
impl Default for MatHold {
fn default() -> Self {
Self::new()
}
}
impl MatHold {
/// Construct a Mat Hold detector with the default 0.5 penetration.
pub const fn new() -> Self {
Self {
penetration: 0.5,
c1: None,
c2: None,
c3: None,
c4: None,
has_emitted: false,
}
}
/// Construct a Mat Hold detector with a custom penetration fraction.
///
/// `penetration` must lie in `[0, 1)`.
pub fn with_penetration(penetration: f64) -> Result<Self> {
if !(0.0..1.0).contains(&penetration) {
return Err(Error::InvalidPeriod {
message: "mat hold penetration must lie in [0, 1)",
});
}
Ok(Self {
penetration,
c1: None,
c2: None,
c3: None,
c4: None,
has_emitted: false,
})
}
/// Configured penetration fraction.
pub fn penetration(&self) -> f64 {
self.penetration
}
}
impl Indicator for MatHold {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
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
}
let small = 0.5 * body1;
if (bar2.close - bar2.open).abs() > small
|| (bar3.close - bar3.open).abs() > small
|| (bar4.close - bar4.open).abs() > small
{
return Some(0.0); // the three pullback bars must be small
}
// bar2 gaps up above bar1's body.
if bar2.open.min(bar2.close) <= bar1.close {
return Some(0.0);
}
// The pullback must hold above the penetration line.
let hold_line = bar1.close - self.penetration * body1;
if bar2.low.min(bar3.low).min(bar4.low) <= hold_line {
return Some(0.0);
}
// bar5 breaks to a new high on a white body.
let max_high = bar1.high.max(bar2.high).max(bar3.high).max(bar4.high);
if candle.close > candle.open && candle.close > max_high {
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 {
"MatHold"
}
}
#[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!(MatHold::with_penetration(-0.01).is_err());
assert!(MatHold::with_penetration(1.0).is_err());
}
#[test]
fn accepts_valid_penetration() {
let t = MatHold::with_penetration(0.3).unwrap();
assert!((t.penetration() - 0.3).abs() < 1e-12);
}
#[test]
fn accessors_and_metadata() {
let t = MatHold::default();
assert_eq!(t.name(), "MatHold");
assert_eq!(t.warmup_period(), 5);
assert!(!t.is_ready());
assert!((t.penetration() - 0.5).abs() < 1e-12);
}
#[test]
fn mat_hold_is_plus_one() {
let mut t = MatHold::new();
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(1.0));
}
#[test]
fn pullback_breaks_hold_yields_zero() {
let mut t = MatHold::new();
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
// bar4 dips below the hold line (close1 - 0.5*body1 = 12.5).
t.update(c(13.0, 13.1, 12.0, 12.4, 3));
assert_eq!(t.update(c(14.5, 17.1, 12.0, 17.0, 4)), Some(0.0));
}
#[test]
fn no_new_high_yields_zero() {
let mut t = MatHold::new();
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
// bar5 white but closes below the prior max high (16.1).
assert_eq!(t.update(c(14.5, 16.0, 14.4, 15.9, 4)), Some(0.0));
}
#[test]
fn first_four_bars_return_zero() {
let mut t = MatHold::new();
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (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 = MatHold::new();
let mut b = MatHold::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = MatHold::new();
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
t.update(c(14.5, 17.1, 14.4, 17.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));
}
}
+16 -1
View File
@@ -121,10 +121,13 @@ mod jma;
mod kama;
mod kelly_criterion;
mod keltner;
mod kicking;
mod kicking_by_length;
mod kst;
mod kurtosis;
mod kvo;
mod kyles_lambda;
mod ladder_bottom;
mod laguerre_rsi;
mod lead_lag_cross_correlation;
mod linreg;
@@ -140,6 +143,7 @@ mod mama;
mod market_facilitation_index;
mod marubozu;
mod mass_index;
mod mat_hold;
mod max_drawdown;
mod mcginley_dynamic;
mod median_absolute_deviation;
@@ -189,6 +193,7 @@ mod rsi;
mod rvi;
mod rvi_volatility;
mod rwi;
mod separating_lines;
mod sharpe_ratio;
mod shooting_star;
mod signed_volume;
@@ -390,10 +395,13 @@ pub use jma::Jma;
pub use kama::Kama;
pub use kelly_criterion::KellyCriterion;
pub use keltner::{Keltner, KeltnerOutput};
pub use kicking::Kicking;
pub use kicking_by_length::KickingByLength;
pub use kst::{Kst, KstOutput};
pub use kurtosis::Kurtosis;
pub use kvo::Kvo;
pub use kyles_lambda::KylesLambda;
pub use ladder_bottom::LadderBottom;
pub use laguerre_rsi::LaguerreRsi;
pub use lead_lag_cross_correlation::{LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput};
pub use linreg::LinearRegression;
@@ -409,6 +417,7 @@ pub use mama::{Mama, MamaOutput};
pub use market_facilitation_index::MarketFacilitationIndex;
pub use marubozu::Marubozu;
pub use mass_index::MassIndex;
pub use mat_hold::MatHold;
pub use max_drawdown::MaxDrawdown;
pub use mcginley_dynamic::McGinleyDynamic;
pub use median_absolute_deviation::MedianAbsoluteDeviation;
@@ -458,6 +467,7 @@ pub use rsi::Rsi;
pub use rvi::Rvi;
pub use rvi_volatility::RviVolatility;
pub use rwi::{Rwi, RwiOutput};
pub use separating_lines::SeparatingLines;
pub use sharpe_ratio::SharpeRatio;
pub use shooting_star::ShootingStar;
pub use signed_volume::SignedVolume;
@@ -830,6 +840,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"OnNeck",
"InNeck",
"Thrusting",
"SeparatingLines",
"Kicking",
"KickingByLength",
"LadderBottom",
"MatHold",
],
),
(
@@ -921,6 +936,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, 264, "FAMILIES total drifted from indicator count");
assert_eq!(total, 269, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,202 @@
//! Separating Lines candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Separating Lines — a 2-bar continuation. After a counter-trend candle, the next
/// candle of the *opposite* colour opens right back at the prior open and runs as
/// an opening marubozu in the trend direction, so the trend "separates" from the
/// pullback and resumes.
///
/// ```text
/// long body = |close open| >= 0.5 * (high low)
/// bar1, bar2 opposite colours
/// bar2 opens at bar1's open (|open2 open1| <= 0.05 · range1)
/// bar2 is a long opening marubozu in its direction
/// white bar2: open2 == low2 (no lower shadow) -> +1.0
/// black bar2: open2 == high2 (no upper shadow) -> 1.0
/// ```
///
/// Output is `+1.0` (bullish continuation) or `1.0` (bearish continuation) when
/// the pattern completes and `0.0` otherwise. The first bar always returns `0.0`
/// because the two-bar window is not yet filled. Open-equality and marubozu
/// 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 directions
/// occupy a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, SeparatingLines};
///
/// let mut indicator = SeparatingLines::new();
/// indicator.update(Candle::new(12.0, 12.1, 9.9, 10.0, 1.0, 0).unwrap());
/// let out = indicator
/// .update(Candle::new(12.0, 14.1, 12.0, 14.0, 1.0, 1).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct SeparatingLines {
prev: Option<Candle>,
has_emitted: bool,
}
impl SeparatingLines {
/// Construct a new Separating Lines detector.
pub const fn new() -> Self {
Self {
prev: None,
has_emitted: false,
}
}
}
impl Indicator for SeparatingLines {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let prev = self.prev;
self.prev = Some(candle);
let Some(bar1) = prev else {
return Some(0.0);
};
let range1 = bar1.high - bar1.low;
let range2 = candle.high - candle.low;
if range1 <= 0.0 || range2 <= 0.0 {
return Some(0.0);
}
// Opens must coincide.
if (candle.open - bar1.open).abs() > 0.05 * range1 {
return Some(0.0);
}
let body2 = candle.close - candle.open;
if body2.abs() < 0.5 * range2 {
return Some(0.0); // bar2 must be a long body
}
let tol = 0.05 * range2;
// Bullish: bar1 black, bar2 a long white opening marubozu (no lower wick).
if bar1.close < bar1.open && body2 > 0.0 && candle.open - candle.low <= tol {
return Some(1.0);
}
// Bearish: bar1 white, bar2 a long black opening marubozu (no upper wick).
if bar1.close > bar1.open && body2 < 0.0 && candle.high - candle.open <= 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 {
"SeparatingLines"
}
}
#[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 = SeparatingLines::new();
assert_eq!(t.name(), "SeparatingLines");
assert_eq!(t.warmup_period(), 2);
assert!(!t.is_ready());
}
#[test]
fn bullish_separating_lines_is_plus_one() {
let mut t = SeparatingLines::new();
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
assert_eq!(t.update(c(12.0, 14.1, 12.0, 14.0, 1)), Some(1.0));
}
#[test]
fn bearish_separating_lines_is_minus_one() {
let mut t = SeparatingLines::new();
assert_eq!(t.update(c(10.0, 12.1, 9.9, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(10.0, 10.0, 7.9, 8.0, 1)), Some(-1.0));
}
#[test]
fn same_color_yields_zero() {
let mut t = SeparatingLines::new();
// Both white -> not separating (need opposite colours).
t.update(c(12.0, 14.1, 11.9, 14.0, 0));
assert_eq!(t.update(c(12.0, 14.1, 12.0, 14.0, 1)), Some(0.0));
}
#[test]
fn different_open_yields_zero() {
let mut t = SeparatingLines::new();
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
// bar2 opens far from bar1's open.
assert_eq!(t.update(c(13.0, 15.1, 13.0, 15.0, 1)), Some(0.0));
}
#[test]
fn opening_shadow_yields_zero() {
let mut t = SeparatingLines::new();
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
// White bar2 but it has a lower shadow -> not an opening marubozu.
assert_eq!(t.update(c(12.0, 14.1, 11.0, 14.0, 1)), Some(0.0));
}
#[test]
fn first_bar_returns_zero() {
let mut t = SeparatingLines::new();
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
c(base, base + 2.0, base, base + 1.9, i)
})
.collect();
let mut a = SeparatingLines::new();
let mut b = SeparatingLines::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = SeparatingLines::new();
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
t.update(c(12.0, 14.1, 12.0, 14.0, 1));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
}
}
+30 -30
View File
@@ -75,36 +75,36 @@ pub use indicators::{
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HurstChannel,
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
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, 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, 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, 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,
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,
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
+1 -1
View File
@@ -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 **269 indicators** across
- A per-indicator deep dive for every one of the **274 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 &
+6 -1
View File
@@ -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, Kvo, LongLeggedDoji, MarketFacilitationIndex, Marubozu, MassIndex, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, 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, 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, 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
};
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
@@ -278,6 +278,11 @@ fuzz_target!(|data: Vec<f64>| {
}
// --- Candlestick Patterns (family 14) ---
drive(MatHold::new, &candles);
drive(LadderBottom::new, &candles);
drive(KickingByLength::new, &candles);
drive(Kicking::new, &candles);
drive(SeparatingLines::new, &candles);
drive(Thrusting::new, &candles);
drive(InNeck::new, &candles);
drive(OnNeck::new, &candles);