diff --git a/CHANGELOG.md b/CHANGELOG.md index 4745a927..9e5b02ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- **Breaking — de-duplicated four indicators that computed identically to another + one.** Each is now its own distinct, correctly-defined indicator (the catalogue + stays at the same count): + - `AverageDrawdown` now reports the mean of the maximum depths of the distinct + drawdown episodes in the window (previously the per-bar mean under-water + fraction, which equalled `PainIndex`). + - `IntradayIntensity` now reports the raw per-bar Bostian intensity + `volume * (2*close − high − low) / (high − low)` (previously a cumulative line + that equalled the A/D Line `Adl`; its normalized form is `Cmf`). + - `AwesomeOscillatorHistogram` now reports the AO momentum + `AO[t] − AO[t−lookback]`; its third parameter is the momentum `lookback` + (default 1) instead of an SMA period (the old `AO − SMA(AO, n)` equalled + `AcceleratorOscillator`). + - `AdOscillator` is now the Williams **A/D Oscillator** (`WAD − SMA(WAD, 13)`), + distinct from the cumulative Williams A/D line `Wad`. Its native (Python / + Node.js / WASM) alias is renamed **`WilliamsAD` → `ADOSC`**. + ## [0.9.1] - 2026-06-14 ### Added diff --git a/README.md b/README.md index 8dabae86..a9cd9d16 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility, Volatility Cone | | Bands & Channels | MA Envelope, Acceleration Bands, STARC Bands, ATR Bands, Hurst Channel, LinReg Channel, Standard Error Bands, Double Bollinger Bands, TTM Squeeze, Fractal Chaos Bands, VWAP StdDev Bands, Quartile Bands, Bomar Bands, Median Channel, Projection Bands, Projection Oscillator | | Trailing Stops | Parabolic SAR, Parabolic SAR Extended (SAREXT), SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop, HiLo Activator, Volty Stop, Yo-Yo Exit, Donchian Channel Stop, Percentage Trailing Stop, Step Trailing Stop, Renko Trailing Stop, Kase DevStop, Elder SafeZone, ATR Ratchet, NRTR, Time-Based Stop, Modified MA Stop | -| Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement, Klinger Volume Oscillator, Volume Oscillator, NVI, PVI, Williams A/D, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index, Volume RSI, Williams Accumulation/Distribution, Twiggs Money Flow, Trade Volume Index, Intraday Intensity Index, Better Volume, Volume-Weighted MACD | +| Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement, Klinger Volume Oscillator, Volume Oscillator, NVI, PVI, Williams A/D Oscillator, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index, Volume RSI, Williams Accumulation/Distribution, Twiggs Money Flow, Trade Volume Index, Intraday Intensity, Better Volume, Volume-Weighted MACD | | Price Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope, Z-Score, Linear Regression Angle, Variance, Coefficient of Variation, Skewness, Kurtosis, Standard Error, Detrended StdDev, R², Median Absolute Deviation, Autocorrelation, Hurst Exponent, Pearson Correlation, Beta, Pairwise Beta, Pair Spread Z-Score, Lead-Lag Cross-Correlation, Cointegration, Relative Strength A-vs-B, Spearman Correlation, Mid Price, Mid Point, Average Price, Linear Regression Intercept, Time Series Forecast, Rolling Correlation, Rolling Covariance, OU Half-Life, Spread Hurst, Distance SSD, Beta-Neutral Spread, Variance Ratio, Granger Causality, Kalman Hedge Ratio, Spread Bollinger Bands, Spread AR(1) Coefficient, Jarque-Bera, Rolling Min-Max Scaler, Shannon Entropy, Sample Entropy, Kendall Tau | | Ehlers / Cycle (DSP) | MAMA, FAMA, Fisher Transform, Inverse Fisher Transform, SuperSmoother, Hilbert Dominant Cycle, Hilbert Phasor, Hilbert DC Phase, Hilbert Trend Mode, Sine Wave, Decycler, Decycler Oscillator, Roofing Filter, Center of Gravity, Cybernetic Cycle, Adaptive Cycle, Empirical Mode Decomposition, Ehlers Stochastic, Instantaneous Trendline, Highpass Filter, Reflex, Trendflex, Correlation Trend Indicator, Adaptive RSI, Universal Oscillator, Adaptive CCI, Bandpass Filter, Even Better Sinewave, Autocorrelation Periodogram | | Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag, Central Pivot Range, Murrey Math Lines, Andrews Pitchfork, Volume-Weighted Support/Resistance, Pivot Reversal | diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js index bd1cdc52..28ce8969 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -259,7 +259,7 @@ const candleScalar = { VolumeOscillator: { make: () => new wickra.VolumeOscillator(14, 28), step: (ind, i) => ind.update(volume[i]), batch: (ind) => ind.batch(volume) }, NVI: { make: () => new wickra.NVI(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, PVI: { make: () => new wickra.PVI(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, - WilliamsAD: { make: () => new wickra.WilliamsAD(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, + ADOSC: { make: () => new wickra.ADOSC(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, AnchoredVWAP: { make: () => new wickra.AnchoredVWAP(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, DemandIndex: { make: () => new wickra.DemandIndex(10), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, TSV: { make: () => new wickra.TSV(18), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, @@ -1009,8 +1009,8 @@ test('AwesomeOscillatorHistogram on a flat median converges to zero', () => { Array(n).fill(11), Array(n).fill(9), ); - // warmup = 5 + 3 - 1 = 7. - for (let i = 6; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12); + // AO momentum; warmup = slow + lookback = 5 + 3 = 8. + for (let i = 7; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12); }); test('STC on a flat series stays at zero', () => { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 6937a845..85895b27 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -2505,8 +2505,8 @@ export declare class KVO { isReady(): boolean warmupPeriod(): number } -export type AdOscillatorNode = WilliamsAD -export declare class WilliamsAD { +export type AdOscillatorNode = ADOSC +export declare class ADOSC { constructor() update(high: number, low: number, close: number): number | null batch(high: Array, low: Array, close: Array): Array diff --git a/bindings/node/index.js b/bindings/node/index.js index e37a14e2..45ac57b1 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, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, SterlingRatio, BurkeRatio, MartinRatio, TailRatio, KRatio, CommonSenseRatio, GainToPainRatio, UpsidePotentialRatio, M2Measure, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, HasbrouckInformationShare, 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, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, 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, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, 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, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, 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, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, ValueArea, NakedPoc, SinglePrints, ProfileShape, HighLowVolumeNodes, CompositeProfile, 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, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, Tristar, HaramiCross, TowerTopBottom, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, EstimatedLeverageRatio, OiToVolumeRatio, PerpetualPremiumIndex, FundingImpliedApr, OpenInterestMomentum, 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, RangeBars, TickBars, VolumeBars, DollarBars, ImbalanceBars, RunBars, ThreeLineBreakBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = 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, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, SterlingRatio, BurkeRatio, MartinRatio, TailRatio, KRatio, CommonSenseRatio, GainToPainRatio, UpsidePotentialRatio, M2Measure, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, HasbrouckInformationShare, 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, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, 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, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, ADOSC, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, 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, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, ValueArea, NakedPoc, SinglePrints, ProfileShape, HighLowVolumeNodes, CompositeProfile, 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, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, Tristar, HaramiCross, TowerTopBottom, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, EstimatedLeverageRatio, OiToVolumeRatio, PerpetualPremiumIndex, FundingImpliedApr, OpenInterestMomentum, 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, RangeBars, TickBars, VolumeBars, DollarBars, ImbalanceBars, RunBars, ThreeLineBreakBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -511,7 +511,7 @@ module.exports.NVI = NVI module.exports.PVI = PVI module.exports.VolumeOscillator = VolumeOscillator module.exports.KVO = KVO -module.exports.WilliamsAD = WilliamsAD +module.exports.ADOSC = ADOSC module.exports.AnchoredRSI = AnchoredRSI module.exports.AnchoredVWAP = AnchoredVWAP module.exports.DemandIndex = DemandIndex diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 5a7cfa1f..a94a68a1 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -5926,7 +5926,7 @@ impl KvoNode { // ============================== Williams A/D ============================== -#[napi(js_name = "WilliamsAD")] +#[napi(js_name = "ADOSC")] pub struct AdOscillatorNode { inner: wc::AdOscillator, } diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index c2800097..e00c9ada 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -236,7 +236,7 @@ from ._wickra import ( VolumeOscillator, NVI, PVI, - WilliamsAD, + ADOSC, AnchoredVWAP, DemandIndex, TSV, @@ -780,7 +780,7 @@ __all__ = [ "VolumeOscillator", "NVI", "PVI", - "WilliamsAD", + "ADOSC", "AnchoredVWAP", "DemandIndex", "TSV", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 5adfc0e9..7c141e18 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -9450,7 +9450,7 @@ impl PyKvo { // ============================== Williams A/D Oscillator ============================== -#[pyclass(name = "WilliamsAD", module = "wickra._wickra", skip_from_py_object)] +#[pyclass(name = "ADOSC", module = "wickra._wickra", skip_from_py_object)] #[derive(Clone)] struct PyAdOscillator { inner: wc::AdOscillator, @@ -9507,7 +9507,7 @@ impl PyAdOscillator { self.inner.warmup_period() } fn __repr__(&self) -> String { - "WilliamsAD()".to_string() + "ADOSC()".to_string() } } diff --git a/bindings/python/tests/test_known_values.py b/bindings/python/tests/test_known_values.py index 4c1c26a5..d768bb3e 100644 --- a/bindings/python/tests/test_known_values.py +++ b/bindings/python/tests/test_known_values.py @@ -258,9 +258,9 @@ def test_awesome_oscillator_histogram_flat_series_converges_to_zero(): n = 50 high = np.full(n, 11.0) low = np.full(n, 9.0) - out = ta.AwesomeOscillatorHistogram(3, 5, 3).batch(high, low) - # warmup = slow + sma - 1 = 5 + 3 - 1 = 7. - np.testing.assert_allclose(out[6:], 0.0, atol=1e-12) + out = ta.AwesomeOscillatorHistogram(3, 5, 3).batch(high, low) # AO momentum + # warmup = slow + lookback = 5 + 3 = 8. + np.testing.assert_allclose(out[7:], 0.0, atol=1e-12) def test_stc_constant_series_yields_zero(): @@ -521,10 +521,10 @@ def test_calmar_ratio_known_path(): def test_average_drawdown_known_window(): - # window [100, 120, 90, 110]: dd = 0, 0, 0.25, 10/120; - # mean = (0.25 + 10/120) / 4. + # window [100, 120, 90, 110]: one drawdown episode (peak 120, trough 90), + # never recovering -> depth (120-90)/120 = 0.25; one episode -> AvgDD = 0.25. out = ta.AverageDrawdown(4).batch(np.array([100.0, 120.0, 90.0, 110.0])) - expected = (0.25 + 10.0 / 120.0) / 4.0 + expected = 0.25 assert math.isclose(out[-1], expected, rel_tol=1e-12) diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py index b852e613..227e2aaf 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -634,8 +634,8 @@ CANDLE_SCALAR = { lambda: ta.PVI(), lambda ind, h, l, c, v: ind.batch(c, v), ), - "WilliamsAD": ( - lambda: ta.WilliamsAD(), + "ADOSC": ( + lambda: ta.ADOSC(), lambda ind, h, l, c, v: ind.batch(h, l, c), ), "AnchoredVWAP": ( @@ -1762,7 +1762,7 @@ def test_wad_reference(): # TR_l = min(10, 8) = 8 -> delta = 12 - 8 = 4. AD = 4. # bar 2: prev=12, today high=11, low=7, close=7 (down day). # TR_h = max(12, 11) = 12 -> delta = 7 - 12 = -5. AD = 4 - 5 = -1. - ad = ta.WilliamsAD() + ad = ta.Wad() high = np.array([11.0, 13.0, 11.0]) low = np.array([9.0, 8.0, 7.0]) close = np.array([10.0, 12.0, 7.0]) diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 2bd646b6..3c4f64c1 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -3145,12 +3145,12 @@ impl WasmKvo { } } -#[wasm_bindgen(js_name = WilliamsAD)] +#[wasm_bindgen(js_name = ADOSC)] pub struct WasmAdOscillator { inner: wc::AdOscillator, } -#[wasm_bindgen(js_class = WilliamsAD)] +#[wasm_bindgen(js_class = ADOSC)] impl WasmAdOscillator { #[wasm_bindgen(constructor)] #[allow(clippy::new_without_default)] diff --git a/crates/wickra-core/src/indicators/ad_oscillator.rs b/crates/wickra-core/src/indicators/ad_oscillator.rs index c978e760..9823bf66 100644 --- a/crates/wickra-core/src/indicators/ad_oscillator.rs +++ b/crates/wickra-core/src/indicators/ad_oscillator.rs @@ -1,28 +1,35 @@ -//! Williams Accumulation/Distribution. +//! Williams A/D Oscillator (ADOSC). +use crate::indicators::sma::Sma; use crate::ohlcv::Candle; use crate::traits::Indicator; -/// Larry Williams' Accumulation/Distribution — a cumulative volume-less price -/// flow that classifies each bar as accumulation or distribution based on its -/// close relative to the previous close, then sums the directional component. +/// Smoothing window applied to the Williams A/D line to form the oscillator. +const SIGNAL_PERIOD: usize = 13; + +/// Williams **A/D Oscillator** — the volume-free Williams Accumulation/ +/// Distribution line measured against its own moving average, so it oscillates +/// around zero instead of drifting like the cumulative line. /// -/// Williams' definition (1972) uses a *true* high/low that includes the prior -/// close as an anchor — the same idea that motivates true range: +/// The underlying line is Larry Williams' volume-less A/D (1972), which uses a +/// *true* high/low anchored on the prior close; the oscillator subtracts its +/// 13-bar simple moving average: /// /// ```text /// TR_h_t = max(close_{t−1}, high_t) /// TR_l_t = min(close_{t−1}, low_t) -/// AD_t = AD_{t−1} + (close_t − TR_l_t) if close_t > close_{t−1} (accumulation) -/// AD_t = AD_{t−1} + (close_t − TR_h_t) if close_t < close_{t−1} (distribution) -/// AD_t = AD_{t−1} if close_t == close_{t−1} (no change) +/// WAD_t = WAD_{t−1} + (close_t − TR_l_t) if close_t > close_{t−1} +/// WAD_t = WAD_{t−1} + (close_t − TR_h_t) if close_t < close_{t−1} +/// WAD_t = WAD_{t−1} if close_t == close_{t−1} +/// ADOSC_t = WAD_t − SMA(WAD, 13)_t /// ``` /// -/// Unlike Chaikin's Accumulation/Distribution Line, the Williams A/D ignores -/// volume entirely — Williams argued that the relative position of the close -/// already encodes the day's "true" buying or selling pressure. The series is -/// unbounded and used primarily for divergence analysis. The first candle only -/// seeds the previous close; the first emission lands at bar 2. +/// This is distinct from the raw cumulative line, which Wickra ships as +/// [`Wad`](crate::Wad): `Wad` is the drifting line for divergence analysis, +/// while this oscillator is its zero-centred, mean-reverting form (positive +/// when accumulation is running ahead of its recent average, negative when +/// distribution is). The first bar only seeds the previous close; the first +/// oscillator value lands once the 13-bar average of the line is full. /// /// # Example /// @@ -39,30 +46,35 @@ use crate::traits::Indicator; /// } /// assert!(last.is_some()); /// ``` -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct AdOscillator { prev_close: Option, - total: f64, - has_emitted: bool, + line: f64, + signal: Sma, + last: Option, +} + +impl Default for AdOscillator { + fn default() -> Self { + Self::new() + } } impl AdOscillator { - /// Construct a new Williams A/D starting at zero. - pub const fn new() -> Self { + /// Construct a new Williams A/D Oscillator. + #[must_use] + pub fn new() -> Self { Self { prev_close: None, - total: 0.0, - has_emitted: false, + line: 0.0, + signal: Sma::new(SIGNAL_PERIOD).expect("SIGNAL_PERIOD is non-zero"), + last: None, } } - /// Current cumulative value if at least one emission has happened. + /// Current oscillator value if available. pub const fn value(&self) -> Option { - if self.has_emitted { - Some(self.total) - } else { - None - } + self.last } } @@ -78,45 +90,47 @@ impl Indicator for AdOscillator { }; let delta = if candle.close > prev { // Accumulation: distance from the true low. - let tr_l = prev.min(candle.low); - candle.close - tr_l + candle.close - prev.min(candle.low) } else if candle.close < prev { // Distribution: distance from the true high (negative). - let tr_h = prev.max(candle.high); - candle.close - tr_h + candle.close - prev.max(candle.high) } else { - // Unchanged close contributes nothing. 0.0 }; - self.total += delta; + self.line += delta; self.prev_close = Some(candle.close); - self.has_emitted = true; - Some(self.total) + let signal = self.signal.update(self.line)?; + let osc = self.line - signal; + self.last = Some(osc); + Some(osc) } fn reset(&mut self) { self.prev_close = None; - self.total = 0.0; - self.has_emitted = false; + self.line = 0.0; + self.signal.reset(); + self.last = None; } fn warmup_period(&self) -> usize { - // One seed bar; the second bar is the first emission. - 2 + // One seed bar establishes the prior close; the line then feeds the + // 13-bar signal SMA, which is full after `SIGNAL_PERIOD` line values. + 1 + SIGNAL_PERIOD } fn is_ready(&self) -> bool { - self.has_emitted + self.last.is_some() } fn name(&self) -> &'static str { - "WilliamsAD" + "ADOSC" } } #[cfg(test)] mod tests { use super::*; + use crate::indicators::wad::Wad; use crate::traits::BatchExt; use approx::assert_relative_eq; @@ -127,94 +141,103 @@ mod tests { #[test] fn accessors_and_metadata() { let ad = AdOscillator::new(); - assert_eq!(ad.name(), "WilliamsAD"); - assert_eq!(ad.warmup_period(), 2); - assert_eq!(ad.value(), None); - } - - #[test] - fn value_returns_total_after_first_emission() { - let mut ad = AdOscillator::new(); - ad.update(c(10.0, 11.0, 9.0, 10.0, 0)); - let v = ad.update(c(11.0, 13.0, 8.0, 12.0, 1)).unwrap(); - assert_relative_eq!(ad.value().unwrap(), v, epsilon = 1e-12); - } - - #[test] - fn first_bar_only_seeds() { - let mut ad = AdOscillator::new(); - assert_eq!(ad.update(c(10.0, 11.0, 9.0, 10.0, 0)), None); + assert_eq!(ad.name(), "ADOSC"); + assert_eq!(ad.warmup_period(), 14); assert!(!ad.is_ready()); + assert_eq!(ad.value(), None); + // `Default` matches `new`. + assert_eq!(AdOscillator::default().warmup_period(), 14); } #[test] - fn accumulation_adds_distance_from_true_low() { - // prev close = 10, today low = 8, today close = 12 (up day). - // TR_l = min(10, 8) = 8, delta = 12 - 8 = 4. AD = 0 + 4 = 4. + fn seed_bar_returns_none() { let mut ad = AdOscillator::new(); - ad.update(c(10.0, 11.0, 9.0, 10.0, 0)); - let v = ad.update(c(11.0, 13.0, 8.0, 12.0, 1)).unwrap(); - assert_relative_eq!(v, 4.0, epsilon = 1e-12); + assert_eq!(ad.update(c(100.0, 101.0, 99.0, 100.0, 0)), None); } #[test] - fn distribution_adds_distance_from_true_high() { - // prev close = 10, today high = 11, today close = 7 (down day). - // TR_h = max(10, 11) = 11, delta = 7 - 11 = -4. AD = -4. - let mut ad = AdOscillator::new(); - ad.update(c(10.0, 11.0, 9.0, 10.0, 0)); - let v = ad.update(c(10.0, 11.0, 7.0, 7.0, 1)).unwrap(); - assert_relative_eq!(v, -4.0, epsilon = 1e-12); + fn equals_wad_line_minus_its_sma() { + // The oscillator is exactly the Williams A/D line minus its 13-SMA, so + // it must match the standalone `Wad` line passed through an SMA(13). + let candles: Vec = (0..80_i64) + .map(|i| { + let base = 100.0 + (i as f64 * 0.3).sin() * 6.0; + c( + base, + base + 2.0, + base - 2.0, + base + (i as f64 * 0.5).cos(), + i, + ) + }) + .collect(); + let osc = AdOscillator::new().batch(&candles); + // Reconstruct: Wad line, then line − SMA(line, 13). + let line = Wad::new().batch(&candles); + let mut sma = Sma::new(SIGNAL_PERIOD).unwrap(); + let expected: Vec> = line + .iter() + .map(|v| v.and_then(|l| sma.update(l).map(|s| l - s))) + .collect(); + assert_eq!(osc, expected); } #[test] - fn unchanged_close_keeps_total() { - // close equals prev close -> no contribution. + fn flat_market_oscillates_at_zero() { + // A flat market never accumulates or distributes, so the line is + // constant and the oscillator sits at zero once warm. let mut ad = AdOscillator::new(); - ad.update(c(10.0, 11.0, 9.0, 10.0, 0)); - let v = ad.update(c(10.0, 12.0, 8.0, 10.0, 1)).unwrap(); - assert_relative_eq!(v, 0.0, epsilon = 1e-12); - } - - #[test] - fn constant_series_yields_zero() { - // Every close equals the previous -> AD stays at zero forever. - let candles: Vec = (0..40).map(|i| c(10.0, 11.0, 9.0, 10.0, i)).collect(); - let mut ad = AdOscillator::new(); - for v in ad.batch(&candles).into_iter().flatten() { - assert_relative_eq!(v, 0.0, epsilon = 1e-12); + let candles: Vec = (0..40).map(|i| c(50.0, 50.0, 50.0, 50.0, i)).collect(); + let out = ad.batch(&candles); + for v in out.iter().skip(ad.warmup_period() - 1).flatten() { + assert_relative_eq!(*v, 0.0, epsilon = 1e-12); } } #[test] - fn batch_equals_streaming() { - let candles: Vec = (0..80i64) + fn warmup_emits_at_warmup_period() { + let mut ad = AdOscillator::new(); + let candles: Vec = (0..20) .map(|i| { - let f = i as f64; - let mid = 100.0 + (f * 0.3).sin() * 5.0; - c(mid, mid + 2.0, mid - 2.0, mid + 0.5, i) + let close = 100.0 + f64::from(i); + c(close, close + 2.0, close - 2.0, close, i64::from(i)) }) .collect(); - let mut a = AdOscillator::new(); - let mut b = AdOscillator::new(); - assert_eq!( - a.batch(&candles), - candles.iter().map(|x| b.update(*x)).collect::>() - ); + let out = ad.batch(&candles); + assert_eq!(ad.warmup_period(), 14); + for v in out.iter().take(13) { + assert!(v.is_none()); + } + assert!(out[13].is_some()); } #[test] fn reset_clears_state() { let mut ad = AdOscillator::new(); - ad.batch(&[ - c(10.0, 11.0, 9.0, 10.0, 0), - c(10.0, 12.0, 9.0, 11.0, 1), - c(11.0, 13.0, 10.0, 12.0, 2), - ]); + let candles: Vec = (0..30) + .map(|i| { + let close = 100.0 + f64::from(i); + c(close, close + 2.0, close - 2.0, close, i64::from(i)) + }) + .collect(); + ad.batch(&candles); assert!(ad.is_ready()); ad.reset(); assert!(!ad.is_ready()); assert_eq!(ad.value(), None); - assert_eq!(ad.update(c(10.0, 11.0, 9.0, 10.0, 3)), None); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..100_i64) + .map(|i| { + let base = 100.0 + (i as f64 * 0.2).sin() * 5.0; + c(base, base + 1.5, base - 1.5, base + 0.4, i) + }) + .collect(); + let batch = AdOscillator::new().batch(&candles); + let mut s = AdOscillator::new(); + let streamed: Vec<_> = candles.iter().map(|x| s.update(*x)).collect(); + assert_eq!(batch, streamed); } } diff --git a/crates/wickra-core/src/indicators/average_drawdown.rs b/crates/wickra-core/src/indicators/average_drawdown.rs index d9aa2b2f..8395bb93 100644 --- a/crates/wickra-core/src/indicators/average_drawdown.rs +++ b/crates/wickra-core/src/indicators/average_drawdown.rs @@ -7,19 +7,23 @@ use crate::traits::Indicator; /// Rolling Average Drawdown. /// -/// Input is treated as an equity-curve sample. The indicator scans the -/// trailing window of `period` values, tracks the running peak inside the -/// window, and reports the **mean** of all bar-by-bar drawdowns (the average -/// "pain" of being under water): +/// Input is treated as an equity-curve sample. Over the trailing window of +/// `period` values the indicator identifies each **distinct drawdown episode** +/// — a stretch where equity is below the running peak — and reports the **mean +/// of the episodes' maximum depths**: /// /// ```text -/// drawdown_t = (peak_t − equity_t) / peak_t (running peak inside window) -/// AvgDD = mean(drawdown_t over window) +/// episode opens when equity < running peak +/// episode closes when equity reaches a new peak (full recovery) +/// depth(episode) = (episode_peak − episode_trough) / episode_peak +/// AvgDD = mean(depth over episodes in window) (0 if no drawdown) /// ``` /// -/// Output is non-negative (a fraction; `0.05` ≈ 5 % average drawdown). This -/// is the **Pain Index** under a different name — see [`crate::PainIndex`] -/// for the same metric exposed under its conventional label. +/// This is the conventional "average drawdown" (mean depth across separate +/// drawdowns), which is distinct from the [`crate::PainIndex`] — the latter +/// averages the under-water fraction at *every* bar, so a long shallow +/// drawdown weighs more there than here. Output is a non-negative fraction +/// (`0.05` ≈ 5 % mean episode depth). /// /// Each `update` is O(period). #[derive(Debug, Clone)] @@ -65,16 +69,40 @@ impl Indicator for AverageDrawdown { return None; } let mut peak = f64::NEG_INFINITY; - let mut sum_dd = 0.0_f64; + let mut sum_depth = 0.0_f64; + let mut episodes = 0_u32; + let mut in_dd = false; + let mut episode_peak = 0.0_f64; + let mut episode_trough = 0.0_f64; for &v in &self.window { - if v > peak { + if v >= peak { + if in_dd { + if episode_peak > 0.0 { + sum_depth += (episode_peak - episode_trough) / episode_peak; + episodes += 1; + } + in_dd = false; + } peak = v; - } - if peak > 0.0 { - sum_dd += (peak - v) / peak; + } else if in_dd { + if v < episode_trough { + episode_trough = v; + } + } else { + in_dd = true; + episode_peak = peak; + episode_trough = v; } } - Some(sum_dd / self.period as f64) + if in_dd && episode_peak > 0.0 { + sum_depth += (episode_peak - episode_trough) / episode_peak; + episodes += 1; + } + Some(if episodes == 0 { + 0.0 + } else { + sum_depth / f64::from(episodes) + }) } fn reset(&mut self) { @@ -124,13 +152,24 @@ mod tests { #[test] fn reference_value() { - // window [100, 120, 90, 110]: - // peaks: 100, 120, 120, 120; dd: 0, 0, (30/120)=.25, (10/120)=.0833... - // avg = (.25 + .0833...) / 4 = .0833... + // window [100, 120, 90, 110]: one drawdown episode, opened at 90 (peak + // 120) and never recovering to 120 within the window. Its depth is + // (120 - 90) / 120 = 0.25; 110 stays inside the same episode and does + // not deepen the trough. One episode -> AvgDD = 0.25. let mut a = AverageDrawdown::new(4).unwrap(); let out = a.batch(&[100.0, 120.0, 90.0, 110.0]); - let expected = (0.25 + (10.0 / 120.0)) / 4.0; - assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-12); + assert_relative_eq!(out[3].unwrap(), 0.25, epsilon = 1e-12); + } + + #[test] + fn averages_distinct_episodes() { + // [100, 90, 100, 80, 100]: episode 1 troughs at 90 then recovers to 100 + // -> depth 0.10; episode 2 troughs at 80 then recovers -> depth 0.20. + // Mean of the two episode depths = 0.15 (distinct from the Pain Index, + // which would weight every under-water bar instead). + let mut a = AverageDrawdown::new(5).unwrap(); + let out = a.batch(&[100.0, 90.0, 100.0, 80.0, 100.0]); + assert_relative_eq!(out[4].unwrap(), 0.15, epsilon = 1e-12); } #[test] diff --git a/crates/wickra-core/src/indicators/awesome_oscillator_histogram.rs b/crates/wickra-core/src/indicators/awesome_oscillator_histogram.rs index 610e03da..270cd3da 100644 --- a/crates/wickra-core/src/indicators/awesome_oscillator_histogram.rs +++ b/crates/wickra-core/src/indicators/awesome_oscillator_histogram.rs @@ -1,24 +1,28 @@ //! Awesome Oscillator Histogram. +use std::collections::VecDeque; + use crate::error::{Error, Result}; use crate::indicators::awesome_oscillator::AwesomeOscillator; -use crate::indicators::sma::Sma; use crate::ohlcv::Candle; use crate::traits::Indicator; -/// "Awesome Oscillator Histogram" — the difference between the Awesome -/// Oscillator and its `sma_period`-bar `SMA`. Positive bars mean `AO` is -/// trending up (bullish acceleration); negative bars mean `AO` is trending -/// down (bearish acceleration). +/// Awesome Oscillator Histogram — the bar-to-bar **momentum** of the Awesome +/// Oscillator over a `lookback` window. This is the value behind the coloured +/// histogram bars in Bill Williams' charts: each bar shows how much `AO` has +/// changed, so positive values mean `AO` is rising (the histogram "greens up") +/// and negative values mean it is falling. /// /// ```text -/// AO = SMA(median, fast) − SMA(median, slow) -/// AOHist = AO − SMA(AO, sma_period) +/// AO = SMA(median, fast) − SMA(median, slow) +/// AOHist = AO_t − AO_{t−lookback} /// ``` /// -/// With Williams' default `sma_period = 5`, this collapses to the existing -/// `AcceleratorOscillator` for `fast = 5, slow = 34, sma_period = 5`; for any -/// other parameterisation this is a more flexible variant. +/// This is distinct from the two related indicators Wickra ships: the raw +/// [`AwesomeOscillator`](crate::AwesomeOscillator) is `AO` itself, and the +/// [`AcceleratorOscillator`](crate::AcceleratorOscillator) is `AO − SMA(AO, n)`. +/// The histogram instead reports `AO`'s rate of change. The default `lookback` +/// is `1` (the classic one-bar histogram delta). /// /// # Example /// @@ -38,17 +42,18 @@ use crate::traits::Indicator; pub struct AwesomeOscillatorHistogram { fast_period: usize, slow_period: usize, - sma_period: usize, + lookback: usize, ao: AwesomeOscillator, - sma: Sma, + history: VecDeque, + emitted: bool, } impl AwesomeOscillatorHistogram { /// # Errors /// - [`Error::PeriodZero`] if any period is zero. /// - [`Error::InvalidPeriod`] if `fast >= slow`. - pub fn new(fast: usize, slow: usize, sma_period: usize) -> Result { - if fast == 0 || slow == 0 || sma_period == 0 { + pub fn new(fast: usize, slow: usize, lookback: usize) -> Result { + if fast == 0 || slow == 0 || lookback == 0 { return Err(Error::PeriodZero); } if fast >= slow { @@ -59,20 +64,21 @@ impl AwesomeOscillatorHistogram { Ok(Self { fast_period: fast, slow_period: slow, - sma_period, + lookback, ao: AwesomeOscillator::new(fast, slow)?, - sma: Sma::new(sma_period)?, + history: VecDeque::with_capacity(lookback + 1), + emitted: false, }) } - /// Bill Williams' Accelerator-equivalent defaults `(5, 34, 5)`. + /// Bill Williams' defaults with a one-bar histogram delta `(5, 34, 1)`. pub fn classic() -> Self { - Self::new(5, 34, 5).expect("classic Awesome Oscillator Histogram parameters are valid") + Self::new(5, 34, 1).expect("classic Awesome Oscillator Histogram parameters are valid") } - /// Configured `(fast_period, slow_period, sma_period)`. + /// Configured `(fast_period, slow_period, lookback)`. pub const fn periods(&self) -> (usize, usize, usize) { - (self.fast_period, self.slow_period, self.sma_period) + (self.fast_period, self.slow_period, self.lookback) } } @@ -82,23 +88,29 @@ impl Indicator for AwesomeOscillatorHistogram { fn update(&mut self, candle: Candle) -> Option { let ao = self.ao.update(candle)?; - let sma = self.sma.update(ao)?; - Some(ao - sma) + self.history.push_back(ao); + if self.history.len() <= self.lookback { + return None; + } + let prev = self.history.pop_front().expect("history is non-empty"); + self.emitted = true; + Some(ao - prev) } fn reset(&mut self) { self.ao.reset(); - self.sma.reset(); + self.history.clear(); + self.emitted = false; } fn warmup_period(&self) -> usize { - // AO emits at `slow` candles; the SMA then needs `sma_period - 1` - // more AO values to fill its window. - self.slow_period + self.sma_period - 1 + // AO first emits at `slow` candles; `lookback` more AO values are then + // needed before `AO_t − AO_{t−lookback}` can be formed. + self.slow_period + self.lookback } fn is_ready(&self) -> bool { - self.sma.is_ready() + self.emitted } fn name(&self) -> &'static str { @@ -119,11 +131,11 @@ mod tests { #[test] fn rejects_zero_period() { assert!(matches!( - AwesomeOscillatorHistogram::new(0, 34, 5), + AwesomeOscillatorHistogram::new(0, 34, 1), Err(Error::PeriodZero) )); assert!(matches!( - AwesomeOscillatorHistogram::new(5, 0, 5), + AwesomeOscillatorHistogram::new(5, 0, 1), Err(Error::PeriodZero) )); assert!(matches!( @@ -135,7 +147,7 @@ mod tests { #[test] fn rejects_fast_geq_slow() { assert!(matches!( - AwesomeOscillatorHistogram::new(34, 5, 5), + AwesomeOscillatorHistogram::new(34, 5, 1), Err(Error::InvalidPeriod { .. }) )); } @@ -143,15 +155,15 @@ mod tests { #[test] fn accessors_and_metadata() { let hist = AwesomeOscillatorHistogram::classic(); - assert_eq!(hist.periods(), (5, 34, 5)); - assert_eq!(hist.warmup_period(), 38); + assert_eq!(hist.periods(), (5, 34, 1)); + assert_eq!(hist.warmup_period(), 35); assert_eq!(hist.name(), "AwesomeOscillatorHistogram"); } #[test] - fn constant_series_converges_to_zero() { - // AO of a flat series is 0; SMA of 0 is 0; difference is 0. - let mut hist = AwesomeOscillatorHistogram::new(3, 5, 3).unwrap(); + fn constant_series_yields_zero() { + // AO of a flat series is 0, so its momentum is 0. + let mut hist = AwesomeOscillatorHistogram::new(3, 5, 1).unwrap(); let candles: Vec = (0..30).map(|i| candle(42.0, i)).collect(); let out = hist.batch(&candles); for v in out.iter().skip(hist.warmup_period() - 1).flatten() { @@ -161,7 +173,7 @@ mod tests { #[test] fn warmup_emits_first_value_at_warmup_period() { - let mut hist = AwesomeOscillatorHistogram::new(2, 4, 3).unwrap(); + let mut hist = AwesomeOscillatorHistogram::new(2, 4, 2).unwrap(); assert_eq!(hist.warmup_period(), 6); let candles: Vec = (0..8) .map(|i| candle(10.0 + f64::from(i), i64::from(i))) @@ -173,6 +185,27 @@ mod tests { assert!(out[5].is_some()); } + #[test] + fn equals_ao_difference() { + // The histogram must equal AO_t − AO_{t−lookback} bar for bar. + let candles: Vec = (0..60_i64) + .map(|i| candle(100.0 + (i as f64 * 0.3).sin() * 5.0, i)) + .collect(); + let lookback = 1; + let ao_series = AwesomeOscillator::new(5, 34).unwrap().batch(&candles); + let hist = AwesomeOscillatorHistogram::new(5, 34, lookback) + .unwrap() + .batch(&candles); + for i in 0..candles.len() { + if let Some(h) = hist[i] { + let ao_now = ao_series[i].expect("AO present once histogram emits"); + let ao_prev = + ao_series[i - lookback].expect("prior AO present once histogram emits"); + assert_relative_eq!(h, ao_now - ao_prev, epsilon = 1e-9); + } + } + } + #[test] fn batch_equals_streaming() { let candles: Vec = (0..100_i64) diff --git a/crates/wickra-core/src/indicators/intraday_intensity.rs b/crates/wickra-core/src/indicators/intraday_intensity.rs index 2b7fef3a..ba5f5dda 100644 --- a/crates/wickra-core/src/indicators/intraday_intensity.rs +++ b/crates/wickra-core/src/indicators/intraday_intensity.rs @@ -1,29 +1,28 @@ -//! Intraday Intensity Index (Bostian) — a cumulative volume-weighted close-location line. +//! Intraday Intensity (Bostian) — the per-bar volume-weighted close-location. use crate::ohlcv::Candle; use crate::traits::Indicator; -/// Intraday Intensity Index — David Bostian's cumulative line that weights each -/// bar's volume by where the close lands inside the bar's range. +/// Intraday Intensity — David Bostian's per-bar measure that weights each bar's +/// volume by where the close lands inside the bar's range: /// /// ```text -/// II_t = volume * (2*close − high − low) / (high − low) (0 if high == low) -/// III_t = III_{t−1} + II_t +/// II_t = volume * (2*close − high − low) / (high − low) (0 if high == low) /// ``` /// /// The fraction `(2*close − high − low) / (high − low)` is `+1` when the bar -/// closes on its high, `−1` when it closes on its low, and `0` at the midpoint. -/// Scaling it by volume and accumulating produces a running measure of how -/// aggressively the close is being pushed toward the extremes — Bostian's proxy -/// for institutional accumulation (rising line) or distribution (falling line). +/// closes on its high, `−1` when it closes on its low, and `0` at the midpoint, +/// so `II_t` is the volume pushed toward the extremes on that single bar — +/// Bostian's proxy for per-bar accumulation (positive) or distribution +/// (negative). /// -/// This is the **cumulative** Intraday Intensity (the original index), not the -/// normalized "Intraday Intensity %" — the latter divides a windowed sum of `II` -/// by a windowed sum of volume and is mathematically identical to -/// [`Cmf`](crate::Cmf), so it is not duplicated here. The level of this line is -/// arbitrary; only its slope and divergences against price matter. A doji whose -/// `high == low` contributes nothing. Each `update` is O(1) and the first bar -/// already emits a value. +/// This emits the **raw per-bar** intensity, which is distinct from the two +/// derived forms Wickra ships separately: the **cumulative** running total is +/// the Accumulation/Distribution Line ([`Adl`](crate::Adl)), and the +/// volume-normalized windowed form ("Intraday Intensity %") is mathematically +/// the Chaikin Money Flow ([`Cmf`](crate::Cmf)). A doji whose `high == low` +/// contributes nothing. Each `update` is O(1) and the first bar already emits a +/// value. /// /// # Example /// @@ -41,12 +40,11 @@ use crate::traits::Indicator; /// ``` #[derive(Debug, Clone, Default)] pub struct IntradayIntensity { - iii: f64, last: Option, } impl IntradayIntensity { - /// Construct a new Intraday Intensity Index. The line is parameter-free. + /// Construct a new Intraday Intensity. It is parameter-free. #[must_use] pub fn new() -> Self { Self::default() @@ -69,13 +67,11 @@ impl Indicator for IntradayIntensity { } else { 0.0 }; - self.iii += ii; - self.last = Some(self.iii); - Some(self.iii) + self.last = Some(ii); + Some(ii) } fn reset(&mut self) { - self.iii = 0.0; self.last = None; } @@ -149,11 +145,14 @@ mod tests { } #[test] - fn accumulates_across_bars() { + fn each_bar_is_independent() { + // Per-bar (non-cumulative): each output depends only on that bar, so a + // close-on-high +1000 bar is not carried into the next close-on-low bar. let mut iii = IntradayIntensity::new(); - iii.update(candle(110.0, 100.0, 110.0, 1_000.0)); // +1000 - let v = iii.update(candle(110.0, 100.0, 100.0, 400.0)).unwrap(); // -400 -> 600 - assert_relative_eq!(v, 600.0, epsilon = 1e-9); + let a = iii.update(candle(110.0, 100.0, 110.0, 1_000.0)).unwrap(); + let b = iii.update(candle(110.0, 100.0, 100.0, 400.0)).unwrap(); + assert_relative_eq!(a, 1_000.0, epsilon = 1e-9); + assert_relative_eq!(b, -400.0, epsilon = 1e-9); } #[test]