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
+83
View File
@@ -197,6 +197,15 @@ node_scalar_indicator!(
node_scalar_indicator!(TrendLabelNode, "TrendLabel", wc::TrendLabel);
node_scalar_indicator!(WinRateNode, "WinRate", wc::WinRate);
node_scalar_indicator!(ExpectancyNode, "Expectancy", wc::Expectancy);
node_scalar_indicator!(SineWeightedMaNode, "SWMA", wc::SineWeightedMa);
node_scalar_indicator!(GeometricMaNode, "GMA", wc::GeometricMa);
node_scalar_indicator!(EhmaNode, "EHMA", wc::Ehma);
node_scalar_indicator!(MedianMaNode, "MedianMA", wc::MedianMa);
node_scalar_indicator!(
AdaptiveLaguerreFilterNode,
"AdaptiveLaguerre",
wc::AdaptiveLaguerreFilter
);
#[napi(js_name = "JumpIndicator")]
pub struct JumpIndicatorNode {
inner: wc::JumpIndicator,
@@ -3794,6 +3803,80 @@ impl T3Node {
}
}
// ============================== GD ==============================
#[napi(js_name = "GD")]
pub struct GeneralizedDemaNode {
inner: wc::GeneralizedDema,
}
#[napi]
impl GeneralizedDemaNode {
#[napi(constructor)]
pub fn new(period: u32, v: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::GeneralizedDema::new(period as usize, v).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
}
}
// ============================== HoltWinters ==============================
#[napi(js_name = "HoltWinters")]
pub struct HoltWintersNode {
inner: wc::HoltWinters,
}
#[napi]
impl HoltWintersNode {
#[napi(constructor)]
pub fn new(alpha: f64, beta: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::HoltWinters::new(alpha, beta).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
}
}
// ============================== TSI ==============================
#[napi(js_name = "TSI")]