F7: add NATR, StdDev, Ulcer Index and Historical Volatility

Completes the F7 family (Volatility) end to end:

- Rust core: natr.rs (ATR as a percentage of close), std_dev.rs
  (rolling population standard deviation), ulcer_index.rs (RMS of
  trailing-high drawdowns — downside-only risk), historical_volatility.rs
  (annualised sample stddev of log returns). Each with a full Indicator
  impl, runnable doctest and reference / constant-series / warmup /
  reset / batch==streaming tests.
- Python: PyNatr / PyStdDev / PyUlcerIndex / PyHistoricalVolatility
  PyO3 classes + module registration + .pyi stubs.
- Node: StdDevNode / UlcerIndexNode via the scalar macro, explicit
  NatrNode and HistoricalVolatilityNode; index.d.ts and index.js updated.
- WASM: WasmStdDev / WasmUlcerIndex / WasmHistoricalVolatility via the
  scalar macro, explicit WasmNatr.
- Wiki: Indicator-Natr/StdDev/UlcerIndex/HistoricalVolatility.md plus
  rows in Indicators-Overview.md and entries in Home.md.

cargo fmt + clippy (core/wickra/data/wasm/node) clean; 350 core tests,
25 data tests and 49 doctests green.
This commit is contained in:
kingchenc
2026-05-22 18:26:29 +02:00
parent 16c0639f0c
commit 6c58d3827c
17 changed files with 1943 additions and 6 deletions
+95
View File
@@ -108,6 +108,8 @@ node_scalar_indicator!(ZlemaNode, "ZLEMA", wc::Zlema);
node_scalar_indicator!(MomNode, "MOM", wc::Mom);
node_scalar_indicator!(CmoNode, "CMO", wc::Cmo);
node_scalar_indicator!(DpoNode, "DPO", wc::Dpo);
node_scalar_indicator!(StdDevNode, "StdDev", wc::StdDev);
node_scalar_indicator!(UlcerIndexNode, "UlcerIndex", wc::UlcerIndex);
// ============================== MACD ==============================
@@ -1145,6 +1147,99 @@ impl PmoNode {
// ============================== VWMA ==============================
// ============================== NATR ==============================
#[napi(js_name = "NATR")]
pub struct NatrNode {
inner: wc::Natr,
}
#[napi]
impl NatrNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Natr::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[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
}
}
// ============================== Historical Volatility ==============================
#[napi(js_name = "HistoricalVolatility")]
pub struct HistoricalVolatilityNode {
inner: wc::HistoricalVolatility,
}
#[napi]
impl HistoricalVolatilityNode {
#[napi(constructor)]
pub fn new(period: u32, trading_periods: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::HistoricalVolatility::new(period as usize, trading_periods as usize)
.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
}
}
// ============================== Aroon Oscillator ==============================
#[napi(js_name = "AroonOscillator")]