a3a1ae4dba
Adds ten pairwise `(f64, f64)` indicators to the **Price Statistics** family, completing the A1 stat-arb expansion block.
## Indicators
**Scalar output:**
- **RollingCorrelation** — rolling Pearson correlation of period-over-period *returns* (distinct from level-based `PearsonCorrelation`).
- **RollingCovariance** — rolling covariance of returns.
- **OuHalfLife** — Ornstein–Uhlenbeck half-life of mean reversion of the spread `a − b`.
- **SpreadHurst** — Hurst exponent of the spread (variance-of-lagged-differences fit) for regime detection.
- **DistanceSsd** — Gatev sum-of-squared-deviations between two start-normalised series.
- **BetaNeutralSpread** — rolling OLS regression residual `a − (α + β·b)`.
- **VarianceRatio** — Lo–MacKinlay variance-ratio test on the spread (two params: `period`, `q`).
- **GrangerCausality** — F-statistic for whether `b` predicts `a` (two params: `period`, `lag`).
**Struct output (custom bindings):**
- **KalmanHedgeRatio** — dynamic hedge ratio via a Kalman filter → `{ hedgeRatio, intercept, spread }`.
- **SpreadBollingerBands** — Bollinger bands on the spread → `{ middle, upper, lower, percentB }`.
## Notes
- No new traits or input families: all use the native `Indicator<Input = (f64, f64)>` (precedent `Beta`, `Cointegration`).
- Adds `Error::InvalidParameter` for floating-point constructor parameters (Kalman `delta`/`observation_var`, `num_std`).
- Full Python/Node/WASM bindings; the two struct-output indicators are hand-written, the rest use the pair macros.
- Indicator count 315 → 325; README, family rows, `__init__`, fuzz target, and CHANGELOG updated.
## Verification
- `cargo test --workspace --all-features` — green (2676 core lib + 308 doc).
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean.
- Node: `npm run build && npm test` — 410 passing (`index.d.ts`/`index.js` regenerated).
- Python: `pytest` — 684 passing.
82 lines
3.2 KiB
Rust
82 lines
3.2 KiB
Rust
#![no_main]
|
|
//! Fuzz two-input `Indicator<(f64, f64)>` implementations with arbitrary
|
|
//! `(asset, benchmark)` return pairs.
|
|
//!
|
|
//! Each iteration consumes a byte stream and interprets it as a sequence of
|
|
//! `(f64, f64)` pairs (8 bytes per `f64`), then drives every two-series
|
|
//! indicator over the sequence both streaming and as a batch. No path may
|
|
//! panic.
|
|
|
|
use libfuzzer_sys::fuzz_target;
|
|
use wickra_core::{Alpha, BatchExt, BetaNeutralSpread, Cointegration, DistanceSsd, GrangerCausality, Indicator, InformationRatio, KalmanHedgeRatio, LeadLagCrossCorrelation, OuHalfLife, PairSpreadZScore, PairwiseBeta, RelativeStrengthAB, RollingCorrelation, RollingCovariance, SpreadBollingerBands, SpreadHurst, TreynorRatio, VarianceRatio};
|
|
|
|
#[inline(never)]
|
|
fn drive<I>(make: impl Fn() -> I, data: &[(f64, f64)])
|
|
where
|
|
I: Indicator<Input = (f64, f64), Output = f64> + BatchExt,
|
|
{
|
|
let mut streaming = make();
|
|
for &x in data {
|
|
let _ = streaming.update(x);
|
|
}
|
|
let _ = make().batch(data);
|
|
}
|
|
|
|
fuzz_target!(|data: &[u8]| {
|
|
// Pack two consecutive 8-byte chunks into one `(f64, f64)` pair.
|
|
let pairs: Vec<(f64, f64)> = data
|
|
.chunks_exact(16)
|
|
.map(|c| {
|
|
let a = f64::from_le_bytes(c[..8].try_into().expect("8 bytes"));
|
|
let b = f64::from_le_bytes(c[8..].try_into().expect("8 bytes"));
|
|
(a, b)
|
|
})
|
|
.collect();
|
|
|
|
drive(|| TreynorRatio::new(10, 0.0).unwrap(), &pairs);
|
|
drive(|| InformationRatio::new(10).unwrap(), &pairs);
|
|
drive(|| Alpha::new(10, 0.0).unwrap(), &pairs);
|
|
drive(|| PairwiseBeta::new(10).unwrap(), &pairs);
|
|
drive(|| PairSpreadZScore::new(10, 10).unwrap(), &pairs);
|
|
drive(|| RollingCorrelation::new(20).unwrap(), &pairs);
|
|
drive(|| RollingCovariance::new(20).unwrap(), &pairs);
|
|
drive(|| OuHalfLife::new(60).unwrap(), &pairs);
|
|
drive(|| SpreadHurst::new(60).unwrap(), &pairs);
|
|
drive(|| DistanceSsd::new(20).unwrap(), &pairs);
|
|
drive(|| BetaNeutralSpread::new(20).unwrap(), &pairs);
|
|
drive(|| VarianceRatio::new(60, 2).unwrap(), &pairs);
|
|
drive(|| GrangerCausality::new(60, 1).unwrap(), &pairs);
|
|
|
|
// Struct-output pair indicator: drive update + batch directly (the generic
|
|
// `drive` above only covers `Output = f64`).
|
|
let mut ll = LeadLagCrossCorrelation::new(8, 3).unwrap();
|
|
for &x in &pairs {
|
|
let _ = ll.update(x);
|
|
}
|
|
let _ = LeadLagCrossCorrelation::new(8, 3).unwrap().batch(&pairs);
|
|
|
|
let mut co = Cointegration::new(12, 1).unwrap();
|
|
for &x in &pairs {
|
|
let _ = co.update(x);
|
|
}
|
|
let _ = Cointegration::new(12, 1).unwrap().batch(&pairs);
|
|
|
|
let mut rs = RelativeStrengthAB::new(10, 14).unwrap();
|
|
for &x in &pairs {
|
|
let _ = rs.update(x);
|
|
}
|
|
let _ = RelativeStrengthAB::new(10, 14).unwrap().batch(&pairs);
|
|
let mut kalman_hedge_ratio = KalmanHedgeRatio::new(0.001, 0.001).unwrap();
|
|
for &x in &pairs {
|
|
let _ = kalman_hedge_ratio.update(x);
|
|
}
|
|
let _ = KalmanHedgeRatio::new(0.001, 0.001).unwrap().batch(&pairs);
|
|
|
|
let mut spread_bollinger_bands = SpreadBollingerBands::new(20, 2.0).unwrap();
|
|
for &x in &pairs {
|
|
let _ = spread_bollinger_bands.update(x);
|
|
}
|
|
let _ = SpreadBollingerBands::new(20, 2.0).unwrap().batch(&pairs);
|
|
|
|
});
|