feat(mcginley): add McGinley Dynamic moving average
John McGinley's self-adjusting moving average with the recurrence MD + (price - MD) / (0.6 * period * (price / MD)^4). Speeds up when price falls below the indicator and damps when price runs above the indicator. Seeded with the simple average of the first period inputs. Reference: McGinley, Technical Analysis of Stocks & Commodities, 1990. Touchpoints: - crates/wickra-core: mcginley_dynamic.rs + mod.rs + lib.rs re-export - bindings/python: PyMcGinleyDynamic + __init__.py + test_new_indicators + test_known_values reference - bindings/node: McGinleyDynamicNode (scalar macro) + index.d.ts/index.js + indicators.test.js factory + reference value - bindings/wasm: wasm_scalar_indicator! macro - fuzz: indicator_update target covers McGinleyDynamic(10) - crates/wickra/benches: bench_scalar entry - README + CHANGELOG: Moving Averages row + Unreleased entry
This commit is contained in:
@@ -39,6 +39,7 @@ from ._wickra import (
|
||||
T3,
|
||||
VWMA,
|
||||
ALMA,
|
||||
McGinleyDynamic,
|
||||
# Momentum
|
||||
RSI,
|
||||
MACD,
|
||||
@@ -121,6 +122,7 @@ __all__ = [
|
||||
"T3",
|
||||
"VWMA",
|
||||
"ALMA",
|
||||
"McGinleyDynamic",
|
||||
# Momentum
|
||||
"RSI",
|
||||
"MACD",
|
||||
|
||||
@@ -812,6 +812,58 @@ impl PyKama {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== McGinley Dynamic ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "McGinleyDynamic",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyMcGinleyDynamic {
|
||||
inner: wc::McGinleyDynamic,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMcGinleyDynamic {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=10))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::McGinleyDynamic::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!("McGinleyDynamic(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== ALMA ==============================
|
||||
|
||||
#[pyclass(name = "ALMA", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -4556,6 +4608,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyHma>()?;
|
||||
m.add_class::<PyKama>()?;
|
||||
m.add_class::<PyAlma>()?;
|
||||
m.add_class::<PyMcGinleyDynamic>()?;
|
||||
m.add_class::<PyCci>()?;
|
||||
m.add_class::<PyRoc>()?;
|
||||
m.add_class::<PyWilliamsR>()?;
|
||||
|
||||
@@ -88,6 +88,23 @@ def test_alma_reference_value_period_3():
|
||||
assert out[2] > 20.0
|
||||
|
||||
|
||||
def test_mcginley_dynamic_constant_series_yields_the_constant():
|
||||
# ratio = 1, so the recurrence collapses to MD + 0 / divisor = MD.
|
||||
out = ta.McGinleyDynamic(5).batch(np.full(30, 42.0, dtype=np.float64))
|
||||
assert np.all(np.isnan(out[:4]))
|
||||
np.testing.assert_allclose(out[4:], 42.0, atol=1e-12)
|
||||
|
||||
|
||||
def test_mcginley_dynamic_reference_value():
|
||||
# Period 3, seed = SMA([10, 20, 30]) = 20.0. Next price 40.0:
|
||||
# ratio = 2; divisor = 0.6 * 3 * 16 = 28.8; next = 20 + 20/28.8.
|
||||
out = ta.McGinleyDynamic(3).batch(np.array([10.0, 20.0, 30.0, 40.0]))
|
||||
assert math.isnan(out[0]) and math.isnan(out[1])
|
||||
assert math.isclose(out[2], 20.0, abs_tol=1e-12)
|
||||
expected = 20.0 + 20.0 / (0.6 * 3.0 * 16.0)
|
||||
assert math.isclose(out[3], expected, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_macd_constant_series_converges_to_zero():
|
||||
out = ta.MACD().batch(np.full(200, 100.0))
|
||||
# Last row's MACD and signal must be ~0.
|
||||
|
||||
@@ -45,6 +45,7 @@ SCALAR = [
|
||||
(ta.TRIMA, (20,)),
|
||||
(ta.ZLEMA, (14,)),
|
||||
(ta.ALMA, (9, 0.85, 6.0)),
|
||||
(ta.McGinleyDynamic, (10,)),
|
||||
(ta.T3, (5, 0.7)),
|
||||
(ta.MOM, (10,)),
|
||||
(ta.CMO, (14,)),
|
||||
|
||||
Reference in New Issue
Block a user