diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e67adc..8ace5a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/README.md b/README.md index 218104ee..32b8fd1a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Wickra — streaming-first technical indicators + Wickra — streaming-first technical indicators

[![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/ diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js index 7baf975c..1c6df223 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -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)) { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 3fdda389..d4cb12c0 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -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, high: Array, low: Array, close: Array): Array + 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, high: Array, low: Array, close: Array): Array + 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, high: Array, low: Array, close: Array): Array + 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, high: Array, low: Array, close: Array): Array + 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, 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 95892149..0a357cdc 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, 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 diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 3a91c182..962385ee 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -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 ============================== // diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index 28bd6f54..0bf7be39 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -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", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index e89a996b..0194bd1c 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -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::()?; 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 52ef4a68..56d9f76d 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -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 ------------------------------------------------------------ diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index d2fffe77..13227a94 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -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 ============================== // diff --git a/crates/wickra-core/src/indicators/kicking.rs b/crates/wickra-core/src/indicators/kicking.rs new file mode 100644 index 00000000..d6766470 --- /dev/null +++ b/crates/wickra-core/src/indicators/kicking.rs @@ -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, + 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 { + 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 = (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::>() + ); + } + + #[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)); + } +} diff --git a/crates/wickra-core/src/indicators/kicking_by_length.rs b/crates/wickra-core/src/indicators/kicking_by_length.rs new file mode 100644 index 00000000..f39e715e --- /dev/null +++ b/crates/wickra-core/src/indicators/kicking_by_length.rs @@ -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, + 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 { + 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 = (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::>() + ); + } + + #[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)); + } +} diff --git a/crates/wickra-core/src/indicators/ladder_bottom.rs b/crates/wickra-core/src/indicators/ladder_bottom.rs new file mode 100644 index 00000000..38f0a7e9 --- /dev/null +++ b/crates/wickra-core/src/indicators/ladder_bottom.rs @@ -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, + c2: Option, + c3: Option, + c4: Option, + 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 { + 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 = (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::>() + ); + } + + #[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)); + } +} diff --git a/crates/wickra-core/src/indicators/mat_hold.rs b/crates/wickra-core/src/indicators/mat_hold.rs new file mode 100644 index 00000000..19c99d1a --- /dev/null +++ b/crates/wickra-core/src/indicators/mat_hold.rs @@ -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, + c2: Option, + c3: Option, + c4: Option, + 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 { + 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 { + 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 = (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::>() + ); + } + + #[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)); + } +} diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs index 335d3238..b5acdd77 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -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"); } } diff --git a/crates/wickra-core/src/indicators/separating_lines.rs b/crates/wickra-core/src/indicators/separating_lines.rs new file mode 100644 index 00000000..8fd73a22 --- /dev/null +++ b/crates/wickra-core/src/indicators/separating_lines.rs @@ -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, + 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 { + 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 = (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::>() + ); + } + + #[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)); + } +} diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs index 8359b6fd..dec21b93 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -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 diff --git a/docs/README.md b/docs/README.md index 3527fd45..6986159c 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 **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 & diff --git a/fuzz/fuzz_targets/indicator_update_candle.rs b/fuzz/fuzz_targets/indicator_update_candle.rs index f6c4b22f..b6216ea5 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, 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` by chunking it into @@ -278,6 +278,11 @@ fuzz_target!(|data: Vec| { } // --- 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);