feat(family-12): add 13 Statistik/Regression indicators (#51)
* feat(family-12): add 13 Statistik/Regression indicators Brings the Price Statistics family to 20 indicators (7 → 20) and the total catalogue to 84 (71 → 84). Every indicator ships in the Rust core plus Python, Node, and WASM bindings with full streaming ↔ batch parity, fuzz coverage, and benches. Scalar (f64 → f64): - Variance, CoefficientOfVariation: rolling population variance and its dimensionless ratio with the mean. O(1) updates. - Skewness, Kurtosis: rolling Pearson skewness and excess kurtosis, derived from running sums of x, x², x³, x⁴ via the binomial identities — also O(1) per bar. - StandardError, DetrendedStdDev: standard error of estimate (n − 2) and population StdDev (n) of OLS residuals, sharing the LinReg O(1) sliding sums. - RSquared: coefficient of determination of the rolling OLS fit; the trend-quality filter, clamped to [0, 1]. - MedianAbsoluteDeviation: robust dispersion estimator; O(period log period) per emission via two in-place sorts of a reusable scratch buffer. - Autocorrelation(period, lag): rolling lag-k Pearson autocorrelation. - HurstExponent(period, chunks): R/S-analysis trend-persistence estimator clamped to [0, 1]. Pair indicators (Input = (f64, f64)): - PearsonCorrelation: rolling cross-series Pearson, O(1). - Beta: rolling OLS slope of asset vs. benchmark (CAPM). - SpearmanCorrelation: rolling rank correlation with mid-rank tie handling; O(period log period). Touchpoints: - crates/wickra-core: 13 new indicator modules + mod.rs / lib.rs re-exports. - bindings/python: pyclasses + add_class registration + __init__.py import & __all__ updates. The pair indicators expose update(x, y) and batch(x, y) over two equally-sized numpy arrays. - bindings/node: scalar indicators via node_scalar_indicator! macro; pair indicators via new node_pair_indicator! macro; explicit structs for Autocorrelation and HurstExponent (two-arg ctors). index.js extended with the new exports. - bindings/wasm: scalar wrappers via wasm_scalar_indicator!; pair wrappers via new wasm_pair_indicator! macro. - fuzz: every scalar drove through the generic helper; pair indicators stress-tested by pairing adjacent samples of the fuzz input. - Python tests (test_new_indicators.py): added to SCALAR parametrisation, plus algebraic reference values (variance of [2,4,6] = 8/3, MAD ignoring outlier = 0, monotone non-linear Spearman = 1, two-to-one Beta = 2, etc.) and a streaming-vs-batch test for the pair indicators. - Node tests (indicators.test.js): extended the scalar factories map and added a pair-indicator section with the same algebraic reference values. - crates/wickra/benches: bench_scalar entries for all 10 single- input new indicators. - README: counter 71 → 84; Price Statistics family-table row expanded with the 13 new indicators. - CHANGELOG: Unreleased section documents the family addition. Wiki drafts (ghost-ignored, manual sync to wickra.wiki at release time): indicator-ideas/families/wiki/family-12-statistik-regression/ contains 13 deep-dive pages plus _Sidebar / Indicators-Overview / Warmup-Periods / Home fragments for the curator merge. cargo check --workspace --all-features: clean. * fix(family-12): remove unreachable defensive guards in hurst_exponent The three guards (m < 2 continue, end > buf.len() break, denom == 0.0 return) are by-construction unreachable given the constructor invariant period >= 2 * chunks: m = period / k for k in 1..=chunks always satisfies m >= 2 and end = (c+1) * m <= k * m <= period = buf.len(), and m_1 = period and m_2 = period / 2 are always distinct so the slope denominator is strictly positive. Removing them brings codecov/patch back to 100%.
This commit is contained in:
@@ -177,6 +177,162 @@ impl RviVolatilityNode {
|
||||
}
|
||||
}
|
||||
|
||||
node_scalar_indicator!(VarianceNode, "Variance", wc::Variance);
|
||||
node_scalar_indicator!(
|
||||
CoefficientOfVariationNode,
|
||||
"CoefficientOfVariation",
|
||||
wc::CoefficientOfVariation
|
||||
);
|
||||
node_scalar_indicator!(SkewnessNode, "Skewness", wc::Skewness);
|
||||
node_scalar_indicator!(KurtosisNode, "Kurtosis", wc::Kurtosis);
|
||||
node_scalar_indicator!(StandardErrorNode, "StandardError", wc::StandardError);
|
||||
node_scalar_indicator!(DetrendedStdDevNode, "DetrendedStdDev", wc::DetrendedStdDev);
|
||||
node_scalar_indicator!(RSquaredNode, "RSquared", wc::RSquared);
|
||||
node_scalar_indicator!(
|
||||
MedianAbsoluteDeviationNode,
|
||||
"MedianAbsoluteDeviation",
|
||||
wc::MedianAbsoluteDeviation
|
||||
);
|
||||
|
||||
// ============================== Autocorrelation (period + lag) ==============================
|
||||
|
||||
#[napi(js_name = "Autocorrelation")]
|
||||
pub struct AutocorrelationNode {
|
||||
inner: wc::Autocorrelation,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AutocorrelationNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, lag: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Autocorrelation::new(period as usize, lag 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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== HurstExponent (period + chunks) ==============================
|
||||
|
||||
#[napi(js_name = "HurstExponent")]
|
||||
pub struct HurstExponentNode {
|
||||
inner: wc::HurstExponent,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl HurstExponentNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, chunks: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HurstExponent::new(period as usize, chunks 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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Two-series indicators (Pearson / Beta / Spearman) ==============================
|
||||
|
||||
macro_rules! node_pair_indicator {
|
||||
($wrapper:ident, $node_name:literal, $rust_ty:ty) => {
|
||||
#[napi(js_name = $node_name)]
|
||||
pub struct $wrapper {
|
||||
inner: $rust_ty,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl $wrapper {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: <$rust_ty>::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, x: f64, y: f64) -> Option<f64> {
|
||||
self.inner.update((x, y))
|
||||
}
|
||||
/// Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
/// with `NaN` for warmup positions.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, x: Vec<f64>, y: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if x.len() != y.len() {
|
||||
return Err(NapiError::new(
|
||||
Status::InvalidArg,
|
||||
"x and y must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(x.len());
|
||||
for i in 0..x.len() {
|
||||
out.push(self.inner.update((x[i], y[i])).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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
node_pair_indicator!(
|
||||
PearsonCorrelationNode,
|
||||
"PearsonCorrelation",
|
||||
wc::PearsonCorrelation
|
||||
);
|
||||
node_pair_indicator!(BetaNode, "Beta", wc::Beta);
|
||||
node_pair_indicator!(
|
||||
SpearmanCorrelationNode,
|
||||
"SpearmanCorrelation",
|
||||
wc::SpearmanCorrelation
|
||||
);
|
||||
|
||||
// ============================== MACD ==============================
|
||||
|
||||
/// MACD triple: macd line, signal line, histogram.
|
||||
|
||||
Reference in New Issue
Block a user