Deepen Moving Averages family with seven additions (#177)

Deepens the **Moving Averages** family with seven widely-used variants
(396 → 403 indicators), the first batch of Part B (family deepening).

All are scalar `f64 → f64`:

| Indicator | Binding | Notes |
|-----------|---------|-------|
| `SineWeightedMa` | `SWMA` | symmetric half-cycle sine-weighted window |
| `GeometricMa` | `GMA` | rolling geometric mean (log-space average) |
| `Ehma` | `EHMA` | exponential Hull MA (Hull construction over EMAs) |
| `MedianMa` | `MedianMA` | rolling median, robust to single outliers |
| `AdaptiveLaguerreFilter` | `AdaptiveLaguerre` | Ehlers' adaptive Laguerre filter (median-of-normalised-error γ) |
| `GeneralizedDema` | `GD` | Tillson's volume-factor double EMA; `v=1` is DEMA, `v=0` is EMA |
| `HoltWinters` | `HoltWinters` | Holt's linear double exponential smoothing (level + trend) |

LSMA was dropped from the planned set: it already ships as `LinearRegression`
(TA-Lib `LINEARREG`, the rolling least-squares endpoint).

The five single-period filters use the generated scalar macro bindings;
`GeneralizedDema` (period, v) and `HoltWinters` (alpha, beta) use hand-written
node/python bindings with the typed wasm macro (precedent `T3` / `Alma`).

Full coverage: core modules with per-branch unit tests (100% intent), mod/lib
catalogue, FAMILIES group + assert, README + docs counters, CHANGELOG, all three
bindings (regenerated `index.d.ts` / `index.js`), fuzz drivers, and the
python/node test registries.

Local verification: `cargo test -p wickra-core` (lib 3255 + doc 361),
`cargo clippy --workspace --all-targets --all-features -D warnings` clean,
node `npm run build && npm test` (478), python `pytest` (791).
This commit is contained in:
kingchenc
2026-06-04 13:44:51 +02:00
committed by GitHub
parent 8dc7158912
commit b228a70d7d
21 changed files with 2492 additions and 55 deletions
+375
View File
@@ -2354,6 +2354,250 @@ impl PyExpectancy {
}
}
// ============================== SineWeightedMa ==============================
#[pyclass(name = "SWMA", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PySineWeightedMa {
inner: wc::SineWeightedMa,
}
#[pymethods]
impl PySineWeightedMa {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::SineWeightedMa::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!("SWMA(period={})", self.inner.period())
}
}
// ============================== GeometricMa ==============================
#[pyclass(name = "GMA", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyGeometricMa {
inner: wc::GeometricMa,
}
#[pymethods]
impl PyGeometricMa {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::GeometricMa::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!("GMA(period={})", self.inner.period())
}
}
// ============================== Ehma ==============================
#[pyclass(name = "EHMA", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyEhma {
inner: wc::Ehma,
}
#[pymethods]
impl PyEhma {
#[new]
#[pyo3(signature = (period=9))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Ehma::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!("EHMA(period={})", self.inner.period())
}
}
// ============================== MedianMa ==============================
#[pyclass(name = "MedianMA", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyMedianMa {
inner: wc::MedianMa,
}
#[pymethods]
impl PyMedianMa {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::MedianMa::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!("MedianMA(period={})", self.inner.period())
}
}
// ============================== AdaptiveLaguerreFilter ==============================
#[pyclass(
name = "AdaptiveLaguerre",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyAdaptiveLaguerreFilter {
inner: wc::AdaptiveLaguerreFilter,
}
#[pymethods]
impl PyAdaptiveLaguerreFilter {
#[new]
#[pyo3(signature = (period=13))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::AdaptiveLaguerreFilter::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!("AdaptiveLaguerre(period={})", self.inner.period())
}
}
// ============================== Stochastic ==============================
#[pyclass(name = "Stochastic", module = "wickra._wickra", skip_from_py_object)]
@@ -6197,6 +6441,130 @@ impl PyT3 {
}
}
// ============================== GD ==============================
#[pyclass(name = "GD", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyGeneralizedDema {
inner: wc::GeneralizedDema,
}
#[pymethods]
impl PyGeneralizedDema {
#[new]
#[pyo3(signature = (period, v=0.7))]
fn new(period: usize, v: f64) -> PyResult<Self> {
Ok(Self {
inner: wc::GeneralizedDema::new(period, v).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 slice = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
#[getter]
fn volume_factor(&self) -> f64 {
self.inner.volume_factor()
}
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!(
"GD(period={}, v={})",
self.inner.period(),
self.inner.volume_factor()
)
}
}
// ============================== HoltWinters ==============================
#[pyclass(name = "HoltWinters", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyHoltWinters {
inner: wc::HoltWinters,
}
#[pymethods]
impl PyHoltWinters {
#[new]
#[pyo3(signature = (alpha=0.2, beta=0.1))]
fn new(alpha: f64, beta: f64) -> PyResult<Self> {
Ok(Self {
inner: wc::HoltWinters::new(alpha, beta).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 slice = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
}
#[getter]
fn alpha(&self) -> f64 {
self.inner.alpha()
}
#[getter]
fn beta(&self) -> f64 {
self.inner.beta()
}
#[getter]
fn level(&self) -> Option<f64> {
self.inner.level()
}
#[getter]
fn trend(&self) -> Option<f64> {
self.inner.trend()
}
#[getter]
fn value(&self) -> Option<f64> {
self.inner.value()
}
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!(
"HoltWinters(alpha={}, beta={})",
self.inner.alpha(),
self.inner.beta()
)
}
}
// ============================== VWMA ==============================
#[pyclass(name = "VWMA", module = "wickra._wickra", skip_from_py_object)]
@@ -19667,6 +20035,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyTrima>()?;
m.add_class::<PyZlema>()?;
m.add_class::<PyT3>()?;
m.add_class::<PyGeneralizedDema>()?;
m.add_class::<PyHoltWinters>()?;
m.add_class::<PyVwma>()?;
m.add_class::<PyMom>()?;
m.add_class::<PyCmo>()?;
@@ -20031,5 +20401,10 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyRegimeLabel>()?;
m.add_class::<PyWinRate>()?;
m.add_class::<PyExpectancy>()?;
m.add_class::<PySineWeightedMa>()?;
m.add_class::<PyGeometricMa>()?;
m.add_class::<PyEhma>()?;
m.add_class::<PyMedianMa>()?;
m.add_class::<PyAdaptiveLaguerreFilter>()?;
Ok(())
}