diff --git a/CHANGELOG.md b/CHANGELOG.md index cad34dff..b90b34f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Three Drives** — three symmetric drives with extension legs; bullish +1, bearish -1 (`THREE_DRIVES`). +- **Cypher** — five-point harmonic whose D retraces XC by 0.786; bullish +1, bearish -1 (`CYPHER`). +- **Shark** — five-point harmonic with an expansion leg and 0.886-1.13 D; bullish +1, bearish -1 (`SHARK`). +- **Crab** — five-point harmonic with the deepest (1.618 XA) D completion; bullish +1, bearish -1 (`CRAB`). +- **Bat** — five-point harmonic with a shallow B and 0.886 D completion; bullish +1, bearish -1 (`BAT`). +- **Butterfly** — five-point harmonic with an extended (1.27-1.618 XA) D; bullish +1, bearish -1 (`BUTTERFLY`). +- **Gartley** — five-point harmonic with a 0.786 D completion; bullish +1, bearish -1 (`GARTLEY`). +- **AB=CD** — four-point AB=CD harmonic: BC retraces AB, CD mirrors AB; bullish +1, bearish -1 (`ABCD`). - **Cup and Handle** — rounded base with a shallow handle near the rim; bullish +1, inverse -1 (`CUP_AND_HANDLE`). - **Rectangle / Range** — flat support and resistance; mean-reversion signal off the just-touched boundary; support +1, resistance -1 (`RECTANGLE_RANGE`). - **Flag / Pennant** — shallow consolidation against a sharp pole; continuation in the pole direction; bull +1, bear -1 (`FLAG_PENNANT`). diff --git a/README.md b/README.md index f6fcdfa0..2805dab6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

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

