fix: de-duplicate four indicators by correcting their definitions (#300)

* fix(core): de-duplicate 3 indicators by correcting their definitions

Behavioral audit found these computed identically to another indicator:

- AverageDrawdown was the mean per-bar under-water fraction = PainIndex.
  Now the conventional average drawdown: mean of the maximum depths of the
  distinct drawdown episodes in the window.
- IntradayIntensity was a cumulative line = the A/D Line (Adl); its normalized
  form is the Chaikin Money Flow (Cmf). Now the raw per-bar Bostian intensity
  volume*(2c-h-l)/(h-l), distinct from both.
- AwesomeOscillatorHistogram was AO - SMA(AO, n) = AcceleratorOscillator. Now
  the AO momentum AO[t] - AO[t-lookback] (the histogram delta); the 3rd
  parameter is reinterpreted from sma_period to lookback (default 1).

Constructor signatures are unchanged, so the bindings keep their API. Core
unit tests rewritten with the new reference values; workspace tests + clippy
green. Binding value-tests and deep-dive docs are updated separately.

* fix(core): redefine AdOscillator as the A/D Oscillator (was a Wad duplicate)

AdOscillator computed the cumulative volume-free Williams A/D line, identical
to the Wad indicator. Redefine it as the Williams A/D *Oscillator*: the same
line minus its 13-bar SMA, so it oscillates around zero (mean-reverting) while
Wad stays the drifting cumulative line for divergence analysis. The canonical
name AdOscillator is now accurate; the trait name() becomes "ADOSC".

Constructor stays no-arg (internal 13-bar signal). Unit tests rewritten and
cross-checked against Wad - SMA(Wad, 13). The native bindings' "WilliamsAD"
alias is renamed to "ADOSC" separately.

* fix(bindings): rename WilliamsAD alias to ADOSC and update value tests

Follows the core de-duplication: the native bindings exposed the Williams A/D
line as 'WilliamsAD', which is now the A/D Oscillator. Rename the Python /
Node.js / WASM alias to 'ADOSC' (regenerated node index.js / index.d.ts) and
update the binding value-tests for the four redefined indicators
(AverageDrawdown episode mean, AwesomeOscillatorHistogram momentum warmup,
the Wad-line reference test now uses ta.Wad()). Python suite and node suite
both pass (pytest all green, node 584/584).

* docs: record indicator de-duplication in README and CHANGELOG

README volume family: 'Williams A/D' -> 'Williams A/D Oscillator', 'Intraday
Intensity Index' -> 'Intraday Intensity'. CHANGELOG [Unreleased] documents the
four redefinitions and the native WilliamsAD -> ADOSC rename as breaking.

* test(core): cover Default impl and drop dead match arm

Codecov flagged AdOscillator::default() (never exercised) and the unreachable
_ => panic!() arm in the AwesomeOscillatorHistogram test. Exercise Default in
the accessors test and rewrite the histogram check as an if-let, removing the
dead arm.
This commit is contained in:
kingchenc
2026-06-15 03:41:15 +02:00
committed by GitHub
parent a3950bf31b
commit 82d7479011
15 changed files with 320 additions and 208 deletions
+2 -2
View File
@@ -236,7 +236,7 @@ from ._wickra import (
VolumeOscillator,
NVI,
PVI,
WilliamsAD,
ADOSC,
AnchoredVWAP,
DemandIndex,
TSV,
@@ -780,7 +780,7 @@ __all__ = [
"VolumeOscillator",
"NVI",
"PVI",
"WilliamsAD",
"ADOSC",
"AnchoredVWAP",
"DemandIndex",
"TSV",
+2 -2
View File
@@ -9450,7 +9450,7 @@ impl PyKvo {
// ============================== Williams A/D Oscillator ==============================
#[pyclass(name = "WilliamsAD", module = "wickra._wickra", skip_from_py_object)]
#[pyclass(name = "ADOSC", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyAdOscillator {
inner: wc::AdOscillator,
@@ -9507,7 +9507,7 @@ impl PyAdOscillator {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
"WilliamsAD()".to_string()
"ADOSC()".to_string()
}
}
+6 -6
View File
@@ -258,9 +258,9 @@ def test_awesome_oscillator_histogram_flat_series_converges_to_zero():
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)
out = ta.AwesomeOscillatorHistogram(3, 5, 3).batch(high, low) # AO momentum
# warmup = slow + lookback = 5 + 3 = 8.
np.testing.assert_allclose(out[7:], 0.0, atol=1e-12)
def test_stc_constant_series_yields_zero():
@@ -521,10 +521,10 @@ def test_calmar_ratio_known_path():
def test_average_drawdown_known_window():
# window [100, 120, 90, 110]: dd = 0, 0, 0.25, 10/120;
# mean = (0.25 + 10/120) / 4.
# window [100, 120, 90, 110]: one drawdown episode (peak 120, trough 90),
# never recovering -> depth (120-90)/120 = 0.25; one episode -> AvgDD = 0.25.
out = ta.AverageDrawdown(4).batch(np.array([100.0, 120.0, 90.0, 110.0]))
expected = (0.25 + 10.0 / 120.0) / 4.0
expected = 0.25
assert math.isclose(out[-1], expected, rel_tol=1e-12)
+3 -3
View File
@@ -634,8 +634,8 @@ CANDLE_SCALAR = {
lambda: ta.PVI(),
lambda ind, h, l, c, v: ind.batch(c, v),
),
"WilliamsAD": (
lambda: ta.WilliamsAD(),
"ADOSC": (
lambda: ta.ADOSC(),
lambda ind, h, l, c, v: ind.batch(h, l, c),
),
"AnchoredVWAP": (
@@ -1762,7 +1762,7 @@ def test_wad_reference():
# TR_l = min(10, 8) = 8 -> delta = 12 - 8 = 4. AD = 4.
# bar 2: prev=12, today high=11, low=7, close=7 (down day).
# TR_h = max(12, 11) = 12 -> delta = 7 - 12 = -5. AD = 4 - 5 = -1.
ad = ta.WilliamsAD()
ad = ta.Wad()
high = np.array([11.0, 13.0, 11.0])
low = np.array([9.0, 8.0, 7.0])
close = np.array([10.0, 12.0, 7.0])