diff --git a/CHANGELOG.md b/CHANGELOG.md index afd60929..4d00b692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Market Breadth family** — a new indicator family built on a new + `CrossSection` input type that carries the per-symbol state of an entire + universe in one tick (each `Member` holds a signed `change`, a `volume`, and + `new_high` / `new_low` flags). `CrossSection::new` validates the universe + (non-empty, finite changes, finite non-negative volumes); `new_unchecked` + skips validation for hot paths. + - `AdvanceDecline` (`ADVANCE_DECLINE`) — the Advance/Decline Line, the running + cumulative sum of net advancing-minus-declining issues across the universe. + ## [0.4.6] - 2026-06-03 ### Added diff --git a/README.md b/README.md index 7a69ffc0..78ad7d22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

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

[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml) @@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**: [Node](https://docs.wickra.org/Quickstart-Node), [WASM](https://docs.wickra.org/Quickstart-WASM). - **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for - every one of the 314 indicators; start at the + every one of the 315 indicators; start at the [indicators overview](https://docs.wickra.org/Indicators-Overview). - **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods), [streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch), @@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries ## Indicators -314 streaming-first indicators across nineteen families. Every one passes the +315 streaming-first indicators across twenty families. Every one passes the `batch == streaming` equivalence test, reference-value tests, and reset semantics tests. Each has a per-indicator deep dive (formula, parameters, warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). @@ -160,6 +160,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint | | Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread | | Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range | +| Market Breadth | Advance/Decline Line | | Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) | Every candlestick pattern emits a signed per-bar value — `+1.0` bullish, @@ -239,7 +240,7 @@ A Python live-trading example using the public `websockets` package lives at ``` wickra/ ├── crates/ -│ ├── wickra-core/ core engine + all 314 indicators +│ ├── wickra-core/ core engine + all 315 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 ae650766..0efda719 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -1202,6 +1202,39 @@ test('derivatives reject bad input', () => { assert.throws(() => new wickra.FundingBasis().update(100, 0)); }); +test('market breadth: AdvanceDecline reference values', () => { + // A breadth tick is the universe as parallel arrays; the sign of `change` + // classifies each symbol as advancing / declining / unchanged. + const change = [ + [1.0, 0.5, 2.0, -1.0], // 3 up, 1 down -> net +2 + [-1.0, -0.5, -2.0, 1.0], // 1 up, 3 down -> net -2 + [0.0, 0.0, 1.0, -1.0], // 1 up, 1 down -> net 0 + ]; + const volume = change.map((row) => row.map(() => 10.0)); + const flags = change.map((row) => row.map(() => false)); + + const ad = new wickra.AdvanceDecline(); + // Cumulative line: +2 -> 0 -> 0. + assert.equal(ad.update(change[0], volume[0], flags[0], flags[0]), 2.0); + assert.equal(ad.update(change[1], volume[1], flags[1], flags[1]), 0.0); + assert.equal(ad.update(change[2], volume[2], flags[2], flags[2]), 0.0); + + // batch matches streaming. + const batch = new wickra.AdvanceDecline().batch(change, volume, flags, flags); + assert.deepEqual(Array.from(batch), [2.0, 0.0, 0.0]); +}); + +test('market breadth: AdvanceDecline rejects ragged universe', () => { + assert.throws(() => + new wickra.AdvanceDecline().update( + [1.0, -1.0], + [10.0], + [false, false], + [false, false], + ), + ); +}); + test('OI / flow / liquidation indicators reference values', () => { // OI +10% while price flat -> divergence +0.1. const div = new wickra.OIPriceDivergence(1); diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 070cf185..a449f4a4 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -3084,6 +3084,15 @@ export declare class CalendarSpread { isReady(): boolean warmupPeriod(): number } +export type AdvanceDeclineNode = AdvanceDecline +export declare class AdvanceDecline { + constructor() + update(change: Array, volume: Array, newHigh: Array, newLow: Array): number | null + batch(change: Array>, volume: Array>, newHigh: Array>, newLow: Array>): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} export type SharpeRatioNode = SharpeRatio export declare class SharpeRatio { constructor(period: number, riskFree: number) diff --git a/bindings/node/index.js b/bindings/node/index.js index bd79395c..5788b90d 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = nativeBinding +const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -607,6 +607,7 @@ module.exports.TakerBuySellRatio = TakerBuySellRatio module.exports.LiquidationFeatures = LiquidationFeatures module.exports.TermStructureBasis = TermStructureBasis module.exports.CalendarSpread = CalendarSpread +module.exports.AdvanceDecline = AdvanceDecline module.exports.SharpeRatio = SharpeRatio module.exports.SortinoRatio = SortinoRatio module.exports.CalmarRatio = CalmarRatio diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index b7281bed..cdfa347f 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -11168,6 +11168,100 @@ impl CalendarSpreadNode { } } +// ---------- Market Breadth (CrossSection input) ---------- +// +// A breadth tick is the per-symbol state of the whole universe, passed as four +// equal-length parallel arrays (`change`, `volume`, `newHigh`, `newLow`). +// `batch` takes one such group of arrays per tick. + +fn build_cross_section( + change: &[f64], + volume: &[f64], + new_high: &[bool], + new_low: &[bool], +) -> napi::Result { + if change.len() != volume.len() + || change.len() != new_high.len() + || change.len() != new_low.len() + { + return Err(NapiError::from_reason( + "change, volume, newHigh and newLow must be equal length".to_string(), + )); + } + let members = (0..change.len()) + .map(|i| wc::Member::new(change[i], volume[i], new_high[i], new_low[i])) + .collect(); + wc::CrossSection::new(members, 0).map_err(map_err) +} + +#[napi(js_name = "AdvanceDecline")] +pub struct AdvanceDeclineNode { + inner: wc::AdvanceDecline, +} + +impl Default for AdvanceDeclineNode { + fn default() -> Self { + Self::new() + } +} + +#[napi] +impl AdvanceDeclineNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::AdvanceDecline::new(), + } + } + #[napi] + pub fn update( + &mut self, + change: Vec, + volume: Vec, + new_high: Vec, + new_low: Vec, + ) -> napi::Result> { + Ok(self + .inner + .update(build_cross_section(&change, &volume, &new_high, &new_low)?)) + } + #[napi] + pub fn batch( + &mut self, + change: Vec>, + volume: Vec>, + new_high: Vec>, + new_low: Vec>, + ) -> napi::Result> { + if change.len() != volume.len() + || change.len() != new_high.len() + || change.len() != new_low.len() + { + return Err(NapiError::from_reason( + "change, volume, newHigh and newLow must have the same number of ticks".to_string(), + )); + } + let mut out = Vec::with_capacity(change.len()); + for i in 0..change.len() { + let section = build_cross_section(&change[i], &volume[i], &new_high[i], &new_low[i])?; + out.push(self.inner.update(section).unwrap_or(f64::NAN)); + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + // ============================== Family 15: Risk / Performance ============================== // Risk metrics with fallible `new` (most need `period >= 2`), so each wrapper diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index a66eea14..707a5a9b 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -341,6 +341,8 @@ from ._wickra import ( LiquidationFeatures, TermStructureBasis, CalendarSpread, + # Market Breadth + AdvanceDecline, # Risk / Performance SharpeRatio, SortinoRatio, @@ -679,6 +681,8 @@ __all__ = [ "LiquidationFeatures", "TermStructureBasis", "CalendarSpread", + # Market Breadth + "AdvanceDecline", # Risk / Performance "SharpeRatio", "SortinoRatio", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 5160def9..c5250bd4 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -29,7 +29,8 @@ fn map_err(e: wc::Error) -> PyErr { | wc::Error::InvalidTick { .. } | wc::Error::InvalidOrderBook { .. } | wc::Error::InvalidTrade { .. } - | wc::Error::InvalidDerivatives { .. } => PyValueError::new_err(e.to_string()), + | wc::Error::InvalidDerivatives { .. } + | wc::Error::InvalidCrossSection { .. } => PyValueError::new_err(e.to_string()), } } @@ -14383,6 +14384,101 @@ impl PyCalendarSpread { } } +// ============================== Market Breadth ============================== +// +// Market-breadth indicators consume a `CrossSection`: one tick carrying the +// per-symbol state of the whole universe. The Python convention passes a tick as +// four equal-length parallel arrays (`change`, `volume`, `new_high`, `new_low`); +// `batch` takes one such group of arrays per tick. + +fn build_cross_section( + change: &[f64], + volume: &[f64], + new_high: &[bool], + new_low: &[bool], +) -> PyResult { + if change.len() != volume.len() + || change.len() != new_high.len() + || change.len() != new_low.len() + { + return Err(PyValueError::new_err( + "change, volume, new_high and new_low must be equal length", + )); + } + let members = (0..change.len()) + .map(|i| wc::Member::new(change[i], volume[i], new_high[i], new_low[i])) + .collect(); + wc::CrossSection::new(members, 0).map_err(map_err) +} + +// AdvanceDecline takes no parameters; streaming `update(change, volume, new_high, +// new_low)` over one universe, `batch` over one such array group per tick. +#[pyclass( + name = "AdvanceDecline", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyAdvanceDecline { + inner: wc::AdvanceDecline, +} + +#[pymethods] +impl PyAdvanceDecline { + #[new] + fn new() -> Self { + Self { + inner: wc::AdvanceDecline::new(), + } + } + fn update( + &mut self, + change: Vec, + volume: Vec, + new_high: Vec, + new_low: Vec, + ) -> PyResult> { + Ok(self + .inner + .update(build_cross_section(&change, &volume, &new_high, &new_low)?)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + change: Vec>, + volume: Vec>, + new_high: Vec>, + new_low: Vec>, + ) -> PyResult>> { + if change.len() != volume.len() + || change.len() != new_high.len() + || change.len() != new_low.len() + { + return Err(PyValueError::new_err( + "change, volume, new_high and new_low must have the same number of ticks", + )); + } + let mut out = Vec::with_capacity(change.len()); + for i in 0..change.len() { + let section = build_cross_section(&change[i], &volume[i], &new_high[i], &new_low[i])?; + out.push(self.inner.update(section).unwrap_or(f64::NAN)); + } + Ok(out.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 { + "AdvanceDecline()".to_string() + } +} + // ============================== Family 15: Risk / Performance ============================== #[pyclass(name = "SharpeRatio", module = "wickra._wickra", skip_from_py_object)] @@ -15776,6 +15872,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // Family 15: Risk / Performance metrics. m.add_class::()?; m.add_class::()?; diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py index 1c0b5567..c0b47347 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -2659,6 +2659,38 @@ def test_funding_indicators_streaming_equals_batch(): assert _eq_nan(batch, streamed) +def test_advance_decline_streaming_equals_batch(): + # Three ticks over a universe of four symbols; the sign of `change` + # classifies each symbol as advancing / declining / unchanged. + change = [ + [1.0, 0.5, 2.0, -1.0], # 3 up, 1 down -> net +2 + [-1.0, -0.5, -2.0, 1.0], # 1 up, 3 down -> net -2 + [0.0, 0.0, 1.0, -1.0], # 1 up, 1 down -> net 0 + ] + volume = [[10.0] * 4 for _ in range(3)] + new_high = [[False] * 4 for _ in range(3)] + new_low = [[False] * 4 for _ in range(3)] + batch = ta.AdvanceDecline().batch(change, volume, new_high, new_low) + streamer = ta.AdvanceDecline() + streamed = np.array( + [ + streamer.update(change[i], volume[i], new_high[i], new_low[i]) + for i in range(3) + ], + dtype=np.float64, + ) + assert batch.shape == (3,) + assert _eq_nan(batch, streamed) + # Cumulative line: +2 -> 0 -> 0. + assert list(batch) == [2.0, 0.0, 0.0] + + +def test_advance_decline_rejects_ragged_universe(): + ad = ta.AdvanceDecline() + with pytest.raises(ValueError): + ad.update([1.0, -1.0], [10.0], [False, False], [False, False]) + + def test_funding_basis_streaming_equals_batch(): n = 40 index = np.array([100.0 + 0.5 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64) diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 89ac25b6..37a087c4 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -8143,6 +8143,78 @@ impl WasmCalendarSpread { } } +// ---------- Market Breadth (CrossSection input) ---------- +// +// A breadth tick is the per-symbol state of the whole universe, passed as four +// equal-length parallel arrays (`change`, `volume`, `newHigh`, `newLow`). The +// high/low flag arrays are numeric (non-zero is true) so the whole tick crosses +// the wasm boundary as `Float64Array`s. The universe is ragged across ticks, so +// only `update` is exposed (no `batch`), matching the other multi-input wasm +// indicators. + +fn build_cross_section( + change: &[f64], + volume: &[f64], + new_high: &[f64], + new_low: &[f64], +) -> Result { + if change.len() != volume.len() + || change.len() != new_high.len() + || change.len() != new_low.len() + { + return Err(JsError::new( + "change, volume, newHigh and newLow must be equal length", + )); + } + let members = (0..change.len()) + .map(|i| wc::Member::new(change[i], volume[i], new_high[i] != 0.0, new_low[i] != 0.0)) + .collect(); + wc::CrossSection::new(members, 0).map_err(map_err) +} + +#[wasm_bindgen(js_name = AdvanceDecline)] +pub struct WasmAdvanceDecline { + inner: wc::AdvanceDecline, +} + +impl Default for WasmAdvanceDecline { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = AdvanceDecline)] +impl WasmAdvanceDecline { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmAdvanceDecline { + Self { + inner: wc::AdvanceDecline::new(), + } + } + pub fn update( + &mut self, + change: Vec, + volume: Vec, + new_high: Vec, + new_low: Vec, + ) -> Result, JsError> { + Ok(self + .inner + .update(build_cross_section(&change, &volume, &new_high, &new_low)?)) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/wickra-core/src/cross_section.rs b/crates/wickra-core/src/cross_section.rs new file mode 100644 index 00000000..5b2c4aa0 --- /dev/null +++ b/crates/wickra-core/src/cross_section.rs @@ -0,0 +1,226 @@ +//! Cross-section value type: a market-breadth snapshot across a whole universe. +//! +//! A [`CrossSection`] is a single tick that carries the per-symbol state of +//! *every* symbol in a universe at one point in time. It is the non-OHLCV input +//! consumed by the market-breadth indicator family (advance/decline, `McClellan`, +//! the TRIN / Arms index, the high-low index, ...), each of which aggregates the +//! whole cross-section into a single breadth reading. This is the same +//! one-rich-type-per-family pattern as [`DerivativesTick`] and [`OrderBook`]. +//! +//! Each [`Member`] precomputes the per-symbol signals the breadth indicators +//! need — a signed price `change` (whose sign classifies the symbol as +//! advancing, declining or unchanged), the period `volume`, and the +//! `new_high` / `new_low` extreme flags — so the indicators stay stateless per +//! tick and never have to track per-symbol history. +//! +//! [`DerivativesTick`]: crate::DerivativesTick +//! [`OrderBook`]: crate::OrderBook + +use crate::error::{Error, Result}; + +/// One symbol's contribution to a [`CrossSection`] tick. +/// +/// Field invariants enforced by [`CrossSection::new`] when the member is placed +/// into a tick: +/// +/// - `change` is finite (its sign classifies the symbol — positive is +/// advancing, negative is declining, zero is unchanged). +/// - `volume` is finite and non-negative. +/// +/// `new_high` / `new_low` are caller-supplied flags marking whether the symbol +/// printed a new period extreme; they carry no numeric invariant. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Member { + /// Price change versus the previous close. Sign classifies the symbol: + /// positive is advancing, negative is declining, zero is unchanged. + pub change: f64, + /// Period volume for the symbol (finite, non-negative). + pub volume: f64, + /// Whether the symbol printed a new period high. + pub new_high: bool, + /// Whether the symbol printed a new period low. + pub new_low: bool, +} + +impl Member { + /// Assemble a cross-section member. + /// + /// The field invariants documented on [`Member`] are validated centrally by + /// [`CrossSection::new`] when the member is placed into a tick; this + /// constructor only assembles the value so the `#[non_exhaustive]` struct can + /// be built from outside the crate. + #[must_use] + pub const fn new(change: f64, volume: f64, new_high: bool, new_low: bool) -> Self { + Self { + change, + volume, + new_high, + new_low, + } + } +} + +/// A market-breadth cross-section: the per-symbol state of an entire universe at +/// a single point in time. +/// +/// Invariants enforced by [`new`](CrossSection::new): +/// +/// - `members` is non-empty (a breadth reading needs at least one symbol). +/// - every member's `change` is finite, and `volume` is finite and non-negative. +/// +/// `timestamp` is a caller-defined epoch / resolution and is not validated. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct CrossSection { + /// Per-symbol members of the universe for this tick. + pub members: Vec, + /// Tick timestamp (caller-defined epoch / resolution). + pub timestamp: i64, +} + +impl CrossSection { + /// Construct a cross-section, validating every member invariant. + /// + /// # Errors + /// + /// Returns [`Error::InvalidCrossSection`] if `members` is empty, if any + /// member has a non-finite `change`, or if any member has a `volume` that is + /// not a finite non-negative number. + pub fn new(members: Vec, timestamp: i64) -> Result { + if members.is_empty() { + return Err(Error::InvalidCrossSection { + message: "cross-section must contain at least one member", + }); + } + for member in &members { + if !member.change.is_finite() { + return Err(Error::InvalidCrossSection { + message: "member change must be finite", + }); + } + if !member.volume.is_finite() || member.volume < 0.0 { + return Err(Error::InvalidCrossSection { + message: "member volume must be finite and non-negative", + }); + } + } + Ok(Self { members, timestamp }) + } + + /// Construct a cross-section without validation. The caller asserts that + /// every invariant documented on [`CrossSection`] holds. + #[must_use] + pub const fn new_unchecked(members: Vec, timestamp: i64) -> Self { + Self { members, timestamp } + } + + /// Number of advancing symbols (those with a strictly positive `change`). + #[must_use] + pub fn advancers(&self) -> usize { + self.members.iter().filter(|m| m.change > 0.0).count() + } + + /// Number of declining symbols (those with a strictly negative `change`). + #[must_use] + pub fn decliners(&self) -> usize { + self.members.iter().filter(|m| m.change < 0.0).count() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn members() -> Vec { + vec![ + Member::new(1.5, 100.0, true, false), + Member::new(-0.5, 50.0, false, true), + Member::new(0.0, 0.0, false, false), + ] + } + + #[test] + fn new_accepts_valid() { + let cs = CrossSection::new(members(), 42).unwrap(); + assert_eq!(cs.members.len(), 3); + assert_eq!(cs.timestamp, 42); + assert_eq!(cs.members[0].change, 1.5); + assert_eq!(cs.members[0].volume, 100.0); + assert!(cs.members[0].new_high); + assert!(cs.members[1].new_low); + } + + #[test] + fn member_new_assembles_fields() { + let m = Member::new(2.0, 10.0, true, false); + assert_eq!(m.change, 2.0); + assert_eq!(m.volume, 10.0); + assert!(m.new_high); + assert!(!m.new_low); + } + + #[test] + fn new_rejects_empty() { + assert!(matches!( + CrossSection::new(Vec::new(), 0), + Err(Error::InvalidCrossSection { .. }) + )); + } + + #[test] + fn new_rejects_non_finite_change() { + assert!(matches!( + CrossSection::new(vec![Member::new(f64::NAN, 10.0, false, false)], 0), + Err(Error::InvalidCrossSection { .. }) + )); + assert!(matches!( + CrossSection::new(vec![Member::new(f64::INFINITY, 10.0, false, false)], 0), + Err(Error::InvalidCrossSection { .. }) + )); + } + + #[test] + fn new_rejects_negative_volume() { + assert!(matches!( + CrossSection::new(vec![Member::new(1.0, -1.0, false, false)], 0), + Err(Error::InvalidCrossSection { .. }) + )); + } + + #[test] + fn new_rejects_non_finite_volume() { + assert!(matches!( + CrossSection::new(vec![Member::new(1.0, f64::NAN, false, false)], 0), + Err(Error::InvalidCrossSection { .. }) + )); + } + + #[test] + fn new_unchecked_skips_validation() { + let cs = CrossSection::new_unchecked(vec![Member::new(f64::NAN, -1.0, false, false)], 7); + assert_eq!(cs.members.len(), 1); + assert_eq!(cs.timestamp, 7); + } + + #[test] + fn advancers_and_decliners_count_by_sign() { + let cs = CrossSection::new(members(), 0).unwrap(); + assert_eq!(cs.advancers(), 1); + assert_eq!(cs.decliners(), 1); + } + + #[test] + fn unchanged_members_count_as_neither() { + let cs = CrossSection::new( + vec![ + Member::new(0.0, 1.0, false, false), + Member::new(0.0, 1.0, false, false), + ], + 0, + ) + .unwrap(); + assert_eq!(cs.advancers(), 0); + assert_eq!(cs.decliners(), 0); + } +} diff --git a/crates/wickra-core/src/error.rs b/crates/wickra-core/src/error.rs index b91f2da3..1150fb62 100644 --- a/crates/wickra-core/src/error.rs +++ b/crates/wickra-core/src/error.rs @@ -52,6 +52,14 @@ pub enum Error { /// own variant. #[error("invalid derivatives tick: {message}")] InvalidDerivatives { message: &'static str }, + + /// A market-breadth cross-section whose members do not satisfy the + /// cross-section invariants (an empty universe, a non-finite change, or a + /// negative / non-finite volume) was provided. A cross-section is a + /// breadth input distinct from candles, ticks, order books and trades, so + /// it surfaces as its own variant. + #[error("invalid cross-section: {message}")] + InvalidCrossSection { message: &'static str }, } /// Convenience alias for `Result`. diff --git a/crates/wickra-core/src/indicators/advance_decline.rs b/crates/wickra-core/src/indicators/advance_decline.rs new file mode 100644 index 00000000..468c9099 --- /dev/null +++ b/crates/wickra-core/src/indicators/advance_decline.rs @@ -0,0 +1,168 @@ +//! Advance/Decline Line — cumulative net advancing-minus-declining issues. + +use crate::cross_section::CrossSection; +use crate::traits::Indicator; + +/// Advance/Decline Line (A/D Line) — the running cumulative sum of net advancing +/// issues across a universe. +/// +/// On each [`CrossSection`] tick the net breadth is `advancers - decliners`: +/// the number of symbols with a positive price change minus the number with a +/// negative change (unchanged symbols are ignored). The line accumulates this +/// net value over time, so a rising line means advancers have persistently +/// outnumbered decliners — broad participation — while a falling line warns that +/// a rally is being carried by fewer and fewer names (a breadth divergence when +/// the index itself is still rising). +/// +/// `Input = CrossSection`, `Output = f64`. The line is defined from the very +/// first tick, so `warmup_period == 1` and the indicator is ready after one +/// update. +/// +/// # Example +/// +/// ``` +/// use wickra_core::{AdvanceDecline, CrossSection, Indicator, Member}; +/// +/// let mut ad = AdvanceDecline::new(); +/// // 3 advancers, 1 decliner -> net +2. +/// let tick = CrossSection::new( +/// vec![ +/// Member::new(1.0, 10.0, false, false), +/// Member::new(0.5, 10.0, false, false), +/// Member::new(2.0, 10.0, false, false), +/// Member::new(-1.0, 10.0, false, false), +/// ], +/// 0, +/// ) +/// .unwrap(); +/// assert_eq!(ad.update(tick), Some(2.0)); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct AdvanceDecline { + line: f64, + has_emitted: bool, +} + +impl AdvanceDecline { + /// Construct a new Advance/Decline Line indicator. + #[must_use] + pub const fn new() -> Self { + Self { + line: 0.0, + has_emitted: false, + } + } +} + +impl Indicator for AdvanceDecline { + type Input = CrossSection; + type Output = f64; + + fn update(&mut self, section: CrossSection) -> Option { + let net = section.advancers() as f64 - section.decliners() as f64; + self.line += net; + self.has_emitted = true; + Some(self.line) + } + + fn reset(&mut self) { + self.line = 0.0; + self.has_emitted = false; + } + + fn warmup_period(&self) -> usize { + 1 + } + + fn is_ready(&self) -> bool { + self.has_emitted + } + + fn name(&self) -> &'static str { + "AdvanceDecline" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cross_section::Member; + use crate::traits::BatchExt; + + /// Build a cross-section with `up` advancers, `down` decliners and `flat` + /// unchanged symbols. + fn section(up: usize, down: usize, flat: usize) -> CrossSection { + let mut members = Vec::new(); + for _ in 0..up { + members.push(Member::new(1.0, 10.0, false, false)); + } + for _ in 0..down { + members.push(Member::new(-1.0, 10.0, false, false)); + } + for _ in 0..flat { + members.push(Member::new(0.0, 10.0, false, false)); + } + CrossSection::new(members, 0).unwrap() + } + + #[test] + fn accessors_and_metadata() { + let ad = AdvanceDecline::new(); + assert_eq!(ad.name(), "AdvanceDecline"); + assert_eq!(ad.warmup_period(), 1); + assert!(!ad.is_ready()); + } + + #[test] + fn first_tick_emits_net_breadth() { + let mut ad = AdvanceDecline::new(); + assert_eq!(ad.update(section(3, 1, 0)), Some(2.0)); + assert!(ad.is_ready()); + } + + #[test] + fn line_accumulates_across_ticks() { + let mut ad = AdvanceDecline::new(); + assert_eq!(ad.update(section(3, 1, 0)), Some(2.0)); // +2 -> 2 + assert_eq!(ad.update(section(1, 4, 0)), Some(-1.0)); // -3 -> -1 + assert_eq!(ad.update(section(2, 0, 0)), Some(1.0)); // +2 -> 1 + } + + #[test] + fn unchanged_symbols_are_ignored() { + let mut ad = AdvanceDecline::new(); + // 2 up, 2 down, 5 unchanged -> net 0, line stays flat. + assert_eq!(ad.update(section(2, 2, 5)), Some(0.0)); + assert_eq!(ad.update(section(2, 2, 5)), Some(0.0)); + } + + #[test] + fn reset_clears_state() { + let mut ad = AdvanceDecline::new(); + ad.update(section(5, 0, 0)); + assert!(ad.is_ready()); + ad.reset(); + assert!(!ad.is_ready()); + // Line restarts from zero, not from the pre-reset value. + assert_eq!(ad.update(section(1, 0, 0)), Some(1.0)); + } + + #[test] + fn batch_equals_streaming() { + let sections = vec![ + section(3, 1, 2), + section(1, 4, 0), + section(2, 2, 1), + section(5, 0, 3), + ]; + let mut a = AdvanceDecline::new(); + let mut b = AdvanceDecline::new(); + assert_eq!( + a.batch(§ions), + sections + .iter() + .map(|s| b.update(s.clone())) + .collect::>() + ); + } +} diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs index 36c246c0..24f8c2dc 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -11,6 +11,7 @@ mod ad_oscillator; mod adaptive_cycle; mod adl; mod advance_block; +mod advance_decline; mod adx; mod adxr; mod alligator; @@ -325,6 +326,7 @@ pub use ad_oscillator::AdOscillator; pub use adaptive_cycle::AdaptiveCycle; pub use adl::Adl; pub use advance_block::AdvanceBlock; +pub use advance_decline::AdvanceDecline; pub use adx::{Adx, AdxOutput}; pub use adxr::Adxr; pub use alligator::{Alligator, AlligatorOutput}; @@ -1038,6 +1040,7 @@ pub const FAMILIES: &[(&str, &[&str])] = &[ "Alt-Chart Bars", &["RenkoBars", "KagiBars", "PointAndFigureBars"], ), + ("Market Breadth", &["AdvanceDecline"]), ]; #[cfg(test)] @@ -1066,6 +1069,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, 314, "FAMILIES total drifted from indicator count"); + assert_eq!(total, 315, "FAMILIES total drifted from indicator count"); } } diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs index 3e1b6fa5..d34f75d5 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -42,6 +42,7 @@ // builds — library code is still linted for genuinely large stack arrays. #![cfg_attr(test, allow(clippy::large_stack_arrays))] +mod cross_section; mod derivatives; mod error; mod microstructure; @@ -50,68 +51,69 @@ mod traits; pub mod indicators; +pub use cross_section::{CrossSection, Member}; pub use derivatives::DerivativesTick; pub use error::{Error, Result}; pub use indicators::{ AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator, - AdaptiveCycle, Adl, AdvanceBlock, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma, - Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands, - AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, AvgPrice, AwesomeOscillator, - AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BollingerBands, BollingerBandwidth, - BollingerOutput, Breakaway, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, - CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, - ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, - ClassicPivotsOutput, ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, - CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, - Counterattack, CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema, - DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar, - Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, - DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx, - EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema, - EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama, - FibonacciPivots, FibonacciPivotsOutput, FisherTransform, Footprint, FootprintOutput, - ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, - FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility, - GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator, - HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, - HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, - HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, - Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline, - InverseFisherTransform, InvertedHammer, Jma, KagiBars, Kama, KellyCriterion, Keltner, - KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, - LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, - LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression, - LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio, - MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput, - MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown, - McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, - MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, Nvi, OIPriceDivergence, - OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, - OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, - PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB, - PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, - ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, - RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, - RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollingVwap, - RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeparatingLines, SharpeRatio, - ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, - SpearmanCorrelation, SpinningTop, StalledPattern, StandardError, StandardErrorBands, - StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, - StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend, - SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, - TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, - TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential, - TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike, - ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tii, TpoProfile, - TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsf, Tsi, Tsv, - TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, - UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput, - ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop, VolumeOscillator, - VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vwap, - VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, - WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, - YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, - Zlema, FAMILIES, T3, + AdaptiveCycle, Adl, AdvanceBlock, AdvanceDecline, Adx, AdxOutput, Adxr, Alligator, + AlligatorOutput, Alma, Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, + AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, + AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, + BollingerBands, BollingerBandwidth, BollingerOutput, Breakaway, CalendarSpread, CalmarRatio, + Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, + ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, + ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, ClosingMarubozu, + Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput, ConcealingBabySwallow, + ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, CumulativeVolumeDelta, + CyberneticCycle, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots, + DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar, Donchian, DonchianOutput, + DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, + DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx, EaseOfMovement, + EffectiveSpread, EhlersStochastic, ElderImpulse, Ema, EmpiricalModeDecomposition, Engulfing, + EveningDojiStar, Evwma, FallingThreeMethods, Fama, FibonacciPivots, FibonacciPivotsOutput, + FisherTransform, Footprint, FootprintOutput, ForceIndex, FractalChaosBands, + FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore, + GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, + Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighWave, Hikkake, HikkakeModified, + HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, + HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, + IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, + InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, + KagiBars, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, + KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, + LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput, + LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, + LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, + MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex, + MatHold, MatchingLow, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, + Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, + Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, + OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, + OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta, + ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo, + PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, + QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB, + RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc, + Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi, + RviVolatility, Rwi, RwiOutput, SarExt, SeparatingLines, SharpeRatio, ShootingStar, ShortLine, + SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, + SpinningTop, StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, + StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, + Stochastic, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, + Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, + TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, + TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis, + ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, + Tii, TpoProfile, TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsf, + Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, + UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, + ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop, + VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, + Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, + WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, + WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, + ZigZag, ZigZagOutput, Zlema, FAMILIES, T3, }; // `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own // line so the indicator-count tooling (which scans the braced block above and diff --git a/docs/README.md b/docs/README.md index e48e2fdc..d56ae461 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 **314 indicators** across +- A per-indicator deep dive for every one of the **315 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/Cargo.toml b/fuzz/Cargo.toml index 4ec4daf7..1974829d 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -80,6 +80,13 @@ test = false doc = false bench = false +[[bin]] +name = "indicator_update_crosssection" +path = "fuzz_targets/indicator_update_crosssection.rs" +test = false +doc = false +bench = false + [[bin]] name = "tick_aggregator" path = "fuzz_targets/tick_aggregator.rs" diff --git a/fuzz/fuzz_targets/indicator_update_crosssection.rs b/fuzz/fuzz_targets/indicator_update_crosssection.rs new file mode 100644 index 00000000..4dd5805a --- /dev/null +++ b/fuzz/fuzz_targets/indicator_update_crosssection.rs @@ -0,0 +1,47 @@ +#![no_main] +//! Fuzz market-breadth `Indicator` implementations with +//! arbitrary cross-section streams. +//! +//! Each iteration consumes a byte stream, interprets it as a sequence of `f64` +//! values (8 bytes each), packs consecutive pairs into [`Member`]s (a `change` +//! and a `volume`, with the high/low flags taken from the value bit parity), and +//! groups the members into bounded-size [`CrossSection`] ticks. Cross-sections +//! are built with `new_unchecked` so the fuzzer can explore degenerate values +//! (non-finite changes, negative volumes, empty-adjacent groups) that the +//! validating constructor would reject — the indicators must never panic, +//! streaming or batched. + +use libfuzzer_sys::fuzz_target; +use wickra_core::{AdvanceDecline, BatchExt, CrossSection, Indicator, Member}; + +#[inline(never)] +fn drive(make: impl Fn() -> I, sections: &[CrossSection]) +where + I: Indicator + BatchExt, +{ + let mut streaming = make(); + for section in sections { + let _ = streaming.update(section.clone()); + } + let _ = make().batch(sections); +} + +fuzz_target!(|data: &[u8]| { + let floats: Vec = data + .chunks_exact(8) + .map(|c| f64::from_le_bytes(c.try_into().expect("8 bytes"))) + .collect(); + let members: Vec = floats + .chunks_exact(2) + .map(|c| Member::new(c[0], c[1], c[0].to_bits() & 1 == 1, c[1].to_bits() & 1 == 1)) + .collect(); + // Group members into cross-sections of up to eight symbols each so a single + // input yields a stream of ragged universes. + let sections: Vec = members + .chunks(8) + .filter(|chunk| !chunk.is_empty()) + .map(|chunk| CrossSection::new_unchecked(chunk.to_vec(), 0)) + .collect(); + + drive(AdvanceDecline::new, §ions); +});