feat: Family 05 Bands & Channels - 11 new price-envelope indicators (#43)
* feat(bands-channels): add Family 05 with 11 indicators
Eleven price-envelope overlays organised into a new "Bands & Channels"
family, exposed across all four bindings (Rust core, Python, Node, WASM)
plus fuzz/test/bench/docs coverage:
- MaEnvelope - SMA centerline with fixed-percent envelope (the oldest
band overlay still in regular use).
- AccelerationBands (Price Headley) - momentum-biased bands that widen
with the bar's relative range (H - L) / (H + L).
- StarcBands (Stoller Average Range Channel) - SMA(close) +/- k*ATR;
Keltner's SMA-centerline sibling.
- AtrBands - close-anchored envelope of width k*ATR; the standard
volatility-targeting stop/target band.
- HurstChannel - SMA centerline wrapped by the rolling high-low range
(Brian Millard / Hurst-cycle channel).
- LinRegChannel - rolling OLS endpoint +/- k * population stddev of the
residuals; dispersion about the trend rather than the mean.
- StandardErrorBands - regression line +/- k * OLS standard error
(denominator n - 2) for prediction-interval bands.
- DoubleBollinger (Kathy Lien) - two concentric BB envelopes
(typically +/- 1 sigma and +/- 2 sigma) for the zone-partition setup.
- TtmSqueeze (John Carter) - BB-inside-KC squeeze flag paired with a
detrended-close linear-regression momentum reading.
- FractalChaosBands - Bill Williams 5-bar fractal high/low envelope.
- VwapStdDevBands - cumulative VWAP with volume-weighted population
standard deviation bands.
Each indicator ships:
- Core impl with the full Indicator trait, classic() where applicable,
and unit tests (rejects_zero_period / multiplier, accessors, flat
market, monotonic ordering, batch == streaming, reset, plus
algebraically verifiable reference values).
- Python PyO3 binding with multi-column NumPy batch (PyArray2).
- Node napi binding with #[napi(object)] struct + interleaved flat
batch.
- WASM wasm-bindgen binding via Object/Reflect for update +
Float64Array for batch.
- Fuzz coverage in fuzz_targets/indicator_update{,_candle}.rs.
- Python streaming-vs-batch parametric test + reference test.
- Node streaming-vs-interleaved-batch test + reference test.
- Criterion microbench under crates/wickra/benches/indicators.rs.
README family table, README indicator-count line, and CHANGELOG
Unreleased entry updated: indicator total rises from 71 to 82 across
nine families. Wiki pages are updated in a separate commit in the
wickra.wiki repo.
* test(acceleration-bands): cover sum_hl==0 zero-price guard
Exercises line 104 (`0.0` branch of the `sum_hl == 0.0` guard) which
was the last patch-coverage miss on the family-05 PR. `Candle::new`
accepts a fully-zero bar so the branch is reachable in principle —
add a degenerate-candle unit test to hit it.
This commit is contained in:
@@ -19,9 +19,11 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use std::hint::black_box;
|
||||
use wickra::{
|
||||
Alma, Atr, BatchExt, BollingerBands, Candle, Ema, Frama, GarmanKlassVolatility, Indicator, Jma,
|
||||
MacdIndicator, McGinleyDynamic, Obv, ParkinsonVolatility, Pgo, RogersSatchellVolatility, Rsi,
|
||||
Rvi, RviVolatility, Sma, Stochastic, Vidya, Wma, YangZhangVolatility,
|
||||
AccelerationBands, Alma, Atr, AtrBands, BatchExt, BollingerBands, Candle, DoubleBollinger, Ema,
|
||||
FractalChaosBands, Frama, GarmanKlassVolatility, HurstChannel, Indicator, Jma, LinRegChannel,
|
||||
MaEnvelope, MacdIndicator, McGinleyDynamic, Obv, ParkinsonVolatility, Pgo,
|
||||
RogersSatchellVolatility, Rsi, Rvi, RviVolatility, Sma, StandardErrorBands, StarcBands,
|
||||
Stochastic, TtmSqueeze, Vidya, VwapStdDevBands, Wma, YangZhangVolatility,
|
||||
};
|
||||
use wickra_data::csv::CandleReader;
|
||||
|
||||
@@ -152,6 +154,8 @@ fn benches(c: &mut Criterion) {
|
||||
bench_candle_input(c, "atr", &candles, || Atr::new(14).unwrap());
|
||||
bench_candle_input(c, "stochastic", &candles, Stochastic::classic);
|
||||
bench_candle_input(c, "obv", &candles, Obv::new);
|
||||
|
||||
// --- Family 04: Volatility ---
|
||||
bench_scalar(c, "rvi_volatility", &closes, || {
|
||||
RviVolatility::new(10).unwrap()
|
||||
});
|
||||
@@ -169,6 +173,64 @@ fn benches(c: &mut Criterion) {
|
||||
});
|
||||
bench_candle_input(c, "rvi", &candles, || Rvi::new(10).unwrap());
|
||||
bench_candle_input(c, "pgo", &candles, || Pgo::new(14).unwrap());
|
||||
|
||||
// --- Family 05: Bands & Channels ---
|
||||
bench_candle_input(c, "acceleration_bands", &candles, || {
|
||||
AccelerationBands::new(20, 0.001).unwrap()
|
||||
});
|
||||
bench_candle_input(c, "starc_bands", &candles, || {
|
||||
StarcBands::new(6, 15, 2.0).unwrap()
|
||||
});
|
||||
bench_candle_input(c, "atr_bands", &candles, || AtrBands::new(14, 3.0).unwrap());
|
||||
bench_candle_input(c, "hurst_channel", &candles, || {
|
||||
HurstChannel::new(10, 0.5).unwrap()
|
||||
});
|
||||
bench_candle_input(c, "ttm_squeeze", &candles, || {
|
||||
TtmSqueeze::new(20, 2.0, 1.5).unwrap()
|
||||
});
|
||||
bench_candle_input(c, "fractal_chaos_bands", &candles, || {
|
||||
FractalChaosBands::new(2).unwrap()
|
||||
});
|
||||
bench_candle_input(c, "vwap_stddev_bands", &candles, || {
|
||||
VwapStdDevBands::new(2.0).unwrap()
|
||||
});
|
||||
bench_scalar_multi(c, "ma_envelope", &closes, || {
|
||||
MaEnvelope::new(20, 0.025).unwrap()
|
||||
});
|
||||
bench_scalar_multi(c, "linreg_channel", &closes, || {
|
||||
LinRegChannel::new(20, 2.0).unwrap()
|
||||
});
|
||||
bench_scalar_multi(c, "standard_error_bands", &closes, || {
|
||||
StandardErrorBands::new(21, 2.0).unwrap()
|
||||
});
|
||||
bench_scalar_multi(c, "double_bollinger", &closes, || {
|
||||
DoubleBollinger::new(20, 1.0, 2.0).unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
/// Variant of `bench_scalar` for scalar-input indicators whose output is *not*
|
||||
/// `f64` (band/channel structs). Streaming-only path keeps the benchmark
|
||||
/// expression flat across all multi-output indicators.
|
||||
fn bench_scalar_multi<I, F, O>(c: &mut Criterion, name: &str, prices: &[f64], make: F)
|
||||
where
|
||||
F: Fn() -> I,
|
||||
I: Indicator<Input = f64, Output = O>,
|
||||
{
|
||||
let mut group = c.benchmark_group(name);
|
||||
for &n in SIZES {
|
||||
let n = n.min(prices.len());
|
||||
let series = &prices[..n];
|
||||
group.throughput(Throughput::Elements(n as u64));
|
||||
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, prices| {
|
||||
b.iter(|| {
|
||||
let mut ind = make();
|
||||
for p in prices {
|
||||
black_box(ind.update(*p));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(name = wickra_benches; config = Criterion::default(); targets = benches);
|
||||
|
||||
Reference in New Issue
Block a user