feat: TA-Lib candlestick patterns — tasuki-gap/unique-three-river/marubozu-pair/concealing-baby-swallow (part 9 of 9) (#141)

The final batch of the TA-Lib candlestick roadmap. Adds five patterns, each a streaming `Indicator<Input = Candle, Output = f64>` emitting the family's uniform `±1.0 / 0.0` sign convention, fully wired across the Rust core, Python / Node / WASM bindings, fuzz target and reference tests.

- **Tasuki Gap** (`CDLTASUKIGAP`) — a 3-bar continuation: two same-coloured candles gap in the trend direction, then an opposite candle opens within the second body and closes back into the gap without filling it; upside +1, downside -1.
- **Unique Three River** (`CDLUNIQUE3RIVER`) — a 3-bar bullish reversal: a long black candle, a black candle probing a new low with its body inside the first, then a small white candle held below it; bullish +1.
- **Closing Marubozu** (`CDLCLOSINGMARUBOZU`) — a single long-bodied candle with no shadow on the close end; +1 (white, closes at the high) or -1 (black, closes at the low).
- **Opening Marubozu** — a single long-bodied candle with no shadow on the open end; +1 (white, opens at the low) or -1 (black, opens at the high). No direct TA-Lib equivalent — completes the pair with the closing marubozu.
- **Concealing Baby Swallow** (`CDLCONCEALBABYSWALL`) — a rare 4-bar bullish capitulation: two black marubozu, a black candle gapping down with an upper shadow into the second, then a large black candle engulfing it entirely; bullish +1.

Body and shadow thresholds follow the geometric house style (fixed fractions of the bar range) rather than TA-Lib's rolling averages.

Counter 284 → 289 (mod-count == lib counted block; FAMILIES total 279 → 284).

Stacked on #140 (`feat/cdl-gap-methods`); base retargets to `main` as the stack merges down.
This commit is contained in:
kingchenc
2026-06-02 17:27:39 +02:00
committed by GitHub
parent 4d0bc08efd
commit 124efb4432
19 changed files with 1385 additions and 40 deletions
+5
View File
@@ -67,6 +67,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Stalled Pattern** — a 3-bar bearish reversal warning: two long white candles then a small white candle riding the shoulder, signalling the rally is stalling; bearish -1 (`CDLSTALLEDPATTERN`).
- **Stick Sandwich** — a 3-bar bullish reversal: two black candles closing at the same level sandwich a white candle, marking a support floor; bullish +1 (`CDLSTICKSANDWICH`).
- **Takuri** — a single-bar bullish reversal, a strict Dragonfly Doji with a negligible upper shadow and very long lower shadow; bullish +1 (`CDLTAKURI`).
- **Closing Marubozu** — a single long-bodied candle with no shadow on the close end; bullish +1 (white, closes at the high) or bearish -1 (black, closes at the low) (`CDLCLOSINGMARUBOZU`).
- **Opening Marubozu** — a single long-bodied candle with no shadow on the open end; bullish +1 (white, opens at the low) or bearish -1 (black, opens at the high). No direct TA-Lib equivalent — completes the pair with the closing marubozu.
- **Tasuki Gap** — a 3-bar continuation: two same-coloured candles gap in the trend direction, then an opposite candle opens within the second body and closes back into the gap without filling it; upside +1, downside -1 (`CDLTASUKIGAP`).
- **Unique Three River** — a 3-bar bullish reversal: a long black candle, a black candle probing a new low with its body inside the first, then a small white candle held below it; bullish +1 (`CDLUNIQUE3RIVER`).
- **Concealing Baby Swallow** — a rare 4-bar bullish capitulation: two black marubozu, a black candle gapping down with an upper shadow into the second, then a large black candle engulfing it entirely; bullish +1 (`CDLCONCEALBABYSWALL`).
- **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=284" 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=289" 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 284 indicators; start at the
every one of the 289 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
284 streaming-first indicators across eighteen families. Every one passes the
289 streaming-first indicators across eighteen families. Every one passes the
`batch == streaming` equivalence test, reference-value tests, and reset
semantics tests. Each has a per-indicator deep dive (formula, parameters,
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
@@ -155,7 +155,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag |
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri |
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow |
| 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 284 indicators
│ ├── wickra-core/ core engine + all 289 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
@@ -275,6 +275,11 @@ const candleScalar = {
StalledPattern: { make: () => new wickra.StalledPattern(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
StickSandwich: { make: () => new wickra.StickSandwich(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
Takuri: { make: () => new wickra.Takuri(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ClosingMarubozu: { make: () => new wickra.ClosingMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
OpeningMarubozu: { make: () => new wickra.OpeningMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TasukiGap: { make: () => new wickra.TasukiGap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
UniqueThreeRiver: { make: () => new wickra.UniqueThreeRiver(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ConcealingBabySwallow: { make: () => new wickra.ConcealingBabySwallow(), 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
@@ -2570,6 +2570,51 @@ export declare class Takuri {
isReady(): boolean
warmupPeriod(): number
}
export type ClosingMarubozuNode = ClosingMarubozu
export declare class ClosingMarubozu {
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 OpeningMarubozuNode = OpeningMarubozu
export declare class OpeningMarubozu {
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 TasukiGapNode = TasukiGap
export declare class TasukiGap {
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 UniqueThreeRiverNode = UniqueThreeRiver
export declare class UniqueThreeRiver {
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 ConcealingBabySwallowNode = ConcealingBabySwallow
export declare class ConcealingBabySwallow {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
export declare class OrderBookImbalanceTop1 {
constructor()
+6 -1
View File
@@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, 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
@@ -555,6 +555,11 @@ module.exports.DownsideGapThreeMethods = DownsideGapThreeMethods
module.exports.StalledPattern = StalledPattern
module.exports.StickSandwich = StickSandwich
module.exports.Takuri = Takuri
module.exports.ClosingMarubozu = ClosingMarubozu
module.exports.OpeningMarubozu = OpeningMarubozu
module.exports.TasukiGap = TasukiGap
module.exports.UniqueThreeRiver = UniqueThreeRiver
module.exports.ConcealingBabySwallow = ConcealingBabySwallow
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
module.exports.Microprice = Microprice
+13
View File
@@ -8825,6 +8825,19 @@ node_candle_pattern!(
node_candle_pattern!(StalledPatternNode, wc::StalledPattern, "StalledPattern");
node_candle_pattern!(StickSandwichNode, wc::StickSandwich, "StickSandwich");
node_candle_pattern!(TakuriNode, wc::Takuri, "Takuri");
node_candle_pattern!(ClosingMarubozuNode, wc::ClosingMarubozu, "ClosingMarubozu");
node_candle_pattern!(OpeningMarubozuNode, wc::OpeningMarubozu, "OpeningMarubozu");
node_candle_pattern!(TasukiGapNode, wc::TasukiGap, "TasukiGap");
node_candle_pattern!(
UniqueThreeRiverNode,
wc::UniqueThreeRiver,
"UniqueThreeRiver"
);
node_candle_pattern!(
ConcealingBabySwallowNode,
wc::ConcealingBabySwallow,
"ConcealingBabySwallow"
);
// ============================== Microstructure: Order Book ==============================
//
+10
View File
@@ -280,6 +280,11 @@ from ._wickra import (
StalledPattern,
StickSandwich,
Takuri,
ClosingMarubozu,
OpeningMarubozu,
TasukiGap,
UniqueThreeRiver,
ConcealingBabySwallow,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -587,6 +592,11 @@ __all__ = [
"StalledPattern",
"StickSandwich",
"Takuri",
"ClosingMarubozu",
"OpeningMarubozu",
"TasukiGap",
"UniqueThreeRiver",
"ConcealingBabySwallow",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
+14
View File
@@ -11688,6 +11688,15 @@ candle_pattern_no_param!(
candle_pattern_no_param!(PyStalledPattern, wc::StalledPattern, "StalledPattern");
candle_pattern_no_param!(PyStickSandwich, wc::StickSandwich, "StickSandwich");
candle_pattern_no_param!(PyTakuri, wc::Takuri, "Takuri");
candle_pattern_no_param!(PyClosingMarubozu, wc::ClosingMarubozu, "ClosingMarubozu");
candle_pattern_no_param!(PyOpeningMarubozu, wc::OpeningMarubozu, "OpeningMarubozu");
candle_pattern_no_param!(PyTasukiGap, wc::TasukiGap, "TasukiGap");
candle_pattern_no_param!(PyUniqueThreeRiver, wc::UniqueThreeRiver, "UniqueThreeRiver");
candle_pattern_no_param!(
PyConcealingBabySwallow,
wc::ConcealingBabySwallow,
"ConcealingBabySwallow"
);
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -14224,6 +14233,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyStalledPattern>()?;
m.add_class::<PyStickSandwich>()?;
m.add_class::<PyTakuri>()?;
m.add_class::<PyClosingMarubozu>()?;
m.add_class::<PyOpeningMarubozu>()?;
m.add_class::<PyTasukiGap>()?;
m.add_class::<PyUniqueThreeRiver>()?;
m.add_class::<PyConcealingBabySwallow>()?;
// Microstructure: order book.
m.add_class::<PyOrderBookImbalanceTop1>()?;
m.add_class::<PyOrderBookImbalanceTopN>()?;
@@ -683,6 +683,26 @@ CANDLE_SCALAR = {
lambda: ta.Takuri(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"ClosingMarubozu": (
lambda: ta.ClosingMarubozu(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"OpeningMarubozu": (
lambda: ta.OpeningMarubozu(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"TasukiGap": (
lambda: ta.TasukiGap(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"UniqueThreeRiver": (
lambda: ta.UniqueThreeRiver(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"ConcealingBabySwallow": (
lambda: ta.ConcealingBabySwallow(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
}
@@ -2004,6 +2024,38 @@ def test_takuri_reference():
t = ta.Takuri()
assert t.update((10.0, 10.05, 7.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
def test_closing_marubozu_reference():
t = ta.ClosingMarubozu()
assert t.update((10.5, 15.0, 10.0, 15.0, 1.0, 0)) == pytest.approx(1.0)
def test_opening_marubozu_reference():
t = ta.OpeningMarubozu()
assert t.update((10.0, 15.0, 10.0, 14.5, 1.0, 0)) == pytest.approx(1.0)
def test_tasuki_gap_reference():
t = ta.TasukiGap()
assert t.update((10.0, 11.2, 9.8, 11.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((12.0, 14.0, 11.9, 13.5, 1.0, 1)) == pytest.approx(0.0)
assert t.update((13.0, 13.1, 11.4, 11.5, 1.0, 2)) == pytest.approx(1.0)
def test_unique_three_river_reference():
t = ta.UniqueThreeRiver()
assert t.update((15.0, 15.1, 10.0, 10.5, 1.0, 0)) == pytest.approx(0.0)
assert t.update((14.0, 14.1, 9.0, 11.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((10.2, 10.9, 9.5, 10.4, 1.0, 2)) == pytest.approx(1.0)
def test_concealing_baby_swallow_reference():
t = ta.ConcealingBabySwallow()
assert t.update((20.0, 20.1, 14.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((16.0, 16.1, 11.9, 12.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((11.0, 13.0, 9.9, 10.0, 1.0, 2)) == pytest.approx(0.0)
assert t.update((14.0, 14.1, 8.9, 9.0, 1.0, 3)) == pytest.approx(1.0)
# --- Lifecycle ------------------------------------------------------------
+9
View File
@@ -6402,6 +6402,15 @@ wasm_candle_pattern!(
wasm_candle_pattern!(WasmStalledPattern, wc::StalledPattern, StalledPattern);
wasm_candle_pattern!(WasmStickSandwich, wc::StickSandwich, StickSandwich);
wasm_candle_pattern!(WasmTakuri, wc::Takuri, Takuri);
wasm_candle_pattern!(WasmClosingMarubozu, wc::ClosingMarubozu, ClosingMarubozu);
wasm_candle_pattern!(WasmOpeningMarubozu, wc::OpeningMarubozu, OpeningMarubozu);
wasm_candle_pattern!(WasmTasukiGap, wc::TasukiGap, TasukiGap);
wasm_candle_pattern!(WasmUniqueThreeRiver, wc::UniqueThreeRiver, UniqueThreeRiver);
wasm_candle_pattern!(
WasmConcealingBabySwallow,
wc::ConcealingBabySwallow,
ConcealingBabySwallow
);
// ============================== Microstructure: Order Book ==============================
//
@@ -0,0 +1,177 @@
//! Closing Marubozu candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Closing Marubozu — a single-bar strong-momentum candle with a long body and no
/// shadow on the *close* end. A white closing marubozu closes right at the high
/// (no upper shadow) and may carry an opening shadow below; a black one closes
/// right at the low (no lower shadow) and may carry an opening shadow above. The
/// shaved close end shows the move ran unopposed into the bell.
///
/// ```text
/// range = high low
/// long body: |close open| >= 0.7 * range
/// white: close > open and high close <= 0.05 * range (close at the high)
/// black: close < open and close low <= 0.05 * range (close at the low)
/// ```
///
/// Output is `+1.0` for a white closing marubozu, `1.0` for a black one, and
/// `0.0` otherwise. Body and shadow thresholds follow the geometric house style
/// rather than TA-Lib's rolling averages. The opposite shaved end is
/// [`crate::OpeningMarubozu`]. Pattern-shape check only — no trend filter is
/// applied; combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `+1.0` bullish, `1.0` bearish, `0.0` no pattern — so it drops
/// straight into a machine-learning feature matrix where the bullish and bearish
/// variants occupy a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, ClosingMarubozu, Indicator};
///
/// let mut indicator = ClosingMarubozu::new();
/// // White: closes at the high, small opening shadow below.
/// let candle = Candle::new(10.5, 15.0, 10.0, 15.0, 1.0, 0).unwrap();
/// assert_eq!(indicator.update(candle), Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ClosingMarubozu {
has_emitted: bool,
}
impl ClosingMarubozu {
/// Construct a new Closing Marubozu detector.
pub const fn new() -> Self {
Self { has_emitted: false }
}
}
impl Indicator for ClosingMarubozu {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let range = candle.high - candle.low;
if range <= 0.0 {
return Some(0.0);
}
let body = candle.close - candle.open;
if body.abs() < 0.7 * range {
return Some(0.0);
}
let tol = 0.05 * range;
if body > 0.0 && candle.high - candle.close <= tol {
return Some(1.0);
}
if body < 0.0 && candle.close - candle.low <= tol {
return Some(-1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"ClosingMarubozu"
}
}
#[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 = ClosingMarubozu::new();
assert_eq!(t.name(), "ClosingMarubozu");
assert_eq!(t.warmup_period(), 1);
assert!(!t.is_ready());
}
#[test]
fn white_closing_marubozu_is_plus_one() {
let mut t = ClosingMarubozu::new();
// Closes at the high, opening shadow below.
assert_eq!(t.update(c(10.5, 15.0, 10.0, 15.0, 0)), Some(1.0));
}
#[test]
fn black_closing_marubozu_is_minus_one() {
let mut t = ClosingMarubozu::new();
// Closes at the low, opening shadow above.
assert_eq!(t.update(c(14.5, 15.0, 10.0, 10.0, 0)), Some(-1.0));
}
#[test]
fn white_with_upper_shadow_yields_zero() {
let mut t = ClosingMarubozu::new();
// Long white body but a clear upper shadow -> close is not at the high.
assert_eq!(t.update(c(10.5, 16.0, 10.0, 15.0, 0)), Some(0.0));
}
#[test]
fn black_with_lower_shadow_yields_zero() {
let mut t = ClosingMarubozu::new();
// Long black body but a clear lower shadow -> close is not at the low.
assert_eq!(t.update(c(14.5, 15.0, 9.0, 10.0, 0)), Some(0.0));
}
#[test]
fn short_body_yields_zero() {
let mut t = ClosingMarubozu::new();
// Body is short relative to range.
assert_eq!(t.update(c(12.0, 15.0, 10.0, 12.5, 0)), Some(0.0));
}
#[test]
fn zero_range_yields_zero() {
let mut t = ClosingMarubozu::new();
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
c(base + 0.5, base + 5.0, base, base + 5.0, i)
})
.collect();
let mut a = ClosingMarubozu::new();
let mut b = ClosingMarubozu::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = ClosingMarubozu::new();
t.update(c(10.5, 15.0, 10.0, 15.0, 0));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
}
}
@@ -0,0 +1,278 @@
//! Concealing Baby Swallow candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Returns `true` when `candle` is a black marubozu: a down candle whose body fills
/// the range with negligible shadows on both ends.
fn black_marubozu(candle: Candle) -> bool {
let range = candle.high - candle.low;
if range <= 0.0 {
return false;
}
let upper = candle.high - candle.open;
let lower = candle.close - candle.low;
candle.open > candle.close && upper <= 0.05 * range && lower <= 0.05 * range
}
/// Concealing Baby Swallow — a rare 4-bar bullish reversal. Two black marubozu lead
/// a steep decline; the third is a black candle that gaps down on the open yet
/// throws a long upper shadow back up into the second body; the fourth is a large
/// black candle that completely engulfs the third, shadows included. The relentless
/// selling that can no longer make ground signals capitulation.
///
/// ```text
/// bar1, bar2 black marubozu (body == range, negligible shadows)
/// bar3 black, opens below bar2's body (open3 < close2) with an upper
/// shadow into it (high3 > close2)
/// bar4 black, engulfs bar3 including shadows: open4 > high3 and close4 < low3
/// ```
///
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Concealing Baby
/// Swallow is a single-direction (bullish-only) reversal, so it never emits `1.0`.
/// The first three bars always return `0.0` because the four-bar window is not yet
/// filled. Body and shadow thresholds follow the geometric house style rather than
/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is applied;
/// combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, ConcealingBabySwallow, Indicator};
///
/// let mut indicator = ConcealingBabySwallow::new();
/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(16.0, 16.1, 11.9, 12.0, 1.0, 1).unwrap());
/// indicator.update(Candle::new(11.0, 13.0, 9.9, 10.0, 1.0, 2).unwrap());
/// let out = indicator
/// .update(Candle::new(14.0, 14.1, 8.9, 9.0, 1.0, 3).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ConcealingBabySwallow {
c1: Option<Candle>,
c2: Option<Candle>,
c3: Option<Candle>,
has_emitted: bool,
}
impl ConcealingBabySwallow {
/// Construct a new Concealing Baby Swallow detector.
pub const fn new() -> Self {
Self {
c1: None,
c2: None,
c3: None,
has_emitted: false,
}
}
}
impl Indicator for ConcealingBabySwallow {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let bar1 = self.c1;
let bar2 = self.c2;
let bar3 = self.c3;
self.c1 = self.c2;
self.c2 = self.c3;
self.c3 = Some(candle);
let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
return Some(0.0);
};
// bar1 and bar2 are black marubozu.
if !black_marubozu(bar1) || !black_marubozu(bar2) {
return Some(0.0);
}
// bar3 is black, gaps down on the open, throws an upper shadow into bar2.
if bar3.open <= bar3.close {
return Some(0.0);
}
if bar3.open >= bar2.close {
return Some(0.0); // no downside open gap
}
if bar3.high <= bar2.close {
return Some(0.0); // upper shadow does not reach into bar2's body
}
// bar4 is black and engulfs bar3 including its shadows.
if candle.open <= candle.close {
return Some(0.0);
}
if candle.open > bar3.high && candle.close < bar3.low {
return Some(1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.c1 = None;
self.c2 = None;
self.c3 = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
4
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"ConcealingBabySwallow"
}
}
#[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 = ConcealingBabySwallow::new();
assert_eq!(t.name(), "ConcealingBabySwallow");
assert_eq!(t.warmup_period(), 4);
assert!(!t.is_ready());
}
#[test]
fn concealing_baby_swallow_is_plus_one() {
let mut t = ConcealingBabySwallow::new();
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(1.0));
}
#[test]
fn warmup_returns_zero() {
let mut t = ConcealingBabySwallow::new();
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
}
#[test]
fn first_bar_not_marubozu_yields_zero() {
let mut t = ConcealingBabySwallow::new();
// bar1 white.
t.update(c(15.0, 20.1, 14.9, 20.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
}
#[test]
fn first_bar_zero_range_yields_zero() {
let mut t = ConcealingBabySwallow::new();
// bar1 zero range -> not a marubozu.
t.update(c(15.0, 15.0, 15.0, 15.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
}
#[test]
fn second_bar_not_marubozu_yields_zero() {
let mut t = ConcealingBabySwallow::new();
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
// bar2 white.
t.update(c(12.0, 16.1, 11.9, 16.0, 1));
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
}
#[test]
fn third_bar_not_black_yields_zero() {
let mut t = ConcealingBabySwallow::new();
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
// bar3 white.
t.update(c(11.0, 13.0, 9.9, 12.5, 2));
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
}
#[test]
fn third_bar_no_gap_yields_zero() {
let mut t = ConcealingBabySwallow::new();
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
// bar3 black but opens at/above bar2's close -> no downside gap.
t.update(c(12.5, 13.0, 9.9, 10.0, 2));
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
}
#[test]
fn third_bar_no_upper_shadow_yields_zero() {
let mut t = ConcealingBabySwallow::new();
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
// bar3 black, gaps down, but its high does not reach into bar2's body.
t.update(c(11.0, 11.5, 9.9, 10.0, 2));
assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
}
#[test]
fn fourth_bar_not_black_yields_zero() {
let mut t = ConcealingBabySwallow::new();
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
// bar4 white.
assert_eq!(t.update(c(14.0, 14.1, 8.9, 14.05, 3)), Some(0.0));
}
#[test]
fn fourth_bar_not_engulfing_yields_zero() {
let mut t = ConcealingBabySwallow::new();
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
// bar4 black but does not engulf bar3's high.
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 3)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 200.0 - i as f64;
c(base, base + 0.05, base - 5.0, base - 5.0, i)
})
.collect();
let mut a = ConcealingBabySwallow::new();
let mut b = ConcealingBabySwallow::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = ConcealingBabySwallow::new();
t.update(c(20.0, 20.1, 14.9, 15.0, 0));
t.update(c(16.0, 16.1, 11.9, 12.0, 1));
t.update(c(11.0, 13.0, 9.9, 10.0, 2));
t.update(c(14.0, 14.1, 8.9, 9.0, 3));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
}
}
+16 -1
View File
@@ -45,10 +45,12 @@ mod chande_kroll_stop;
mod chandelier_exit;
mod choppiness_index;
mod classic_pivots;
mod closing_marubozu;
mod cmf;
mod cmo;
mod coefficient_of_variation;
mod cointegration;
mod concealing_baby_swallow;
mod conditional_value_at_risk;
mod connors_rsi;
mod coppock;
@@ -168,6 +170,7 @@ mod oi_price_divergence;
mod oi_weighted;
mod omega_ratio;
mod on_neck;
mod opening_marubozu;
mod opening_range;
mod pain_index;
mod pair_spread_zscore;
@@ -226,6 +229,7 @@ mod super_trend;
mod t3;
mod taker_buy_sell_ratio;
mod takuri;
mod tasuki_gap;
mod td_combo;
mod td_countdown;
mod td_demarker;
@@ -260,6 +264,7 @@ mod two_crows;
mod typical_price;
mod ulcer_index;
mod ultimate_oscillator;
mod unique_three_river;
mod upside_gap_three_methods;
mod upside_gap_two_crows;
mod value_area;
@@ -329,10 +334,12 @@ pub use chande_kroll_stop::{ChandeKrollStop, ChandeKrollStopOutput};
pub use chandelier_exit::{ChandelierExit, ChandelierExitOutput};
pub use choppiness_index::ChoppinessIndex;
pub use classic_pivots::{ClassicPivots, ClassicPivotsOutput};
pub use closing_marubozu::ClosingMarubozu;
pub use cmf::ChaikinMoneyFlow;
pub use cmo::Cmo;
pub use coefficient_of_variation::CoefficientOfVariation;
pub use cointegration::{Cointegration, CointegrationOutput};
pub use concealing_baby_swallow::ConcealingBabySwallow;
pub use conditional_value_at_risk::ConditionalValueAtRisk;
pub use connors_rsi::ConnorsRsi;
pub use coppock::Coppock;
@@ -452,6 +459,7 @@ pub use oi_price_divergence::OIPriceDivergence;
pub use oi_weighted::OIWeighted;
pub use omega_ratio::OmegaRatio;
pub use on_neck::OnNeck;
pub use opening_marubozu::OpeningMarubozu;
pub use opening_range::{OpeningRange, OpeningRangeOutput};
pub use pain_index::PainIndex;
pub use pair_spread_zscore::PairSpreadZScore;
@@ -510,6 +518,7 @@ pub use super_trend::{SuperTrend, SuperTrendOutput};
pub use t3::T3;
pub use taker_buy_sell_ratio::TakerBuySellRatio;
pub use takuri::Takuri;
pub use tasuki_gap::TasukiGap;
pub use td_combo::TdCombo;
pub use td_countdown::TdCountdown;
pub use td_demarker::TdDeMarker;
@@ -544,6 +553,7 @@ pub use two_crows::TwoCrows;
pub use typical_price::TypicalPrice;
pub use ulcer_index::UlcerIndex;
pub use ultimate_oscillator::UltimateOscillator;
pub use unique_three_river::UniqueThreeRiver;
pub use upside_gap_three_methods::UpsideGapThreeMethods;
pub use upside_gap_two_crows::UpsideGapTwoCrows;
pub use value_area::{ValueArea, ValueAreaOutput};
@@ -875,6 +885,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"StalledPattern",
"StickSandwich",
"Takuri",
"ClosingMarubozu",
"OpeningMarubozu",
"TasukiGap",
"UniqueThreeRiver",
"ConcealingBabySwallow",
],
),
(
@@ -966,6 +981,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, 279, "FAMILIES total drifted from indicator count");
assert_eq!(total, 284, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,178 @@
//! Opening Marubozu candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Opening Marubozu — a single-bar strong-momentum candle with a long body and no
/// shadow on the *open* end. A white opening marubozu opens right at the low (no
/// lower shadow) and may carry a closing shadow above; a black one opens right at
/// the high (no upper shadow) and may carry a closing shadow below. The shaved
/// open end shows the move took off from the bell without hesitation.
///
/// ```text
/// range = high low
/// long body: |close open| >= 0.7 * range
/// white: close > open and open low <= 0.05 * range (open at the low)
/// black: close < open and high open <= 0.05 * range (open at the high)
/// ```
///
/// Output is `+1.0` for a white opening marubozu, `1.0` for a black one, and
/// `0.0` otherwise. Body and shadow thresholds follow the geometric house style
/// rather than TA-Lib's rolling averages. TA-Lib has no direct equivalent; this
/// completes the pair with [`crate::ClosingMarubozu`], which shaves the close end.
/// Pattern-shape check only — no trend filter is applied; combine with a trend
/// indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `+1.0` bullish, `1.0` bearish, `0.0` no pattern — so it drops
/// straight into a machine-learning feature matrix where the bullish and bearish
/// variants occupy a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, OpeningMarubozu};
///
/// let mut indicator = OpeningMarubozu::new();
/// // White: opens at the low, small closing shadow above.
/// let candle = Candle::new(10.0, 15.0, 10.0, 14.5, 1.0, 0).unwrap();
/// assert_eq!(indicator.update(candle), Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct OpeningMarubozu {
has_emitted: bool,
}
impl OpeningMarubozu {
/// Construct a new Opening Marubozu detector.
pub const fn new() -> Self {
Self { has_emitted: false }
}
}
impl Indicator for OpeningMarubozu {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let range = candle.high - candle.low;
if range <= 0.0 {
return Some(0.0);
}
let body = candle.close - candle.open;
if body.abs() < 0.7 * range {
return Some(0.0);
}
let tol = 0.05 * range;
if body > 0.0 && candle.open - candle.low <= tol {
return Some(1.0);
}
if body < 0.0 && candle.high - candle.open <= tol {
return Some(-1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"OpeningMarubozu"
}
}
#[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 = OpeningMarubozu::new();
assert_eq!(t.name(), "OpeningMarubozu");
assert_eq!(t.warmup_period(), 1);
assert!(!t.is_ready());
}
#[test]
fn white_opening_marubozu_is_plus_one() {
let mut t = OpeningMarubozu::new();
// Opens at the low, closing shadow above.
assert_eq!(t.update(c(10.0, 15.0, 10.0, 14.5, 0)), Some(1.0));
}
#[test]
fn black_opening_marubozu_is_minus_one() {
let mut t = OpeningMarubozu::new();
// Opens at the high, closing shadow below.
assert_eq!(t.update(c(15.0, 15.0, 10.0, 10.5, 0)), Some(-1.0));
}
#[test]
fn white_with_lower_shadow_yields_zero() {
let mut t = OpeningMarubozu::new();
// Long white body but a clear lower shadow -> open is not at the low.
assert_eq!(t.update(c(11.0, 15.0, 10.0, 15.0, 0)), Some(0.0));
}
#[test]
fn black_with_upper_shadow_yields_zero() {
let mut t = OpeningMarubozu::new();
// Long black body but a clear upper shadow -> open is not at the high.
assert_eq!(t.update(c(14.0, 16.0, 10.0, 10.5, 0)), Some(0.0));
}
#[test]
fn short_body_yields_zero() {
let mut t = OpeningMarubozu::new();
// Body is short relative to range.
assert_eq!(t.update(c(10.0, 15.0, 10.0, 12.5, 0)), Some(0.0));
}
#[test]
fn zero_range_yields_zero() {
let mut t = OpeningMarubozu::new();
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
c(base, base + 5.0, base, base + 4.5, i)
})
.collect();
let mut a = OpeningMarubozu::new();
let mut b = OpeningMarubozu::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = OpeningMarubozu::new();
t.update(c(10.0, 15.0, 10.0, 14.5, 0));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
}
}
@@ -0,0 +1,278 @@
//! Tasuki Gap candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Tasuki Gap — a 3-bar continuation. Two same-coloured candles open a body gap in
/// the trend direction, then an opposite-coloured candle opens inside the second
/// body and closes back *into* the gap without filling it — the gap holds, so the
/// trend is expected to continue.
///
/// ```text
/// Upside (bullish, +1):
/// bar1 white, bar2 white with an upside body gap (open2 > close1)
/// bar3 black, opens within bar2's body, closes inside the gap
/// (close1 < close3 < open2)
/// Downside (bearish, 1): the mirror image with black candles and a downside gap
/// ```
///
/// Output is `+1.0` for an upside Tasuki gap, `1.0` for a downside one, and `0.0`
/// otherwise. The first two bars always return `0.0` because the three-bar window
/// is not yet filled. 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 bullish and bearish
/// variants occupy a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, TasukiGap};
///
/// let mut indicator = TasukiGap::new();
/// indicator.update(Candle::new(10.0, 11.2, 9.8, 11.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(12.0, 14.0, 11.9, 13.5, 1.0, 1).unwrap());
/// let out = indicator
/// .update(Candle::new(13.0, 13.1, 11.4, 11.5, 1.0, 2).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct TasukiGap {
c1: Option<Candle>,
c2: Option<Candle>,
has_emitted: bool,
}
impl TasukiGap {
/// Construct a new Tasuki Gap detector.
pub const fn new() -> Self {
Self {
c1: None,
c2: None,
has_emitted: false,
}
}
}
impl Indicator for TasukiGap {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let bar1 = self.c1;
let bar2 = self.c2;
self.c1 = self.c2;
self.c2 = Some(candle);
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
return Some(0.0);
};
let up = bar1.close > bar1.open && bar2.close > bar2.open;
let down = bar1.close < bar1.open && bar2.close < bar2.open;
if up {
if bar2.open <= bar1.close {
return Some(0.0); // no upside body gap
}
if candle.close >= candle.open {
return Some(0.0); // bar3 must be black
}
if candle.open <= bar2.open || candle.open >= bar2.close {
return Some(0.0); // bar3 must open within bar2's body
}
if candle.close < bar2.open && candle.close > bar1.close {
return Some(1.0); // bar3 closes inside the gap
}
return Some(0.0);
}
if down {
if bar2.open >= bar1.close {
return Some(0.0); // no downside body gap
}
if candle.close <= candle.open {
return Some(0.0); // bar3 must be white
}
if candle.open >= bar2.open || candle.open <= bar2.close {
return Some(0.0); // bar3 must open within bar2's body
}
if candle.close > bar2.open && candle.close < bar1.close {
return Some(-1.0); // bar3 closes inside the gap
}
return Some(0.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.c1 = None;
self.c2 = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"TasukiGap"
}
}
#[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 = TasukiGap::new();
assert_eq!(t.name(), "TasukiGap");
assert_eq!(t.warmup_period(), 3);
assert!(!t.is_ready());
}
#[test]
fn upside_tasuki_gap_is_plus_one() {
let mut t = TasukiGap::new();
assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), Some(0.0));
assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 2)), Some(1.0));
}
#[test]
fn downside_tasuki_gap_is_minus_one() {
let mut t = TasukiGap::new();
assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(11.0, 11.1, 9.5, 10.0, 1)), Some(0.0));
assert_eq!(t.update(c(10.5, 11.6, 10.4, 11.5, 2)), Some(-1.0));
}
#[test]
fn first_two_bars_return_zero() {
let mut t = TasukiGap::new();
assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
assert_eq!(t.update(c(12.0, 14.0, 11.9, 13.5, 1)), Some(0.0));
}
#[test]
fn up_no_gap_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
// bar2 white but opens below bar1's close -> no upside gap.
t.update(c(10.5, 13.1, 10.4, 13.0, 1));
assert_eq!(t.update(c(12.5, 12.6, 10.9, 11.0, 2)), Some(0.0));
}
#[test]
fn up_third_not_black_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
t.update(c(12.0, 14.0, 11.9, 13.5, 1));
// bar3 white.
assert_eq!(t.update(c(12.5, 13.1, 12.4, 13.0, 2)), Some(0.0));
}
#[test]
fn up_third_open_outside_body_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
t.update(c(12.0, 14.0, 11.9, 13.5, 1));
// bar3 black but opens above bar2's body.
assert_eq!(t.update(c(14.0, 14.1, 11.4, 11.5, 2)), Some(0.0));
}
#[test]
fn up_third_close_not_in_gap_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
t.update(c(12.0, 14.0, 11.9, 13.5, 1));
// bar3 black, opens in body, but closes below the gap (under bar1's close).
assert_eq!(t.update(c(13.0, 13.1, 10.4, 10.5, 2)), Some(0.0));
}
#[test]
fn down_no_gap_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
// bar2 black but opens above bar1's close -> no downside gap.
t.update(c(12.5, 12.6, 10.4, 10.5, 1));
assert_eq!(t.update(c(11.0, 12.6, 10.9, 12.0, 2)), Some(0.0));
}
#[test]
fn down_third_not_white_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
t.update(c(11.0, 11.1, 9.5, 10.0, 1));
// bar3 black.
assert_eq!(t.update(c(11.5, 11.6, 10.4, 10.5, 2)), Some(0.0));
}
#[test]
fn down_third_open_outside_body_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
t.update(c(11.0, 11.1, 9.5, 10.0, 1));
// bar3 white but opens below bar2's body.
assert_eq!(t.update(c(9.5, 11.6, 9.4, 11.5, 2)), Some(0.0));
}
#[test]
fn down_third_close_not_in_gap_yields_zero() {
let mut t = TasukiGap::new();
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
t.update(c(11.0, 11.1, 9.5, 10.0, 1));
// bar3 white, opens in body, but closes above the gap (over bar1's close).
assert_eq!(t.update(c(10.5, 13.0, 10.4, 12.5, 2)), Some(0.0));
}
#[test]
fn mixed_colours_yield_zero() {
let mut t = TasukiGap::new();
// bar1 white, bar2 black -> neither an upside nor downside setup.
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
t.update(c(13.0, 13.2, 11.0, 11.5, 1));
assert_eq!(t.update(c(12.0, 12.6, 10.9, 11.0, 2)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
c(base, base + 5.2, base - 0.1, base + 5.0, i)
})
.collect();
let mut a = TasukiGap::new();
let mut b = TasukiGap::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = TasukiGap::new();
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
t.update(c(12.0, 14.0, 11.9, 13.5, 1));
t.update(c(13.0, 13.1, 11.4, 11.5, 2));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,256 @@
//! Unique Three River candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Unique Three River (Bottom) — a 3-bar bullish reversal. A long black candle is
/// followed by a smaller black candle whose body sits inside the first but whose
/// long lower shadow probes a new low, then a small white candle that stays below
/// the second body. The fresh low that fails to hold marks an exhausted decline.
///
/// ```text
/// bar1 long black: open1 close1 >= 0.5 * (high1 low1)
/// bar2 black, body inside bar1's body, with a new low (low2 < low1)
/// bar3 small white, contained below bar2's body (high3 <= close2)
/// small body: close3 open3 <= 0.3 * (high3 low3)
/// ```
///
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Unique Three
/// River is a single-direction (bullish-only) reversal, so it never emits `1.0`.
/// The first two bars always return `0.0` because the three-bar window is not yet
/// filled. 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, UniqueThreeRiver};
///
/// let mut indicator = UniqueThreeRiver::new();
/// indicator.update(Candle::new(15.0, 15.1, 10.0, 10.5, 1.0, 0).unwrap());
/// indicator.update(Candle::new(14.0, 14.1, 9.0, 11.0, 1.0, 1).unwrap());
/// let out = indicator
/// .update(Candle::new(10.2, 10.9, 9.5, 10.4, 1.0, 2).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct UniqueThreeRiver {
c1: Option<Candle>,
c2: Option<Candle>,
has_emitted: bool,
}
impl UniqueThreeRiver {
/// Construct a new Unique Three River detector.
pub const fn new() -> Self {
Self {
c1: None,
c2: None,
has_emitted: false,
}
}
}
impl Indicator for UniqueThreeRiver {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let bar1 = self.c1;
let bar2 = self.c2;
self.c1 = self.c2;
self.c2 = Some(candle);
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
return Some(0.0);
};
// bar1 is a long black body.
if bar1.open <= bar1.close {
return Some(0.0);
}
let range1 = bar1.high - bar1.low;
if bar1.open - bar1.close < 0.5 * range1 {
return Some(0.0);
}
// bar2 is black, its body inside bar1's body, with a new low.
if bar2.open <= bar2.close {
return Some(0.0);
}
if bar2.open > bar1.open || bar2.close < bar1.close {
return Some(0.0);
}
if bar2.low >= bar1.low {
return Some(0.0);
}
// bar3 is a small white candle contained below bar2's body.
if candle.close <= candle.open {
return Some(0.0);
}
let range3 = candle.high - candle.low;
if candle.close - candle.open > 0.3 * range3 {
return Some(0.0);
}
if candle.high > bar2.close {
return Some(0.0);
}
Some(1.0)
}
fn reset(&mut self) {
self.c1 = None;
self.c2 = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"UniqueThreeRiver"
}
}
#[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 = UniqueThreeRiver::new();
assert_eq!(t.name(), "UniqueThreeRiver");
assert_eq!(t.warmup_period(), 3);
assert!(!t.is_ready());
}
#[test]
fn unique_three_river_is_plus_one() {
let mut t = UniqueThreeRiver::new();
assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), Some(0.0));
assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(1.0));
}
#[test]
fn first_two_bars_return_zero() {
let mut t = UniqueThreeRiver::new();
assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
assert_eq!(t.update(c(14.0, 14.1, 9.0, 11.0, 1)), Some(0.0));
}
#[test]
fn first_bar_not_black_yields_zero() {
let mut t = UniqueThreeRiver::new();
// bar1 white.
t.update(c(10.5, 15.1, 10.0, 15.0, 0));
t.update(c(14.0, 14.1, 9.0, 11.0, 1));
assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
}
#[test]
fn first_bar_short_body_yields_zero() {
let mut t = UniqueThreeRiver::new();
// bar1 black but its body is short relative to range.
t.update(c(15.0, 15.1, 10.0, 14.5, 0));
t.update(c(14.0, 14.1, 9.0, 11.0, 1));
assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
}
#[test]
fn second_bar_not_black_yields_zero() {
let mut t = UniqueThreeRiver::new();
t.update(c(15.0, 15.1, 10.0, 10.5, 0));
// bar2 white.
t.update(c(11.0, 14.1, 9.0, 13.0, 1));
assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
}
#[test]
fn second_bar_not_inside_yields_zero() {
let mut t = UniqueThreeRiver::new();
t.update(c(15.0, 15.1, 10.0, 10.5, 0));
// bar2 black but opens above bar1's open -> body not inside.
t.update(c(16.0, 16.1, 9.0, 11.0, 1));
assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
}
#[test]
fn second_bar_no_new_low_yields_zero() {
let mut t = UniqueThreeRiver::new();
t.update(c(15.0, 15.1, 10.0, 10.5, 0));
// bar2 black, inside, but does not make a new low.
t.update(c(14.0, 14.1, 10.5, 11.0, 1));
assert_eq!(t.update(c(10.2, 10.9, 9.5, 10.4, 2)), Some(0.0));
}
#[test]
fn third_bar_not_white_yields_zero() {
let mut t = UniqueThreeRiver::new();
t.update(c(15.0, 15.1, 10.0, 10.5, 0));
t.update(c(14.0, 14.1, 9.0, 11.0, 1));
// bar3 black.
assert_eq!(t.update(c(10.6, 10.9, 9.5, 10.2, 2)), Some(0.0));
}
#[test]
fn third_bar_large_body_yields_zero() {
let mut t = UniqueThreeRiver::new();
t.update(c(15.0, 15.1, 10.0, 10.5, 0));
t.update(c(14.0, 14.1, 9.0, 11.0, 1));
// bar3 white but with a large body.
assert_eq!(t.update(c(9.6, 10.9, 9.5, 10.8, 2)), Some(0.0));
}
#[test]
fn third_bar_not_below_second_yields_zero() {
let mut t = UniqueThreeRiver::new();
t.update(c(15.0, 15.1, 10.0, 10.5, 0));
t.update(c(14.0, 14.1, 9.0, 11.0, 1));
// bar3 small white but pokes above bar2's close.
assert_eq!(t.update(c(10.5, 11.5, 10.4, 10.7, 2)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 200.0 - i as f64;
c(base, base + 0.1, base - 5.2, base - 5.0, i)
})
.collect();
let mut a = UniqueThreeRiver::new();
let mut b = UniqueThreeRiver::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = UniqueThreeRiver::new();
t.update(c(15.0, 15.1, 10.0, 10.5, 0));
t.update(c(14.0, 14.1, 9.0, 11.0, 1));
t.update(c(10.2, 10.9, 9.5, 10.4, 2));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(15.0, 15.1, 10.0, 10.5, 0)), Some(0.0));
}
}
+31 -31
View File
@@ -61,31 +61,31 @@ pub use indicators::{
BollingerOutput, Breakaway, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci,
CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop,
ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots,
ClassicPivotsOutput, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, CumulativeVolumeDelta,
CyberneticCycle, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar, Donchian, DonchianOutput,
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, EaseOfMovement, EffectiveSpread,
EhlersStochastic, ElderImpulse, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar,
Evwma, FallingThreeMethods, Fama, FibonacciPivots, FibonacciPivotsOutput, FisherTransform,
Footprint, FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama,
FundingBasis, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami,
HeikinAshi, HeikinAshiOutput, HiLoActivator, HighWave, Hikkake, HikkakeModified,
HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HurstChannel,
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput,
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
ClassicPivotsOutput, ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration,
CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock,
Counterattack, CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema,
DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar,
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration,
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
FibonacciPivots, FibonacciPivotsOutput, FisherTransform, Footprint, FootprintOutput,
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator,
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
HomingPigeon, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion,
Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda,
LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, LiquidationFeatures,
LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown, McGinleyDynamic,
MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom, MorningDojiStar,
MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck,
OpenInterestDelta, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
@@ -96,18 +96,18 @@ pub use indicators::{
SpinningTop, StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput,
StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi,
Stochastic, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio,
Takuri, 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, UpsideGapThreeMethods,
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,
Takuri, TasukiGap, 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,
UniqueThreeRiver, UpsideGapThreeMethods, 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 **284 indicators** across
- A per-indicator deep dive for every one of the **289 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, DownsideGapThreeMethods, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, 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, UpsideGapThreeMethods, 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, ClosingMarubozu, ConcealingBabySwallow, Counterattack, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, 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, UniqueThreeRiver, UpsideGapThreeMethods, 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(ConcealingBabySwallow::new, &candles);
drive(UniqueThreeRiver::new, &candles);
drive(TasukiGap::new, &candles);
drive(OpeningMarubozu::new, &candles);
drive(ClosingMarubozu::new, &candles);
drive(Takuri::new, &candles);
drive(StickSandwich::new, &candles);
drive(StalledPattern::new, &candles);