feat(ao-histogram): add Awesome Oscillator Histogram
AO - SMA(AO, sma_period). A configurable variant of the existing AcceleratorOscillator (which fixes fast=5, slow=34, sma=5). Three parameters; defaults match Bill Williams' Accelerator. Touchpoints: awesome_oscillator_histogram.rs + mod.rs + lib.rs re-export, PyAoHist + __init__.py + test_new_indicators CANDLE_SCALAR + test_known_values flat reference, AwesomeOscillatorHistogramNode + index.d.ts/index.js + indicators.test.js factory + reference, WasmAoHist, candle-fuzz target, README + CHANGELOG.
This commit is contained in:
@@ -57,6 +57,7 @@ from ._wickra import (
|
||||
StochRSI,
|
||||
UltimateOscillator,
|
||||
APO,
|
||||
AwesomeOscillatorHistogram,
|
||||
PPO,
|
||||
DPO,
|
||||
Coppock,
|
||||
@@ -139,6 +140,7 @@ __all__ = [
|
||||
"StochRSI",
|
||||
"UltimateOscillator",
|
||||
"APO",
|
||||
"AwesomeOscillatorHistogram",
|
||||
"PPO",
|
||||
"DPO",
|
||||
"Coppock",
|
||||
|
||||
@@ -812,6 +812,68 @@ impl PyKama {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== AwesomeOscillatorHistogram ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "AwesomeOscillatorHistogram",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyAoHist {
|
||||
inner: wc::AwesomeOscillatorHistogram,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAoHist {
|
||||
#[new]
|
||||
#[pyo3(signature = (fast=5, slow=34, sma_period=5))]
|
||||
fn new(fast: usize, slow: usize, sma_period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AwesomeOscillatorHistogram::new(fast, slow, sma_period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
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 {
|
||||
let (f, s, k) = self.inner.periods();
|
||||
format!("AwesomeOscillatorHistogram(fast={f}, slow={s}, sma_period={k})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== APO ==============================
|
||||
|
||||
#[pyclass(name = "APO", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -4540,6 +4602,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyHma>()?;
|
||||
m.add_class::<PyKama>()?;
|
||||
m.add_class::<PyApo>()?;
|
||||
m.add_class::<PyAoHist>()?;
|
||||
m.add_class::<PyCci>()?;
|
||||
m.add_class::<PyRoc>()?;
|
||||
m.add_class::<PyWilliamsR>()?;
|
||||
|
||||
@@ -66,6 +66,16 @@ def test_rsi_wilder_textbook_first_value():
|
||||
assert math.isclose(out[14], 70.464, abs_tol=0.05)
|
||||
|
||||
|
||||
def test_awesome_oscillator_histogram_flat_series_converges_to_zero():
|
||||
# Flat median price -> AO = 0 -> SMA(AO) = 0 -> AOHist = 0.
|
||||
n = 50
|
||||
high = np.full(n, 11.0)
|
||||
low = np.full(n, 9.0)
|
||||
out = ta.AwesomeOscillatorHistogram(3, 5, 3).batch(high, low)
|
||||
# warmup = slow + sma - 1 = 5 + 3 - 1 = 7.
|
||||
np.testing.assert_allclose(out[6:], 0.0, atol=1e-12)
|
||||
|
||||
|
||||
def test_apo_constant_series_converges_to_zero():
|
||||
# Both EMAs reproduce a constant exactly, so APO = 0 after warmup.
|
||||
out = ta.APO(3, 5).batch(np.full(30, 42.0, dtype=np.float64))
|
||||
|
||||
@@ -139,6 +139,10 @@ CANDLE_SCALAR = {
|
||||
lambda: ta.AcceleratorOscillator(5, 34, 5),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
"AwesomeOscillatorHistogram": (
|
||||
lambda: ta.AwesomeOscillatorHistogram(5, 34, 5),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
"BalanceOfPower": (
|
||||
# The streaming 6-tuple feeds open == close, so batch matches with
|
||||
# the close column standing in for open.
|
||||
|
||||
Reference in New Issue
Block a user