Files
wickra/fuzz/fuzz_targets/indicator_update_derivatives.rs
T
kingchenc ff5a047078 feat(derivatives): leverage, OI/volume, perpetual premium, funding APR, OI momentum (B16) (#214)
## B16 Derivatives — five new indicators (488 → 493)

All consume a `DerivativesTick` and emit `f64`:

| Indicator | Reads | Formula |
|-----------|-------|---------|
| `EstimatedLeverageRatio` | open_interest, long_size, short_size | `OI / (long + short)` |
| `OiToVolumeRatio` | open_interest, taker_buy_volume, taker_sell_volume | `OI / (buy + sell)` |
| `PerpetualPremiumIndex` | mark_price, index_price | `(mark − index) / index` |
| `FundingImpliedApr` | funding_rate | `rate × intervals_per_year` |
| `OpenInterestMomentum` | open_interest | `100 · (OI_t − OI_{t−period}) / OI_{t−period}` |

### Wiring
- Core structs + full unit tests (incl. zero-denominator branches).
- Hand-written Python/Node/WASM tick bindings; two new tick helpers (`deriv_oi_long_short`, `deriv_oi_taker`).
- Fuzz drives in `indicator_update_derivatives.rs`; dedicated reference + streaming-vs-batch tests (Python + Node).
- README counter + `docs/README.md` + `FAMILIES` assert bumped to 493.

### Verify (local, all green)
- `cargo test -p wickra-core --lib`: 4028 · `--doc`: 443
- clippy workspace: clean
- node: 563 · pytest: 928
2026-06-08 03:33:59 +02:00

65 lines
2.6 KiB
Rust

#![no_main]
//! Fuzz derivatives `Indicator<Input = DerivativesTick>` implementations with
//! arbitrary perpetual / futures tick streams.
//!
//! Each iteration consumes a byte stream, interprets it as a sequence of `f64`
//! values (8 bytes each), and packs consecutive groups of eleven into a
//! [`DerivativesTick`]'s numeric fields. Ticks are built with `new_unchecked`
//! so the fuzzer can explore degenerate values (non-finite, negative, zero
//! prices) that the validating constructor would reject — the indicators must
//! never panic, streaming or batched.
use libfuzzer_sys::fuzz_target;
use wickra_core::{BatchExt, CalendarSpread, DerivativesTick, EstimatedLeverageRatio, FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, Indicator, LiquidationFeatures, LongShortRatio, OIPriceDivergence, OIWeighted, OiToVolumeRatio, OpenInterestDelta, OpenInterestMomentum, PerpetualPremiumIndex, TakerBuySellRatio, TermStructureBasis};
#[inline(never)]
fn drive<I>(make: impl Fn() -> I, ticks: &[DerivativesTick])
where
I: Indicator<Input = DerivativesTick, Output = f64> + BatchExt,
{
let mut streaming = make();
for &tick in ticks {
let _ = streaming.update(tick);
}
let _ = make().batch(ticks);
}
fuzz_target!(|data: &[u8]| {
let floats: Vec<f64> = data
.chunks_exact(8)
.map(|c| f64::from_le_bytes(c.try_into().expect("8 bytes")))
.collect();
let ticks: Vec<DerivativesTick> = floats
.chunks_exact(11)
.map(|c| {
DerivativesTick::new_unchecked(
c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], 0,
)
})
.collect();
drive(FundingRate::new, &ticks);
drive(|| FundingRateMean::new(5).unwrap(), &ticks);
drive(|| FundingRateZScore::new(5).unwrap(), &ticks);
drive(FundingBasis::new, &ticks);
drive(OpenInterestDelta::new, &ticks);
drive(|| OIPriceDivergence::new(5).unwrap(), &ticks);
drive(OIWeighted::new, &ticks);
drive(LongShortRatio::new, &ticks);
drive(TakerBuySellRatio::new, &ticks);
drive(TermStructureBasis::new, &ticks);
drive(CalendarSpread::new, &ticks);
drive(EstimatedLeverageRatio::new, &ticks);
drive(OiToVolumeRatio::new, &ticks);
drive(PerpetualPremiumIndex::new, &ticks);
drive(|| FundingImpliedApr::new(1095.0).unwrap(), &ticks);
drive(|| OpenInterestMomentum::new(14).unwrap(), &ticks);
// LiquidationFeatures emits a struct, not an f64, so drive it directly.
let mut liq = LiquidationFeatures::new();
for &tick in &ticks {
let _ = liq.update(tick);
}
let _ = LiquidationFeatures::new().batch(&ticks);
});