feat(family-15): add 17 risk/performance metrics (#54)

* feat(family-15): add 17 risk/performance metrics

Implements Family 15 pragmatically as standard `Indicator`s instead of a
separate `wickra-metrics` crate. Input is scalar `f64` per bar — period
return, equity sample, or per-trade P&L depending on the metric.

Scalar `Indicator<f64>` (14):
- SharpeRatio(period, risk_free)
- SortinoRatio(period, mar)
- CalmarRatio(period)
- OmegaRatio(period, threshold)
- MaxDrawdown(period)          — rolling, peak-to-trough
- AverageDrawdown(period)
- DrawdownDuration             — cumulative, bars under water (u32 output)
- PainIndex(period)
- ValueAtRisk(period, confidence)
- ConditionalValueAtRisk(period, confidence)
- ProfitFactor(period)
- GainLossRatio(period)
- RecoveryFactor               — cumulative, net return / max drawdown
- KellyCriterion(period)

Two-series `Indicator<(f64, f64)>` for (asset, benchmark) returns (3):
- TreynorRatio(period, risk_free)
- InformationRatio(period)
- Alpha(period, risk_free)     — Jensen / CAPM

Touchpoints:
- 17 new files under `crates/wickra-core/src/indicators/`.
- `mod.rs` + `lib.rs` re-exports.
- Python bindings (`bindings/python/src/lib.rs`, `__init__.py`).
- Node bindings (`bindings/node/src/lib.rs`, `index.js`).
- WASM bindings (`bindings/wasm/src/lib.rs`).
- Fuzz: scalar metrics appended to `indicator_update.rs`; new
  `indicator_update_pair.rs` fuzz target for `(f64, f64)` indicators.
- Python tests: SCALAR + new PAIR parameter lists in `test_new_indicators.py`,
  reference-value cases in `test_known_values.py`.
- Node tests: scalar factories + new pair-factory block in
  `bindings/node/__tests__/indicators.test.js`.
- Benches: 5 Family-15 benches added in `crates/wickra/benches/indicators.rs`.
- Docs: README family-table row + counter (71 -> 88), CHANGELOG entry under
  [Unreleased].

Note: Family 12 (statistik-regression, PR #51) introduces
`node_pair_indicator!` and `wasm_pair_indicator!` macros for Pearson /
Beta / Spearman. Family 15 needs the same pair-input pattern but Family 12
is not yet in main, so the three pair wrappers below are written by hand
in this PR. When PR #51 lands, the trivial merge-conflict is resolved by
keeping the macros from Family 12 and re-using them for Treynor / IR /
Alpha (drop the three handwritten wrappers).

cargo check --workspace --all-features: green.

* fix(family-15): satisfy clippy doc_markdown / if_not_else / digit_grouping

* fix(family-15): unused TreynorRatio import, duplicate pairFactories, _eq_nan inf handling

* fix(family-15): node eq() handles matching infinities for ratio indicators

* test(family-15): cover cold paths flagged by codecov patch
This commit is contained in:
kingchenc
2026-05-26 20:44:21 +02:00
committed by GitHub
parent 55284a3042
commit 4e3c41ea80
34 changed files with 5727 additions and 73 deletions
+45 -12
View File
@@ -15,18 +15,20 @@
use libfuzzer_sys::fuzz_target;
use wickra_core::{
AdaptiveCycle, Alma, Apo, Autocorrelation, BatchExt, Beta, BollingerBands, CenterOfGravity,
Cfo, Cmo, CoefficientOfVariation, ConnorsRsi, Coppock, CyberneticCycle, Decycler,
DecyclerOscillator, Dema, DetrendedStdDev, DoubleBollinger, Dpo, EhlersStochastic,
ElderImpulse, Ema, EmpiricalModeDecomposition, Fama, FisherTransform, Frama,
HilbertDominantCycle, HistoricalVolatility, Hma, HurstExponent, Indicator,
InstantaneousTrendline, InverseFisherTransform, Jma, Kama, Kst, Kurtosis, LaguerreRsi,
LinRegAngle, LinRegChannel, LinRegSlope, LinearRegression, MaEnvelope, MacdIndicator, Mama,
McGinleyDynamic, MedianAbsoluteDeviation, Mom, PearsonCorrelation, PercentageTrailingStop,
Pmo, Ppo, RSquared, RenkoTrailingStop, Roc, RoofingFilter, Rsi, RviVolatility, SineWave,
Skewness, Sma, Smma, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev,
StepTrailingStop, StochRsi, SuperSmoother, T3, Tema, Tii, Trima, Trix, Tsi, UlcerIndex,
Variance, VerticalHorizontalFilter, Vidya, Wma, ZScore, ZeroLagMacd, Zlema,
AdaptiveCycle, Alma, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands,
CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk,
ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DetrendedStdDev,
DoubleBollinger, Dpo, DrawdownDuration, EhlersStochastic, ElderImpulse, Ema,
EmpiricalModeDecomposition, Fama, FisherTransform, Frama, GainLossRatio, HilbertDominantCycle,
HistoricalVolatility, Hma, HurstExponent, Indicator, InstantaneousTrendline,
InverseFisherTransform, Jma, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle,
LinRegChannel, LinRegSlope, LinearRegression, MaEnvelope, MacdIndicator, Mama, MaxDrawdown,
McGinleyDynamic, MedianAbsoluteDeviation, Mom, OmegaRatio, PainIndex, PearsonCorrelation,
PercentageTrailingStop, Pmo, Ppo, ProfitFactor, RSquared, RecoveryFactor, RenkoTrailingStop,
Roc, RoofingFilter, Rsi, RviVolatility, SharpeRatio, SineWave, Skewness, Sma, Smma,
SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev,
StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, Trima, Trix, Tsi, UlcerIndex,
ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, Wma, ZScore, ZeroLagMacd, Zlema, T3,
};
/// Drive a single streaming + batch run through one scalar indicator. Marked
@@ -146,6 +148,37 @@ fuzz_target!(|data: Vec<f64>| {
drive(SineWave::new, &data);
drive(|| Fama::new(0.5, 0.05).unwrap(), &data);
// Family 15 — Risk / Performance metrics (scalar inputs).
drive(|| SharpeRatio::new(20, 0.0).unwrap(), &data);
drive(|| SortinoRatio::new(20, 0.0).unwrap(), &data);
drive(|| CalmarRatio::new(20).unwrap(), &data);
drive(|| OmegaRatio::new(20, 0.0).unwrap(), &data);
drive(|| MaxDrawdown::new(20).unwrap(), &data);
drive(|| AverageDrawdown::new(20).unwrap(), &data);
drive(|| PainIndex::new(20).unwrap(), &data);
drive(|| ValueAtRisk::new(20, 0.95).unwrap(), &data);
drive(|| ConditionalValueAtRisk::new(20, 0.95).unwrap(), &data);
drive(|| ProfitFactor::new(20).unwrap(), &data);
drive(|| GainLossRatio::new(20).unwrap(), &data);
drive(|| KellyCriterion::new(20).unwrap(), &data);
// RecoveryFactor and DrawdownDuration produce non-`f64` outputs / have
// no `period` knob, so they cannot use the `drive` helper directly.
{
let mut rf = RecoveryFactor::new();
for &x in &data {
let _ = rf.update(x);
}
let _ = RecoveryFactor::new().batch(&data);
}
{
let mut dd = DrawdownDuration::new();
for &x in &data {
let _ = dd.update(x);
}
let _ = DrawdownDuration::new().batch(&data);
}
// MACD, Bollinger Bands and MAMA have non-`f64` outputs, so they cannot
// use the generic `drive` helper above. Streaming + batch are still both
// exercised.