feat(apo): add Absolute Price Oscillator

EMA(close, fast) - EMA(close, slow). Like MACD without the signal EMA.
Defaults to (fast = 12, slow = 26); fast must be strictly less than
slow.

Touchpoints: apo.rs + mod.rs + lib.rs re-export, PyApo + __init__.py
+ test_new_indicators SCALAR + test_known_values flat reference,
ApoNode + index.d.ts/index.js + indicators.test.js factory + reference,
WasmApo via scalar macro, scalar-fuzz target, README + CHANGELOG.
This commit is contained in:
kingchenc
2026-05-24 21:38:48 +02:00
parent e30b3c6b35
commit ec269d8aeb
11 changed files with 217 additions and 6 deletions
@@ -56,6 +56,7 @@ from ._wickra import (
PMO,
StochRSI,
UltimateOscillator,
APO,
PPO,
DPO,
Coppock,
@@ -137,6 +138,7 @@ __all__ = [
"PMO",
"StochRSI",
"UltimateOscillator",
"APO",
"PPO",
"DPO",
"Coppock",
@@ -66,6 +66,13 @@ def test_rsi_wilder_textbook_first_value():
assert math.isclose(out[14], 70.464, abs_tol=0.05)
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))
assert np.all(np.isnan(out[:4]))
np.testing.assert_allclose(out[4:], 0.0, atol=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.
@@ -51,6 +51,7 @@ SCALAR = [
(ta.PMO, (35, 20)),
(ta.StochRSI, (14, 14)),
(ta.PPO, (12, 26)),
(ta.APO, (12, 26)),
(ta.DPO, (20,)),
(ta.Coppock, (14, 11, 10)),
(ta.StdDev, (20,)),