diff --git a/CHANGELOG.md b/CHANGELOG.md index 324b74ef..6822ccb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **Fibonacci Confluence** — densest cluster of retracement levels across recent swing legs (price + strength) (`FIB_CONFLUENCE`). +- **Golden Pocket** — the 0.618-0.65 optimal-trade-entry band of the most recent swing leg (`GOLDEN_POCKET`). +- **Auto-Fibonacci** — retracement anchored on the dominant (largest-magnitude) leg among recent swings (`AUTO_FIB`). +- **Fibonacci Projection** — measured-move target zone from the last three pivots (A-B-C), projecting A->B from C (`FIB_PROJECTION`). +- **Fibonacci Extension** — projects the latest swing leg to the canonical extension ratios (127.2/141.4/161.8/200/261.8%) (`FIB_EXTENSION`). +- **Fibonacci Retracement** — seven retracement levels (0/23.6/38.2/50/61.8/78.6/100%) of the most recent confirmed swing leg (`FIB_RETRACEMENT`). ## [0.5.2] - 2026-06-03 diff --git a/README.md b/README.md index 2805dab6..762553d0 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 367 indicators; start at the + every one of the 373 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 -367 streaming-first indicators across twenty-three families. Every one passes the +373 streaming-first indicators across twenty-four 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). @@ -160,6 +160,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | 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 | +| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence | | 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 | @@ -244,7 +245,7 @@ A Python live-trading example using the public `websockets` package lives at ``` wickra/ ├── crates/ -│ ├── wickra-core/ core engine + all 367 indicators +│ ├── wickra-core/ core engine + all 373 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 2f0d4bbc..92cf7707 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -385,6 +385,12 @@ const multi = { // Family 13: Ichimoku & alternative charts Ichimoku: { make: () => new wickra.Ichimoku(9, 26, 52, 26), fields: ['tenkan', 'kijun', 'senkouA', 'senkouB', 'chikou'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, HeikinAshi: { make: () => new wickra.HeikinAshi(), fields: ['open', 'high', 'low', 'close'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + FibRetracement: { make: () => new wickra.FibRetracement(), fields: ['level0', 'level236', 'level382', 'level500', 'level618', 'level786', 'level1000'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + FibExtension: { make: () => new wickra.FibExtension(), fields: ['level1272', 'level1414', 'level1618', 'level2000', 'level2618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + FibProjection: { make: () => new wickra.FibProjection(), fields: ['level618', 'level1000', 'level1618', 'level2618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + AutoFib: { make: () => new wickra.AutoFib(), fields: ['level0', 'level236', 'level382', 'level500', 'level618', 'level786', 'level1000'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + GoldenPocket: { make: () => new wickra.GoldenPocket(), fields: ['low', 'mid', 'high'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + FibConfluence: { make: () => new wickra.FibConfluence(), fields: ['price', 'strength'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, }; for (const [name, d] of Object.entries(multi)) { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 1763259e..5af4fcb4 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -362,6 +362,46 @@ export interface OvernightIntradayReturnValue { overnight: number intraday: number } +export interface FibRetracementValue { + level0: number + level236: number + level382: number + level500: number + level618: number + level786: number + level1000: number +} +export interface FibExtensionValue { + level1272: number + level1414: number + level1618: number + level2000: number + level2618: number +} +export interface FibProjectionValue { + level618: number + level1000: number + level1618: number + level2618: number +} +export interface AutoFibValue { + level0: number + level236: number + level382: number + level500: number + level618: number + level786: number + level1000: number +} +export interface GoldenPocketValue { + low: number + mid: number + high: number +} +export interface FibConfluenceValue { + price: number + strength: number +} export type SmaNode = SMA export declare class SMA { constructor(period: number) @@ -3832,3 +3872,57 @@ export declare class OvernightIntradayReturn { isReady(): boolean warmupPeriod(): number } +export type FibRetracementNode = FibRetracement +export declare class FibRetracement { + constructor() + update(high: number, low: number): FibRetracementValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type FibExtensionNode = FibExtension +export declare class FibExtension { + constructor() + update(high: number, low: number): FibExtensionValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type FibProjectionNode = FibProjection +export declare class FibProjection { + constructor() + update(high: number, low: number): FibProjectionValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type AutoFibNode = AutoFib +export declare class AutoFib { + constructor() + update(high: number, low: number): AutoFibValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type GoldenPocketNode = GoldenPocket +export declare class GoldenPocket { + constructor() + update(high: number, low: number): GoldenPocketValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type FibConfluenceNode = FibConfluence +export declare class FibConfluence { + constructor() + update(high: number, low: number): FibConfluenceValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} diff --git a/bindings/node/index.js b/bindings/node/index.js index dbeca076..6e1c77ea 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, 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 +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, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -680,3 +680,9 @@ module.exports.TurnOfMonth = TurnOfMonth module.exports.SessionHighLow = SessionHighLow module.exports.SessionRange = SessionRange module.exports.OvernightIntradayReturn = OvernightIntradayReturn +module.exports.FibRetracement = FibRetracement +module.exports.FibExtension = FibExtension +module.exports.FibProjection = FibProjection +module.exports.AutoFib = AutoFib +module.exports.GoldenPocket = GoldenPocket +module.exports.FibConfluence = FibConfluence diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 5502af95..2634f8e5 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -14030,3 +14030,455 @@ impl OvernightIntradayReturnNode { self.inner.warmup_period() as u32 } } + +// ============================== Fibonacci ============================== + +/// Build a candle for the swing-based Fibonacci tools from a `high`/`low` pair; +/// only the high and low drive the swing tracker, so open/close are the midpoint. +fn swing_cnd(high: f64, low: f64) -> napi::Result { + cnd(high, low, f64::midpoint(high, low), 0.0) +} + +#[napi(object)] +pub struct FibRetracementValue { + pub level_0: f64, + pub level_236: f64, + pub level_382: f64, + pub level_500: f64, + pub level_618: f64, + pub level_786: f64, + pub level_1000: f64, +} + +#[napi(js_name = "FibRetracement")] +pub struct FibRetracementNode { + inner: wc::FibRetracement, +} + +#[napi] +impl FibRetracementNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::FibRetracement::new(), + } + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(swing_cnd(high, low)?) + .map(|o| FibRetracementValue { + level_0: o.level_0, + level_236: o.level_236, + level_382: o.level_382, + level_500: o.level_500, + level_618: o.level_618, + level_786: o.level_786, + level_1000: o.level_1000, + })) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 7]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) { + out[i * 7] = o.level_0; + out[i * 7 + 1] = o.level_236; + out[i * 7 + 2] = o.level_382; + out[i * 7 + 3] = o.level_500; + out[i * 7 + 4] = o.level_618; + out[i * 7 + 5] = o.level_786; + out[i * 7 + 6] = o.level_1000; + } + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +#[napi(object)] +pub struct FibExtensionValue { + pub level_1272: f64, + pub level_1414: f64, + pub level_1618: f64, + pub level_2000: f64, + pub level_2618: f64, +} + +#[napi(js_name = "FibExtension")] +pub struct FibExtensionNode { + inner: wc::FibExtension, +} + +#[napi] +impl FibExtensionNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::FibExtension::new(), + } + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(swing_cnd(high, low)?) + .map(|o| FibExtensionValue { + level_1272: o.level_1272, + level_1414: o.level_1414, + level_1618: o.level_1618, + level_2000: o.level_2000, + level_2618: o.level_2618, + })) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 5]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) { + out[i * 5] = o.level_1272; + out[i * 5 + 1] = o.level_1414; + out[i * 5 + 2] = o.level_1618; + out[i * 5 + 3] = o.level_2000; + out[i * 5 + 4] = o.level_2618; + } + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +#[napi(object)] +pub struct FibProjectionValue { + pub level_618: f64, + pub level_1000: f64, + pub level_1618: f64, + pub level_2618: f64, +} + +#[napi(js_name = "FibProjection")] +pub struct FibProjectionNode { + inner: wc::FibProjection, +} + +#[napi] +impl FibProjectionNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::FibProjection::new(), + } + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(swing_cnd(high, low)?) + .map(|o| FibProjectionValue { + level_618: o.level_618, + level_1000: o.level_1000, + level_1618: o.level_1618, + level_2618: o.level_2618, + })) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 4]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) { + out[i * 4] = o.level_618; + out[i * 4 + 1] = o.level_1000; + out[i * 4 + 2] = o.level_1618; + out[i * 4 + 3] = o.level_2618; + } + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +#[napi(object)] +pub struct AutoFibValue { + pub level_0: f64, + pub level_236: f64, + pub level_382: f64, + pub level_500: f64, + pub level_618: f64, + pub level_786: f64, + pub level_1000: f64, +} + +#[napi(js_name = "AutoFib")] +pub struct AutoFibNode { + inner: wc::AutoFib, +} + +#[napi] +impl AutoFibNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::AutoFib::new(), + } + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(swing_cnd(high, low)?) + .map(|o| AutoFibValue { + level_0: o.level_0, + level_236: o.level_236, + level_382: o.level_382, + level_500: o.level_500, + level_618: o.level_618, + level_786: o.level_786, + level_1000: o.level_1000, + })) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 7]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) { + out[i * 7] = o.level_0; + out[i * 7 + 1] = o.level_236; + out[i * 7 + 2] = o.level_382; + out[i * 7 + 3] = o.level_500; + out[i * 7 + 4] = o.level_618; + out[i * 7 + 5] = o.level_786; + out[i * 7 + 6] = o.level_1000; + } + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +#[napi(object)] +pub struct GoldenPocketValue { + pub low: f64, + pub mid: f64, + pub high: f64, +} + +#[napi(js_name = "GoldenPocket")] +pub struct GoldenPocketNode { + inner: wc::GoldenPocket, +} + +#[napi] +impl GoldenPocketNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::GoldenPocket::new(), + } + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(swing_cnd(high, low)?) + .map(|o| GoldenPocketValue { + low: o.low, + mid: o.mid, + high: o.high, + })) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) { + out[i * 3] = o.low; + out[i * 3 + 1] = o.mid; + out[i * 3 + 2] = o.high; + } + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +#[napi(object)] +pub struct FibConfluenceValue { + pub price: f64, + pub strength: f64, +} + +#[napi(js_name = "FibConfluence")] +pub struct FibConfluenceNode { + inner: wc::FibConfluence, +} + +#[napi] +impl FibConfluenceNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::FibConfluence::new(), + } + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(swing_cnd(high, low)?) + .map(|o| FibConfluenceValue { + price: o.price, + strength: o.strength, + })) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) { + out[i * 2] = o.price; + out[i * 2 + 1] = o.strength; + } + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +impl Default for FibRetracementNode { + fn default() -> Self { + Self::new() + } +} + +impl Default for FibExtensionNode { + fn default() -> Self { + Self::new() + } +} + +impl Default for FibProjectionNode { + fn default() -> Self { + Self::new() + } +} + +impl Default for AutoFibNode { + fn default() -> Self { + Self::new() + } +} + +impl Default for GoldenPocketNode { + fn default() -> Self { + Self::new() + } +} + +impl Default for FibConfluenceNode { + fn default() -> Self { + Self::new() + } +} diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index 59c65c3e..b5c2c550 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -339,6 +339,13 @@ from ._wickra import ( Butterfly, Gartley, Abcd, + # Fibonacci + FibConfluence, + GoldenPocket, + AutoFib, + FibProjection, + FibExtension, + FibRetracement, # Microstructure: order book OrderBookImbalanceTop1, OrderBookImbalanceTopN, @@ -734,6 +741,13 @@ __all__ = [ "Butterfly", "Gartley", "Abcd", + # Fibonacci + "FibConfluence", + "GoldenPocket", + "AutoFib", + "FibProjection", + "FibExtension", + "FibRetracement", # Microstructure: order book "OrderBookImbalanceTop1", "OrderBookImbalanceTopN", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index b79c93e8..ec00e7dd 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -49,6 +49,8 @@ const NON_CONTIGUOUS: &str = "array must be C-contiguous; pass np.ascontiguousar /// `(pp, r1, r2, r3, s1, s2, s3)` pivot levels returned by Classic/Fibonacci pivots. type PivotLevels = (f64, f64, f64, f64, f64, f64, f64); +/// The five Fibonacci-extension levels returned by `FibExtension`. +type FibExtLevels = (f64, f64, f64, f64, f64); /// `(pp, r1, r2, s1, s2)` pivot levels returned by Woodie pivots. type WoodieLevels = (f64, f64, f64, f64, f64); /// `(tenkan, kijun, senkou_a, senkou_b, chikou)` Ichimoku lines, each optional during warmup. @@ -17846,6 +17848,451 @@ impl PyOvernightIntradayReturn { } } +// ============================== Fibonacci ============================== + +/// Build a candle for the swing-based Fibonacci tools from a `high`/`low` pair. +/// Only the high and low drive the swing tracker, so open and close are pinned +/// to the midpoint to keep the OHLC invariants valid. +fn swing_candle(high: f64, low: f64) -> Result { + let mid = f64::midpoint(high, low); + wc::Candle::new(mid, high, low, mid, 0.0, 0) +} + +#[pyclass( + name = "FibRetracement", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyFibRetracement { + inner: wc::FibRetracement, +} + +#[pymethods] +impl PyFibRetracement { + #[new] + fn new() -> Self { + Self { + inner: wc::FibRetracement::new(), + } + } + /// Returns `(level_0, …, level_1000)` (seven levels) or None during warmup. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| { + ( + o.level_0, + o.level_236, + o.level_382, + o.level_500, + o.level_618, + o.level_786, + o.level_1000, + ) + })) + } + /// Batch over numpy columns high, low. Returns shape `(n, 7)`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 7]; + for i in 0..n { + if let Some(o) = self + .inner + .update(swing_candle(h[i], l[i]).map_err(map_err)?) + { + out[i * 7] = o.level_0; + out[i * 7 + 1] = o.level_236; + out[i * 7 + 2] = o.level_382; + out[i * 7 + 3] = o.level_500; + out[i * 7 + 4] = o.level_618; + out[i * 7 + 5] = o.level_786; + out[i * 7 + 6] = o.level_1000; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 7), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "FibRetracement()".to_string() + } +} + +#[pyclass(name = "FibExtension", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyFibExtension { + inner: wc::FibExtension, +} + +#[pymethods] +impl PyFibExtension { + #[new] + fn new() -> Self { + Self { + inner: wc::FibExtension::new(), + } + } + /// Returns `(level_1272, level_1414, level_1618, level_2000, level_2618)` or None. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| { + ( + o.level_1272, + o.level_1414, + o.level_1618, + o.level_2000, + o.level_2618, + ) + })) + } + /// Batch over numpy columns high, low. Returns shape `(n, 5)`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 5]; + for i in 0..n { + if let Some(o) = self + .inner + .update(swing_candle(h[i], l[i]).map_err(map_err)?) + { + out[i * 5] = o.level_1272; + out[i * 5 + 1] = o.level_1414; + out[i * 5 + 2] = o.level_1618; + out[i * 5 + 3] = o.level_2000; + out[i * 5 + 4] = o.level_2618; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 5), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "FibExtension()".to_string() + } +} + +#[pyclass(name = "FibProjection", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyFibProjection { + inner: wc::FibProjection, +} + +#[pymethods] +impl PyFibProjection { + #[new] + fn new() -> Self { + Self { + inner: wc::FibProjection::new(), + } + } + /// Returns `(level_618, level_1000, level_1618, level_2618)` or None during warmup. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self + .inner + .update(c) + .map(|o| (o.level_618, o.level_1000, o.level_1618, o.level_2618))) + } + /// Batch over numpy columns high, low. Returns shape `(n, 4)`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 4]; + for i in 0..n { + if let Some(o) = self + .inner + .update(swing_candle(h[i], l[i]).map_err(map_err)?) + { + out[i * 4] = o.level_618; + out[i * 4 + 1] = o.level_1000; + out[i * 4 + 2] = o.level_1618; + out[i * 4 + 3] = o.level_2618; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 4), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "FibProjection()".to_string() + } +} + +#[pyclass(name = "AutoFib", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyAutoFib { + inner: wc::AutoFib, +} + +#[pymethods] +impl PyAutoFib { + #[new] + fn new() -> Self { + Self { + inner: wc::AutoFib::new(), + } + } + /// Returns `(level_0, …, level_1000)` for the dominant leg, or None during warmup. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| { + ( + o.level_0, + o.level_236, + o.level_382, + o.level_500, + o.level_618, + o.level_786, + o.level_1000, + ) + })) + } + /// Batch over numpy columns high, low. Returns shape `(n, 7)`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 7]; + for i in 0..n { + if let Some(o) = self + .inner + .update(swing_candle(h[i], l[i]).map_err(map_err)?) + { + out[i * 7] = o.level_0; + out[i * 7 + 1] = o.level_236; + out[i * 7 + 2] = o.level_382; + out[i * 7 + 3] = o.level_500; + out[i * 7 + 4] = o.level_618; + out[i * 7 + 5] = o.level_786; + out[i * 7 + 6] = o.level_1000; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 7), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "AutoFib()".to_string() + } +} + +#[pyclass(name = "GoldenPocket", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyGoldenPocket { + inner: wc::GoldenPocket, +} + +#[pymethods] +impl PyGoldenPocket { + #[new] + fn new() -> Self { + Self { + inner: wc::GoldenPocket::new(), + } + } + /// Returns `(low, mid, high)` of the golden-pocket band, or None during warmup. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.low, o.mid, o.high))) + } + /// Batch over numpy columns high, low. Returns shape `(n, 3)`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + if let Some(o) = self + .inner + .update(swing_candle(h[i], l[i]).map_err(map_err)?) + { + out[i * 3] = o.low; + out[i * 3 + 1] = o.mid; + out[i * 3 + 2] = o.high; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "GoldenPocket()".to_string() + } +} + +#[pyclass(name = "FibConfluence", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyFibConfluence { + inner: wc::FibConfluence, +} + +#[pymethods] +impl PyFibConfluence { + #[new] + fn new() -> Self { + Self { + inner: wc::FibConfluence::new(), + } + } + /// Returns `(price, strength)` of the densest cluster, or None during warmup. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.price, o.strength))) + } + /// Batch over numpy columns high, low. Returns shape `(n, 2)`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + if let Some(o) = self + .inner + .update(swing_candle(h[i], l[i]).map_err(map_err)?) + { + out[i * 2] = o.price; + out[i * 2 + 1] = o.strength; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "FibConfluence()".to_string() + } +} + #[pymodule] #[allow(clippy::too_many_lines)] fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -18228,5 +18675,12 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + // Fibonacci. + 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 a80b807b..2d34cc89 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -860,6 +860,36 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv): # --- Candle-input, multi-output indicators -------------------------------- MULTI = { + "FibRetracement": ( + lambda: ta.FibRetracement(), + lambda ind, h, l, c, v: ind.batch(h, l), + 7, + ), + "FibExtension": ( + lambda: ta.FibExtension(), + lambda ind, h, l, c, v: ind.batch(h, l), + 5, + ), + "FibProjection": ( + lambda: ta.FibProjection(), + lambda ind, h, l, c, v: ind.batch(h, l), + 4, + ), + "AutoFib": ( + lambda: ta.AutoFib(), + lambda ind, h, l, c, v: ind.batch(h, l), + 7, + ), + "GoldenPocket": ( + lambda: ta.GoldenPocket(), + lambda ind, h, l, c, v: ind.batch(h, l), + 3, + ), + "FibConfluence": ( + lambda: ta.FibConfluence(), + lambda ind, h, l, c, v: ind.batch(h, l), + 2, + ), "Vortex": ( lambda: ta.Vortex(14), lambda ind, h, l, c, v: ind.batch(h, l, c), @@ -2578,6 +2608,50 @@ def test_three_drives_reference(): 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) + +def test_fib_retracement_reference(): + t = ta.FibRetracement() + assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None + assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None + assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((100.0, 123.6, 138.2, 150.0, 161.8, 178.6, 200.0)) + + +def test_fib_extension_reference(): + t = ta.FibExtension() + assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None + assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None + assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((72.8, 58.6, 38.2, 0.0, -61.8)) + + +def test_fib_projection_reference(): + t = ta.FibProjection() + assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None + assert t.update((160.0, 198.0, 160.0, 160.0, 1.0, 1)) is None + assert t.update((161.6, 190.0, 161.6, 161.6, 1.0, 2)) is None + assert t.update((171.0, 188.1, 171.0, 171.0, 1.0, 3)) == pytest.approx((165.28, 150.0, 125.28, 85.28)) + + +def test_auto_fib_reference(): + t = ta.AutoFib() + assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None + assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None + assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((100.0, 123.6, 138.2, 150.0, 161.8, 178.6, 200.0)) + + +def test_golden_pocket_reference(): + t = ta.GoldenPocket() + assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None + assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None + assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((161.8, 163.4, 165.0)) + + +def test_fib_confluence_reference(): + t = ta.FibConfluence() + assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None + assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None + assert t.update((101.0, 160.0, 101.0, 101.0, 1.0, 2)) is None + assert t.update((144.0, 158.4, 144.0, 144.0, 1.0, 3)) == pytest.approx((137.64, 2.0)) + # --- Lifecycle ------------------------------------------------------------ diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index e9d88894..99c9a2b5 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -10629,3 +10629,391 @@ impl WasmOvernightIntradayReturn { self.inner.warmup_period() } } + +// ============================== Fibonacci ============================== + +/// Candle for the swing-based Fibonacci tools: only high/low drive the tracker, +/// so open/close are pinned to the midpoint. +fn swing_make_candle(high: f64, low: f64) -> Result { + make_candle(high, low, f64::midpoint(high, low), 0.0) +} + +#[wasm_bindgen(js_name = FibRetracement)] +pub struct WasmFibRetracement { + inner: wc::FibRetracement, +} + +impl Default for WasmFibRetracement { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = FibRetracement)] +impl WasmFibRetracement { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmFibRetracement { + Self { + inner: wc::FibRetracement::new(), + } + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let c = swing_make_candle(high, low)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"level0".into(), &o.level_0.into()).ok(); + Reflect::set(&obj, &"level236".into(), &o.level_236.into()).ok(); + Reflect::set(&obj, &"level382".into(), &o.level_382.into()).ok(); + Reflect::set(&obj, &"level500".into(), &o.level_500.into()).ok(); + Reflect::set(&obj, &"level618".into(), &o.level_618.into()).ok(); + Reflect::set(&obj, &"level786".into(), &o.level_786.into()).ok(); + Reflect::set(&obj, &"level1000".into(), &o.level_1000.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 7]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) { + out[i * 7] = o.level_0; + out[i * 7 + 1] = o.level_236; + out[i * 7 + 2] = o.level_382; + out[i * 7 + 3] = o.level_500; + out[i * 7 + 4] = o.level_618; + out[i * 7 + 5] = o.level_786; + out[i * 7 + 6] = o.level_1000; + } + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + +#[wasm_bindgen(js_name = FibExtension)] +pub struct WasmFibExtension { + inner: wc::FibExtension, +} + +impl Default for WasmFibExtension { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = FibExtension)] +impl WasmFibExtension { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmFibExtension { + Self { + inner: wc::FibExtension::new(), + } + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let c = swing_make_candle(high, low)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"level1272".into(), &o.level_1272.into()).ok(); + Reflect::set(&obj, &"level1414".into(), &o.level_1414.into()).ok(); + Reflect::set(&obj, &"level1618".into(), &o.level_1618.into()).ok(); + Reflect::set(&obj, &"level2000".into(), &o.level_2000.into()).ok(); + Reflect::set(&obj, &"level2618".into(), &o.level_2618.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 5]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) { + out[i * 5] = o.level_1272; + out[i * 5 + 1] = o.level_1414; + out[i * 5 + 2] = o.level_1618; + out[i * 5 + 3] = o.level_2000; + out[i * 5 + 4] = o.level_2618; + } + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + +#[wasm_bindgen(js_name = FibProjection)] +pub struct WasmFibProjection { + inner: wc::FibProjection, +} + +impl Default for WasmFibProjection { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = FibProjection)] +impl WasmFibProjection { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmFibProjection { + Self { + inner: wc::FibProjection::new(), + } + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let c = swing_make_candle(high, low)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"level618".into(), &o.level_618.into()).ok(); + Reflect::set(&obj, &"level1000".into(), &o.level_1000.into()).ok(); + Reflect::set(&obj, &"level1618".into(), &o.level_1618.into()).ok(); + Reflect::set(&obj, &"level2618".into(), &o.level_2618.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 4]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) { + out[i * 4] = o.level_618; + out[i * 4 + 1] = o.level_1000; + out[i * 4 + 2] = o.level_1618; + out[i * 4 + 3] = o.level_2618; + } + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + +#[wasm_bindgen(js_name = AutoFib)] +pub struct WasmAutoFib { + inner: wc::AutoFib, +} + +impl Default for WasmAutoFib { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = AutoFib)] +impl WasmAutoFib { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmAutoFib { + Self { + inner: wc::AutoFib::new(), + } + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let c = swing_make_candle(high, low)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"level0".into(), &o.level_0.into()).ok(); + Reflect::set(&obj, &"level236".into(), &o.level_236.into()).ok(); + Reflect::set(&obj, &"level382".into(), &o.level_382.into()).ok(); + Reflect::set(&obj, &"level500".into(), &o.level_500.into()).ok(); + Reflect::set(&obj, &"level618".into(), &o.level_618.into()).ok(); + Reflect::set(&obj, &"level786".into(), &o.level_786.into()).ok(); + Reflect::set(&obj, &"level1000".into(), &o.level_1000.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 7]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) { + out[i * 7] = o.level_0; + out[i * 7 + 1] = o.level_236; + out[i * 7 + 2] = o.level_382; + out[i * 7 + 3] = o.level_500; + out[i * 7 + 4] = o.level_618; + out[i * 7 + 5] = o.level_786; + out[i * 7 + 6] = o.level_1000; + } + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + +#[wasm_bindgen(js_name = GoldenPocket)] +pub struct WasmGoldenPocket { + inner: wc::GoldenPocket, +} + +impl Default for WasmGoldenPocket { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = GoldenPocket)] +impl WasmGoldenPocket { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmGoldenPocket { + Self { + inner: wc::GoldenPocket::new(), + } + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let c = swing_make_candle(high, low)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"low".into(), &o.low.into()).ok(); + Reflect::set(&obj, &"mid".into(), &o.mid.into()).ok(); + Reflect::set(&obj, &"high".into(), &o.high.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) { + out[i * 3] = o.low; + out[i * 3 + 1] = o.mid; + out[i * 3 + 2] = o.high; + } + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + +#[wasm_bindgen(js_name = FibConfluence)] +pub struct WasmFibConfluence { + inner: wc::FibConfluence, +} + +impl Default for WasmFibConfluence { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = FibConfluence)] +impl WasmFibConfluence { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmFibConfluence { + Self { + inner: wc::FibConfluence::new(), + } + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let c = swing_make_candle(high, low)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"price".into(), &o.price.into()).ok(); + Reflect::set(&obj, &"strength".into(), &o.strength.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) { + out[i * 2] = o.price; + out[i * 2 + 1] = o.strength; + } + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} diff --git a/crates/wickra-core/src/indicators/auto_fib.rs b/crates/wickra-core/src/indicators/auto_fib.rs new file mode 100644 index 00000000..4184bb19 --- /dev/null +++ b/crates/wickra-core/src/indicators/auto_fib.rs @@ -0,0 +1,176 @@ +//! Auto-Fibonacci — retracement of the most significant recent swing leg. + +use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// How many recent pivots to consider when picking the dominant leg. +const PIVOT_HISTORY: usize = 6; + +/// The seven canonical retracement ratios, in ascending order. +const RATIOS: [f64; 7] = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0]; + +/// Auto-Fibonacci retracement levels for the dominant recent swing leg. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AutoFibOutput { + /// 0.0% — the dominant leg's end. + pub level_0: f64, + /// 23.6% retracement. + pub level_236: f64, + /// 38.2% retracement. + pub level_382: f64, + /// 50% retracement. + pub level_500: f64, + /// 61.8% retracement. + pub level_618: f64, + /// 78.6% retracement. + pub level_786: f64, + /// 100% — the dominant leg's start. + pub level_1000: f64, +} + +/// Auto-Fibonacci (`AutoFib`). +/// +/// Like [`crate::indicators::FibRetracement`], but instead of always using the +/// immediate last leg it scans the last six confirmed pivots and anchors the +/// retracement on the single largest-magnitude leg among them — the dominant +/// swing the market is most likely respecting. +/// +/// Parameter-free; construction is infallible. Returns `None` until two pivots +/// have confirmed. +/// +/// See `crates/wickra-core/src/indicators/auto_fib.rs`. +#[derive(Debug, Clone)] +pub struct AutoFib { + swing: SwingTracker, +} + +impl AutoFib { + /// Construct a new Auto-Fibonacci tracker. + #[must_use] + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, PIVOT_HISTORY), + } + } + + fn levels(&self) -> Option { + let dominant = self.swing.pivots().windows(2).max_by(|x, y| { + (x[0].price - x[1].price) + .abs() + .total_cmp(&(y[0].price - y[1].price).abs()) + })?; + let (start, end) = (dominant[0].price, dominant[1].price); + let level = |r: f64| end + r * (start - end); + Some(AutoFibOutput { + level_0: level(RATIOS[0]), + level_236: level(RATIOS[1]), + level_382: level(RATIOS[2]), + level_500: level(RATIOS[3]), + level_618: level(RATIOS[4]), + level_786: level(RATIOS[5]), + level_1000: level(RATIOS[6]), + }) + } +} + +impl Default for AutoFib { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for AutoFib { + type Input = Candle; + type Output = AutoFibOutput; + + fn update(&mut self, candle: Candle) -> Option { + self.swing.update(candle); + self.levels() + } + + fn reset(&mut self) { + self.swing.reset(); + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.swing.pivots().len() >= 2 + } + + fn name(&self) -> &'static str { + "AutoFib" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + #[test] + fn accessors_and_metadata() { + let indicator = AutoFib::new(); + assert_eq!(indicator.name(), "AutoFib"); + assert_eq!(indicator.warmup_period(), 2); + assert!(!indicator.is_ready()); + assert!(!AutoFib::default().is_ready()); + } + + #[test] + fn no_output_before_two_pivots() { + let mut indicator = AutoFib::new(); + let outputs: Vec<_> = candles_for_pivots(&[120.0]) + .into_iter() + .map(|c| indicator.update(c)) + .collect(); + assert!(outputs.iter().all(Option::is_none)); + } + + #[test] + fn anchors_on_the_largest_leg() { + // Pivots: 130 -> 120 (small, 10) -> 220 (large, 100) -> 200 (small, 20). + // The dominant leg is 120 -> 220; its retracement spans [120, 220]. + let mut indicator = AutoFib::new(); + let mut last = None; + for candle in candles_for_pivots(&[130.0, 120.0, 220.0, 200.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert!(indicator.is_ready()); + // Largest leg 120 -> 220: 0% on 220 (end), 100% on 120 (start). + assert_relative_eq!(v.level_0, 220.0); + assert_relative_eq!(v.level_1000, 120.0); + assert_relative_eq!(v.level_500, 170.0); + assert_relative_eq!(v.level_618, 220.0 + 0.618 * (120.0 - 220.0)); + } + + #[test] + fn reset_clears_state() { + let mut indicator = AutoFib::new(); + for candle in candles_for_pivots(&[200.0, 100.0]) { + let _ = indicator.update(candle); + } + assert!(indicator.is_ready()); + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert!(indicator.update(c).is_none()); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[130.0, 120.0, 220.0, 200.0]); + let mut a = AutoFib::new(); + let mut b = AutoFib::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/fib_confluence.rs b/crates/wickra-core/src/indicators/fib_confluence.rs new file mode 100644 index 00000000..de5c9314 --- /dev/null +++ b/crates/wickra-core/src/indicators/fib_confluence.rs @@ -0,0 +1,181 @@ +//! Fibonacci Confluence — the strongest retracement cluster across recent legs. + +use crate::indicators::pattern_swing::{ + approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD, +}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// How many recent pivots to consider; six pivots yield up to five legs. +const PIVOT_HISTORY: usize = 6; + +/// The retracement ratios contributed by each leg to the confluence search. +const RATIOS: [f64; 3] = [0.382, 0.5, 0.618]; + +/// The strongest Fibonacci confluence zone found across recent swing legs. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct FibConfluenceOutput { + /// Mean price of the densest cluster of retracement levels. + pub price: f64, + /// Number of retracement levels that fall inside the cluster (its strength). + pub strength: f64, +} + +/// Fibonacci Confluence (`FibConfluence`). +/// +/// Computes the 38.2% / 50% / 61.8% retracement prices of every leg among the +/// last six confirmed pivots, then reports the densest price cluster — where +/// levels from different legs stack up, the zone the market is most likely to +/// react to. `price` is the cluster mean; `strength` is how many levels it +/// gathers. +/// +/// Parameter-free; construction is infallible. Returns `None` until at least two +/// legs (three pivots) exist. +/// +/// See `crates/wickra-core/src/indicators/fib_confluence.rs`. +#[derive(Debug, Clone)] +pub struct FibConfluence { + swing: SwingTracker, +} + +impl FibConfluence { + /// Construct a new Fibonacci Confluence tracker. + #[must_use] + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, PIVOT_HISTORY), + } + } + + fn confluence(&self) -> Option { + let pivots = self.swing.pivots(); + if pivots.len() < 3 { + return None; + } + let levels: Vec = pivots + .windows(2) + .flat_map(|leg| { + let (start, end) = (leg[0].price, leg[1].price); + RATIOS.map(|r| end + r * (start - end)) + }) + .collect(); + // The `len < 3` guard guarantees at least two legs, hence a non-empty + // level set, so `max_by` always yields a cluster. + let (count, total) = levels + .iter() + .map(|¢er| { + let members: Vec = levels + .iter() + .copied() + .filter(|&x| approx_equal(x, center, LEVEL_TOLERANCE)) + .collect(); + (members.len(), members.iter().sum::()) + }) + .max_by(|a, b| a.0.cmp(&b.0)) + .expect("at least two legs guarantee a non-empty level set"); + Some(FibConfluenceOutput { + price: total / count as f64, + strength: count as f64, + }) + } +} + +impl Default for FibConfluence { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for FibConfluence { + type Input = Candle; + type Output = FibConfluenceOutput; + + fn update(&mut self, candle: Candle) -> Option { + self.swing.update(candle); + self.confluence() + } + + fn reset(&mut self) { + self.swing.reset(); + } + + fn warmup_period(&self) -> usize { + 3 + } + + fn is_ready(&self) -> bool { + self.swing.pivots().len() >= 3 + } + + fn name(&self) -> &'static str { + "FibConfluence" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + #[test] + fn accessors_and_metadata() { + let indicator = FibConfluence::new(); + assert_eq!(indicator.name(), "FibConfluence"); + assert_eq!(indicator.warmup_period(), 3); + assert!(!indicator.is_ready()); + assert!(!FibConfluence::default().is_ready()); + } + + #[test] + fn no_output_before_two_legs() { + let mut indicator = FibConfluence::new(); + let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0]) + .into_iter() + .map(|c| indicator.update(c)) + .collect(); + assert!(outputs.iter().all(Option::is_none)); + assert!(!indicator.is_ready()); + } + + #[test] + fn picks_the_densest_cluster() { + // Legs 200->100 and 100->160. The 38.2% of each (138.2 and ~137.08) + // sit within 3% of each other and form the densest cluster (strength 2). + let mut indicator = FibConfluence::new(); + let mut last = None; + for candle in candles_for_pivots(&[200.0, 100.0, 160.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert!(indicator.is_ready()); + assert_relative_eq!(v.strength, 2.0); + let want = (138.2 + (160.0 + 0.382 * (100.0 - 160.0))) / 2.0; + assert_relative_eq!(v.price, want, epsilon = 1e-9); + } + + #[test] + fn reset_clears_state() { + let mut indicator = FibConfluence::new(); + for candle in candles_for_pivots(&[200.0, 100.0, 160.0]) { + let _ = indicator.update(candle); + } + assert!(indicator.is_ready()); + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert!(indicator.update(c).is_none()); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[200.0, 100.0, 160.0, 120.0]); + let mut a = FibConfluence::new(); + let mut b = FibConfluence::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/fib_extension.rs b/crates/wickra-core/src/indicators/fib_extension.rs new file mode 100644 index 00000000..444546e8 --- /dev/null +++ b/crates/wickra-core/src/indicators/fib_extension.rs @@ -0,0 +1,171 @@ +//! Fibonacci Extension of the most recent confirmed swing leg. + +use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// The five canonical extension ratios, in ascending order. Each is a multiple +/// of the swing leg measured from its origin, so `1.0` sits on the leg's end and +/// every ratio here projects further in the direction of the move. +const RATIOS: [f64; 5] = [1.272, 1.414, 1.618, 2.0, 2.618]; + +/// Fibonacci Extension levels for the most recent swing leg. +/// +/// Each field is the price reached if the move continues to the matching +/// multiple of the leg, measured from the leg's start. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct FibExtensionOutput { + /// 127.2% extension. + pub level_1272: f64, + /// 141.4% extension. + pub level_1414: f64, + /// 161.8% extension — the "golden" extension. + pub level_1618: f64, + /// 200% extension. + pub level_2000: f64, + /// 261.8% extension. + pub level_2618: f64, +} + +/// Fibonacci Extension (`FibExtension`). +/// +/// Tracks confirmed swing pivots with a baked-in 5% reversal threshold and, once +/// two pivots exist, projects the leg between them to the canonical extension +/// ratios — the price targets a continuation of the move would reach. +/// +/// Parameter-free; construction is infallible. Returns `None` until the first +/// leg is complete. +/// +/// See `crates/wickra-core/src/indicators/fib_extension.rs`. +#[derive(Debug, Clone)] +pub struct FibExtension { + swing: SwingTracker, +} + +impl FibExtension { + /// Construct a new Fibonacci Extension tracker. + #[must_use] + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 2), + } + } + + /// Extension price at ratio `e` for a leg from `start` to `end`: the total + /// move is `e` times the leg, measured from `start`. + fn level(start: f64, end: f64, e: f64) -> f64 { + start + e * (end - start) + } + + fn levels(&self) -> Option { + let pivots = self.swing.pivots(); + let [start, end] = [pivots.first()?.price, pivots.get(1)?.price]; + Some(FibExtensionOutput { + level_1272: Self::level(start, end, RATIOS[0]), + level_1414: Self::level(start, end, RATIOS[1]), + level_1618: Self::level(start, end, RATIOS[2]), + level_2000: Self::level(start, end, RATIOS[3]), + level_2618: Self::level(start, end, RATIOS[4]), + }) + } +} + +impl Default for FibExtension { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for FibExtension { + type Input = Candle; + type Output = FibExtensionOutput; + + fn update(&mut self, candle: Candle) -> Option { + self.swing.update(candle); + self.levels() + } + + fn reset(&mut self) { + self.swing.reset(); + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.swing.pivots().len() >= 2 + } + + fn name(&self) -> &'static str { + "FibExtension" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + #[test] + fn accessors_and_metadata() { + let indicator = FibExtension::new(); + assert_eq!(indicator.name(), "FibExtension"); + assert_eq!(indicator.warmup_period(), 2); + assert!(!indicator.is_ready()); + assert!(!FibExtension::default().is_ready()); + } + + #[test] + fn no_output_before_two_pivots() { + let mut indicator = FibExtension::new(); + let outputs: Vec<_> = candles_for_pivots(&[120.0]) + .into_iter() + .map(|c| indicator.update(c)) + .collect(); + assert!(outputs.iter().all(Option::is_none)); + } + + #[test] + fn extension_levels_of_a_down_leg() { + // Leg start = 200 (high), end = 100 (low): a 100-point drop continued. + let mut indicator = FibExtension::new(); + let mut last = None; + for candle in candles_for_pivots(&[200.0, 100.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert!(indicator.is_ready()); + // 161.8% extension projects 1.618 * (-100) below the 200 origin. + assert_relative_eq!(v.level_1272, 200.0 - 127.2); + assert_relative_eq!(v.level_1414, 200.0 - 141.4); + assert_relative_eq!(v.level_1618, 200.0 - 161.8); + assert_relative_eq!(v.level_2000, 0.0); + assert_relative_eq!(v.level_2618, 200.0 - 261.8); + } + + #[test] + fn reset_clears_state() { + let mut indicator = FibExtension::new(); + for candle in candles_for_pivots(&[200.0, 100.0]) { + let _ = indicator.update(candle); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert!(indicator.update(c).is_none()); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[200.0, 100.0, 150.0]); + let mut a = FibExtension::new(); + let mut b = FibExtension::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/fib_projection.rs b/crates/wickra-core/src/indicators/fib_projection.rs new file mode 100644 index 00000000..30f0d438 --- /dev/null +++ b/crates/wickra-core/src/indicators/fib_projection.rs @@ -0,0 +1,165 @@ +//! Fibonacci Projection — a measured move from the last three swing pivots. + +use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// The four canonical projection ratios, in ascending order. Each scales the +/// A→B leg and projects it from C; `1.0` is the classic AB=CD measured move. +const RATIOS: [f64; 4] = [0.618, 1.0, 1.618, 2.618]; + +/// Fibonacci Projection levels (the C→D target zone of a measured move). +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct FibProjectionOutput { + /// 61.8% projection of the A→B leg from C. + pub level_618: f64, + /// 100% projection — the AB=CD measured move. + pub level_1000: f64, + /// 161.8% projection. + pub level_1618: f64, + /// 261.8% projection. + pub level_2618: f64, +} + +/// Fibonacci Projection (`FibProjection`). +/// +/// Reads the last three confirmed swing pivots as the points A, B and C of a +/// measured move and projects the A→B leg from C at the canonical ratios — the +/// price targets for the C→D leg. +/// +/// Parameter-free; construction is infallible. Returns `None` until three +/// pivots have confirmed. +/// +/// See `crates/wickra-core/src/indicators/fib_projection.rs`. +#[derive(Debug, Clone)] +pub struct FibProjection { + swing: SwingTracker, +} + +impl FibProjection { + /// Construct a new Fibonacci Projection tracker. + #[must_use] + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 3), + } + } + + fn levels(&self) -> Option { + let pivots = self.swing.pivots(); + let [a, b, c] = [ + pivots.first()?.price, + pivots.get(1)?.price, + pivots.get(2)?.price, + ]; + let project = |p: f64| c + p * (b - a); + Some(FibProjectionOutput { + level_618: project(RATIOS[0]), + level_1000: project(RATIOS[1]), + level_1618: project(RATIOS[2]), + level_2618: project(RATIOS[3]), + }) + } +} + +impl Default for FibProjection { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for FibProjection { + type Input = Candle; + type Output = FibProjectionOutput; + + fn update(&mut self, candle: Candle) -> Option { + self.swing.update(candle); + self.levels() + } + + fn reset(&mut self) { + self.swing.reset(); + } + + fn warmup_period(&self) -> usize { + 3 + } + + fn is_ready(&self) -> bool { + self.swing.pivots().len() >= 3 + } + + fn name(&self) -> &'static str { + "FibProjection" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + #[test] + fn accessors_and_metadata() { + let indicator = FibProjection::new(); + assert_eq!(indicator.name(), "FibProjection"); + assert_eq!(indicator.warmup_period(), 3); + assert!(!indicator.is_ready()); + assert!(!FibProjection::default().is_ready()); + } + + #[test] + fn no_output_before_three_pivots() { + let mut indicator = FibProjection::new(); + let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0]) + .into_iter() + .map(|c| indicator.update(c)) + .collect(); + assert!(outputs.iter().all(Option::is_none)); + assert!(!indicator.is_ready()); + } + + #[test] + fn measured_move_from_three_pivots() { + // A = 200 (high), B = 160 (low), C = 190 (high). A->B = -40, projected + // down from C. + let mut indicator = FibProjection::new(); + let mut last = None; + for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert!(indicator.is_ready()); + let (a, b, c) = (200.0, 160.0, 190.0); + assert_relative_eq!(v.level_618, c + 0.618 * (b - a)); + assert_relative_eq!(v.level_1000, c + (b - a)); + assert_relative_eq!(v.level_1618, c + 1.618 * (b - a)); + assert_relative_eq!(v.level_2618, c + 2.618 * (b - a)); + } + + #[test] + fn reset_clears_state() { + let mut indicator = FibProjection::new(); + for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) { + let _ = indicator.update(candle); + } + assert!(indicator.is_ready()); + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert!(indicator.update(c).is_none()); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[200.0, 160.0, 190.0, 150.0]); + let mut a = FibProjection::new(); + let mut b = FibProjection::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/fib_retracement.rs b/crates/wickra-core/src/indicators/fib_retracement.rs new file mode 100644 index 00000000..b9aa4c42 --- /dev/null +++ b/crates/wickra-core/src/indicators/fib_retracement.rs @@ -0,0 +1,201 @@ +//! Fibonacci Retracement of the most recent confirmed swing leg. + +use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// The seven canonical retracement ratios, in ascending order. `0.0` marks the +/// most recent swing extreme (the end of the leg) and `1.0` the swing origin +/// (its start); the interior ratios are the classic Fibonacci pullbacks. +const RATIOS: [f64; 7] = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0]; + +/// Fibonacci Retracement levels for the most recent swing leg. +/// +/// Each field is the price at the matching retracement ratio, measured from the +/// leg's end (`level_0`, the latest confirmed extreme) back toward its start +/// (`level_1000`, the prior pivot). +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct FibRetracementOutput { + /// 0.0% — the most recent confirmed swing extreme. + pub level_0: f64, + /// 23.6% retracement. + pub level_236: f64, + /// 38.2% retracement. + pub level_382: f64, + /// 50% retracement (not a Fibonacci ratio, but conventionally drawn). + pub level_500: f64, + /// 61.8% retracement — the "golden ratio" pullback. + pub level_618: f64, + /// 78.6% retracement. + pub level_786: f64, + /// 100% — the swing origin. + pub level_1000: f64, +} + +/// Fibonacci Retracement (`FibRetracement`). +/// +/// Tracks confirmed swing pivots with a baked-in 5% reversal threshold (the +/// same non-repainting logic as [`crate::indicators::ZigZag`]) and, once two +/// pivots exist, reports the seven retracement levels of the leg between them. +/// +/// The levels are recomputed each time a new pivot confirms; between +/// confirmations [`Indicator::update`] returns the locked levels of the current +/// leg. Before the first leg is complete it returns `None`. +/// +/// Parameter-free: the threshold is a compile-time constant, mirroring the +/// chart- and harmonic-pattern detectors, so construction is infallible. +/// +/// See `crates/wickra-core/src/indicators/fib_retracement.rs`. +#[derive(Debug, Clone)] +pub struct FibRetracement { + swing: SwingTracker, +} + +impl FibRetracement { + /// Construct a new Fibonacci Retracement tracker. + #[must_use] + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 2), + } + } + + /// Retracement price at ratio `r` for a leg from `start` to `end`: `0.0` + /// sits on `end`, `1.0` on `start`. + fn level(start: f64, end: f64, r: f64) -> f64 { + end + r * (start - end) + } + + fn levels(&self) -> Option { + let pivots = self.swing.pivots(); + let [start, end] = [pivots.first()?.price, pivots.get(1)?.price]; + Some(FibRetracementOutput { + level_0: Self::level(start, end, RATIOS[0]), + level_236: Self::level(start, end, RATIOS[1]), + level_382: Self::level(start, end, RATIOS[2]), + level_500: Self::level(start, end, RATIOS[3]), + level_618: Self::level(start, end, RATIOS[4]), + level_786: Self::level(start, end, RATIOS[5]), + level_1000: Self::level(start, end, RATIOS[6]), + }) + } +} + +impl Default for FibRetracement { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for FibRetracement { + type Input = Candle; + type Output = FibRetracementOutput; + + fn update(&mut self, candle: Candle) -> Option { + self.swing.update(candle); + self.levels() + } + + fn reset(&mut self) { + self.swing.reset(); + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.swing.pivots().len() >= 2 + } + + fn name(&self) -> &'static str { + "FibRetracement" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + #[test] + fn accessors_and_metadata() { + let indicator = FibRetracement::new(); + assert_eq!(indicator.name(), "FibRetracement"); + assert_eq!(indicator.warmup_period(), 2); + assert!(!indicator.is_ready()); + assert!(!FibRetracement::default().is_ready()); + } + + #[test] + fn no_output_before_two_pivots() { + let mut indicator = FibRetracement::new(); + // A single confirmed pivot is not enough to define a leg. + let candles = candles_for_pivots(&[120.0]); + let outputs: Vec<_> = candles.into_iter().map(|c| indicator.update(c)).collect(); + assert!(outputs.iter().all(Option::is_none)); + assert!(!indicator.is_ready()); + } + + #[test] + fn retracement_levels_of_a_down_leg() { + // Leg start = 200 (high), end = 100 (low): a 100-point drop. + let mut indicator = FibRetracement::new(); + let mut last = None; + for candle in candles_for_pivots(&[200.0, 100.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert!(indicator.is_ready()); + // 0% on the low (end), 100% on the high (start). + assert_relative_eq!(v.level_0, 100.0); + assert_relative_eq!(v.level_1000, 200.0); + // 61.8% retracement of a 100-point drop, measured up from the low. + assert_relative_eq!(v.level_618, 161.8); + assert_relative_eq!(v.level_500, 150.0); + assert_relative_eq!(v.level_382, 138.2); + assert_relative_eq!(v.level_236, 123.6); + assert_relative_eq!(v.level_786, 178.6); + } + + #[test] + fn levels_refresh_on_a_new_leg() { + // Four pivots, cap = 2: once the third and fourth confirm, the reported + // leg shifts to the latest pair (130 high -> 90 low). + let mut indicator = FibRetracement::new(); + let mut last = None; + for candle in candles_for_pivots(&[200.0, 100.0, 130.0, 90.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert_relative_eq!(v.level_0, 90.0); + assert_relative_eq!(v.level_1000, 130.0); + assert_relative_eq!(v.level_618, 90.0 + 0.618 * 40.0); + } + + #[test] + fn reset_clears_state() { + let mut indicator = FibRetracement::new(); + for candle in candles_for_pivots(&[200.0, 100.0]) { + let _ = indicator.update(candle); + } + assert!(indicator.is_ready()); + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert!(indicator.update(c).is_none()); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[200.0, 100.0, 150.0]); + let mut a = FibRetracement::new(); + let mut b = FibRetracement::new(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/golden_pocket.rs b/crates/wickra-core/src/indicators/golden_pocket.rs new file mode 100644 index 00000000..98ee21cd --- /dev/null +++ b/crates/wickra-core/src/indicators/golden_pocket.rs @@ -0,0 +1,175 @@ +//! Golden Pocket — the 0.618-0.65 optimal-trade-entry zone of the last swing. + +use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Lower bound of the golden pocket (the 61.8% retracement). +const RATIO_LOW: f64 = 0.618; +/// Upper bound of the golden pocket (the 65% retracement). +const RATIO_HIGH: f64 = 0.65; + +/// The golden-pocket zone of the most recent swing leg. +/// +/// `low`/`high` bracket the 0.618-0.65 retracement band (sorted, so `low <= +/// high` regardless of swing direction); `mid` is their midpoint. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct GoldenPocketOutput { + /// Lower price of the golden-pocket band. + pub low: f64, + /// Midpoint of the band. + pub mid: f64, + /// Upper price of the golden-pocket band. + pub high: f64, +} + +/// Golden Pocket (`GoldenPocket`). +/// +/// The 0.618-0.65 retracement band of the most recent confirmed swing leg — the +/// "optimal trade entry" zone many swing traders watch for continuation. +/// +/// Parameter-free; construction is infallible. Returns `None` until the first +/// leg is complete. +/// +/// See `crates/wickra-core/src/indicators/golden_pocket.rs`. +#[derive(Debug, Clone)] +pub struct GoldenPocket { + swing: SwingTracker, +} + +impl GoldenPocket { + /// Construct a new Golden Pocket tracker. + #[must_use] + pub const fn new() -> Self { + Self { + swing: SwingTracker::new(SWING_THRESHOLD, 2), + } + } + + fn zone(&self) -> Option { + let pivots = self.swing.pivots(); + let [start, end] = [pivots.first()?.price, pivots.get(1)?.price]; + let span = start - end; + let edge_low = end + RATIO_LOW * span; + let edge_high = end + RATIO_HIGH * span; + let low = edge_low.min(edge_high); + let high = edge_low.max(edge_high); + Some(GoldenPocketOutput { + low, + mid: f64::midpoint(low, high), + high, + }) + } +} + +impl Default for GoldenPocket { + fn default() -> Self { + Self::new() + } +} + +impl Indicator for GoldenPocket { + type Input = Candle; + type Output = GoldenPocketOutput; + + fn update(&mut self, candle: Candle) -> Option { + self.swing.update(candle); + self.zone() + } + + fn reset(&mut self) { + self.swing.reset(); + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.swing.pivots().len() >= 2 + } + + fn name(&self) -> &'static str { + "GoldenPocket" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::indicators::pattern_swing::candles_for_pivots; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + #[test] + fn accessors_and_metadata() { + let indicator = GoldenPocket::new(); + assert_eq!(indicator.name(), "GoldenPocket"); + assert_eq!(indicator.warmup_period(), 2); + assert!(!indicator.is_ready()); + assert!(!GoldenPocket::default().is_ready()); + } + + #[test] + fn no_output_before_two_pivots() { + let mut indicator = GoldenPocket::new(); + let outputs: Vec<_> = candles_for_pivots(&[120.0]) + .into_iter() + .map(|c| indicator.update(c)) + .collect(); + assert!(outputs.iter().all(Option::is_none)); + } + + #[test] + fn zone_of_a_down_leg() { + // Leg 200 (high) -> 100 (low), span = 100. + let mut indicator = GoldenPocket::new(); + let mut last = None; + for candle in candles_for_pivots(&[200.0, 100.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert!(indicator.is_ready()); + // 61.8% = 161.8, 65% = 165 → sorted band [161.8, 165], mid 163.4. + assert_relative_eq!(v.low, 161.8); + assert_relative_eq!(v.high, 165.0); + assert_relative_eq!(v.mid, 163.4); + } + + #[test] + fn band_is_sorted_for_an_up_leg() { + // Latest leg 100 (low) -> 250 (high): span negative, edges flip, but + // low <= high must still hold. + let mut indicator = GoldenPocket::new(); + let mut last = None; + for candle in candles_for_pivots(&[200.0, 100.0, 250.0]) { + last = indicator.update(candle); + } + let v = last.unwrap(); + assert!(v.low <= v.high); + assert_relative_eq!(v.mid, f64::midpoint(v.low, v.high)); + } + + #[test] + fn reset_clears_state() { + let mut indicator = GoldenPocket::new(); + for candle in candles_for_pivots(&[200.0, 100.0]) { + let _ = indicator.update(candle); + } + indicator.reset(); + assert!(!indicator.is_ready()); + let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap(); + assert!(indicator.update(c).is_none()); + } + + #[test] + fn batch_equals_streaming() { + let candles = candles_for_pivots(&[200.0, 100.0, 150.0]); + let mut a = GoldenPocket::new(); + let mut b = GoldenPocket::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 3f2a1dda..1cd5b973 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -34,6 +34,7 @@ mod aroon_oscillator; mod atr; mod atr_bands; mod atr_trailing_stop; +mod auto_fib; mod autocorrelation; mod average_daily_range; mod average_drawdown; @@ -110,6 +111,10 @@ mod evening_doji_star; mod evwma; mod falling_three_methods; mod fama; +mod fib_confluence; +mod fib_extension; +mod fib_projection; +mod fib_retracement; mod fibonacci_pivots; mod fisher_transform; mod flag_pennant; @@ -125,6 +130,7 @@ mod gain_loss_ratio; mod gap_side_by_side_white; mod garman_klass; mod gartley; +mod golden_pocket; mod granger_causality; mod gravestone_doji; mod hammer; @@ -401,6 +407,7 @@ pub use aroon_oscillator::AroonOscillator; pub use atr::Atr; pub use atr_bands::{AtrBands, AtrBandsOutput}; pub use atr_trailing_stop::AtrTrailingStop; +pub use auto_fib::{AutoFib, AutoFibOutput}; pub use autocorrelation::Autocorrelation; pub use average_daily_range::AverageDailyRange; pub use average_drawdown::AverageDrawdown; @@ -477,6 +484,10 @@ pub use evening_doji_star::EveningDojiStar; pub use evwma::Evwma; pub use falling_three_methods::FallingThreeMethods; pub use fama::Fama; +pub use fib_confluence::{FibConfluence, FibConfluenceOutput}; +pub use fib_extension::{FibExtension, FibExtensionOutput}; +pub use fib_projection::{FibProjection, FibProjectionOutput}; +pub use fib_retracement::{FibRetracement, FibRetracementOutput}; pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput}; pub use fisher_transform::FisherTransform; pub use flag_pennant::FlagPennant; @@ -492,6 +503,7 @@ 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 golden_pocket::{GoldenPocket, GoldenPocketOutput}; pub use granger_causality::GrangerCausality; pub use gravestone_doji::GravestoneDoji; pub use hammer::Hammer; @@ -1222,6 +1234,17 @@ pub const FAMILIES: &[(&str, &[&str])] = &[ "ThreeDrives", ], ), + ( + "Fibonacci", + &[ + "FibRetracement", + "FibExtension", + "FibProjection", + "AutoFib", + "GoldenPocket", + "FibConfluence", + ], + ), ]; #[cfg(test)] @@ -1250,6 +1273,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, 367, "FAMILIES total drifted from indicator count"); + assert_eq!(total, 373, "FAMILIES total drifted from indicator count"); } } diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs index db7f5bc1..265c478a 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -60,12 +60,12 @@ pub use indicators::{ 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, Bat, BeltHold, Beta, - BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, BreadthThrust, - Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio, Camarilla, - CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, - ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, + AtrBandsOutput, AtrTrailingStop, AutoFib, AutoFibOutput, Autocorrelation, AverageDailyRange, + AverageDrawdown, AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, + BeltHold, Beta, BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, + BreadthThrust, 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, @@ -76,10 +76,12 @@ pub use indicators::{ 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, Gartley, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, + FibConfluence, FibConfluenceOutput, FibExtension, FibExtensionOutput, FibProjection, + FibProjectionOutput, FibRetracement, FibRetracementOutput, FibonacciPivots, + FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex, + FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, + FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility, Gartley, + GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent, diff --git a/docs/README.md b/docs/README.md index e8c95478..690aecf0 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 **367 indicators** across +- A per-indicator deep dive for every one of the **373 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 ee779be6..f1661602 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, 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}; +use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, 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, FibConfluence, FibExtension, FibProjection, FibRetracement, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GoldenPocket, 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 @@ -392,4 +392,12 @@ fuzz_target!(|data: Vec| { drive(Gartley::new, &candles); drive(Abcd::new, &candles); + // --- Fibonacci (multi-output) --- + drive(FibConfluence::new, &candles); + drive(GoldenPocket::new, &candles); + drive(AutoFib::new, &candles); + drive(FibProjection::new, &candles); + drive(FibExtension::new, &candles); + drive(FibRetracement::new, &candles); + });