* feat(family-11): add DeMark suite (TD Setup, Sequential, DeMarker, REI, Pressure)
Family 11 (DeMark) was previously empty; this PR adds five
streaming-first DeMark indicators in one batch.
- **TD Setup** (`TdSetup`): parameterised buy/sell setup counter.
Counts consecutive bars whose close is less-than (buy) or
greater-than (sell) the close `lookback` bars earlier, saturating
at `target`. Emits a signed `f64` so callers read direction from
the sign and run length from the magnitude. Classic config:
`lookback = 4`, `target = 9`.
- **TD Sequential** (`TdSequential`): the canonical Setup + Countdown
exhaustion pattern. Output struct `{ setup, countdown, direction }`
exposes both phase counts as signed numbers plus the active
countdown direction (+1 buy / -1 sell / 0 none). Countdown
activates when a setup completes and tracks the close-vs-high/low
comparison `countdown_lookback` bars back, capped at
`countdown_target`. Classic: 4/9/2/13.
- **TD DeMarker** (`TdDeMarker`): bounded [0, 1] oscillator from the
rolling average of upward high expansion (DeMax) and downward low
expansion (DeMin). Falls back to the neutral 0.5 on a flat market
(denominator zero).
- **TD REI** (`TdRei`): Range Expansion Index, bounded [-100, 100].
Per-bar numerator gated on a range-overlap condition vs the bars
5 and 6 back, normalised by a `period`-bar sum of absolute moves.
Classic period = 5. Saturates at +100 in a slow steady uptrend
and at -100 in the mirror downtrend; emits 0 on a flat market.
- **TD Pressure** (`TdPressure`): volume-weighted buying / selling
pressure normalised to [-100, 100]. Per-bar pressure is the
intra-bar close-vs-open ratio scaled by volume; the output is the
rolling mean divided by the rolling mean volume. Zero-range bars
contribute zero (avoid the undefined ratio) and a flat zero-volume
window falls back to 0.
Bindings: all five exposed in Python (`ta.TDSetup`, `ta.TDSequential`,
`ta.TDDeMarker`, `ta.TDREI`, `ta.TDPressure`), Node (`wickra.TDSetup`
etc.), and WASM. Multi-output classes (`TDSequential`) return either
a struct `{ setup, countdown, direction }` per bar (streaming) or a
flat interleaved Float64Array of length `3 * n` (batch).
Tests: 47 unit tests across the five new core files (pure-trend
saturation, flat-market neutral fallback, batch-equals-streaming,
zero-parameter rejection, reset semantics, accessors). Python
test_new_indicators.py picks up all five plus a multi-output TD
Sequential block. Node indicators.test.js picks up all five.
Reference values added to test_known_values.py.
Fuzz: candle fuzz target sweeps all five DeMark indicators with the
existing `Vec<f64>` -> `Vec<Candle>` driver.
Benches: BTCUSDT 1-minute dataset benches for each DeMark indicator
in `crates/wickra/benches/indicators.rs`.
Docs: README family table gains a "DeMark" row; indicator counter
bumped 71 -> 76. CHANGELOG entry added under [Unreleased]. Wiki
drafts (deep-dive pages + Sidebar / Overview / Warmup-Periods / Home
deltas) live under `indicator-ideas/families/wiki/family-11-demark/`
for manual merge into the wiki repo.
* feat(family-11): add 7 missing DeMark indicators
Complete the DeMark suite (family 11) with the seven indicators not
covered by the first commit: TD Combo, TD Countdown, TD Lines (TDST),
TD Range Projection, TD Differential, TD Open, and TD Risk Level.
- TdCombo: aggressive countdown variant with three strictness rules
on top of the classic close-vs-low/high lookback rule (monotone
low/high, monotone close vs prior bar).
- TdCountdown: standalone 13-bar countdown packaging only the signed
countdown count (the setup machine runs internally).
- TdLines: TDST horizontal support/resistance levels from the
highest-high / lowest-low bars of the most-recently-completed
setup, exposed as a multi-output struct.
- TdRangeProjection: DeMark X-projection of the next bar's high and
low from the current bar's OHLC via an open-vs-close-weighted
pivot (three branches: close<open, close>open, close==open).
- TdDifferential: two-bar buying-pressure vs selling-pressure
reversal pattern emitting +1/-1/0.
- TdOpen: gap-and-fade reversal pattern (open outside prior range
with subsequent recovery into it) emitting +1/-1/0.
- TdRiskLevel: protective stop levels derived from the setup
extreme bar +/- its true range.
All seven are wired through Rust core, Python, Node and WASM
bindings, registered in the candle-stream fuzz target, given
benchmark entries on the BTCUSDT 1-minute dataset, and covered by
streaming-vs-batch equivalence, reference-value, lifecycle and
input-validation tests on the Python and Node sides. README counter
moves 76 -> 83 and the CHANGELOG "family 11" entry is extended to
list all twelve indicators.
* fix(td_risk_level tests): check first emission at idx 12, not last bar
TdRiskLevel re-ratchets the sell-risk level on each subsequent setup
completion, so a strictly rising series produces 22.0 at idx 19 (latest
setup) rather than 15.0 (first setup). The test comment already named
idx 12 as the reference; switch the assertion from out[-1] to out[12]
to match the reference computation.
* test(family-11): cover buy-direction branches in TD indicators
Add downtrend tests to TdSequential, TdCombo and TdCountdown so the
buy-side countdown/combo increment branches are exercised; remove an
empty `if buy_countdown == target {}` block in TdSequential whose
behavior is already enforced by the outer strict `<` guard.
Closes codecov/patch gaps reported on PR #48 (10 missed lines across
the three files).
269 lines
11 KiB
Rust
269 lines
11 KiB
Rust
#![no_main]
|
|
//! Fuzz OHLCV-input indicator updates with arbitrary candle sequences.
|
|
//!
|
|
//! Every candle-input indicator must tolerate any sequence of validated OHLCV
|
|
//! candles — extreme magnitudes, micro-spreads, zero-volume bars, abrupt
|
|
//! reversals — without panicking. The fuzzer chunks the raw `f64` stream into
|
|
//! `[open, high, low, close, volume]` tuples and constructs each candle via
|
|
//! `Candle::new`; entries that fail OHLCV-invariant validation are skipped so
|
|
//! the indicator only ever sees structurally-valid candles. Each iteration
|
|
//! then drives that candle stream through every candle-input indicator twice
|
|
//! (streaming `update` + batch).
|
|
//!
|
|
//! Audit finding R9: the previous fuzz suite had no candle-input coverage at
|
|
//! all. This target now covers every candle-input indicator including the
|
|
//! ones the audit named explicitly (ATR, ADX, Stochastic, PSAR) plus the
|
|
//! complete catalogue: Keltner, Donchian, SuperTrend, Chandelier Exit, ATR
|
|
//! Trailing Stop, Aroon, AwesomeOscillator, CCI, WilliamsR, MFI, OBV, VWAP,
|
|
//! RollingVWAP, ADL, VPT, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex,
|
|
//! EaseOfMovement, NATR, AroonOscillator, ChandeKrollStop, Vortex, MassIndex,
|
|
//! ChoppinessIndex, TrueRange, ChaikinVolatility, AcceleratorOscillator,
|
|
//! BalanceOfPower, UltimateOscillator, VWMA, TypicalPrice, MedianPrice,
|
|
//! WeightedClose.
|
|
|
|
use libfuzzer_sys::fuzz_target;
|
|
use wickra_core::{
|
|
AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, Adx, Adxr, Alligator,
|
|
AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator,
|
|
AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, Camarilla, Candle, Cci, ChaikinMoneyFlow,
|
|
ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex,
|
|
ClassicPivots, DemandIndex, DemarkPivots, Donchian, DonchianStop, EaseOfMovement, Evwma,
|
|
FibonacciPivots, ForceIndex, FractalChaosBands, GarmanKlassVolatility, HiLoActivator,
|
|
HurstChannel, Indicator, Inertia, Keltner, Kvo, MarketFacilitationIndex, MassIndex, MedianPrice,
|
|
Mfi, Natr, Nvi, Obv, ParkinsonVolatility, Pgo, Psar, Pvi, RogersSatchellVolatility, RollingVwap,
|
|
Rvi, Rwi, Smi, StarcBands, Stochastic, SuperTrend, TdCombo, TdCountdown, TdDeMarker,
|
|
TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel,
|
|
TdSequential, TdSetup, TrueRange, Tsv, TtmSqueeze, TypicalPrice, UltimateOscillator, VoltyStop,
|
|
VolumeOscillator, VolumePriceTrend, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend,
|
|
WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag,
|
|
};
|
|
|
|
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
|
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
|
/// validation are dropped so the indicator under test only ever sees a
|
|
/// structurally-valid candle stream (the *parser* is fuzz-tested elsewhere;
|
|
/// this target focuses on indicator robustness).
|
|
fn candles_from(data: &[f64]) -> Vec<Candle> {
|
|
data.chunks_exact(5)
|
|
.enumerate()
|
|
.filter_map(|(i, ch)| {
|
|
// A monotonic timestamp avoids surprising any indicator that might
|
|
// care about ordering. The fuzz input drives OHLCV; time is just a
|
|
// tie-breaker.
|
|
Candle::new(ch[0], ch[1], ch[2], ch[3], ch[4], i as i64).ok()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Streaming + batch sweep through one candle-input indicator. `#[inline(never)]`
|
|
/// keeps each indicator on its own frame in any panic backtrace.
|
|
#[inline(never)]
|
|
fn drive<I, O>(make: impl Fn() -> I, candles: &[Candle])
|
|
where
|
|
I: Indicator<Input = Candle, Output = O> + BatchExt,
|
|
{
|
|
let mut streaming = make();
|
|
for c in candles {
|
|
let _ = streaming.update(*c);
|
|
}
|
|
let _ = make().batch(candles);
|
|
}
|
|
|
|
fuzz_target!(|data: Vec<f64>| {
|
|
let candles = candles_from(&data);
|
|
if candles.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// --- Volatility & ATR family ---
|
|
drive(|| Atr::new(14).unwrap(), &candles);
|
|
drive(|| Natr::new(14).unwrap(), &candles);
|
|
drive(TrueRange::new, &candles);
|
|
drive(|| ChaikinVolatility::new(10, 10).unwrap(), &candles);
|
|
drive(|| ParkinsonVolatility::new(20, 252).unwrap(), &candles);
|
|
drive(|| GarmanKlassVolatility::new(20, 252).unwrap(), &candles);
|
|
drive(|| RogersSatchellVolatility::new(20, 252).unwrap(), &candles);
|
|
drive(|| YangZhangVolatility::new(20, 252).unwrap(), &candles);
|
|
|
|
// --- Bands & Channels ---
|
|
drive(|| Keltner::new(20, 10, 2.0).unwrap(), &candles);
|
|
drive(|| Donchian::new(20).unwrap(), &candles);
|
|
|
|
// --- Trailing Stops ---
|
|
drive(|| Psar::new(0.02, 0.02, 0.20).unwrap(), &candles);
|
|
drive(|| SuperTrend::new(14, 3.0).unwrap(), &candles);
|
|
drive(|| ChandelierExit::new(22, 3.0).unwrap(), &candles);
|
|
drive(|| ChandeKrollStop::new(10, 1.0, 9).unwrap(), &candles);
|
|
drive(|| AtrTrailingStop::new(14, 3.0).unwrap(), &candles);
|
|
drive(|| HiLoActivator::new(3).unwrap(), &candles);
|
|
drive(|| VoltyStop::new(14, 2.0).unwrap(), &candles);
|
|
drive(|| YoyoExit::new(14, 2.0).unwrap(), &candles);
|
|
|
|
// --- Trend & Directional ---
|
|
drive(|| Adx::new(14).unwrap(), &candles);
|
|
drive(|| Adxr::new(14).unwrap(), &candles);
|
|
drive(|| Aroon::new(14).unwrap(), &candles);
|
|
drive(|| Alligator::new(13, 8, 5).unwrap(), &candles);
|
|
drive(|| AroonOscillator::new(14).unwrap(), &candles);
|
|
drive(|| Vortex::new(14).unwrap(), &candles);
|
|
drive(|| Rwi::new(14).unwrap(), &candles);
|
|
drive(|| WaveTrend::classic().unwrap(), &candles);
|
|
drive(|| MassIndex::new(9, 25).unwrap(), &candles);
|
|
drive(|| ChoppinessIndex::new(14).unwrap(), &candles);
|
|
|
|
// --- Momentum & Oscillators ---
|
|
drive(|| Cci::new(20).unwrap(), &candles);
|
|
drive(|| Rvi::new(10).unwrap(), &candles);
|
|
drive(|| Inertia::new(14, 20).unwrap(), &candles);
|
|
drive(|| Pgo::new(14).unwrap(), &candles);
|
|
drive(|| Smi::classic(), &candles);
|
|
drive(|| WilliamsR::new(14).unwrap(), &candles);
|
|
drive(|| AwesomeOscillator::new(5, 34).unwrap(), &candles);
|
|
drive(
|
|
|| AwesomeOscillatorHistogram::new(5, 34, 5).unwrap(),
|
|
&candles,
|
|
);
|
|
drive(|| AcceleratorOscillator::new(5, 34, 5).unwrap(), &candles);
|
|
drive(|| UltimateOscillator::new(7, 14, 28).unwrap(), &candles);
|
|
drive(BalanceOfPower::new, &candles);
|
|
|
|
// --- Volume ---
|
|
drive(Obv::new, &candles);
|
|
drive(|| Mfi::new(14).unwrap(), &candles);
|
|
drive(Vwap::new, &candles);
|
|
drive(|| RollingVwap::new(20).unwrap(), &candles);
|
|
drive(|| Vwma::new(20).unwrap(), &candles);
|
|
drive(|| Evwma::new(20).unwrap(), &candles);
|
|
drive(Adl::new, &candles);
|
|
drive(VolumePriceTrend::new, &candles);
|
|
drive(|| ChaikinMoneyFlow::new(20).unwrap(), &candles);
|
|
drive(|| ChaikinOscillator::new(3, 10).unwrap(), &candles);
|
|
drive(|| ForceIndex::new(13).unwrap(), &candles);
|
|
drive(|| EaseOfMovement::with_divisor(14, 1e8).unwrap(), &candles);
|
|
drive(|| Kvo::new(34, 55).unwrap(), &candles);
|
|
drive(|| VolumeOscillator::new(14, 28).unwrap(), &candles);
|
|
drive(Nvi::new, &candles);
|
|
drive(Pvi::new, &candles);
|
|
drive(AdOscillator::new, &candles);
|
|
drive(AnchoredVwap::new, &candles);
|
|
drive(|| DemandIndex::new(10).unwrap(), &candles);
|
|
drive(|| Tsv::new(18).unwrap(), &candles);
|
|
drive(|| Vzo::new(14).unwrap(), &candles);
|
|
drive(MarketFacilitationIndex::new, &candles);
|
|
|
|
// --- Price transformations ---
|
|
drive(TypicalPrice::new, &candles);
|
|
drive(MedianPrice::new, &candles);
|
|
drive(WeightedClose::new, &candles);
|
|
|
|
// --- Stochastic (multi-output) ---
|
|
{
|
|
let mut s = Stochastic::new(14, 3).unwrap();
|
|
for c in &candles {
|
|
let _ = s.update(*c);
|
|
}
|
|
let _ = Stochastic::new(14, 3).unwrap().batch(&candles);
|
|
}
|
|
|
|
// --- DeMark family ---
|
|
drive(|| TdSetup::new(4, 9).unwrap(), &candles);
|
|
drive(|| TdDeMarker::new(14).unwrap(), &candles);
|
|
drive(|| TdRei::new(5).unwrap(), &candles);
|
|
drive(|| TdPressure::new(5).unwrap(), &candles);
|
|
drive(|| TdCombo::new(4, 9, 2, 13).unwrap(), &candles);
|
|
drive(|| TdCountdown::new(4, 9, 2, 13).unwrap(), &candles);
|
|
drive(TdDifferential::new, &candles);
|
|
drive(TdOpen::new, &candles);
|
|
drive(TdRangeProjection::new, &candles);
|
|
{
|
|
let mut s = TdSequential::new(4, 9, 2, 13).unwrap();
|
|
for c in &candles {
|
|
let _ = s.update(*c);
|
|
}
|
|
let _ = TdSequential::new(4, 9, 2, 13).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut s = TdLines::new(4, 9).unwrap();
|
|
for c in &candles {
|
|
let _ = s.update(*c);
|
|
}
|
|
let _ = TdLines::new(4, 9).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut s = TdRiskLevel::new(4, 9).unwrap();
|
|
for c in &candles {
|
|
let _ = s.update(*c);
|
|
}
|
|
let _ = TdRiskLevel::new(4, 9).unwrap().batch(&candles);
|
|
}
|
|
|
|
// --- Pivots & Support/Resistance (multi-output) ---
|
|
drive(ClassicPivots::new, &candles);
|
|
drive(FibonacciPivots::new, &candles);
|
|
drive(Camarilla::new, &candles);
|
|
drive(WoodiePivots::new, &candles);
|
|
drive(DemarkPivots::new, &candles);
|
|
drive(WilliamsFractals::new, &candles);
|
|
drive(|| ZigZag::new(0.05).unwrap(), &candles);
|
|
|
|
// --- Donchian Stop (multi-output) ---
|
|
{
|
|
let mut s = DonchianStop::new(10).unwrap();
|
|
for c in &candles {
|
|
let _ = s.update(*c);
|
|
}
|
|
let _ = DonchianStop::new(10).unwrap().batch(&candles);
|
|
}
|
|
|
|
// --- Family 05: candle-input band/channel indicators (multi-output) ---
|
|
{
|
|
let mut ab = AccelerationBands::new(20, 0.001).unwrap();
|
|
for c in &candles {
|
|
let _ = ab.update(*c);
|
|
}
|
|
let _ = AccelerationBands::new(20, 0.001).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut sb = StarcBands::new(6, 15, 2.0).unwrap();
|
|
for c in &candles {
|
|
let _ = sb.update(*c);
|
|
}
|
|
let _ = StarcBands::new(6, 15, 2.0).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut atrb = AtrBands::new(14, 3.0).unwrap();
|
|
for c in &candles {
|
|
let _ = atrb.update(*c);
|
|
}
|
|
let _ = AtrBands::new(14, 3.0).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut hc = HurstChannel::new(10, 0.5).unwrap();
|
|
for c in &candles {
|
|
let _ = hc.update(*c);
|
|
}
|
|
let _ = HurstChannel::new(10, 0.5).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut ts = TtmSqueeze::new(20, 2.0, 1.5).unwrap();
|
|
for c in &candles {
|
|
let _ = ts.update(*c);
|
|
}
|
|
let _ = TtmSqueeze::new(20, 2.0, 1.5).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut fc = FractalChaosBands::new(2).unwrap();
|
|
for c in &candles {
|
|
let _ = fc.update(*c);
|
|
}
|
|
let _ = FractalChaosBands::new(2).unwrap().batch(&candles);
|
|
}
|
|
{
|
|
let mut vb = VwapStdDevBands::new(2.0).unwrap();
|
|
for c in &candles {
|
|
let _ = vb.update(*c);
|
|
}
|
|
let _ = VwapStdDevBands::new(2.0).unwrap().batch(&candles);
|
|
}
|
|
});
|