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 @@
-
+
[](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);
+});