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:
kingchenc
2026-05-25 23:42:05 +02:00
committed by GitHub
parent 5aa0949bce
commit 05fcdd9a5e
26 changed files with 4303 additions and 42 deletions
@@ -91,6 +91,17 @@ const scalarFactories = {
AdaptiveCycle: () => new wickra.AdaptiveCycle(),
SineWave: () => new wickra.SineWave(),
FAMA: () => new wickra.FAMA(0.5, 0.05),
// Family 12 — Statistik / Regression
Variance: () => new wickra.Variance(20),
CoefficientOfVariation: () => new wickra.CoefficientOfVariation(20),
Skewness: () => new wickra.Skewness(20),
Kurtosis: () => new wickra.Kurtosis(20),
StandardError: () => new wickra.StandardError(14),
DetrendedStdDev: () => new wickra.DetrendedStdDev(14),
RSquared: () => new wickra.RSquared(14),
MedianAbsoluteDeviation: () => new wickra.MedianAbsoluteDeviation(20),
Autocorrelation: () => new wickra.Autocorrelation(20, 1),
HurstExponent: () => new wickra.HurstExponent(40, 4),
};
for (const [name, make] of Object.entries(scalarFactories)) {
@@ -360,6 +371,80 @@ test('LinRegAngle of a unit-slope series is 45 degrees', () => {
assert.ok(Math.abs(out[4] - 45) < 1e-9);
});
// --- Family 12: two-series indicators (Pearson / Beta / Spearman) ---
const pairFactories = {
PearsonCorrelation: () => new wickra.PearsonCorrelation(14),
Beta: () => new wickra.Beta(14),
SpearmanCorrelation: () => new wickra.SpearmanCorrelation(14),
};
for (const [name, make] of Object.entries(pairFactories)) {
test(`${name}: streaming update matches batch over a pair of series`, () => {
const xs = Array.from({ length: N }, (_, i) => Math.sin(i * 0.2) + 0.05 * i);
const ys = Array.from({ length: N }, (_, i) => Math.cos(i * 0.3) + 0.02 * i);
const batch = make().batch(xs, ys);
const streaming = make();
assert.equal(batch.length, N);
for (let i = 0; i < N; i++) {
const s = num(streaming.update(xs[i], ys[i]));
assert.ok(eq(s, batch[i]), `${name} mismatch at ${i}: ${s} vs ${batch[i]}`);
}
});
}
test('PearsonCorrelation perfect positive is 1', () => {
const x = Array.from({ length: 10 }, (_, i) => i);
const y = x.map((v) => 2 * v + 3);
const out = new wickra.PearsonCorrelation(5).batch(x, y);
assert.ok(Math.abs(out[out.length - 1] - 1) < 1e-9);
});
test('Beta perfect two-to-one', () => {
const bench = Array.from({ length: 10 }, (_, i) => i);
const asset = bench.map((v) => 2 * v);
const out = new wickra.Beta(5).batch(asset, bench);
assert.ok(Math.abs(out[out.length - 1] - 2) < 1e-9);
});
test('SpearmanCorrelation monotone non-linear is 1', () => {
const x = Array.from({ length: 10 }, (_, i) => i + 1);
const y = x.map((v) => v ** 3);
const out = new wickra.SpearmanCorrelation(5).batch(x, y);
assert.ok(Math.abs(out[out.length - 1] - 1) < 1e-9);
});
test('Variance(3) of [2, 4, 6] equals 8/3', () => {
const out = new wickra.Variance(3).batch([2, 4, 6]);
assert.ok(Math.abs(out[2] - 8 / 3) < 1e-12);
});
test('RSquared on a perfect line is 1', () => {
const xs = Array.from({ length: 20 }, (_, i) => 2 * i + 5);
const out = new wickra.RSquared(5).batch(xs);
for (let i = 5; i < out.length; i++) {
assert.ok(Math.abs(out[i] - 1) < 1e-9);
}
});
test('MedianAbsoluteDeviation ignores a single huge outlier', () => {
const xs = Array(9).fill(5).concat([1000]);
const out = new wickra.MedianAbsoluteDeviation(10).batch(xs);
assert.ok(Math.abs(out[9]) < 1e-12);
});
test('Autocorrelation of an alternating series is strongly negative at lag 1', () => {
const xs = Array.from({ length: 20 }, (_, i) => (i % 2 === 0 ? -1 : 1));
const out = new wickra.Autocorrelation(10, 1).batch(xs);
assert.ok(out[out.length - 1] < -0.5);
});
test('HurstExponent of a monotone ramp is above 0.5', () => {
const xs = Array.from({ length: 200 }, (_, i) => i);
const out = new wickra.HurstExponent(100, 4).batch(xs);
assert.ok(out[out.length - 1] > 0.5);
});
test('Ichimoku classic warmup is 77 and tenkan emits at bar 9', () => {
const ichi = new wickra.Ichimoku(9, 26, 52, 26);
assert.equal(ichi.warmupPeriod(), 77);
+14 -1
View File
@@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, KVO, VolumeOscillator, NVI, PVI, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, RWI, WaveTrend, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, SuperSmoother, FisherTransform, InverseFisherTransform, Decycler, DecyclerOscillator, RoofingFilter, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi } = nativeBinding
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, KVO, VolumeOscillator, NVI, PVI, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, RWI, WaveTrend, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, SuperSmoother, FisherTransform, InverseFisherTransform, Decycler, DecyclerOscillator, RoofingFilter, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, SpearmanCorrelation } = nativeBinding
module.exports.version = version
module.exports.SMA = SMA
@@ -479,3 +479,16 @@ module.exports.MAMA = MAMA
module.exports.FAMA = FAMA
module.exports.Ichimoku = Ichimoku
module.exports.HeikinAshi = HeikinAshi
module.exports.Variance = Variance
module.exports.CoefficientOfVariation = CoefficientOfVariation
module.exports.Skewness = Skewness
module.exports.Kurtosis = Kurtosis
module.exports.StandardError = StandardError
module.exports.DetrendedStdDev = DetrendedStdDev
module.exports.RSquared = RSquared
module.exports.MedianAbsoluteDeviation = MedianAbsoluteDeviation
module.exports.Autocorrelation = Autocorrelation
module.exports.HurstExponent = HurstExponent
module.exports.PearsonCorrelation = PearsonCorrelation
module.exports.Beta = Beta
module.exports.SpearmanCorrelation = SpearmanCorrelation
+156
View File
@@ -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.
+26
View File
@@ -149,6 +149,19 @@ from ._wickra import (
LinRegSlope,
ZScore,
LinRegAngle,
Variance,
CoefficientOfVariation,
Skewness,
Kurtosis,
StandardError,
DetrendedStdDev,
RSquared,
Autocorrelation,
MedianAbsoluteDeviation,
HurstExponent,
PearsonCorrelation,
Beta,
SpearmanCorrelation,
# Ehlers / Cycle
SuperSmoother,
FisherTransform,
@@ -330,6 +343,19 @@ __all__ = [
"LinRegSlope",
"ZScore",
"LinRegAngle",
"Variance",
"CoefficientOfVariation",
"Skewness",
"Kurtosis",
"StandardError",
"DetrendedStdDev",
"RSquared",
"Autocorrelation",
"MedianAbsoluteDeviation",
"HurstExponent",
"PearsonCorrelation",
"Beta",
"SpearmanCorrelation",
# Ehlers / Cycle
"SuperSmoother",
"FisherTransform",
+713
View File
@@ -10116,6 +10116,706 @@ impl PyHeikinAshi {
}
}
#[pyclass(name = "Variance", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyVariance {
inner: wc::Variance,
}
#[pymethods]
impl PyVariance {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Variance::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("Variance(period={})", self.inner.period())
}
}
// ============================== CoefficientOfVariation ==============================
#[pyclass(
name = "CoefficientOfVariation",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyCoefficientOfVariation {
inner: wc::CoefficientOfVariation,
}
#[pymethods]
impl PyCoefficientOfVariation {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::CoefficientOfVariation::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("CoefficientOfVariation(period={})", self.inner.period())
}
}
// ============================== Skewness ==============================
#[pyclass(name = "Skewness", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PySkewness {
inner: wc::Skewness,
}
#[pymethods]
impl PySkewness {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Skewness::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("Skewness(period={})", self.inner.period())
}
}
// ============================== Kurtosis ==============================
#[pyclass(name = "Kurtosis", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyKurtosis {
inner: wc::Kurtosis,
}
#[pymethods]
impl PyKurtosis {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Kurtosis::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("Kurtosis(period={})", self.inner.period())
}
}
// ============================== StandardError ==============================
#[pyclass(name = "StandardError", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyStandardError {
inner: wc::StandardError,
}
#[pymethods]
impl PyStandardError {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::StandardError::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("StandardError(period={})", self.inner.period())
}
}
// ============================== DetrendedStdDev ==============================
#[pyclass(
name = "DetrendedStdDev",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyDetrendedStdDev {
inner: wc::DetrendedStdDev,
}
#[pymethods]
impl PyDetrendedStdDev {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::DetrendedStdDev::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("DetrendedStdDev(period={})", self.inner.period())
}
}
// ============================== RSquared ==============================
#[pyclass(name = "RSquared", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyRSquared {
inner: wc::RSquared,
}
#[pymethods]
impl PyRSquared {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::RSquared::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("RSquared(period={})", self.inner.period())
}
}
// ============================== Autocorrelation ==============================
#[pyclass(
name = "Autocorrelation",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyAutocorrelation {
inner: wc::Autocorrelation,
}
#[pymethods]
impl PyAutocorrelation {
#[new]
#[pyo3(signature = (period=20, lag=1))]
fn new(period: usize, lag: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Autocorrelation::new(period, lag).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
#[getter]
fn lag(&self) -> usize {
self.inner.lag()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!(
"Autocorrelation(period={}, lag={})",
self.inner.period(),
self.inner.lag()
)
}
}
// ============================== MedianAbsoluteDeviation ==============================
#[pyclass(
name = "MedianAbsoluteDeviation",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyMedianAbsoluteDeviation {
inner: wc::MedianAbsoluteDeviation,
}
#[pymethods]
impl PyMedianAbsoluteDeviation {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::MedianAbsoluteDeviation::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("MedianAbsoluteDeviation(period={})", self.inner.period())
}
}
// ============================== HurstExponent ==============================
#[pyclass(name = "HurstExponent", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyHurstExponent {
inner: wc::HurstExponent,
}
#[pymethods]
impl PyHurstExponent {
#[new]
#[pyo3(signature = (period=100, chunks=4))]
fn new(period: usize, chunks: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::HurstExponent::new(period, chunks).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
#[getter]
fn chunks(&self) -> usize {
self.inner.chunks()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!(
"HurstExponent(period={}, chunks={})",
self.inner.period(),
self.inner.chunks()
)
}
}
// ============================== PearsonCorrelation ==============================
#[pyclass(
name = "PearsonCorrelation",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyPearsonCorrelation {
inner: wc::PearsonCorrelation,
}
#[pymethods]
impl PyPearsonCorrelation {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::PearsonCorrelation::new(period).map_err(map_err)?,
})
}
fn update(&mut self, x: f64, y: f64) -> Option<f64> {
self.inner.update((x, y))
}
/// Batch over two equally-sized numpy arrays.
fn batch<'py>(
&mut self,
py: Python<'py>,
x: PyReadonlyArray1<'py, f64>,
y: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let xs = x
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let ys = y
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if xs.len() != ys.len() {
return Err(PyValueError::new_err("x and y must be equal length"));
}
let mut out = Vec::with_capacity(xs.len());
for i in 0..xs.len() {
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("PearsonCorrelation(period={})", self.inner.period())
}
}
// ============================== Beta ==============================
#[pyclass(name = "Beta", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyBeta {
inner: wc::Beta,
}
#[pymethods]
impl PyBeta {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Beta::new(period).map_err(map_err)?,
})
}
fn update(&mut self, asset: f64, benchmark: f64) -> Option<f64> {
self.inner.update((asset, benchmark))
}
/// Batch over two equally-sized numpy arrays: asset and benchmark.
fn batch<'py>(
&mut self,
py: Python<'py>,
asset: PyReadonlyArray1<'py, f64>,
benchmark: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let a = asset
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let b = benchmark
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if a.len() != b.len() {
return Err(PyValueError::new_err(
"asset and benchmark must be equal length",
));
}
let mut out = Vec::with_capacity(a.len());
for i in 0..a.len() {
out.push(self.inner.update((a[i], b[i])).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("Beta(period={})", self.inner.period())
}
}
// ============================== SpearmanCorrelation ==============================
#[pyclass(
name = "SpearmanCorrelation",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PySpearmanCorrelation {
inner: wc::SpearmanCorrelation,
}
#[pymethods]
impl PySpearmanCorrelation {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::SpearmanCorrelation::new(period).map_err(map_err)?,
})
}
fn update(&mut self, x: f64, y: f64) -> Option<f64> {
self.inner.update((x, y))
}
/// Batch over two equally-sized numpy arrays.
fn batch<'py>(
&mut self,
py: Python<'py>,
x: PyReadonlyArray1<'py, f64>,
y: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let xs = x
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let ys = y
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if xs.len() != ys.len() {
return Err(PyValueError::new_err("x and y must be equal length"));
}
let mut out = Vec::with_capacity(xs.len());
for i in 0..xs.len() {
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("SpearmanCorrelation(period={})", self.inner.period())
}
}
// ============================== Module ==============================
#[pymodule]
@@ -10291,5 +10991,18 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
// Family 13 — Ichimoku & alternative charts
m.add_class::<PyIchimoku>()?;
m.add_class::<PyHeikinAshi>()?;
m.add_class::<PyVariance>()?;
m.add_class::<PyCoefficientOfVariation>()?;
m.add_class::<PySkewness>()?;
m.add_class::<PyKurtosis>()?;
m.add_class::<PyStandardError>()?;
m.add_class::<PyDetrendedStdDev>()?;
m.add_class::<PyRSquared>()?;
m.add_class::<PyAutocorrelation>()?;
m.add_class::<PyMedianAbsoluteDeviation>()?;
m.add_class::<PyHurstExponent>()?;
m.add_class::<PyPearsonCorrelation>()?;
m.add_class::<PyBeta>()?;
m.add_class::<PySpearmanCorrelation>()?;
Ok(())
}
@@ -95,6 +95,17 @@ SCALAR = [
(ta.AdaptiveCycle, ()),
(ta.SineWave, ()),
(ta.FAMA, (0.5, 0.05)),
# Family 12 — Statistik / Regression
(ta.Variance, (20,)),
(ta.CoefficientOfVariation, (20,)),
(ta.Skewness, (20,)),
(ta.Kurtosis, (20,)),
(ta.StandardError, (14,)),
(ta.DetrendedStdDev, (14,)),
(ta.RSquared, (14,)),
(ta.MedianAbsoluteDeviation, (20,)),
(ta.Autocorrelation, (20, 1)),
(ta.HurstExponent, (40, 4)),
]
@@ -908,6 +919,111 @@ def test_z_score_reference():
assert out[1] == pytest.approx(1.0)
# --- Family 12: Statistik / Regression reference values ------------------
def test_variance_reference():
# Variance(3) of [2, 4, 6]: mean 4, variance (4 + 0 + 4) / 3 = 8/3.
out = ta.Variance(3).batch(np.array([2.0, 4.0, 6.0]))
assert math.isnan(out[1])
assert out[2] == pytest.approx(8.0 / 3.0)
def test_coefficient_of_variation_reference():
# CV(3) of [2, 4, 6]: sd / mean = sqrt(8/3) / 4.
out = ta.CoefficientOfVariation(3).batch(np.array([2.0, 4.0, 6.0]))
assert out[2] == pytest.approx(math.sqrt(8.0 / 3.0) / 4.0)
def test_skewness_symmetric_window_is_zero():
# Symmetric window has zero Pearson skewness.
out = ta.Skewness(5).batch(np.array([-2.0, -1.0, 0.0, 1.0, 2.0]))
assert out[4] == pytest.approx(0.0, abs=1e-9)
def test_kurtosis_two_point_distribution_minimum():
# Alternating {-1, 1} has m4/m2² = 1, so excess kurtosis = -2.
out = ta.Kurtosis(4).batch(np.array([-1.0, 1.0, -1.0, 1.0]))
assert out[3] == pytest.approx(-2.0, abs=1e-9)
def test_standard_error_perfect_line_is_zero():
# Residuals are zero on a perfectly linear series.
out = ta.StandardError(5).batch(np.linspace(1.0, 20.0, num=20, dtype=np.float64))
finite = out[~np.isnan(out)]
assert np.allclose(finite, 0.0, atol=1e-9)
def test_detrended_std_dev_perfect_line_is_zero():
out = ta.DetrendedStdDev(5).batch(np.linspace(1.0, 20.0, num=20, dtype=np.float64))
finite = out[~np.isnan(out)]
assert np.allclose(finite, 0.0, atol=1e-9)
def test_r_squared_perfect_line_is_one():
out = ta.RSquared(5).batch(np.linspace(1.0, 20.0, num=20, dtype=np.float64))
finite = out[~np.isnan(out)]
assert np.allclose(finite, 1.0, atol=1e-9)
def test_median_absolute_deviation_ignores_single_outlier():
# 9 equal values + 1 huge outlier: MAD is still 0 (more than half agree).
prices = np.array([5.0] * 9 + [1000.0], dtype=np.float64)
out = ta.MedianAbsoluteDeviation(10).batch(prices)
assert out[9] == pytest.approx(0.0, abs=1e-12)
def test_autocorrelation_alternating_series_negative():
# ±1 alternating: lag-1 ACF must be strongly negative.
prices = np.array([-1.0 if i % 2 == 0 else 1.0 for i in range(20)], dtype=np.float64)
out = ta.Autocorrelation(10, 1).batch(prices)
assert out[-1] < -0.5
def test_hurst_exponent_trending_above_half():
# A clean monotone ramp is the textbook persistent series.
prices = np.arange(200, dtype=np.float64)
out = ta.HurstExponent(100, 4).batch(prices)
assert out[-1] > 0.5
def test_pearson_correlation_perfect_positive_is_one():
x = np.arange(10, dtype=np.float64)
y = 2.0 * x + 3.0
out = ta.PearsonCorrelation(5).batch(x, y)
assert out[-1] == pytest.approx(1.0, abs=1e-9)
def test_beta_perfect_two_to_one():
benchmark = np.arange(10, dtype=np.float64)
asset = 2.0 * benchmark
out = ta.Beta(5).batch(asset, benchmark)
assert out[-1] == pytest.approx(2.0, abs=1e-9)
def test_spearman_correlation_monotone_nonlinear_is_one():
# y = x^3 is monotone non-linear; Spearman = 1 (Pearson would not be).
x = np.arange(1.0, 11.0, dtype=np.float64)
y = x**3
out = ta.SpearmanCorrelation(5).batch(x, y)
assert out[-1] == pytest.approx(1.0, abs=1e-9)
def test_pair_indicators_streaming_matches_batch():
rng = np.linspace(0.0, 10.0, num=60, dtype=np.float64)
x = np.sin(rng) + 0.1 * rng
y = np.cos(rng * 0.3) + 0.05 * rng
for cls, args in [(ta.PearsonCorrelation, (14,)), (ta.Beta, (14,)), (ta.SpearmanCorrelation, (14,))]:
batch = cls(*args).batch(x, y)
streamer = cls(*args)
streamed = []
for i in range(x.size):
v = streamer.update(float(x[i]), float(y[i]))
streamed.append(math.nan if v is None else float(v))
assert _eq_nan(batch, np.array(streamed, dtype=np.float64)), f"{cls.__name__} mismatch"
# --- Family 10 — Ehlers / Cycle ---
+69
View File
@@ -227,6 +227,16 @@ wasm_scalar_indicator!(WasmLinRegSlope, "LinRegSlope", wc::LinRegSlope, period:
wasm_scalar_indicator!(WasmVerticalHorizontalFilter, "VerticalHorizontalFilter", wc::VerticalHorizontalFilter, period: usize);
wasm_scalar_indicator!(WasmZScore, "ZScore", wc::ZScore, period: usize);
wasm_scalar_indicator!(WasmLinRegAngle, "LinRegAngle", wc::LinRegAngle, period: usize);
wasm_scalar_indicator!(WasmVariance, "Variance", wc::Variance, period: usize);
wasm_scalar_indicator!(WasmCoefficientOfVariation, "CoefficientOfVariation", wc::CoefficientOfVariation, period: usize);
wasm_scalar_indicator!(WasmSkewness, "Skewness", wc::Skewness, period: usize);
wasm_scalar_indicator!(WasmKurtosis, "Kurtosis", wc::Kurtosis, period: usize);
wasm_scalar_indicator!(WasmStandardError, "StandardError", wc::StandardError, period: usize);
wasm_scalar_indicator!(WasmDetrendedStdDev, "DetrendedStdDev", wc::DetrendedStdDev, period: usize);
wasm_scalar_indicator!(WasmRSquared, "RSquared", wc::RSquared, period: usize);
wasm_scalar_indicator!(WasmMedianAbsoluteDeviation, "MedianAbsoluteDeviation", wc::MedianAbsoluteDeviation, period: usize);
wasm_scalar_indicator!(WasmAutocorrelation, "Autocorrelation", wc::Autocorrelation, period: usize, lag: usize);
wasm_scalar_indicator!(WasmHurstExponent, "HurstExponent", wc::HurstExponent, period: usize, chunks: usize);
wasm_scalar_indicator!(WasmRviVolatility, "RVIVolatility", wc::RviVolatility, period: usize);
wasm_scalar_indicator!(WasmLaguerreRsi, "LaguerreRSI", wc::LaguerreRsi, gamma: f64);
wasm_scalar_indicator!(WasmConnorsRsi, "ConnorsRSI", wc::ConnorsRsi, period_rsi: usize, period_streak: usize, period_rank: usize);
@@ -462,6 +472,65 @@ wasm_scalar_indicator!(WasmEhlersStochastic, "EhlersStochastic", wc::EhlersStoch
wasm_scalar_indicator!(WasmEmpiricalModeDecomposition, "EmpiricalModeDecomposition", wc::EmpiricalModeDecomposition, period: usize, fraction: f64);
wasm_scalar_indicator!(WasmFama, "FAMA", wc::Fama, fast_limit: f64, slow_limit: f64);
// ---------- Family 12: Two-series indicators (Pearson / Beta / Spearman) ----------
macro_rules! wasm_pair_indicator {
($name:ident, $js_name:literal, $rust_ty:ty) => {
#[wasm_bindgen(js_name = $js_name)]
pub struct $name {
inner: $rust_ty,
}
#[wasm_bindgen(js_class = $js_name)]
impl $name {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<$name, JsError> {
Ok($name {
inner: <$rust_ty>::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, x: f64, y: f64) -> Option<f64> {
self.inner.update((x, y))
}
/// Batch over two equally-sized arrays. Returns one `f64` per
/// input position (`NaN` during warmup).
pub fn batch(&mut self, x: &[f64], y: &[f64]) -> Result<Float64Array, JsError> {
if x.len() != y.len() {
return Err(JsError::new("x and y must be equal length"));
}
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(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
};
}
wasm_pair_indicator!(
WasmPearsonCorrelation,
"PearsonCorrelation",
wc::PearsonCorrelation
);
wasm_pair_indicator!(WasmBeta, "Beta", wc::Beta);
wasm_pair_indicator!(
WasmSpearmanCorrelation,
"SpearmanCorrelation",
wc::SpearmanCorrelation
);
// ---------- KAMA (three params) ----------
#[wasm_bindgen(js_name = KAMA)]