feat: TA-Lib candlestick patterns — hikkake-mod/pigeon/neck-lines (part 5 of 9) (#137)

* feat: add hikkake-modified, homing-pigeon and neck-line candlestick patterns

Five patterns, all `Input = Candle`, `Output = f64`:

- Modified Hikkake (CDLHIKKAKEMOD) — a close-confirmed Hikkake: an inside bar
  then a breakout that closes back inside the inside-bar range; bullish +1,
  bearish -1.
- Homing Pigeon (CDLHOMINGPIGEON) — two black candles, the second a small body
  inside the first, a bullish reversal; +1.
- On-Neck (CDLONNECK) — long black bar then a white bar closing at its low (the
  neckline), a bearish continuation; -1.
- In-Neck (CDLINNECK) — long black bar then a white bar closing just into its
  body, a bearish continuation; -1.
- Thrusting (CDLTHRUSTING) — long black bar then a white bar closing well into
  but below the midpoint of its body, a bearish continuation; -1.

Counter 264 -> 269 (mod-count == lib counted block; FAMILIES total 259 -> 264).

* chore: sync indicator count to 269

---------

Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
This commit is contained in:
kingchenc
2026-06-02 17:03:49 +02:00
committed by GitHub
parent d43bc9ddf3
commit e4ca9c3f8f
19 changed files with 1106 additions and 28 deletions
+5
View File
@@ -47,6 +47,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Gap Side-by-Side White** — two similar white candles opening side by side after a gap, a continuation; gap up +1, gap down -1 (`CDLGAPSIDESIDEWHITE`).
- **High-Wave** — a small body with very long shadows on both sides, an extreme indecision signal; +1 detection (`CDLHIGHWAVE`).
- **Hikkake** — an inside bar followed by a failed breakout, a trap; bullish +1, bearish -1 (`CDLHIKKAKE`).
- **Modified Hikkake** — a close-confirmed Hikkake: an inside bar then a failed breakout closing back inside; bullish +1, bearish -1 (`CDLHIKKAKEMOD`).
- **Homing Pigeon** — two black candles, the second a small body inside the first, a bullish reversal; +1 (`CDLHOMINGPIGEON`).
- **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`).
- **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=264" 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=269" 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 264 indicators; start at the
every one of the 269 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
264 streaming-first indicators across eighteen families. Every one passes the
269 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 |
| 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 |
| 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 264 indicators
│ ├── wickra-core/ core engine + all 269 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
@@ -255,6 +255,11 @@ const candleScalar = {
GapSideBySideWhite: { make: () => new wickra.GapSideBySideWhite(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
HighWave: { make: () => new wickra.HighWave(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
Hikkake: { make: () => new wickra.Hikkake(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
HikkakeModified: { make: () => new wickra.HikkakeModified(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
HomingPigeon: { make: () => new wickra.HomingPigeon(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
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) },
};
for (const [name, d] of Object.entries(candleScalar)) {
+45
View File
@@ -2390,6 +2390,51 @@ export declare class Hikkake {
isReady(): boolean
warmupPeriod(): number
}
export type HikkakeModifiedNode = HikkakeModified
export declare class HikkakeModified {
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 HomingPigeonNode = HomingPigeon
export declare class HomingPigeon {
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 OnNeckNode = OnNeck
export declare class OnNeck {
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 InNeckNode = InNeck
export declare class InNeck {
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 ThrustingNode = Thrusting
export declare class Thrusting {
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, 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, 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
@@ -535,6 +535,11 @@ module.exports.MorningDojiStar = MorningDojiStar
module.exports.GapSideBySideWhite = GapSideBySideWhite
module.exports.HighWave = HighWave
module.exports.Hikkake = Hikkake
module.exports.HikkakeModified = HikkakeModified
module.exports.HomingPigeon = HomingPigeon
module.exports.OnNeck = OnNeck
module.exports.InNeck = InNeck
module.exports.Thrusting = Thrusting
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
module.exports.Microprice = Microprice
+5
View File
@@ -8789,6 +8789,11 @@ node_candle_pattern!(
);
node_candle_pattern!(HighWaveNode, wc::HighWave, "HighWave");
node_candle_pattern!(HikkakeNode, wc::Hikkake, "Hikkake");
node_candle_pattern!(HikkakeModifiedNode, wc::HikkakeModified, "HikkakeModified");
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");
// ============================== Microstructure: Order Book ==============================
//
+10
View File
@@ -260,6 +260,11 @@ from ._wickra import (
GapSideBySideWhite,
HighWave,
Hikkake,
HikkakeModified,
HomingPigeon,
OnNeck,
InNeck,
Thrusting,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -547,6 +552,11 @@ __all__ = [
"GapSideBySideWhite",
"HighWave",
"Hikkake",
"HikkakeModified",
"HomingPigeon",
"OnNeck",
"InNeck",
"Thrusting",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
+10
View File
@@ -11652,6 +11652,11 @@ candle_pattern_no_param!(
);
candle_pattern_no_param!(PyHighWave, wc::HighWave, "HighWave");
candle_pattern_no_param!(PyHikkake, wc::Hikkake, "Hikkake");
candle_pattern_no_param!(PyHikkakeModified, wc::HikkakeModified, "HikkakeModified");
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");
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14168,6 +14173,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyGapSideBySideWhite>()?;
m.add_class::<PyHighWave>()?;
m.add_class::<PyHikkake>()?;
m.add_class::<PyHikkakeModified>()?;
m.add_class::<PyHomingPigeon>()?;
m.add_class::<PyOnNeck>()?;
m.add_class::<PyInNeck>()?;
m.add_class::<PyThrusting>()?;
// Microstructure: order book.
m.add_class::<PyOrderBookImbalanceTop1>()?;
m.add_class::<PyOrderBookImbalanceTopN>()?;
@@ -603,6 +603,26 @@ CANDLE_SCALAR = {
lambda: ta.Hikkake(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"HikkakeModified": (
lambda: ta.HikkakeModified(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"HomingPigeon": (
lambda: ta.HomingPigeon(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"OnNeck": (
lambda: ta.OnNeck(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"InNeck": (
lambda: ta.InNeck(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"Thrusting": (
lambda: ta.Thrusting(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
}
@@ -1784,6 +1804,37 @@ def test_hikkake_reference():
assert t.update((11.0, 13.0, 8.0, 12.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((9.0, 12.0, 6.0, 7.0, 1.0, 2)) == pytest.approx(1.0)
def test_hikkake_modified_reference():
t = ta.HikkakeModified()
assert t.update((10.0, 15.0, 5.0, 12.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((11.0, 13.0, 8.0, 12.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((9.0, 12.0, 6.0, 9.0, 1.0, 2)) == pytest.approx(1.0)
def test_homing_pigeon_reference():
t = ta.HomingPigeon()
assert t.update((15.0, 15.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((14.0, 14.1, 10.9, 11.0, 1.0, 1)) == pytest.approx(1.0)
def test_on_neck_reference():
t = ta.OnNeck()
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((7.0, 9.1, 6.9, 9.0, 1.0, 1)) == pytest.approx(-1.0)
def test_in_neck_reference():
t = ta.InNeck()
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((7.0, 10.3, 6.9, 10.2, 1.0, 1)) == pytest.approx(-1.0)
def test_thrusting_reference():
t = ta.Thrusting()
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)
# --- Lifecycle ------------------------------------------------------------
+5
View File
@@ -6366,6 +6366,11 @@ wasm_candle_pattern!(
);
wasm_candle_pattern!(WasmHighWave, wc::HighWave, HighWave);
wasm_candle_pattern!(WasmHikkake, wc::Hikkake, Hikkake);
wasm_candle_pattern!(WasmHikkakeModified, wc::HikkakeModified, HikkakeModified);
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);
// ============================== Microstructure: Order Book ==============================
//
@@ -0,0 +1,196 @@
//! Modified Hikkake candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Modified Hikkake — a close-confirmed variant of the [`Hikkake`](crate::Hikkake)
/// trap. An inside bar is followed by a bar that breaks out *and is immediately
/// rejected*: it pierces the inside bar's range intrabar but closes back inside,
/// a stronger signal than the plain breakout setup.
///
/// ```text
/// inside bar : bar2.high < bar1.high && bar2.low > bar1.low
/// bullish (+1.0): bar3 makes a lower high AND lower low than bar2,
/// yet closes back above the inside-bar low (close3 > bar2.low)
/// bearish (1.0): bar3 makes a higher high AND higher low than bar2,
/// yet closes back below the inside-bar high (close3 < bar2.high)
/// ```
///
/// Output is `+1.0` (bullish), `1.0` (bearish), or `0.0` otherwise. The extra
/// close-recovery condition is what distinguishes it from the plain Hikkake, which
/// fires on the high/low break alone. The first two bars always return `0.0`
/// because the three-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, `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, HikkakeModified, Indicator};
///
/// let mut indicator = HikkakeModified::new();
/// indicator.update(Candle::new(10.0, 15.0, 5.0, 12.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(11.0, 13.0, 8.0, 12.0, 1.0, 1).unwrap());
/// let out = indicator
/// .update(Candle::new(9.0, 12.0, 6.0, 9.0, 1.0, 2).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct HikkakeModified {
prev: Option<Candle>,
prev_prev: Option<Candle>,
has_emitted: bool,
}
impl HikkakeModified {
/// Construct a new Modified Hikkake detector.
pub const fn new() -> Self {
Self {
prev: None,
prev_prev: None,
has_emitted: false,
}
}
}
impl Indicator for HikkakeModified {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let bar1 = self.prev_prev;
let bar2 = self.prev;
self.prev_prev = self.prev;
self.prev = Some(candle);
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
return Some(0.0);
};
if !(bar2.high < bar1.high && bar2.low > bar1.low) {
return Some(0.0);
}
// Bullish: false downside break that closes back above the inside-bar low.
if candle.high < bar2.high && candle.low < bar2.low && candle.close > bar2.low {
return Some(1.0);
}
// Bearish: false upside break that closes back below the inside-bar high.
if candle.high > bar2.high && candle.low > bar2.low && candle.close < bar2.high {
return Some(-1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.prev = None;
self.prev_prev = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"HikkakeModified"
}
}
#[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 = HikkakeModified::new();
assert_eq!(t.name(), "HikkakeModified");
assert_eq!(t.warmup_period(), 3);
assert!(!t.is_ready());
}
#[test]
fn bullish_modified_hikkake_is_plus_one() {
let mut t = HikkakeModified::new();
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
assert_eq!(t.update(c(9.0, 12.0, 6.0, 9.0, 2)), Some(1.0));
}
#[test]
fn bearish_modified_hikkake_is_minus_one() {
let mut t = HikkakeModified::new();
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
assert_eq!(t.update(c(13.0, 14.0, 9.0, 10.0, 2)), Some(-1.0));
}
#[test]
fn break_without_close_recovery_yields_zero() {
let mut t = HikkakeModified::new();
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
// Lower high and lower low, but closes below the inside-bar low -> plain
// Hikkake break, not the close-confirmed modified version.
assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(0.0));
}
#[test]
fn not_inside_bar_yields_zero() {
let mut t = HikkakeModified::new();
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
t.update(c(11.0, 16.0, 8.0, 12.0, 1));
assert_eq!(t.update(c(9.0, 12.0, 6.0, 9.0, 2)), Some(0.0));
}
#[test]
fn first_two_bars_return_zero() {
let mut t = HikkakeModified::new();
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
match i % 3 {
0 => c(base, base + 6.0, base - 6.0, base, i),
1 => c(base, base + 2.0, base - 2.0, base, i),
_ => c(base, base + 1.0, base - 5.0, base, i),
}
})
.collect();
let mut a = HikkakeModified::new();
let mut b = HikkakeModified::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = HikkakeModified::new();
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
t.update(c(9.0, 12.0, 6.0, 9.0, 2));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,169 @@
//! Homing Pigeon candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Homing Pigeon — a 2-bar bullish reversal. Two black candles in a decline, the
/// second a small body sitting entirely inside the first body (a same-colour
/// harami). The shrinking range signals selling pressure is fading.
///
/// ```text
/// bar1 black (close < open)
/// bar2 black & its body sits inside bar1's body
/// (open2 <= open1 && close2 >= close1)
/// bar2 body is smaller than bar1's
/// ```
///
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Homing Pigeon
/// is a single-direction (bullish-only) reversal, so it never emits `1.0`. The
/// first bar always returns `0.0` because the two-bar window is not yet filled.
/// 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, HomingPigeon, Indicator};
///
/// let mut indicator = HomingPigeon::new();
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
/// let out = indicator
/// .update(Candle::new(14.0, 14.1, 10.9, 11.0, 1.0, 1).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct HomingPigeon {
prev: Option<Candle>,
has_emitted: bool,
}
impl HomingPigeon {
/// Construct a new Homing Pigeon detector.
pub const fn new() -> Self {
Self {
prev: None,
has_emitted: false,
}
}
}
impl Indicator for HomingPigeon {
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);
};
// Both bars black, bar2's body inside bar1's body and smaller.
if bar1.close < bar1.open
&& candle.close < candle.open
&& candle.open <= bar1.open
&& candle.close >= bar1.close
&& (candle.open - candle.close) < (bar1.open - bar1.close)
{
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 {
"HomingPigeon"
}
}
#[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 = HomingPigeon::new();
assert_eq!(t.name(), "HomingPigeon");
assert_eq!(t.warmup_period(), 2);
assert!(!t.is_ready());
}
#[test]
fn homing_pigeon_is_plus_one() {
let mut t = HomingPigeon::new();
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
assert_eq!(t.update(c(14.0, 14.1, 10.9, 11.0, 1)), Some(1.0));
}
#[test]
fn second_bar_white_yields_zero() {
let mut t = HomingPigeon::new();
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
// bar2 white -> not a homing pigeon.
assert_eq!(t.update(c(11.0, 14.1, 10.9, 14.0, 1)), Some(0.0));
}
#[test]
fn second_body_not_inside_yields_zero() {
let mut t = HomingPigeon::new();
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
// bar2 opens above bar1's open -> body not contained.
assert_eq!(t.update(c(16.0, 16.1, 10.9, 11.0, 1)), Some(0.0));
}
#[test]
fn first_bar_returns_zero() {
let mut t = HomingPigeon::new();
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
c(base + 5.0, base + 5.1, base - 0.1, base, i)
})
.collect();
let mut a = HomingPigeon::new();
let mut b = HomingPigeon::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = HomingPigeon::new();
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
t.update(c(14.0, 14.1, 10.9, 11.0, 1));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,183 @@
//! In-Neck candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// In-Neck — a 2-bar bearish continuation, slightly stronger than On-Neck. A long
/// black candle in a decline is followed by a white candle that opens below the
/// black bar's low and closes just barely *into* the black body, around its close
/// level. The shallow recovery still favours the sellers.
///
/// ```text
/// long body = |close open| >= 0.5 * (high low)
/// bar1 black & long
/// bar2 white, opens below bar1's low (open2 < low1)
/// bar2 closes just into bar1's body (close1 <= close2 <= close1 + 0.1 · body1)
/// ```
///
/// Output is `1.0` when the pattern completes and `0.0` otherwise. In-Neck is a
/// single-direction (bearish-only) continuation, so it never emits `+1.0`. The
/// first bar always returns `0.0` because the two-bar window is not yet filled.
/// Body and neckline thresholds follow the geometric house style rather than
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
/// applied; combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `1.0` bearish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, InNeck, Indicator};
///
/// let mut indicator = InNeck::new();
/// indicator.update(Candle::new(15.0, 15.1, 9.0, 10.0, 1.0, 0).unwrap());
/// let out = indicator
/// .update(Candle::new(7.0, 10.3, 6.9, 10.2, 1.0, 1).unwrap());
/// assert_eq!(out, Some(-1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct InNeck {
prev: Option<Candle>,
has_emitted: bool,
}
impl InNeck {
/// Construct a new In-Neck detector.
pub const fn new() -> Self {
Self {
prev: None,
has_emitted: false,
}
}
}
impl Indicator for InNeck {
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;
if range1 <= 0.0 {
return Some(0.0);
}
let body1 = bar1.open - bar1.close;
if bar1.close < bar1.open
&& body1 >= 0.5 * range1
&& candle.close > candle.open
&& candle.open < bar1.low
&& candle.close >= bar1.close
&& candle.close <= bar1.close + 0.1 * body1
{
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 {
"InNeck"
}
}
#[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 = InNeck::new();
assert_eq!(t.name(), "InNeck");
assert_eq!(t.warmup_period(), 2);
assert!(!t.is_ready());
}
#[test]
fn in_neck_is_minus_one() {
let mut t = InNeck::new();
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
assert_eq!(t.update(c(7.0, 10.3, 6.9, 10.2, 1)), Some(-1.0));
}
#[test]
fn close_at_low_yields_zero() {
let mut t = InNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
// Closes at the prior low, not into the body -> on-neck, not in-neck.
assert_eq!(t.update(c(7.0, 9.1, 6.9, 9.0, 1)), Some(0.0));
}
#[test]
fn close_past_neck_yields_zero() {
let mut t = InNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
// Closes well into the body -> thrusting, not in-neck.
assert_eq!(t.update(c(7.0, 11.6, 6.9, 11.5, 1)), Some(0.0));
}
#[test]
fn second_bar_black_yields_zero() {
let mut t = InNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
assert_eq!(t.update(c(10.4, 10.5, 6.9, 10.1, 1)), Some(0.0));
}
#[test]
fn first_bar_returns_zero() {
let mut t = InNeck::new();
assert_eq!(t.update(c(15.0, 15.1, 9.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;
c(base + 5.0, base + 5.1, base - 1.0, base, i)
})
.collect();
let mut a = InNeck::new();
let mut b = InNeck::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = InNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
t.update(c(7.0, 10.3, 6.9, 10.2, 1));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
}
}
+16 -1
View File
@@ -100,14 +100,17 @@ mod harami;
mod heikin_ashi;
mod high_wave;
mod hikkake;
mod hikkake_modified;
mod hilbert_dominant_cycle;
mod hilo_activator;
mod historical_volatility;
mod hma;
mod homing_pigeon;
mod hurst_channel;
mod hurst_exponent;
mod ichimoku;
mod identical_three_crows;
mod in_neck;
mod inertia;
mod information_ratio;
mod initial_balance;
@@ -156,6 +159,7 @@ mod oi_delta;
mod oi_price_divergence;
mod oi_weighted;
mod omega_ratio;
mod on_neck;
mod opening_range;
mod pain_index;
mod pair_spread_zscore;
@@ -227,6 +231,7 @@ mod three_line_strike;
mod three_outside;
mod three_soldiers_or_crows;
mod three_stars_in_south;
mod thrusting;
mod tii;
mod trade_imbalance;
mod treynor_ratio;
@@ -364,14 +369,17 @@ pub use harami::Harami;
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
pub use high_wave::HighWave;
pub use hikkake::Hikkake;
pub use hikkake_modified::HikkakeModified;
pub use hilbert_dominant_cycle::HilbertDominantCycle;
pub use hilo_activator::HiLoActivator;
pub use historical_volatility::HistoricalVolatility;
pub use hma::Hma;
pub use homing_pigeon::HomingPigeon;
pub use hurst_channel::{HurstChannel, HurstChannelOutput};
pub use hurst_exponent::HurstExponent;
pub use ichimoku::{Ichimoku, IchimokuOutput};
pub use identical_three_crows::IdenticalThreeCrows;
pub use in_neck::InNeck;
pub use inertia::Inertia;
pub use information_ratio::InformationRatio;
pub use initial_balance::{InitialBalance, InitialBalanceOutput};
@@ -420,6 +428,7 @@ pub use oi_delta::OpenInterestDelta;
pub use oi_price_divergence::OIPriceDivergence;
pub use oi_weighted::OIWeighted;
pub use omega_ratio::OmegaRatio;
pub use on_neck::OnNeck;
pub use opening_range::{OpeningRange, OpeningRangeOutput};
pub use pain_index::PainIndex;
pub use pair_spread_zscore::PairSpreadZScore;
@@ -491,6 +500,7 @@ pub use three_line_strike::ThreeLineStrike;
pub use three_outside::ThreeOutside;
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
pub use three_stars_in_south::ThreeStarsInSouth;
pub use thrusting::Thrusting;
pub use tii::Tii;
pub use trade_imbalance::TradeImbalance;
pub use treynor_ratio::TreynorRatio;
@@ -815,6 +825,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"GapSideBySideWhite",
"HighWave",
"Hikkake",
"HikkakeModified",
"HomingPigeon",
"OnNeck",
"InNeck",
"Thrusting",
],
),
(
@@ -906,6 +921,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, 259, "FAMILIES total drifted from indicator count");
assert_eq!(total, 264, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,181 @@
//! On-Neck candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// On-Neck — a 2-bar bearish continuation. In a decline a long black candle is
/// followed by a white candle that opens below the black bar's low yet rallies
/// only as far as the black bar's *low* (the "neckline"). The feeble bounce shows
/// sellers remain in control.
///
/// ```text
/// long body = |close open| >= 0.5 * (high low)
/// bar1 black & long
/// bar2 white, opens below bar1's low (open2 < low1)
/// bar2 closes at bar1's low (the neckline) (|close2 low1| <= 0.05 · range1)
/// ```
///
/// Output is `1.0` when the pattern completes and `0.0` otherwise. On-Neck is a
/// single-direction (bearish-only) continuation, so it never emits `+1.0`. The
/// first bar always returns `0.0` because the two-bar window is not yet filled.
/// Body and neckline thresholds follow the geometric house style rather than
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is
/// applied; combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `1.0` bearish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, OnNeck};
///
/// let mut indicator = OnNeck::new();
/// indicator.update(Candle::new(15.0, 15.1, 9.0, 10.0, 1.0, 0).unwrap());
/// let out = indicator
/// .update(Candle::new(7.0, 9.1, 6.9, 9.0, 1.0, 1).unwrap());
/// assert_eq!(out, Some(-1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct OnNeck {
prev: Option<Candle>,
has_emitted: bool,
}
impl OnNeck {
/// Construct a new On-Neck detector.
pub const fn new() -> Self {
Self {
prev: None,
has_emitted: false,
}
}
}
impl Indicator for OnNeck {
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;
if range1 <= 0.0 {
return Some(0.0);
}
if bar1.close < bar1.open
&& (bar1.open - bar1.close) >= 0.5 * range1
&& candle.close > candle.open
&& candle.open < bar1.low
&& (candle.close - bar1.low).abs() <= 0.05 * range1
{
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 {
"OnNeck"
}
}
#[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 = OnNeck::new();
assert_eq!(t.name(), "OnNeck");
assert_eq!(t.warmup_period(), 2);
assert!(!t.is_ready());
}
#[test]
fn on_neck_is_minus_one() {
let mut t = OnNeck::new();
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
assert_eq!(t.update(c(7.0, 9.1, 6.9, 9.0, 1)), Some(-1.0));
}
#[test]
fn close_into_body_yields_zero() {
let mut t = OnNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
// Closes at the prior close, not the low -> in-neck, not on-neck.
assert_eq!(t.update(c(7.0, 10.2, 6.9, 10.1, 1)), Some(0.0));
}
#[test]
fn second_bar_black_yields_zero() {
let mut t = OnNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
assert_eq!(t.update(c(9.5, 9.6, 6.9, 9.0, 1)), Some(0.0));
}
#[test]
fn opens_above_low_yields_zero() {
let mut t = OnNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
// Opens above bar1's low.
assert_eq!(t.update(c(9.5, 10.1, 9.4, 10.0, 1)), Some(0.0));
}
#[test]
fn first_bar_returns_zero() {
let mut t = OnNeck::new();
assert_eq!(t.update(c(15.0, 15.1, 9.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;
c(base + 5.0, base + 5.1, base - 1.0, base, i)
})
.collect();
let mut a = OnNeck::new();
let mut b = OnNeck::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = OnNeck::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
t.update(c(7.0, 9.1, 6.9, 9.0, 1));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,186 @@
//! Thrusting candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Thrusting — a 2-bar bearish continuation, deeper than In-Neck but short of a
/// piercing reversal. A long black candle in a decline is followed by a white
/// candle that opens below the black bar's low and closes well into the black
/// body — but still below its midpoint, so the bounce is not yet a reversal.
///
/// ```text
/// long body = |close open| >= 0.5 * (high low)
/// bar1 black & long
/// bar2 white, opens below bar1's low (open2 < low1)
/// bar2 closes above the in-neck zone but below the body midpoint
/// (close1 + 0.1·body1 < close2 < midpoint(open1, close1))
/// ```
///
/// Output is `1.0` when the pattern completes and `0.0` otherwise. Thrusting is a
/// single-direction (bearish-only) continuation, so it never emits `+1.0`. A close
/// at or above the midpoint would be a piercing pattern instead. The first bar
/// always returns `0.0` because the two-bar window is not yet filled. Body and
/// neckline thresholds follow the geometric house style rather than TA-Lib's
/// rolling averages. Pattern-shape check only — no trend filter is applied;
/// combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `1.0` bearish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, Thrusting};
///
/// let mut indicator = Thrusting::new();
/// indicator.update(Candle::new(15.0, 15.1, 9.0, 10.0, 1.0, 0).unwrap());
/// let out = indicator
/// .update(Candle::new(7.0, 11.6, 6.9, 11.5, 1.0, 1).unwrap());
/// assert_eq!(out, Some(-1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct Thrusting {
prev: Option<Candle>,
has_emitted: bool,
}
impl Thrusting {
/// Construct a new Thrusting detector.
pub const fn new() -> Self {
Self {
prev: None,
has_emitted: false,
}
}
}
impl Indicator for Thrusting {
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;
if range1 <= 0.0 {
return Some(0.0);
}
let body1 = bar1.open - bar1.close;
let mid1 = f64::midpoint(bar1.open, bar1.close);
if bar1.close < bar1.open
&& body1 >= 0.5 * range1
&& candle.close > candle.open
&& candle.open < bar1.low
&& candle.close > bar1.close + 0.1 * body1
&& candle.close < mid1
{
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 {
"Thrusting"
}
}
#[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 = Thrusting::new();
assert_eq!(t.name(), "Thrusting");
assert_eq!(t.warmup_period(), 2);
assert!(!t.is_ready());
}
#[test]
fn thrusting_is_minus_one() {
let mut t = Thrusting::new();
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
assert_eq!(t.update(c(7.0, 11.6, 6.9, 11.5, 1)), Some(-1.0));
}
#[test]
fn shallow_close_yields_zero() {
let mut t = Thrusting::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
// Closes barely into the body -> in-neck, not thrusting.
assert_eq!(t.update(c(7.0, 10.3, 6.9, 10.2, 1)), Some(0.0));
}
#[test]
fn close_past_midpoint_yields_zero() {
let mut t = Thrusting::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
// Closes above the midpoint -> piercing, not thrusting.
assert_eq!(t.update(c(7.0, 13.1, 6.9, 13.0, 1)), Some(0.0));
}
#[test]
fn second_bar_black_yields_zero() {
let mut t = Thrusting::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
assert_eq!(t.update(c(12.0, 12.1, 6.9, 11.5, 1)), Some(0.0));
}
#[test]
fn first_bar_returns_zero() {
let mut t = Thrusting::new();
assert_eq!(t.update(c(15.0, 15.1, 9.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;
c(base + 5.0, base + 5.1, base - 1.0, base, i)
})
.collect();
let mut a = Thrusting::new();
let mut b = Thrusting::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = Thrusting::new();
t.update(c(15.0, 15.1, 9.0, 10.0, 0));
t.update(c(7.0, 11.6, 6.9, 11.5, 1));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(15.0, 15.1, 9.0, 10.0, 0)), Some(0.0));
}
}
+21 -19
View File
@@ -72,17 +72,18 @@ pub use indicators::{
FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean,
FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji,
Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighWave, Hikkake,
HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel, HurstChannelOutput,
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, Inertia, InformationRatio,
InitialBalance, InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform,
InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis,
Kvo, KylesLambda, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput,
LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression,
LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongShortRatio, MaEnvelope,
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
Microprice, Mom, MorningDojiStar, MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted,
Obv, OmegaRatio, OpenInterestDelta, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
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,
@@ -95,14 +96,15 @@ pub use indicators::{
TakerBuySellRatio, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput,
TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis,
ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Tii,
TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UpsideGapTwoCrows, ValueArea,
ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap, VwapStdDevBands,
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
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 **264 indicators** across
- A per-indicator deep dive for every one of the **269 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, HurstChannel, Ichimoku, IdenticalThreeCrows, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kvo, LongLeggedDoji, MarketFacilitationIndex, Marubozu, MassIndex, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, ShootingStar, Smi, SpinningTop, StarcBands, Stochastic, SuperTrend, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag
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
};
/// 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(Thrusting::new, &candles);
drive(InNeck::new, &candles);
drive(OnNeck::new, &candles);
drive(HomingPigeon::new, &candles);
drive(HikkakeModified::new, &candles);
drive(Hikkake::new, &candles);
drive(HighWave::new, &candles);
drive(GapSideBySideWhite::new, &candles);