Files
wickra/fuzz/fuzz_targets/indicator_update.rs
T
kingchenc 3287146f44 feat(mcginley): add McGinley Dynamic moving average
John McGinley's self-adjusting moving average with the recurrence
MD + (price - MD) / (0.6 * period * (price / MD)^4). Speeds up when
price falls below the indicator and damps when price runs above the
indicator. Seeded with the simple average of the first period inputs.

Reference: McGinley, Technical Analysis of Stocks & Commodities, 1990.

Touchpoints:
- crates/wickra-core: mcginley_dynamic.rs + mod.rs + lib.rs re-export
- bindings/python: PyMcGinleyDynamic + __init__.py + test_new_indicators
  + test_known_values reference
- bindings/node: McGinleyDynamicNode (scalar macro) + index.d.ts/index.js
  + indicators.test.js factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers McGinleyDynamic(10)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
2026-05-24 12:26:08 +02:00

93 lines
4.0 KiB
Rust

#![no_main]
//! Fuzz scalar-input indicator updates with arbitrary `f64` sequences.
//!
//! Every scalar indicator must tolerate any finite-or-not input stream — NaN,
//! ±inf, subnormals, abrupt jumps — without panicking. Each fuzz iteration
//! runs the **same** input sequence through every scalar indicator twice:
//! once as a streaming `update` loop and once as a full `batch` call. Neither
//! path may panic; `batch` is also expected to agree with the streaming path
//! (the `BatchExt` blanket implementation replays `update` internally, so the
//! agreement is structural — but exercising both paths surfaces any
//! state-mutation bugs in `update` that would only manifest mid-batch).
//!
//! Audit finding R9: the previous version covered only `Rsi(14)` and
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
use libfuzzer_sys::fuzz_target;
use wickra_core::{
Alma, BatchExt, BollingerBands, Cmo, Coppock, Dema, Dpo, Ema, HistoricalVolatility, Hma,
Indicator, Kama, LinRegAngle, LinRegSlope, LinearRegression, MacdIndicator, McGinleyDynamic,
Mom, Pmo, Ppo, Roc, Rsi, Sma, Smma, StdDev, StochRsi, T3, Tema, Trima, Trix, Tsi, UlcerIndex,
VerticalHorizontalFilter, Wma, ZScore, Zlema,
};
/// Drive a single streaming + batch run through one scalar indicator. Marked
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
#[inline(never)]
fn drive<I>(make: impl Fn() -> I, data: &[f64])
where
I: Indicator<Input = f64, Output = f64> + BatchExt,
{
let mut streaming = make();
for &x in data {
let _ = streaming.update(x);
}
let _ = make().batch(data);
}
fuzz_target!(|data: Vec<f64>| {
// Bounded periods keep each iteration cheap and bias the fuzzer toward
// adversarial input patterns rather than enormous windows. The constants
// mirror the README's "common defaults" so we cover the parameterisations
// most users actually instantiate.
drive(|| Sma::new(14).unwrap(), &data);
drive(|| Ema::new(20).unwrap(), &data);
drive(|| Wma::new(14).unwrap(), &data);
drive(|| Rsi::new(14).unwrap(), &data);
drive(|| Dema::new(14).unwrap(), &data);
drive(|| Tema::new(14).unwrap(), &data);
drive(|| Hma::new(14).unwrap(), &data);
drive(|| Roc::new(14).unwrap(), &data);
drive(|| Trix::new(14).unwrap(), &data);
drive(|| Smma::new(14).unwrap(), &data);
drive(|| Trima::new(14).unwrap(), &data);
drive(|| Zlema::new(14).unwrap(), &data);
drive(|| Kama::new(10, 2, 30).unwrap(), &data);
drive(|| Alma::new(9, 0.85, 6.0).unwrap(), &data);
drive(|| McGinleyDynamic::new(10).unwrap(), &data);
drive(|| T3::new(14, 0.7).unwrap(), &data);
drive(|| Mom::new(14).unwrap(), &data);
drive(|| Cmo::new(14).unwrap(), &data);
drive(|| Tsi::new(25, 13).unwrap(), &data);
drive(|| Pmo::new(35, 20).unwrap(), &data);
drive(|| StochRsi::new(14, 14).unwrap(), &data);
drive(|| Dpo::new(14).unwrap(), &data);
drive(|| Ppo::new(12, 26).unwrap(), &data);
drive(|| Coppock::new(14, 11, 10).unwrap(), &data);
drive(|| StdDev::new(14).unwrap(), &data);
drive(|| UlcerIndex::new(14).unwrap(), &data);
drive(|| HistoricalVolatility::new(14, 252).unwrap(), &data);
drive(|| LinearRegression::new(14).unwrap(), &data);
drive(|| LinRegSlope::new(14).unwrap(), &data);
drive(|| LinRegAngle::new(14).unwrap(), &data);
drive(|| VerticalHorizontalFilter::new(14).unwrap(), &data);
drive(|| ZScore::new(14).unwrap(), &data);
// MACD and Bollinger Bands have non-`f64` outputs, so they cannot use the
// generic `drive` helper above. Streaming + batch are still both exercised.
{
let mut macd = MacdIndicator::new(12, 26, 9).unwrap();
for &x in &data {
let _ = macd.update(x);
}
let _ = MacdIndicator::new(12, 26, 9).unwrap().batch(&data);
}
{
let mut bb = BollingerBands::new(20, 2.0).unwrap();
for &x in &data {
let _ = bb.update(x);
}
let _ = BollingerBands::new(20, 2.0).unwrap().batch(&data);
}
});