diff --git a/CHANGELOG.md b/CHANGELOG.md
index a62b614d..c91a71c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ 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]
+- **TSF Oscillator** — the percentage gap of the close to the one-bar-ahead time-series forecast, a close-relative companion to CFO (`TsfOscillator`).
+- **MACD Histogram** — the standalone macd-minus-signal bar of MACD as a scalar series (`MacdHistogram`).
+- **PPO Histogram** — the Percentage Price Oscillator with its signal EMA and the resulting zero-centered histogram (`PpoHistogram`).
## [0.5.7] - 2026-06-04
- **Qstick** — Qstick (Chande), the SMA of the candle body (close − open) as a net buying/selling pressure gauge (`QSTICK`).
diff --git a/README.md b/README.md
index 020b383e..60a89d4f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
[](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 420 indicators; start at the
+ every one of the 423 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
-420 streaming-first indicators across twenty-four families. Every one passes the
+423 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).
@@ -146,7 +146,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, SWMA, GMA, EHMA, Median MA, Adaptive Laguerre, GD, Holt-Winters |
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100), Disparity Index, Fisher RSI, RSX, Dynamic Momentum Index, Stochastic CCI, RMI, Derivative Oscillator, Elder Ray, Intraday Momentum Index, QQE |
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX, TTM Trend, Trend Strength Index, Qstick, Polarized Fractal Efficiency, Wave PM, Gator Oscillator, Kase Permission Stochastic |
-| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
+| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC, TSF Oscillator, MACD Histogram, PPO Histogram |
| 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 |
| 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 |
| 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 |
@@ -245,7 +245,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
-│ ├── wickra-core/ core engine + all 420 indicators
+│ ├── wickra-core/ core engine + all 423 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 5db99c4e..39ea2bc1 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -28,6 +28,9 @@ function num(v) {
// --- Scalar indicators: update(value) vs batch(prices) ---
const scalarFactories = {
+ PpoHistogram: () => new wickra.PpoHistogram(3, 6, 3),
+ MacdHistogram: () => new wickra.MacdHistogram(3, 6, 3),
+ TsfOscillator: () => new wickra.TsfOscillator(3),
WAVE_PM: () => new wickra.WAVE_PM(32, 3),
POLARIZED_FRACTAL_EFFICIENCY: () => new wickra.POLARIZED_FRACTAL_EFFICIENCY(10, 5),
TREND_STRENGTH_INDEX: () => new wickra.TREND_STRENGTH_INDEX(20),
diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts
index ee319f22..969cd7d4 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -978,6 +978,15 @@ export declare class TREND_STRENGTH_INDEX {
isReady(): boolean
warmupPeriod(): number
}
+export type TsfOscillatorNode = TsfOscillator
+export declare class TsfOscillator {
+ constructor(period: number)
+ update(value: number): number | null
+ batch(prices: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
export type JumpIndicatorNode = JumpIndicator
export declare class JumpIndicator {
constructor(period: number, threshold: number)
@@ -1909,6 +1918,24 @@ export declare class DerivativeOscillator {
isReady(): boolean
warmupPeriod(): number
}
+export type MacdHistogramNode = MacdHistogram
+export declare class MacdHistogram {
+ constructor(fast: number, slow: number, signal: number)
+ update(value: number): number | null
+ batch(prices: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type PpoHistogramNode = PpoHistogram
+export declare class PpoHistogram {
+ constructor(fast: number, slow: number, signal: number)
+ update(value: number): number | null
+ batch(prices: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
export type TsiNode = TSI
export declare class TSI {
constructor(long: number, short: number)
diff --git a/bindings/node/index.js b/bindings/node/index.js
index 5c07f084..574a27ce 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, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, 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, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, 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, 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, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, 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, FibFan, FibArcs, FibChannel, FibTimeZones } = 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, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, 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, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, 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, 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, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, 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, FibFan, FibArcs, FibChannel, FibTimeZones } = nativeBinding
module.exports.version = version
module.exports.SMA = SMA
@@ -373,6 +373,7 @@ module.exports.FisherRSI = FisherRSI
module.exports.RSX = RSX
module.exports.DynamicMomentumIndex = DynamicMomentumIndex
module.exports.TREND_STRENGTH_INDEX = TREND_STRENGTH_INDEX
+module.exports.TsfOscillator = TsfOscillator
module.exports.JumpIndicator = JumpIndicator
module.exports.RegimeLabel = RegimeLabel
module.exports.RollingQuantile = RollingQuantile
@@ -463,6 +464,8 @@ module.exports.GD = GD
module.exports.HoltWinters = HoltWinters
module.exports.RMI = RMI
module.exports.DerivativeOscillator = DerivativeOscillator
+module.exports.MacdHistogram = MacdHistogram
+module.exports.PpoHistogram = PpoHistogram
module.exports.TSI = TSI
module.exports.PMO = PMO
module.exports.TII = TII
diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs
index fabdab26..aec93780 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -219,6 +219,7 @@ node_scalar_indicator!(
"TREND_STRENGTH_INDEX",
wc::TrendStrengthIndex
);
+node_scalar_indicator!(TsfOscillatorNode, "TsfOscillator", wc::TsfOscillator);
#[napi(js_name = "JumpIndicator")]
pub struct JumpIndicatorNode {
inner: wc::JumpIndicator,
@@ -4533,6 +4534,82 @@ impl DerivativeOscillatorNode {
}
}
+// ============================== MacdHistogram ==============================
+
+#[napi(js_name = "MacdHistogram")]
+pub struct MacdHistogramNode {
+ inner: wc::MacdHistogram,
+}
+
+#[napi]
+impl MacdHistogramNode {
+ #[napi(constructor)]
+ pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result {
+ Ok(Self {
+ inner: wc::MacdHistogram::new(fast as usize, slow as usize, signal as usize)
+ .map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(&mut self, value: f64) -> Option {
+ self.inner.update(value)
+ }
+ #[napi]
+ pub fn batch(&mut self, prices: Vec) -> Vec {
+ flatten(self.inner.batch(&prices))
+ }
+ #[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
+ }
+}
+
+// ============================== PpoHistogram ==============================
+
+#[napi(js_name = "PpoHistogram")]
+pub struct PpoHistogramNode {
+ inner: wc::PpoHistogram,
+}
+
+#[napi]
+impl PpoHistogramNode {
+ #[napi(constructor)]
+ pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result {
+ Ok(Self {
+ inner: wc::PpoHistogram::new(fast as usize, slow as usize, signal as usize)
+ .map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(&mut self, value: f64) -> Option {
+ self.inner.update(value)
+ }
+ #[napi]
+ pub fn batch(&mut self, prices: Vec) -> Vec {
+ flatten(self.inner.batch(&prices))
+ }
+ #[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
+ }
+}
+
// ============================== TSI ==============================
#[napi(js_name = "TSI")]
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index e0ffa12b..a53d52c3 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -25,6 +25,9 @@ from __future__ import annotations
from ._wickra import (
__version__,
+ PpoHistogram,
+ MacdHistogram,
+ TsfOscillator,
Qstick,
GatorOscillator,
KasePermissionStochastic,
@@ -473,6 +476,9 @@ from ._wickra import (
)
__all__ = [
+ "PpoHistogram",
+ "MacdHistogram",
+ "TsfOscillator",
"Qstick",
"GatorOscillator",
"KasePermissionStochastic",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 690c4b9c..6de67d43 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -3295,6 +3295,144 @@ impl PyKasePermissionStochastic {
}
}
+// ============================== TsfOscillator ==============================
+
+#[pyclass(name = "TsfOscillator", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyTsfOscillator {
+ inner: wc::TsfOscillator,
+}
+
+#[pymethods]
+impl PyTsfOscillator {
+ #[new]
+ #[pyo3(signature = (period=14))]
+ fn new(period: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::TsfOscillator::new(period).map_err(map_err)?,
+ })
+ }
+ fn update(&mut self, value: f64) -> Option {
+ self.inner.update(value)
+ }
+ fn batch<'py>(
+ &mut self,
+ py: Python<'py>,
+ prices: PyReadonlyArray1<'py, f64>,
+ ) -> PyResult>> {
+ let s = prices
+ .as_slice()
+ .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
+ Ok(flatten(self.inner.batch(s)).into_pyarray(py))
+ }
+ #[getter]
+ fn period(&self) -> usize {
+ self.inner.period()
+ }
+ 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 {
+ format!("TsfOscillator(period={})", self.inner.period())
+ }
+}
+
+// ============================== MacdHistogram ==============================
+
+#[pyclass(name = "MacdHistogram", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyMacdHistogram {
+ inner: wc::MacdHistogram,
+}
+
+#[pymethods]
+impl PyMacdHistogram {
+ #[new]
+ #[pyo3(signature = (fast=12, slow=26, signal=9))]
+ fn new(fast: usize, slow: usize, signal: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::MacdHistogram::new(fast, slow, signal).map_err(map_err)?,
+ })
+ }
+ fn update(&mut self, value: f64) -> Option {
+ self.inner.update(value)
+ }
+ fn batch<'py>(
+ &mut self,
+ py: Python<'py>,
+ prices: PyReadonlyArray1<'py, f64>,
+ ) -> PyResult>> {
+ let s = prices
+ .as_slice()
+ .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
+ Ok(flatten(self.inner.batch(s)).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 {
+ let (fast, slow, signal) = self.inner.periods();
+ format!("MacdHistogram(fast={fast}, slow={slow}, signal={signal})")
+ }
+}
+
+// ============================== PpoHistogram ==============================
+
+#[pyclass(name = "PpoHistogram", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyPpoHistogram {
+ inner: wc::PpoHistogram,
+}
+
+#[pymethods]
+impl PyPpoHistogram {
+ #[new]
+ #[pyo3(signature = (fast=12, slow=26, signal=9))]
+ fn new(fast: usize, slow: usize, signal: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::PpoHistogram::new(fast, slow, signal).map_err(map_err)?,
+ })
+ }
+ fn update(&mut self, value: f64) -> Option {
+ self.inner.update(value)
+ }
+ fn batch<'py>(
+ &mut self,
+ py: Python<'py>,
+ prices: PyReadonlyArray1<'py, f64>,
+ ) -> PyResult>> {
+ let s = prices
+ .as_slice()
+ .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
+ Ok(flatten(self.inner.batch(s)).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 {
+ let (fast, slow, signal) = self.inner.periods();
+ format!("PpoHistogram(fast={fast}, slow={slow}, signal={signal})")
+ }
+}
+
// ============================== Stochastic ==============================
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
@@ -21422,5 +21560,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
Ok(())
}
diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py
index 45461061..5e140f1b 100644
--- a/bindings/python/tests/test_new_indicators.py
+++ b/bindings/python/tests/test_new_indicators.py
@@ -45,6 +45,9 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
# --- Scalar (f64 -> f64) indicators ---------------------------------------
SCALAR = [
+ (ta.PpoHistogram, (3, 6, 3)),
+ (ta.MacdHistogram, (3, 6, 3)),
+ (ta.TsfOscillator, (3,)),
(ta.WAVE_PM, (32, 3)),
(ta.POLARIZED_FRACTAL_EFFICIENCY, (10, 5)),
(ta.TREND_STRENGTH_INDEX, (20,)),
@@ -2862,6 +2865,31 @@ def test_kase_permission_stochastic_reference():
assert out[-1][0] == pytest.approx(50.0)
assert out[-1][1] == pytest.approx(50.0)
+
+def test_tsf_oscillator_reference():
+ t = ta.TsfOscillator(3)
+ assert t.update(1.0) is None
+ assert t.update(2.0) is None
+ assert t.update(9.0) == pytest.approx(-33.33333333333333)
+
+
+def test_macd_histogram_reference():
+ # On a constant-slope ramp the MACD line is flat once seeded, so the
+ # signal EMA catches up and the histogram collapses to 0.
+ t = ta.MacdHistogram(3, 6, 3)
+ for i in range(7):
+ assert t.update(100.0 + i * 2.0) is None
+ assert t.update(100.0 + 7 * 2.0) == pytest.approx(0.0, abs=1e-9)
+
+
+def test_ppo_histogram_reference():
+ # PPO divides the EMA gap by the slow EMA, so on the same ramp the ratio
+ # keeps drifting and the histogram stays non-zero.
+ t = ta.PpoHistogram(3, 6, 3)
+ for i in range(7):
+ assert t.update(100.0 + i * 2.0) is None
+ assert t.update(100.0 + 7 * 2.0) == pytest.approx(-0.052098, abs=1e-6)
+
# --- Lifecycle ------------------------------------------------------------
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 7e19e139..13579ddf 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -10653,6 +10653,9 @@ wasm_scalar_indicator!(WasmDynamicMomentumIndex, "DynamicMomentumIndex", wc::Dyn
wasm_scalar_indicator!(WasmRmi, "RMI", wc::Rmi, period: usize, momentum: usize);
wasm_scalar_indicator!(WasmDerivativeOscillator, "DerivativeOscillator", wc::DerivativeOscillator, rsi_period: usize, smooth1: usize, smooth2: usize, signal_period: usize);
wasm_scalar_indicator!(WasmTrendStrengthIndex, "TREND_STRENGTH_INDEX", wc::TrendStrengthIndex, period: usize);
+wasm_scalar_indicator!(WasmTsfOscillator, "TsfOscillator", wc::TsfOscillator, period: usize);
+wasm_scalar_indicator!(WasmMacdHistogram, "MacdHistogram", wc::MacdHistogram, fast: usize, slow: usize, signal: usize);
+wasm_scalar_indicator!(WasmPpoHistogram, "PpoHistogram", wc::PpoHistogram, fast: usize, slow: usize, signal: usize);
// --- DrawdownDuration: u32 output, no constructor args ---
diff --git a/crates/wickra-core/src/indicators/macd_histogram.rs b/crates/wickra-core/src/indicators/macd_histogram.rs
new file mode 100644
index 00000000..dc26fe9b
--- /dev/null
+++ b/crates/wickra-core/src/indicators/macd_histogram.rs
@@ -0,0 +1,184 @@
+//! MACD Histogram (standalone).
+
+use crate::error::Result;
+use crate::indicators::macd::MacdIndicator;
+use crate::traits::Indicator;
+
+/// MACD Histogram — the `macd − signal` bar of [`MacdIndicator`] as a
+/// standalone scalar indicator.
+///
+/// ```text
+/// macd = EMA(fast) − EMA(slow)
+/// signal = EMA(macd, signal)
+/// histogram = macd − signal
+/// ```
+///
+/// The histogram is the most actively traded part of MACD: it crosses zero
+/// exactly when the MACD line crosses its signal, and its slope measures
+/// whether that momentum is accelerating or fading. This wrapper exposes just
+/// that series for pipelines that want a plain `f64` stream rather than the
+/// full [`MacdOutput`](crate::MacdOutput); for the line and signal alongside
+/// it, use [`MacdIndicator`](crate::MacdIndicator) directly.
+///
+/// Standard parameters are `fast = 12`, `slow = 26`, `signal = 9`, so the
+/// first value lands after `slow + signal − 1` inputs — exactly when
+/// [`MacdIndicator`] emits its first full output.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Indicator, MacdHistogram};
+///
+/// let mut indicator = MacdHistogram::new(12, 26, 9).unwrap();
+/// let mut last = None;
+/// for i in 0..80 {
+/// last = indicator.update(100.0 + f64::from(i));
+/// }
+/// assert!(last.is_some());
+/// ```
+#[derive(Debug, Clone)]
+pub struct MacdHistogram {
+ macd: MacdIndicator,
+}
+
+impl MacdHistogram {
+ /// Construct a MACD histogram with the given periods.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::PeriodZero`] if any period is zero, and
+ /// [`Error::InvalidPeriod`] if `fast >= slow`.
+ pub fn new(fast: usize, slow: usize, signal: usize) -> Result {
+ Ok(Self {
+ macd: MacdIndicator::new(fast, slow, signal)?,
+ })
+ }
+
+ /// Default `(12, 26, 9)` configuration, matching every classical chart package.
+ pub fn classic() -> Self {
+ Self::new(12, 26, 9).expect("classic MACD periods are valid")
+ }
+
+ /// Configured periods as `(fast, slow, signal)`.
+ pub const fn periods(&self) -> (usize, usize, usize) {
+ self.macd.periods()
+ }
+}
+
+impl Indicator for MacdHistogram {
+ type Input = f64;
+ type Output = f64;
+
+ fn update(&mut self, input: f64) -> Option {
+ self.macd.update(input).map(|out| out.histogram)
+ }
+
+ fn reset(&mut self) {
+ self.macd.reset();
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.macd.warmup_period()
+ }
+
+ fn is_ready(&self) -> bool {
+ self.macd.is_ready()
+ }
+
+ fn name(&self) -> &'static str {
+ "MacdHistogram"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::error::Error;
+ use crate::traits::BatchExt;
+ use approx::assert_relative_eq;
+
+ #[test]
+ fn rejects_invalid_periods() {
+ assert!(matches!(
+ MacdHistogram::new(0, 26, 9),
+ Err(Error::PeriodZero)
+ ));
+ assert!(matches!(
+ MacdHistogram::new(12, 26, 0),
+ Err(Error::PeriodZero)
+ ));
+ assert!(matches!(
+ MacdHistogram::new(26, 12, 9),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let osc = MacdHistogram::classic();
+ assert_eq!(osc.periods(), (12, 26, 9));
+ assert_eq!(osc.name(), "MacdHistogram");
+ assert_eq!(osc.warmup_period(), 26 + 9 - 1);
+ assert!(!osc.is_ready());
+ }
+
+ #[test]
+ fn equals_macd_histogram_field() {
+ // The standalone series must be exactly MacdIndicator's histogram bar.
+ let prices: Vec = (1..=120)
+ .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
+ .collect();
+ let hist = MacdHistogram::classic().batch(&prices);
+ let full = MacdIndicator::classic().batch(&prices);
+ assert_eq!(hist.len(), full.len());
+ for (h, m) in hist.iter().zip(full.iter()) {
+ assert_eq!(h.is_some(), m.is_some());
+ if let (Some(h), Some(m)) = (h, m) {
+ assert_relative_eq!(*h, m.histogram, epsilon = 1e-12);
+ }
+ }
+ }
+
+ #[test]
+ fn warmup_emits_first_value_at_warmup_period() {
+ let mut osc = MacdHistogram::new(3, 6, 3).unwrap();
+ let warmup = osc.warmup_period();
+ assert_eq!(warmup, 6 + 3 - 1);
+ for i in 1..warmup {
+ assert!(osc.update(100.0 + i as f64).is_none());
+ }
+ assert!(osc.update(100.0 + warmup as f64).is_some());
+ assert!(osc.is_ready());
+ }
+
+ #[test]
+ fn constant_series_converges_to_zero() {
+ let mut osc = MacdHistogram::classic();
+ let out = osc.batch(&[100.0_f64; 200]);
+ let last = out.iter().rev().flatten().next().expect("emits a value");
+ assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let prices: Vec = (1..=100)
+ .map(|i| (f64::from(i) * 0.4).cos() * 10.0)
+ .collect();
+ let mut a = MacdHistogram::classic();
+ let mut b = MacdHistogram::classic();
+ assert_eq!(
+ a.batch(&prices),
+ prices.iter().map(|p| b.update(*p)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut osc = MacdHistogram::classic();
+ osc.batch(&(1..=80).map(f64::from).collect::>());
+ assert!(osc.is_ready());
+ osc.reset();
+ assert!(!osc.is_ready());
+ assert_eq!(osc.update(1.0), None);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs
index 44de5f66..52edeeb0 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -214,6 +214,7 @@ mod ma_envelope;
mod macd;
mod macd_ext;
mod macd_fix;
+mod macd_histogram;
mod mama;
mod market_facilitation_index;
mod marubozu;
@@ -270,6 +271,7 @@ mod pmo;
mod point_and_figure_bars;
mod polarized_fractal_efficiency;
mod ppo;
+mod ppo_histogram;
mod profit_factor;
mod psar;
mod pvi;
@@ -381,6 +383,7 @@ mod triple_top_bottom;
mod trix;
mod true_range;
mod tsf;
+mod tsf_oscillator;
mod tsi;
mod tsv;
mod ttm_squeeze;
@@ -634,6 +637,7 @@ pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
pub use macd::{MacdIndicator, MacdOutput};
pub use macd_ext::{MaType, MacdExt};
pub use macd_fix::MacdFix;
+pub use macd_histogram::MacdHistogram;
pub use mama::{Mama, MamaOutput};
pub use market_facilitation_index::MarketFacilitationIndex;
pub use marubozu::Marubozu;
@@ -690,6 +694,7 @@ pub use pmo::Pmo;
pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
pub use ppo::Ppo;
+pub use ppo_histogram::PpoHistogram;
pub use profit_factor::ProfitFactor;
pub use psar::Psar;
pub use pvi::Pvi;
@@ -801,6 +806,7 @@ pub use triple_top_bottom::TripleTopBottom;
pub use trix::Trix;
pub use true_range::TrueRange;
pub use tsf::Tsf;
+pub use tsf_oscillator::TsfOscillator;
pub use tsi::Tsi;
pub use tsv::Tsv;
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
@@ -973,6 +979,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"ZeroLagMacd",
"ElderImpulse",
"Stc",
+ "TsfOscillator",
+ "MacdHistogram",
+ "PpoHistogram",
],
),
(
@@ -1414,6 +1423,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, 420, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 423, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/indicators/ppo_histogram.rs b/crates/wickra-core/src/indicators/ppo_histogram.rs
new file mode 100644
index 00000000..b3cedf8d
--- /dev/null
+++ b/crates/wickra-core/src/indicators/ppo_histogram.rs
@@ -0,0 +1,230 @@
+//! Percentage Price Oscillator Histogram.
+
+use crate::error::{Error, Result};
+use crate::indicators::ema::Ema;
+use crate::indicators::ppo::Ppo;
+use crate::traits::Indicator;
+
+/// PPO Histogram — the `ppo − signal` bar of the Percentage Price Oscillator.
+///
+/// ```text
+/// ppo = 100 · (EMA_fast − EMA_slow) / EMA_slow
+/// signal = EMA(ppo, signal_period)
+/// histogram = ppo − signal
+/// ```
+///
+/// [`Ppo`](crate::Ppo) itself only emits the percentage line; this indicator
+/// adds the classic 9-period signal EMA on top and reports the resulting
+/// zero-centered histogram. Because PPO is scale-free (the EMA gap is divided
+/// by the slow EMA), the histogram is **comparable across instruments** — a
+/// PPO histogram of `0.4` means the same relative momentum on any asset, unlike
+/// the price-unit [`MacdHistogram`](crate::MacdHistogram).
+///
+/// With Appel's defaults `fast = 12`, `slow = 26`, `signal = 9`, the first
+/// value lands after `slow + signal − 1` inputs — the point at which the slow
+/// EMA and then the signal EMA are both seeded.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Indicator, PpoHistogram};
+///
+/// let mut indicator = PpoHistogram::new(12, 26, 9).unwrap();
+/// let mut last = None;
+/// for i in 0..80 {
+/// last = indicator.update(100.0 + f64::from(i));
+/// }
+/// assert!(last.is_some());
+/// ```
+#[derive(Debug, Clone)]
+pub struct PpoHistogram {
+ ppo: Ppo,
+ signal_ema: Ema,
+ signal_period: usize,
+ current: Option,
+}
+
+impl PpoHistogram {
+ /// Construct a PPO histogram with the `fast`/`slow` EMA periods and the
+ /// `signal` EMA period.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::PeriodZero`] if any period is `0`, or
+ /// [`Error::InvalidPeriod`] if `fast >= slow`.
+ pub fn new(fast: usize, slow: usize, signal: usize) -> Result {
+ if signal == 0 {
+ return Err(Error::PeriodZero);
+ }
+ Ok(Self {
+ ppo: Ppo::new(fast, slow)?,
+ signal_ema: Ema::new(signal)?,
+ signal_period: signal,
+ current: None,
+ })
+ }
+
+ /// Default `(12, 26, 9)` configuration.
+ pub fn classic() -> Self {
+ Self::new(12, 26, 9).expect("classic PPO periods are valid")
+ }
+
+ /// Configured periods as `(fast, slow, signal)`.
+ pub const fn periods(&self) -> (usize, usize, usize) {
+ let (fast, slow) = self.ppo.periods();
+ (fast, slow, self.signal_period)
+ }
+
+ /// Current value if available.
+ pub const fn value(&self) -> Option {
+ self.current
+ }
+}
+
+impl Indicator for PpoHistogram {
+ type Input = f64;
+ type Output = f64;
+
+ fn update(&mut self, input: f64) -> Option {
+ // Guard before touching either stage so a non-finite input never
+ // advances the signal EMA on a stale, re-fed PPO value.
+ if !input.is_finite() {
+ return self.current;
+ }
+ let ppo = self.ppo.update(input)?;
+ let signal = self.signal_ema.update(ppo)?;
+ let histogram = ppo - signal;
+ self.current = Some(histogram);
+ Some(histogram)
+ }
+
+ fn reset(&mut self) {
+ self.ppo.reset();
+ self.signal_ema.reset();
+ self.current = None;
+ }
+
+ fn warmup_period(&self) -> usize {
+ // Slow EMA seeds the PPO, then the signal EMA needs `signal − 1` more.
+ self.ppo.warmup_period() + self.signal_period - 1
+ }
+
+ fn is_ready(&self) -> bool {
+ self.current.is_some()
+ }
+
+ fn name(&self) -> &'static str {
+ "PpoHistogram"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+ use approx::assert_relative_eq;
+
+ #[test]
+ fn rejects_invalid_periods() {
+ assert!(matches!(
+ PpoHistogram::new(0, 26, 9),
+ Err(Error::PeriodZero)
+ ));
+ assert!(matches!(
+ PpoHistogram::new(12, 0, 9),
+ Err(Error::PeriodZero)
+ ));
+ assert!(matches!(
+ PpoHistogram::new(12, 26, 0),
+ Err(Error::PeriodZero)
+ ));
+ assert!(matches!(
+ PpoHistogram::new(26, 12, 9),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let osc = PpoHistogram::classic();
+ assert_eq!(osc.periods(), (12, 26, 9));
+ assert_eq!(osc.name(), "PpoHistogram");
+ assert_eq!(osc.warmup_period(), 26 + 9 - 1);
+ assert_eq!(osc.value(), None);
+ assert!(!osc.is_ready());
+ }
+
+ #[test]
+ fn equals_ppo_minus_signal_ema() {
+ // The histogram must equal PPO minus an EMA(signal) composed by hand.
+ let prices: Vec = (1..=120)
+ .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 6.0)
+ .collect();
+ let got = PpoHistogram::new(12, 26, 9).unwrap().batch(&prices);
+
+ let mut ppo = Ppo::new(12, 26).unwrap();
+ let mut sig = Ema::new(9).unwrap();
+ let mut expected = Vec::with_capacity(prices.len());
+ for p in &prices {
+ let out = ppo
+ .update(*p)
+ .and_then(|line| sig.update(line).map(|signal| line - signal));
+ expected.push(out);
+ }
+ assert_eq!(got, expected);
+ }
+
+ #[test]
+ fn warmup_emits_first_value_at_warmup_period() {
+ let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
+ let warmup = osc.warmup_period();
+ assert_eq!(warmup, 6 + 3 - 1);
+ for i in 1..warmup {
+ assert!(osc.update(100.0 + i as f64).is_none());
+ }
+ assert!(osc.update(100.0 + warmup as f64).is_some());
+ assert!(osc.is_ready());
+ }
+
+ #[test]
+ fn constant_series_converges_to_zero() {
+ let mut osc = PpoHistogram::classic();
+ let out = osc.batch(&[100.0_f64; 200]);
+ let last = out.iter().rev().flatten().next().expect("emits a value");
+ assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
+ }
+
+ #[test]
+ fn ignores_non_finite_input() {
+ let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
+ let out = osc.batch(&(1..=40).map(f64::from).collect::>());
+ let before = *out.last().unwrap();
+ assert!(before.is_some());
+ assert_eq!(osc.update(f64::NAN), before);
+ assert_eq!(osc.update(f64::INFINITY), before);
+ assert_eq!(osc.value(), before);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let prices: Vec = (1..=100)
+ .map(|i| 100.0 + (f64::from(i) * 0.4).cos() * 10.0)
+ .collect();
+ let mut a = PpoHistogram::classic();
+ let mut b = PpoHistogram::classic();
+ assert_eq!(
+ a.batch(&prices),
+ prices.iter().map(|p| b.update(*p)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut osc = PpoHistogram::classic();
+ osc.batch(&(1..=80).map(f64::from).collect::>());
+ assert!(osc.is_ready());
+ osc.reset();
+ assert!(!osc.is_ready());
+ assert_eq!(osc.update(1.0), None);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/tsf_oscillator.rs b/crates/wickra-core/src/indicators/tsf_oscillator.rs
new file mode 100644
index 00000000..08c897e8
--- /dev/null
+++ b/crates/wickra-core/src/indicators/tsf_oscillator.rs
@@ -0,0 +1,206 @@
+//! Time Series Forecast Oscillator (TSF Oscillator).
+
+use crate::error::{Error, Result};
+use crate::indicators::tsf::Tsf;
+use crate::traits::Indicator;
+
+/// Time Series Forecast Oscillator — the percentage gap between the close and
+/// the **one-bar-ahead** time-series forecast of the close.
+///
+/// ```text
+/// TSFOsc_t = 100 · (close_t − TSF(close, period)_t) / close_t
+/// ```
+///
+/// where [`Tsf`](crate::Tsf) projects the rolling least-squares line one bar
+/// past the window (`a + b·period`). It is the close-relative companion to
+/// [`Cfo`](crate::Cfo), which measures the same percentage gap against the
+/// regression value at the *current* bar (`a + b·(period − 1)`). Because `TSF`
+/// advances one bar further than `LinearRegression`, the two differ by exactly
+/// the slope term `100·b/close`: on a trending series `TSFOsc` reads more
+/// negative in an uptrend (the forecast has already stepped above price) and
+/// more positive in a downtrend.
+///
+/// Positive readings mean the close sits *above* its forward forecast (price
+/// has overshot the projected trend); negative readings mean it sits below.
+/// Wraps the existing `Tsf` so the warmup matches.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Indicator, TsfOscillator};
+///
+/// let mut indicator = TsfOscillator::new(14).unwrap();
+/// let mut last = None;
+/// for i in 0..40 {
+/// last = indicator.update(100.0 + f64::from(i));
+/// }
+/// assert!(last.is_some());
+/// ```
+#[derive(Debug, Clone)]
+pub struct TsfOscillator {
+ period: usize,
+ tsf: Tsf,
+ current: Option,
+}
+
+impl TsfOscillator {
+ /// Construct a new TSF oscillator over `period` inputs.
+ ///
+ /// # Errors
+ /// Returns [`Error::InvalidPeriod`] if `period < 2` — a regression line is
+ /// undefined for fewer than two points.
+ pub fn new(period: usize) -> Result {
+ if period < 2 {
+ return Err(Error::InvalidPeriod {
+ message: "TSF oscillator needs period >= 2",
+ });
+ }
+ Ok(Self {
+ period,
+ tsf: Tsf::new(period)?,
+ current: None,
+ })
+ }
+
+ /// Configured period.
+ pub const fn period(&self) -> usize {
+ self.period
+ }
+}
+
+impl Indicator for TsfOscillator {
+ type Input = f64;
+ type Output = f64;
+
+ fn update(&mut self, input: f64) -> Option {
+ let forecast = self.tsf.update(input)?;
+ // Hold the previous value if the close is zero — the percentage form
+ // is undefined and a return of inf would propagate badly.
+ if input == 0.0 {
+ return self.current;
+ }
+ let value = 100.0 * (input - forecast) / input;
+ self.current = Some(value);
+ Some(value)
+ }
+
+ fn reset(&mut self) {
+ self.tsf.reset();
+ self.current = None;
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.period
+ }
+
+ fn is_ready(&self) -> bool {
+ self.current.is_some()
+ }
+
+ fn name(&self) -> &'static str {
+ "TsfOscillator"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+ use approx::assert_relative_eq;
+
+ #[test]
+ fn rejects_short_period() {
+ assert!(matches!(
+ TsfOscillator::new(1),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ assert!(matches!(
+ TsfOscillator::new(0),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let osc = TsfOscillator::new(14).unwrap();
+ assert_eq!(osc.period(), 14);
+ assert_eq!(osc.warmup_period(), 14);
+ assert_eq!(osc.name(), "TsfOscillator");
+ assert!(!osc.is_ready());
+ }
+
+ #[test]
+ fn reference_value() {
+ // period 3 over [1, 2, 9]: fit y = 0 + 4x, one-bar-ahead TSF at x = 3
+ // is 12. With close = 9, TSFOsc = 100·(9 − 12)/9 = −33.3333…%.
+ let mut osc = TsfOscillator::new(3).unwrap();
+ let out = osc.batch(&[1.0_f64, 2.0, 9.0]);
+ assert!(out[0].is_none());
+ assert!(out[1].is_none());
+ assert_relative_eq!(out[2].unwrap(), -100.0 / 3.0, epsilon = 1e-9);
+ assert!(osc.is_ready());
+ }
+
+ #[test]
+ fn constant_series_yields_zero() {
+ // On a flat series the regression slope is 0, so the one-bar-ahead TSF
+ // equals the constant and close − forecast is exactly 0.
+ let mut osc = TsfOscillator::new(5).unwrap();
+ let out = osc.batch(&[42.0_f64; 30]);
+ for v in out.iter().skip(4).flatten() {
+ assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
+ }
+ }
+
+ #[test]
+ fn linear_uptrend_reads_negative() {
+ // Unlike CFO (evaluated at the current bar), the forecast steps one bar
+ // ahead, so on a rising line the projection sits above the close and the
+ // oscillator is negative: TSFOsc = −100·slope/close.
+ let mut osc = TsfOscillator::new(5).unwrap();
+ let prices: Vec = (1..=20).map(|i| f64::from(i) * 2.0).collect();
+ let out = osc.batch(&prices);
+ for v in out.iter().skip(4).flatten() {
+ assert!(*v < 0.0, "uptrend forecast overshoots close, got {v}");
+ }
+ }
+
+ #[test]
+ fn warmup_emits_first_value_at_period() {
+ let mut osc = TsfOscillator::new(3).unwrap();
+ assert_eq!(osc.update(1.0), None);
+ assert_eq!(osc.update(2.0), None);
+ assert!(osc.update(3.0).is_some());
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let prices: Vec = (1..=80)
+ .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
+ .collect();
+ let mut a = TsfOscillator::new(14).unwrap();
+ let mut b = TsfOscillator::new(14).unwrap();
+ assert_eq!(
+ a.batch(&prices),
+ prices.iter().map(|p| b.update(*p)).collect::>()
+ );
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut osc = TsfOscillator::new(5).unwrap();
+ osc.batch(&(1..=20).map(f64::from).collect::>());
+ assert!(osc.is_ready());
+ osc.reset();
+ assert!(!osc.is_ready());
+ assert_eq!(osc.update(1.0), None);
+ }
+
+ #[test]
+ fn zero_close_holds_value() {
+ let mut osc = TsfOscillator::new(3).unwrap();
+ osc.batch(&[1.0_f64, 2.0, 3.0]);
+ let before = osc.current;
+ assert_eq!(osc.update(0.0), before);
+ }
+}
diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs
index f0cc5c4c..88b55178 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -99,7 +99,7 @@ pub use indicators::{
LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures,
LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
- MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput,
+ MaEnvelopeOutput, MacdExt, MacdFix, MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput,
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom,
@@ -109,10 +109,10 @@ pub use indicators::{
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
- PointAndFigureBars, PolarizedFractalEfficiency, Ppo, ProfitFactor, Psar, Pvi, Qqe, QqeOutput,
- Qstick, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility, RecoveryFactor,
- RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars,
- RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
+ PointAndFigureBars, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Psar, Pvi,
+ Qqe, QqeOutput, Qstick, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
+ RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
+ RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi,
RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow,
@@ -129,11 +129,11 @@ pub use indicators::{
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
Tii, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
TradeImbalance, TrendLabel, TrendStrengthIndex, TreynorRatio, Triangle, Trima, Trin,
- TripleTopBottom, Trix, TrueRange, Tsf, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
- TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
- UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
- ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VoltyStop,
- VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
+ TripleTopBottom, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
+ TtmTrend, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
+ UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
+ ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
+ VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
VwapStdDevBandsOutput, Vwma, Vzo, WavePm, WaveTrend, WaveTrendOutput, Wedge, WeightedClose,
WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots,
diff --git a/docs/README.md b/docs/README.md
index 9dbe22cf..dad2a7ff 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 **420 indicators** across
+- A per-indicator deep dive for every one of the **423 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.rs b/fuzz/fuzz_targets/indicator_update.rs
index 8bd6bccb..1accd724 100644
--- a/fuzz/fuzz_targets/indicator_update.rs
+++ b/fuzz/fuzz_targets/indicator_update.rs
@@ -14,7 +14,7 @@
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
use libfuzzer_sys::fuzz_target;
-use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
+use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
/// Drive a single streaming + batch run through one scalar indicator. Marked
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
@@ -84,6 +84,9 @@ fuzz_target!(|data: Vec| {
drive(|| Ppo::new(12, 26).unwrap(), &data);
drive(|| Apo::new(12, 26).unwrap(), &data);
drive(|| Cfo::new(14).unwrap(), &data);
+ drive(|| TsfOscillator::new(14).unwrap(), &data);
+ drive(|| MacdHistogram::new(12, 26, 9).unwrap(), &data);
+ drive(|| PpoHistogram::new(12, 26, 9).unwrap(), &data);
drive(|| ElderImpulse::classic(), &data);
drive(|| Stc::classic(), &data);
drive(|| Coppock::new(14, 11, 10).unwrap(), &data);