feat: add 9 Risk / Performance indicators (B18) (#218)

Adds nine risk/performance metrics to the existing **Risk / Performance** family, all consuming a per-period return series (`f64` in, `f64` out). Indicator count **498 → 507**.

## Indicators

Single-param (`new(period)`, macro bindings):
- **SterlingRatio** — mean return over average drawdown of the equity curve.
- **BurkeRatio** — return over root-sum-squared drawdowns.
- **MartinRatio** — Ulcer Performance Index; return over RMS percentage drawdown.
- **TailRatio** — 95th percentile over the absolute 5th percentile return.
- **KRatio** — Kestner; equity-curve OLS slope over the standard error of that slope.
- **CommonSenseRatio** — tail ratio times gain-to-pain.
- **GainToPainRatio** — sum of returns over the sum of absolute losses.

Multi-param (hand-written Python/Node bindings, variadic WASM macro):
- **UpsidePotentialRatio** — `new(period, mar)`; upside mean over downside deviation (Sortino philosophy).
- **M2Measure** — `new(period, risk_free, benchmark_stddev)`; Modigliani M², Sharpe rescaled into benchmark return units.

## Touchpoints
Core modules + unit tests, `mod.rs`/`lib.rs` wiring, Python/Node/WASM bindings (`index.d.ts`/`index.js` regenerated), fuzz drive lines, Python `SCALAR` registry + Node factories, CHANGELOG, and the indicator counters.

## Verification
- `cargo test -p wickra-core --lib` — 4149 passed
- `cargo test -p wickra-core --doc` — 457 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean
- `npm test` (node) — 577 passed
- `pytest` (python) — 947 passed
This commit is contained in:
kingchenc
2026-06-08 13:23:01 +02:00
committed by GitHub
parent fc6f619550
commit bca61322b5
23 changed files with 2843 additions and 58 deletions
+82
View File
@@ -245,9 +245,91 @@ node_scalar_indicator!(
"UNIVERSALOSC",
wc::UniversalOscillator
);
node_scalar_indicator!(SterlingRatioNode, "SterlingRatio", wc::SterlingRatio);
node_scalar_indicator!(BurkeRatioNode, "BurkeRatio", wc::BurkeRatio);
node_scalar_indicator!(MartinRatioNode, "MartinRatio", wc::MartinRatio);
node_scalar_indicator!(TailRatioNode, "TailRatio", wc::TailRatio);
node_scalar_indicator!(KRatioNode, "KRatio", wc::KRatio);
node_scalar_indicator!(
CommonSenseRatioNode,
"CommonSenseRatio",
wc::CommonSenseRatio
);
node_scalar_indicator!(GainToPainRatioNode, "GainToPainRatio", wc::GainToPainRatio);
// Multi-arg Ehlers scalars: hand-written (node_scalar_indicator! is single-period).
#[napi(js_name = "UpsidePotentialRatio")]
pub struct UpsidePotentialRatioNode {
inner: wc::UpsidePotentialRatio,
}
#[napi]
impl UpsidePotentialRatioNode {
#[napi(constructor)]
pub fn new(period: u32, mar: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::UpsidePotentialRatio::new(period as usize, mar).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "M2Measure")]
pub struct M2MeasureNode {
inner: wc::M2Measure,
}
#[napi]
impl M2MeasureNode {
#[napi(constructor)]
pub fn new(period: u32, risk_free: f64, benchmark_stddev: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::M2Measure::new(period as usize, risk_free, benchmark_stddev)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "BANDPASS")]
pub struct BandpassFilterNode {
inner: wc::BandpassFilter,