[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml) @@ -48,7 +48,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 359 indicators; start at the + every one of the 367 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), @@ -136,7 +136,7 @@ python -m benchmarks.compare_libraries ## Indicators -359 streaming-first indicators across twenty-two families. Every one passes the +367 streaming-first indicators across twenty-three 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). @@ -159,6 +159,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) | | 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 | | Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle | +| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives | | 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), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range | @@ -243,7 +244,7 @@ A Python live-trading example using the public `websockets` package lives at ``` wickra/ ├── crates/ -│ ├── wickra-core/ core engine + all 359 indicators +│ ├── wickra-core/ core engine + all 367 indicators │ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/ │ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds ├── bindings/ diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js index 37d7f33e..2f0d4bbc 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -305,6 +305,14 @@ const candleScalar = { FlagPennant: { make: () => new wickra.FlagPennant(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, RectangleRange: { make: () => new wickra.RectangleRange(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, CupAndHandle: { make: () => new wickra.CupAndHandle(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + Abcd: { make: () => new wickra.Abcd(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + Gartley: { make: () => new wickra.Gartley(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + Butterfly: { make: () => new wickra.Butterfly(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + Bat: { make: () => new wickra.Bat(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + Crab: { make: () => new wickra.Crab(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + Shark: { make: () => new wickra.Shark(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + Cypher: { make: () => new wickra.Cypher(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + ThreeDrives: { make: () => new wickra.ThreeDrives(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, }; for (const [name, d] of Object.entries(candleScalar)) { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 6e9d82d3..1763259e 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -3104,6 +3104,78 @@ export declare class CupAndHandle { isReady(): boolean warmupPeriod(): number } +export type AbcdNode = Abcd +export declare class Abcd { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type GartleyNode = Gartley +export declare class Gartley { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type ButterflyNode = Butterfly +export declare class Butterfly { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type BatNode = Bat +export declare class Bat { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type CrabNode = Crab +export declare class Crab { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type SharkNode = Shark +export declare class Shark { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type CypherNode = Cypher +export declare class Cypher { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type ThreeDrivesNode = ThreeDrives +export declare class ThreeDrives { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1 export declare class OrderBookImbalanceTop1 { constructor() diff --git a/bindings/node/index.js b/bindings/node/index.js index cfbdb0cc..dbeca076 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, 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, AnchoredRSI, 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, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, 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, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, 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, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn } = 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, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, 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, AnchoredRSI, 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, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, 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, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, 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, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -600,6 +600,14 @@ module.exports.Wedge = Wedge module.exports.FlagPennant = FlagPennant module.exports.RectangleRange = RectangleRange module.exports.CupAndHandle = CupAndHandle +module.exports.Abcd = Abcd +module.exports.Gartley = Gartley +module.exports.Butterfly = Butterfly +module.exports.Bat = Bat +module.exports.Crab = Crab +module.exports.Shark = Shark +module.exports.Cypher = Cypher +module.exports.ThreeDrives = ThreeDrives module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1 module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull module.exports.Microprice = Microprice diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index dcea756c..5502af95 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -10026,6 +10026,14 @@ node_candle_pattern!(WedgeNode, wc::Wedge, "Wedge"); node_candle_pattern!(FlagPennantNode, wc::FlagPennant, "FlagPennant"); node_candle_pattern!(RectangleRangeNode, wc::RectangleRange, "RectangleRange"); node_candle_pattern!(CupAndHandleNode, wc::CupAndHandle, "CupAndHandle"); +node_candle_pattern!(AbcdNode, wc::Abcd, "Abcd"); +node_candle_pattern!(GartleyNode, wc::Gartley, "Gartley"); +node_candle_pattern!(ButterflyNode, wc::Butterfly, "Butterfly"); +node_candle_pattern!(BatNode, wc::Bat, "Bat"); +node_candle_pattern!(CrabNode, wc::Crab, "Crab"); +node_candle_pattern!(SharkNode, wc::Shark, "Shark"); +node_candle_pattern!(CypherNode, wc::Cypher, "Cypher"); +node_candle_pattern!(ThreeDrivesNode, wc::ThreeDrives, "ThreeDrives"); // ============================== Microstructure: Order Book ============================== // diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index 5f4dea8f..59c65c3e 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -330,6 +330,15 @@ from ._wickra import ( HeadAndShoulders, TripleTopBottom, DoubleTopBottom, + # Harmonic patterns + ThreeDrives, + Cypher, + Shark, + Crab, + Bat, + Butterfly, + Gartley, + Abcd, # Microstructure: order book OrderBookImbalanceTop1, OrderBookImbalanceTopN, @@ -716,6 +725,15 @@ __all__ = [ "HeadAndShoulders", "TripleTopBottom", "DoubleTopBottom", + # Harmonic patterns + "ThreeDrives", + "Cypher", + "Shark", + "Crab", + "Bat", + "Butterfly", + "Gartley", + "Abcd", # Microstructure: order book "OrderBookImbalanceTop1", "OrderBookImbalanceTopN", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 891636bb..b79c93e8 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -13683,6 +13683,14 @@ candle_pattern_no_param!(PyWedge, wc::Wedge, "Wedge"); candle_pattern_no_param!(PyFlagPennant, wc::FlagPennant, "FlagPennant"); candle_pattern_no_param!(PyRectangleRange, wc::RectangleRange, "RectangleRange"); candle_pattern_no_param!(PyCupAndHandle, wc::CupAndHandle, "CupAndHandle"); +candle_pattern_no_param!(PyAbcd, wc::Abcd, "Abcd"); +candle_pattern_no_param!(PyGartley, wc::Gartley, "Gartley"); +candle_pattern_no_param!(PyButterfly, wc::Butterfly, "Butterfly"); +candle_pattern_no_param!(PyBat, wc::Bat, "Bat"); +candle_pattern_no_param!(PyCrab, wc::Crab, "Crab"); +candle_pattern_no_param!(PyShark, wc::Shark, "Shark"); +candle_pattern_no_param!(PyCypher, wc::Cypher, "Cypher"); +candle_pattern_no_param!(PyThreeDrives, wc::ThreeDrives, "ThreeDrives"); // ============================== Microstructure: Order Book ============================== // // Order-book indicators consume a depth snapshot rather than OHLCV. Streaming @@ -18212,5 +18220,13 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py index 1fd657e2..a80b807b 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -330,6 +330,38 @@ def test_relative_strength_streaming_matches_batch(): # 6-tuple candle; the batch helper takes only the columns it needs. CANDLE_SCALAR = { + "ThreeDrives": ( + lambda: ta.ThreeDrives(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "Cypher": ( + lambda: ta.Cypher(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "Shark": ( + lambda: ta.Shark(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "Crab": ( + lambda: ta.Crab(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "Bat": ( + lambda: ta.Bat(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "Butterfly": ( + lambda: ta.Butterfly(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "Gartley": ( + lambda: ta.Gartley(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "Abcd": ( + lambda: ta.Abcd(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), "CupAndHandle": ( lambda: ta.CupAndHandle(), lambda ind, h, l, c, v: ind.batch(c, h, l, c), @@ -2461,6 +2493,91 @@ def test_cup_and_handle_reference(): assert t.update((110.0, 119.79, 110.0, 110.0, 1.0, 3)) == pytest.approx(0.0) assert t.update((111.1, 121.0, 111.1, 111.1, 1.0, 4)) == pytest.approx(1.0) + +def test_abcd_reference(): + t = ta.Abcd() + assert t.update((139.86, 140.0, 139.86, 139.86, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 138.6, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 124.7, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((84.7, 123.453, 84.7, 84.7, 1.0, 3)) == pytest.approx(0.0) + assert t.update((85.547, 93.17, 85.547, 85.547, 1.0, 4)) == pytest.approx(1.0) + + +def test_gartley_reference(): + t = ta.Gartley() + assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((115.3, 138.6, 115.3, 115.3, 1.0, 3)) == pytest.approx(0.0) + assert t.update((116.453, 127.65, 116.453, 116.453, 1.0, 4)) == pytest.approx(0.0) + assert t.update((108.56, 126.3735, 108.56, 108.56, 1.0, 5)) == pytest.approx(0.0) + assert t.update((109.6456, 119.416, 109.6456, 109.6456, 1.0, 6)) == pytest.approx(1.0) + + +def test_butterfly_reference(): + t = ta.Butterfly() + assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((108.6, 138.6, 108.6, 108.6, 1.0, 3)) == pytest.approx(0.0) + assert t.update((109.686, 128.0, 109.686, 109.686, 1.0, 4)) == pytest.approx(0.0) + assert t.update((79.8, 126.72, 79.8, 79.8, 1.0, 5)) == pytest.approx(0.0) + assert t.update((80.598, 87.78, 80.598, 80.598, 1.0, 6)) == pytest.approx(1.0) + + +def test_bat_reference(): + t = ta.Bat() + assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((122.0, 138.6, 122.0, 122.0, 1.0, 3)) == pytest.approx(0.0) + assert t.update((123.22, 137.0, 123.22, 123.22, 1.0, 4)) == pytest.approx(0.0) + assert t.update((104.56, 135.63, 104.56, 104.56, 1.0, 5)) == pytest.approx(0.0) + assert t.update((105.6056, 115.016, 105.6056, 105.6056, 1.0, 6)) == pytest.approx(1.0) + + +def test_crab_reference(): + t = ta.Crab() + assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((120.0, 138.6, 120.0, 120.0, 1.0, 3)) == pytest.approx(0.0) + assert t.update((121.2, 137.5, 121.2, 121.2, 1.0, 4)) == pytest.approx(0.0) + assert t.update((75.3, 136.125, 75.3, 75.3, 1.0, 5)) == pytest.approx(0.0) + assert t.update((76.053, 82.83, 76.053, 76.053, 1.0, 6)) == pytest.approx(1.0) + + +def test_shark_reference(): + t = ta.Shark() + assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((88.0, 138.6, 88.0, 88.0, 1.0, 3)) == pytest.approx(0.0) + assert t.update((88.88, 186.8, 88.88, 88.88, 1.0, 4)) == pytest.approx(0.0) + assert t.update((100.0, 184.932, 100.0, 100.0, 1.0, 5)) == pytest.approx(0.0) + assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 6)) == pytest.approx(1.0) + + +def test_cypher_reference(): + t = ta.Cypher() + assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((120.0, 138.6, 120.0, 120.0, 1.0, 3)) == pytest.approx(0.0) + assert t.update((121.2, 168.0, 121.2, 121.2, 1.0, 4)) == pytest.approx(0.0) + assert t.update((114.55, 166.32, 114.55, 114.55, 1.0, 5)) == pytest.approx(0.0) + assert t.update((115.6955, 126.005, 115.6955, 115.6955, 1.0, 6)) == pytest.approx(1.0) + + +def test_three_drives_reference(): + t = ta.ThreeDrives() + assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0) + assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((101.0, 128.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0) + assert t.update((108.0, 126.72, 108.0, 108.0, 1.0, 3)) == pytest.approx(0.0) + assert t.update((109.08, 136.0, 109.08, 109.08, 1.0, 4)) == pytest.approx(0.0) + assert t.update((122.4, 134.64, 122.4, 122.4, 1.0, 5)) == pytest.approx(-1.0) + # --- Lifecycle ------------------------------------------------------------ diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 85f76bb7..e9d88894 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -7362,6 +7362,14 @@ wasm_candle_pattern!(WasmWedge, wc::Wedge, Wedge); wasm_candle_pattern!(WasmFlagPennant, wc::FlagPennant, FlagPennant); wasm_candle_pattern!(WasmRectangleRange, wc::RectangleRange, RectangleRange); wasm_candle_pattern!(WasmCupAndHandle, wc::CupAndHandle, CupAndHandle); +wasm_candle_pattern!(WasmAbcd, wc::Abcd, Abcd); +wasm_candle_pattern!(WasmGartley, wc::Gartley, Gartley); +wasm_candle_pattern!(WasmButterfly, wc::Butterfly, Butterfly); +wasm_candle_pattern!(WasmBat, wc::Bat, Bat); +wasm_candle_pattern!(WasmCrab, wc::Crab, Crab); +wasm_candle_pattern!(WasmShark, wc::Shark, Shark); +wasm_candle_pattern!(WasmCypher, wc::Cypher, Cypher); +wasm_candle_pattern!(WasmThreeDrives, wc::ThreeDrives, ThreeDrives); // ============================== Microstructure: Order Book ============================== // diff --git a/crates/wickra-core/src/indicators/abcd.rs b/crates/wickra-core/src/indicators/abcd.rs new file mode 100644 index 00000000..63db16f6 --- /dev/null +++ b/crates/wickra-core/src/indicators/abcd.rs @@ -0,0 +1,154 @@ +//! AB=CD harmonic pattern. + +use crate::indicators::pattern_swing::{approx_equal, ratios_in, SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// AB=CD — the simplest four-point harmonic pattern: an A→B leg, a B→C +/// retracement, and a C→D leg that mirrors A→B in length: +/// +/// ```text +/// BC / AB ∈ [0.382, 0.886] (C retraces AB) +/// CD / BC ∈ [1.13, 2.618] (D extends BC) +/// AB ≈ CD (within 10%) (the two legs are equal — the defining symmetry) +/// ``` +/// +/// Read from the last four confirmed pivots `A-B-C-D`. Output is `+1.0` +/// (bullish, D a swing low), `-1.0` (bearish, D a swing high), or `0.0`; never +/// `None`. See `crates/wickra-core/src/indicators/abcd.rs`. +#[derive(Debug, Clone)] +pub struct Abcd { + swing: SwingTracker, + has_emitted: bool, +} + +impl Abcd { + /// Construct a new AB=CD detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 4), + has_emitted: false, + } + } +} + +impl Default for Abcd { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for Abcd { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 4 { + return Some(0.0); + } + let len = pivots.len(); + let pa = pivots[len - 4]; + let pb = pivots[len - 3]; + let pc = pivots[len - 2]; + let pd = pivots[len - 1]; + let ab = (pb.price - pa.price).abs(); + let bc = (pc.price - pb.price).abs(); + let cd = (pd.price - pc.price).abs(); + let ratios_ok = ratios_in(&[(bc / ab, 0.382, 0.886), (cd / bc, 1.13, 2.618)]); + let legs_equal = approx_equal(ab, cd, 0.10); + if ratios_ok && legs_equal { + return Some(if pd.direction < 0.0 { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 5 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "Abcd" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = Abcd::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = Abcd::new(); + assert_eq!(indicator.name(), "Abcd"); + assert_eq!(indicator.warmup_period(), 5); + assert!(!indicator.is_ready()); + assert!(!Abcd::default().is_ready()); + } + + #[test] + fn bullish_abcd_is_plus_one() { + // AB = 40 down, BC = 24.7 up (0.618), CD = 40 down → AB = CD. + let out = run(&[140.0, 100.0, 124.7, 84.7]); + assert_eq!(*out.last().unwrap(), 1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bearish_abcd_is_minus_one() { + let out = run(&[150.0, 100.0, 140.0, 115.3, 155.3]); + assert_eq!(*out.last().unwrap(), -1.0); + } + + #[test] + fn unequal_legs_do_not_trigger() { + // CD (82) far longer than AB (40) → not an AB=CD. + let out = run(&[150.0, 100.0, 140.0, 118.0, 200.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = Abcd::new(); + for c in candles_for_pivots(&[140.0, 100.0, 124.7]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[140.0, 100.0, 124.7, 84.7]); + let mut a = Abcd::new(); + let mut b = Abcd::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/bat.rs b/crates/wickra-core/src/indicators/bat.rs new file mode 100644 index 00000000..44b5b766 --- /dev/null +++ b/crates/wickra-core/src/indicators/bat.rs @@ -0,0 +1,154 @@ +//! Bat harmonic pattern. + +use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Bat — a 5-point (X-A-B-C-D) harmonic pattern with a shallow B and a deep +/// `0.886` D completion: +/// +/// ```text +/// AB / XA ∈ [0.382, 0.50] +/// BC / AB ∈ [0.382, 0.886] +/// CD / BC ∈ [1.618, 2.618] +/// AD / XA ∈ [0.84, 0.93] (≈ 0.886 — the defining D completion) +/// ``` +/// +/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high), +/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/bat.rs`. +#[derive(Debug, Clone)] +pub struct Bat { + swing: SwingTracker, + has_emitted: bool, +} + +impl Bat { + /// Construct a new Bat detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 5), + has_emitted: false, + } + } +} + +impl Default for Bat { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for Bat { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 5 { + return Some(0.0); + } + let p = xabcd(pivots); + let xa = (p.a - p.x).abs(); + let ab = (p.b - p.a).abs(); + let bc = (p.c - p.b).abs(); + let cd = (p.d - p.c).abs(); + let ad = (p.d - p.a).abs(); + let matched = ratios_in(&[ + (ab / xa, 0.382, 0.50), + (bc / ab, 0.382, 0.886), + (cd / bc, 1.618, 2.618), + (ad / xa, 0.84, 0.93), + ]); + if matched { + return Some(if p.bullish { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 6 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "Bat" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = Bat::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = Bat::new(); + assert_eq!(indicator.name(), "Bat"); + assert_eq!(indicator.warmup_period(), 6); + assert!(!indicator.is_ready()); + assert!(!Bat::default().is_ready()); + } + + #[test] + fn bullish_bat_is_plus_one() { + let out = run(&[150.0, 100.0, 140.0, 122.0, 137.0, 104.56]); + assert_eq!(*out.last().unwrap(), 1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bearish_bat_is_minus_one() { + let out = run(&[150.0, 110.0, 128.0, 113.0, 145.44]); + assert_eq!(*out.last().unwrap(), -1.0); + } + + #[test] + fn out_of_ratio_does_not_trigger() { + let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = Bat::new(); + for c in candles_for_pivots(&[150.0, 100.0, 140.0]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 122.0, 137.0, 104.56]); + let mut a = Bat::new(); + let mut b = Bat::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/butterfly.rs b/crates/wickra-core/src/indicators/butterfly.rs new file mode 100644 index 00000000..77182295 --- /dev/null +++ b/crates/wickra-core/src/indicators/butterfly.rs @@ -0,0 +1,154 @@ +//! Butterfly harmonic pattern. + +use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Butterfly — a 5-point (X-A-B-C-D) harmonic pattern with a `0.786` B and an +/// **extended** D that overshoots X: +/// +/// ```text +/// AB / XA ∈ [0.74, 0.84] (≈ 0.786) +/// BC / AB ∈ [0.382, 0.886] +/// CD / BC ∈ [1.618, 2.618] +/// AD / XA ∈ [1.27, 1.618] (the defining extended D completion) +/// ``` +/// +/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high), +/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/butterfly.rs`. +#[derive(Debug, Clone)] +pub struct Butterfly { + swing: SwingTracker, + has_emitted: bool, +} + +impl Butterfly { + /// Construct a new Butterfly detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 5), + has_emitted: false, + } + } +} + +impl Default for Butterfly { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for Butterfly { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 5 { + return Some(0.0); + } + let p = xabcd(pivots); + let xa = (p.a - p.x).abs(); + let ab = (p.b - p.a).abs(); + let bc = (p.c - p.b).abs(); + let cd = (p.d - p.c).abs(); + let ad = (p.d - p.a).abs(); + let matched = ratios_in(&[ + (ab / xa, 0.74, 0.84), + (bc / ab, 0.382, 0.886), + (cd / bc, 1.618, 2.618), + (ad / xa, 1.27, 1.618), + ]); + if matched { + return Some(if p.bullish { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 6 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "Butterfly" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = Butterfly::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = Butterfly::new(); + assert_eq!(indicator.name(), "Butterfly"); + assert_eq!(indicator.warmup_period(), 6); + assert!(!indicator.is_ready()); + assert!(!Butterfly::default().is_ready()); + } + + #[test] + fn bullish_butterfly_is_plus_one() { + let out = run(&[150.0, 100.0, 140.0, 108.6, 128.0, 79.8]); + assert_eq!(*out.last().unwrap(), 1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bearish_butterfly_is_minus_one() { + let out = run(&[150.0, 110.0, 141.4, 121.4, 170.2]); + assert_eq!(*out.last().unwrap(), -1.0); + } + + #[test] + fn out_of_ratio_does_not_trigger() { + let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = Butterfly::new(); + for c in candles_for_pivots(&[150.0, 100.0, 140.0]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 108.6, 128.0, 79.8]); + let mut a = Butterfly::new(); + let mut b = Butterfly::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/crab.rs b/crates/wickra-core/src/indicators/crab.rs new file mode 100644 index 00000000..2b692c0b --- /dev/null +++ b/crates/wickra-core/src/indicators/crab.rs @@ -0,0 +1,154 @@ +//! Crab harmonic pattern. + +use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Crab — a 5-point (X-A-B-C-D) harmonic pattern with the deepest D completion +/// of the family, an `1.618` extension of XA: +/// +/// ```text +/// AB / XA ∈ [0.382, 0.618] +/// BC / AB ∈ [0.382, 0.886] +/// CD / BC ∈ [2.24, 3.618] (a very long terminal leg) +/// AD / XA ∈ [1.55, 1.65] (≈ 1.618 — the defining D completion) +/// ``` +/// +/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high), +/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/crab.rs`. +#[derive(Debug, Clone)] +pub struct Crab { + swing: SwingTracker, + has_emitted: bool, +} + +impl Crab { + /// Construct a new Crab detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 5), + has_emitted: false, + } + } +} + +impl Default for Crab { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for Crab { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 5 { + return Some(0.0); + } + let p = xabcd(pivots); + let xa = (p.a - p.x).abs(); + let ab = (p.b - p.a).abs(); + let bc = (p.c - p.b).abs(); + let cd = (p.d - p.c).abs(); + let ad = (p.d - p.a).abs(); + let matched = ratios_in(&[ + (ab / xa, 0.382, 0.618), + (bc / ab, 0.382, 0.886), + (cd / bc, 2.24, 3.618), + (ad / xa, 1.55, 1.65), + ]); + if matched { + return Some(if p.bullish { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 6 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "Crab" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = Crab::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = Crab::new(); + assert_eq!(indicator.name(), "Crab"); + assert_eq!(indicator.warmup_period(), 6); + assert!(!indicator.is_ready()); + assert!(!Crab::default().is_ready()); + } + + #[test] + fn bullish_crab_is_plus_one() { + let out = run(&[150.0, 100.0, 140.0, 120.0, 137.5, 75.3]); + assert_eq!(*out.last().unwrap(), 1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bearish_crab_is_minus_one() { + let out = run(&[150.0, 110.0, 130.0, 112.5, 174.7]); + assert_eq!(*out.last().unwrap(), -1.0); + } + + #[test] + fn out_of_ratio_does_not_trigger() { + let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = Crab::new(); + for c in candles_for_pivots(&[150.0, 100.0, 140.0]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 120.0, 137.5, 75.3]); + let mut a = Crab::new(); + let mut b = Crab::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/cypher.rs b/crates/wickra-core/src/indicators/cypher.rs new file mode 100644 index 00000000..57c017ed --- /dev/null +++ b/crates/wickra-core/src/indicators/cypher.rs @@ -0,0 +1,152 @@ +//! Cypher harmonic pattern. + +use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Cypher — a 5-point (X-A-B-C-D) harmonic pattern whose C leg is measured +/// against XA (not AB) and whose D retraces the XC leg by `0.786`: +/// +/// ```text +/// AB / XA ∈ [0.382, 0.618] +/// BC / XA ∈ [1.13, 1.414] (C extends beyond A, measured on XA) +/// CD / XC ∈ [0.74, 0.83] (≈ 0.786 retracement of XC — the D completion) +/// ``` +/// +/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high), +/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/cypher.rs`. +#[derive(Debug, Clone)] +pub struct Cypher { + swing: SwingTracker, + has_emitted: bool, +} + +impl Cypher { + /// Construct a new Cypher detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 5), + has_emitted: false, + } + } +} + +impl Default for Cypher { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for Cypher { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 5 { + return Some(0.0); + } + let p = xabcd(pivots); + let xa = (p.a - p.x).abs(); + let ab = (p.b - p.a).abs(); + let bc = (p.c - p.b).abs(); + let xc = (p.c - p.x).abs(); + let cd = (p.d - p.c).abs(); + let matched = ratios_in(&[ + (ab / xa, 0.382, 0.618), + (bc / xa, 1.13, 1.414), + (cd / xc, 0.74, 0.83), + ]); + if matched { + return Some(if p.bullish { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 6 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "Cypher" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = Cypher::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = Cypher::new(); + assert_eq!(indicator.name(), "Cypher"); + assert_eq!(indicator.warmup_period(), 6); + assert!(!indicator.is_ready()); + assert!(!Cypher::default().is_ready()); + } + + #[test] + fn bullish_cypher_is_plus_one() { + let out = run(&[150.0, 100.0, 140.0, 120.0, 168.0, 114.55]); + assert_eq!(*out.last().unwrap(), 1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bearish_cypher_is_minus_one() { + let out = run(&[150.0, 110.0, 130.0, 82.0, 135.45]); + assert_eq!(*out.last().unwrap(), -1.0); + } + + #[test] + fn out_of_ratio_does_not_trigger() { + let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = Cypher::new(); + for c in candles_for_pivots(&[150.0, 100.0, 140.0]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 120.0, 168.0, 114.55]); + let mut a = Cypher::new(); + let mut b = Cypher::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/gartley.rs b/crates/wickra-core/src/indicators/gartley.rs new file mode 100644 index 00000000..a941fd20 --- /dev/null +++ b/crates/wickra-core/src/indicators/gartley.rs @@ -0,0 +1,157 @@ +//! Gartley harmonic pattern. + +use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Gartley — the classic 5-point (X-A-B-C-D) harmonic pattern, recognised from +/// confirmed swing pivots when the legs fall inside the Gartley Fibonacci +/// windows: +/// +/// ```text +/// AB / XA ∈ [0.55, 0.70] (≈ 0.618 retracement of XA) +/// BC / AB ∈ [0.382, 0.886] +/// CD / BC ∈ [1.13, 1.618] +/// AD / XA ∈ [0.74, 0.84] (≈ 0.786 — the defining D completion) +/// ``` +/// +/// Output is `+1.0` when the terminal point D is a swing low (bullish +/// completion), `-1.0` when D is a swing high (bearish), and `0.0` otherwise; +/// never `None`. See `crates/wickra-core/src/indicators/gartley.rs`. +#[derive(Debug, Clone)] +pub struct Gartley { + swing: SwingTracker, + has_emitted: bool, +} + +impl Gartley { + /// Construct a new Gartley detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 5), + has_emitted: false, + } + } +} + +impl Default for Gartley { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for Gartley { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 5 { + return Some(0.0); + } + let p = xabcd(pivots); + let xa = (p.a - p.x).abs(); + let ab = (p.b - p.a).abs(); + let bc = (p.c - p.b).abs(); + let cd = (p.d - p.c).abs(); + let ad = (p.d - p.a).abs(); + let matched = ratios_in(&[ + (ab / xa, 0.55, 0.70), + (bc / ab, 0.382, 0.886), + (cd / bc, 1.13, 1.618), + (ad / xa, 0.74, 0.84), + ]); + if matched { + return Some(if p.bullish { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 6 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "Gartley" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = Gartley::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = Gartley::new(); + assert_eq!(indicator.name(), "Gartley"); + assert_eq!(indicator.warmup_period(), 6); + assert!(!indicator.is_ready()); + assert!(!Gartley::default().is_ready()); + } + + #[test] + fn bullish_gartley_is_plus_one() { + let out = run(&[150.0, 100.0, 140.0, 115.3, 127.65, 108.56]); + assert_eq!(*out.last().unwrap(), 1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bearish_gartley_is_minus_one() { + let out = run(&[150.0, 110.0, 134.7, 122.35, 141.44]); + assert_eq!(*out.last().unwrap(), -1.0); + } + + #[test] + fn out_of_ratio_does_not_trigger() { + // Five pivots but the D completion (AD/XA ≈ 0.25) is far from 0.786. + let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = Gartley::new(); + for c in candles_for_pivots(&[150.0, 100.0, 140.0]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 115.3, 127.65, 108.56]); + let mut a = Gartley::new(); + let mut b = Gartley::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs index d2c0d16d..3f2a1dda 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod pattern_swing; mod abandoned_baby; +mod abcd; mod absolute_breadth_index; mod acceleration_bands; mod accelerator_oscillator; @@ -40,6 +41,7 @@ mod avg_price; mod awesome_oscillator; mod awesome_oscillator_histogram; mod balance_of_power; +mod bat; mod belt_hold; mod beta; mod beta_neutral_spread; @@ -48,6 +50,7 @@ mod bollinger_bandwidth; mod breadth_thrust; mod breakaway; mod bullish_percent_index; +mod butterfly; mod calendar_spread; mod calmar_ratio; mod camarilla_pivots; @@ -70,10 +73,12 @@ mod conditional_value_at_risk; mod connors_rsi; mod coppock; mod counterattack; +mod crab; mod cumulative_volume_index; mod cup_and_handle; mod cvd; mod cybernetic_cycle; +mod cypher; mod day_of_week_profile; mod decycler; mod decycler_oscillator; @@ -119,6 +124,7 @@ mod funding_rate_zscore; mod gain_loss_ratio; mod gap_side_by_side_white; mod garman_klass; +mod gartley; mod granger_causality; mod gravestone_doji; mod hammer; @@ -262,6 +268,7 @@ mod separating_lines; mod session_high_low; mod session_range; mod session_vwap; +mod shark; mod sharpe_ratio; mod shooting_star; mod short_line; @@ -306,6 +313,7 @@ mod td_sequential; mod td_setup; mod tema; mod term_structure_basis; +mod three_drives; mod three_inside; mod three_line_strike; mod three_outside; @@ -369,6 +377,7 @@ mod zig_zag; mod zlema; pub use abandoned_baby::AbandonedBaby; +pub use abcd::Abcd; pub use absolute_breadth_index::AbsoluteBreadthIndex; pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput}; pub use accelerator_oscillator::AcceleratorOscillator; @@ -399,6 +408,7 @@ pub use avg_price::AvgPrice; pub use awesome_oscillator::AwesomeOscillator; pub use awesome_oscillator_histogram::AwesomeOscillatorHistogram; pub use balance_of_power::BalanceOfPower; +pub use bat::Bat; pub use belt_hold::BeltHold; pub use beta::Beta; pub use beta_neutral_spread::BetaNeutralSpread; @@ -407,6 +417,7 @@ pub use bollinger_bandwidth::BollingerBandwidth; pub use breadth_thrust::BreadthThrust; pub use breakaway::Breakaway; pub use bullish_percent_index::BullishPercentIndex; +pub use butterfly::Butterfly; pub use calendar_spread::CalendarSpread; pub use calmar_ratio::CalmarRatio; pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput}; @@ -429,10 +440,12 @@ pub use conditional_value_at_risk::ConditionalValueAtRisk; pub use connors_rsi::ConnorsRsi; pub use coppock::Coppock; pub use counterattack::Counterattack; +pub use crab::Crab; pub use cumulative_volume_index::CumulativeVolumeIndex; pub use cup_and_handle::CupAndHandle; pub use cvd::CumulativeVolumeDelta; pub use cybernetic_cycle::CyberneticCycle; +pub use cypher::Cypher; pub use day_of_week_profile::{DayOfWeekProfile, DayOfWeekProfileOutput}; pub use decycler::Decycler; pub use decycler_oscillator::DecyclerOscillator; @@ -478,6 +491,7 @@ pub use funding_rate_zscore::FundingRateZScore; pub use gain_loss_ratio::GainLossRatio; pub use gap_side_by_side_white::GapSideBySideWhite; pub use garman_klass::GarmanKlassVolatility; +pub use gartley::Gartley; pub use granger_causality::GrangerCausality; pub use gravestone_doji::GravestoneDoji; pub use hammer::Hammer; @@ -621,6 +635,7 @@ pub use separating_lines::SeparatingLines; pub use session_high_low::{SessionHighLow, SessionHighLowOutput}; pub use session_range::{SessionRange, SessionRangeOutput}; pub use session_vwap::SessionVwap; +pub use shark::Shark; pub use sharpe_ratio::SharpeRatio; pub use shooting_star::ShootingStar; pub use short_line::ShortLine; @@ -665,6 +680,7 @@ pub use td_sequential::{TdSequential, TdSequentialOutput}; pub use td_setup::TdSetup; pub use tema::Tema; pub use term_structure_basis::TermStructureBasis; +pub use three_drives::ThreeDrives; pub use three_inside::ThreeInside; pub use three_line_strike::ThreeLineStrike; pub use three_outside::ThreeOutside; @@ -1193,6 +1209,19 @@ pub const FAMILIES: &[(&str, &[&str])] = &[ "CupAndHandle", ], ), + ( + "Harmonic Patterns", + &[ + "Abcd", + "Gartley", + "Butterfly", + "Bat", + "Crab", + "Shark", + "Cypher", + "ThreeDrives", + ], + ), ]; #[cfg(test)] @@ -1221,6 +1250,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, 359, "FAMILIES total drifted from indicator count"); + assert_eq!(total, 367, "FAMILIES total drifted from indicator count"); } } diff --git a/crates/wickra-core/src/indicators/pattern_swing.rs b/crates/wickra-core/src/indicators/pattern_swing.rs index fe5462e1..05222181 100644 --- a/crates/wickra-core/src/indicators/pattern_swing.rs +++ b/crates/wickra-core/src/indicators/pattern_swing.rs @@ -191,6 +191,47 @@ pub(crate) fn approx_equal(a: f64, b: f64, tol: f64) -> bool { (a - b).abs() <= tol * scale } +/// The five most recent pivots interpreted as the X-A-B-C-D points of a harmonic +/// pattern, with the terminal direction. The slice must hold at least five +/// pivots. Each detector derives the leg lengths and Fibonacci ratios it needs +/// from these five prices. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Xabcd { + pub x: f64, + pub a: f64, + pub b: f64, + pub c: f64, + pub d: f64, + /// `true` when the terminal point D is a swing low (a bullish, buy-side + /// completion); `false` when D is a swing high (bearish). + pub bullish: bool, +} + +/// Read the last five pivots as an [`Xabcd`]. Pivots are guaranteed nonzero-leg +/// (the swing tracker only confirms moves of at least the threshold), so the +/// leg-ratio divisions in the detectors never divide by zero. +pub(crate) fn xabcd(pivots: &[Pivot]) -> Xabcd { + let n = pivots.len(); + Xabcd { + x: pivots[n - 5].price, + a: pivots[n - 4].price, + b: pivots[n - 3].price, + c: pivots[n - 2].price, + d: pivots[n - 1].price, + bullish: pivots[n - 1].direction < 0.0, + } +} + +/// `true` when every `(value, low, high)` triple satisfies `low <= value <= high`. +/// Harmonic detectors express their Fibonacci windows as a list of these triples; +/// evaluating them in one expression keeps the per-triple comparison on a single +/// line (no multi-line `&&` coverage gaps). +pub(crate) fn ratios_in(checks: &[(f64, f64, f64)]) -> bool { + checks + .iter() + .all(|&(value, low, high)| value >= low && value <= high) +} + /// Build a candle sequence that drives a `SwingTracker` (or any detector built /// on one) to confirm exactly the given alternating pivot prices, in order. /// @@ -378,6 +419,49 @@ mod tests { assert_eq!(recent_legs(&ending_low), (120.0, 110.0, 100.0, 99.0)); } + #[test] + fn xabcd_reads_last_five_pivots_and_direction() { + let pivots = [ + Pivot { + price: 50.0, + direction: 1.0, + }, + Pivot { + price: 100.0, + direction: -1.0, + }, // X + Pivot { + price: 140.0, + direction: 1.0, + }, // A + Pivot { + price: 115.0, + direction: -1.0, + }, // B + Pivot { + price: 128.0, + direction: 1.0, + }, // C + Pivot { + price: 108.0, + direction: -1.0, + }, // D (low → bullish) + ]; + let p = xabcd(&pivots); + assert_eq!( + (p.x, p.a, p.b, p.c, p.d), + (100.0, 140.0, 115.0, 128.0, 108.0) + ); + assert!(p.bullish); + } + + #[test] + fn ratios_in_checks_every_window() { + assert!(ratios_in(&[(0.6, 0.5, 0.7), (1.5, 1.0, 2.0)])); + assert!(!ratios_in(&[(0.6, 0.5, 0.7), (3.0, 1.0, 2.0)])); // second out of range + assert!(!ratios_in(&[(0.4, 0.5, 0.7)])); // below the window + } + #[test] fn candles_for_pivots_realizes_the_requested_swings() { let want = [120.0, 100.0, 125.0, 95.0]; diff --git a/crates/wickra-core/src/indicators/shark.rs b/crates/wickra-core/src/indicators/shark.rs new file mode 100644 index 00000000..59814a8a --- /dev/null +++ b/crates/wickra-core/src/indicators/shark.rs @@ -0,0 +1,155 @@ +//! Shark harmonic pattern. + +use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Shark — a 5-point (X-A-B-C-D) harmonic pattern characterised by an +/// **expansion** leg (AB longer than XA) and a `0.886`–`1.13` D completion: +/// +/// ```text +/// AB / XA ∈ [1.13, 1.618] (expansion — B overshoots X) +/// BC / AB ∈ [1.618, 2.24] +/// CD / BC ∈ [0.382, 0.886] +/// AD / XA ∈ [0.886, 1.13] (the defining D completion near A) +/// ``` +/// +/// This is the 5-point reading of the Shark; output is `+1.0` (bullish, D a +/// swing low), `-1.0` (bearish, D a swing high), or `0.0`; never `None`. See +/// `crates/wickra-core/src/indicators/shark.rs`. +#[derive(Debug, Clone)] +pub struct Shark { + swing: SwingTracker, + has_emitted: bool, +} + +impl Shark { + /// Construct a new Shark detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 5), + has_emitted: false, + } + } +} + +impl Default for Shark { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for Shark { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 5 { + return Some(0.0); + } + let p = xabcd(pivots); + let xa = (p.a - p.x).abs(); + let ab = (p.b - p.a).abs(); + let bc = (p.c - p.b).abs(); + let cd = (p.d - p.c).abs(); + let ad = (p.d - p.a).abs(); + let matched = ratios_in(&[ + (ab / xa, 1.13, 1.618), + (bc / ab, 1.618, 2.24), + (cd / bc, 0.382, 0.886), + (ad / xa, 0.886, 1.13), + ]); + if matched { + return Some(if p.bullish { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 6 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "Shark" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = Shark::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = Shark::new(); + assert_eq!(indicator.name(), "Shark"); + assert_eq!(indicator.warmup_period(), 6); + assert!(!indicator.is_ready()); + assert!(!Shark::default().is_ready()); + } + + #[test] + fn bullish_shark_is_plus_one() { + let out = run(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]); + assert_eq!(*out.last().unwrap(), 1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bearish_shark_is_minus_one() { + let out = run(&[150.0, 110.0, 162.0, 60.2, 150.0]); + assert_eq!(*out.last().unwrap(), -1.0); + } + + #[test] + fn out_of_ratio_does_not_trigger() { + let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = Shark::new(); + for c in candles_for_pivots(&[150.0, 100.0, 140.0]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]); + let mut a = Shark::new(); + let mut b = Shark::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/three_drives.rs b/crates/wickra-core/src/indicators/three_drives.rs new file mode 100644 index 00000000..e42d3039 --- /dev/null +++ b/crates/wickra-core/src/indicators/three_drives.rs @@ -0,0 +1,155 @@ +//! Three Drives harmonic pattern. + +use crate::indicators::pattern_swing::{ + approx_equal, ratios_in, xabcd, SwingTracker, SWING_THRESHOLD, +}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Three Drives — a symmetric harmonic pattern of two visible drives separated +/// by two retracements, read from the last five pivots `X-A-B-C-D` (the two +/// drive legs are `A→B` and `C→D`): +/// +/// ```text +/// AB / XA ∈ [1.13, 1.75] (drive 1 extends the prior retracement) +/// CD / BC ∈ [1.13, 1.75] (drive 2 extends symmetrically) +/// AB ≈ CD (within 20%) (the two drives are similar in size) +/// XA ≈ BC (within 30%) (the two retracements are similar) +/// ``` +/// +/// Output is `+1.0` (bullish, terminal D a swing low — drives down), `-1.0` +/// (bearish, drives up), or `0.0`; never `None`. See +/// `crates/wickra-core/src/indicators/three_drives.rs`. +#[derive(Debug, Clone)] +pub struct ThreeDrives { + swing: SwingTracker, + has_emitted: bool, +} + +impl ThreeDrives { + /// Construct a new Three Drives detector. + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 5), + has_emitted: false, + } + } +} + +impl Default for ThreeDrives { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for ThreeDrives { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + self.has_emitted = true; + if !self.swing.update(candle) { + return Some(0.0); + } + let pivots = self.swing.pivots(); + if pivots.len() < 5 { + return Some(0.0); + } + let p = xabcd(pivots); + let xa = (p.a - p.x).abs(); + let ab = (p.b - p.a).abs(); + let bc = (p.c - p.b).abs(); + let cd = (p.d - p.c).abs(); + let extensions = ratios_in(&[(ab / xa, 1.13, 1.75), (cd / bc, 1.13, 1.75)]); + let symmetric = approx_equal(ab, cd, 0.20) && approx_equal(xa, bc, 0.30); + if extensions && symmetric { + return Some(if p.bullish { 1.0 } else { -1.0 }); + } + Some(0.0) + } + + fn reset(&mut self) { + self.swing.reset(); + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 6 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "ThreeDrives" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + + fn run(pivots: &[f64]) -> Vec { + let mut indicator = ThreeDrives::new(); + candles_for_pivots(pivots) + .into_iter() + .map(|c| indicator.update(c).unwrap()) + .collect() + } + + #[test] + fn accessors_and_metadata() { + let indicator = ThreeDrives::new(); + assert_eq!(indicator.name(), "ThreeDrives"); + assert_eq!(indicator.warmup_period(), 6); + assert!(!indicator.is_ready()); + assert!(!ThreeDrives::default().is_ready()); + } + + #[test] + fn bearish_three_drives_is_minus_one() { + // Three rising drives (120, 128, 136) → bearish exhaustion. + let out = run(&[120.0, 100.0, 128.0, 108.0, 136.0]); + assert_eq!(*out.last().unwrap(), -1.0); + assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0)); + } + + #[test] + fn bullish_three_drives_is_plus_one() { + // Three falling drives → bullish exhaustion. + let out = run(&[150.0, 120.0, 140.0, 112.0, 132.0, 104.0]); + assert_eq!(*out.last().unwrap(), 1.0); + } + + #[test] + fn asymmetric_drives_do_not_trigger() { + let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]); + assert_eq!(*out.last().unwrap(), 0.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = ThreeDrives::new(); + for c in candles_for_pivots(&[120.0, 100.0, 128.0]) { + let _ = indicator.update(c); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert_eq!(indicator.update(c), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[120.0, 100.0, 128.0, 108.0, 136.0]); + let mut a = ThreeDrives::new(); + let mut b = ThreeDrives::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs index 66acb496..db7f5bc1 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -56,29 +56,30 @@ pub use cross_section::{CrossSection, Member}; pub use derivatives::DerivativesTick; pub use error::{Error, Result}; pub use indicators::{ - AbandonedBaby, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput, + AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock, AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma, Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDailyRange, AverageDrawdown, AvgPrice, - AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, + AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BeltHold, Beta, BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, BreadthThrust, - Breakaway, BullishPercentIndex, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, - Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, - ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, - ClassicPivots, ClassicPivotsOutput, ClosingMarubozu, Cmo, CoefficientOfVariation, - Cointegration, CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, - Coppock, Counterattack, CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle, - CyberneticCycle, DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, - DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, - DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, - DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, - DrawdownDuration, Dx, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema, + Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio, Camarilla, + CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, + ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, + ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, ClosingMarubozu, + Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput, ConcealingBabySwallow, + ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, Crab, CumulativeVolumeDelta, + CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher, DayOfWeekProfile, + DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots, + DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar, Donchian, + DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, + DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx, + EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama, FibonacciPivots, FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, - GarmanKlassVolatility, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, + GarmanKlassVolatility, Gartley, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent, @@ -105,7 +106,7 @@ pub use indicators::{ RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollingCorrelation, RollingCovariance, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines, - SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap, + SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands, @@ -114,9 +115,9 @@ pub use indicators::{ SuperTrendOutput, TakerBuySellRatio, 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, TickIndex, Tii, - TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput, + TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside, + ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, + Tii, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput, TradeImbalance, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, diff --git a/docs/README.md b/docs/README.md index 235e2e9f..e8c95478 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ That includes: [Python](https://docs.wickra.org/Quickstart-Python), [Node](https://docs.wickra.org/Quickstart-Node), and [WASM](https://docs.wickra.org/Quickstart-WASM). -- A per-indicator deep dive for every one of the **359 indicators** across +- A per-indicator deep dive for every one of the **367 indicators** across the sixteen families (Moving Averages, Momentum Oscillators, Trend & Directional, Price Oscillators, Volatility & Bands, Bands & Channels, Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots & diff --git a/fuzz/fuzz_targets/indicator_update_candle.rs b/fuzz/fuzz_targets/indicator_update_candle.rs index fc04e3ca..ee779be6 100644 --- a/fuzz/fuzz_targets/indicator_update_candle.rs +++ b/fuzz/fuzz_targets/indicator_update_candle.rs @@ -22,7 +22,7 @@ //! WeightedClose. use libfuzzer_sys::fuzz_target; -use wickra_core::{AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, CupAndHandle, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, 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, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag}; +use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag}; /// Convert a flat `f64` stream into a `Vec` by chunking it into /// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV @@ -382,4 +382,14 @@ fuzz_target!(|data: Vec| { drive(TripleTopBottom::new, &candles); drive(DoubleTopBottom::new, &candles); + // --- Harmonic Patterns --- + drive(ThreeDrives::new, &candles); + drive(Cypher::new, &candles); + drive(Shark::new, &candles); + drive(Crab::new, &candles); + drive(Bat::new, &candles); + drive(Butterfly::new, &candles); + drive(Gartley::new, &candles); + drive(Abcd::new, &candles); + });