"""Unit tests for ferro_ta.indicators.volume""" import numpy as np from ferro_ta.indicators.volume import AD, ADOSC, OBV # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- RNG = np.random.default_rng(5) N = 100 _CLOSE = 100 + np.cumsum(RNG.normal(0, 0.5, N)) _HIGH = _CLOSE + np.abs(RNG.normal(0, 0.3, N)) _LOW = _CLOSE - np.abs(RNG.normal(0, 0.3, N)) _VOL = RNG.uniform(1000, 5000, N) SMALL_H = np.array([12.0, 13.0, 14.0, 15.0, 16.0]) SMALL_L = np.array([9.0, 10.0, 11.0, 12.0, 13.0]) SMALL_C = np.array([11.0, 12.0, 13.0, 14.0, 15.0]) SMALL_V = np.array([1000.0, 2000.0, 3000.0, 4000.0, 5000.0]) # --------------------------------------------------------------------------- # OBV # --------------------------------------------------------------------------- class TestOBV: def test_known_values_rising(self): # Rising close: OBV accumulates all volume c = np.array([10.0, 11.0, 12.0, 13.0, 14.0]) v = np.array([1000.0, 1000.0, 1000.0, 1000.0, 1000.0]) result = OBV(c, v) np.testing.assert_allclose(result[0], 0.0, atol=1e-10) np.testing.assert_allclose(result[1], 1000.0, atol=1e-10) np.testing.assert_allclose(result[4], 4000.0, atol=1e-10) def test_known_values_falling(self): c = np.array([14.0, 13.0, 12.0, 11.0, 10.0]) v = np.array([1000.0, 1000.0, 1000.0, 1000.0, 1000.0]) result = OBV(c, v) np.testing.assert_allclose(result[0], 0.0, atol=1e-10) np.testing.assert_allclose(result[1], -1000.0, atol=1e-10) np.testing.assert_allclose(result[4], -4000.0, atol=1e-10) def test_unchanged_price_no_change(self): c = np.array([10.0, 10.0, 10.0]) v = np.array([500.0, 500.0, 500.0]) result = OBV(c, v) np.testing.assert_allclose(result, [0.0, 0.0, 0.0], atol=1e-10) def test_no_nan(self): result = OBV(SMALL_C, SMALL_V) assert np.all(np.isfinite(result)) def test_length(self): assert len(OBV(_CLOSE, _VOL)) == N def test_starts_zero(self): result = OBV(_CLOSE, _VOL) np.testing.assert_allclose(result[0], 0.0, atol=1e-10) # --------------------------------------------------------------------------- # AD # --------------------------------------------------------------------------- class TestAD: def test_known_formula(self): # AD = cumsum(CLV * volume) # CLV = ((close - low) - (high - close)) / (high - low) h = np.array([15.0]) l = np.array([10.0]) c = np.array([12.0]) v = np.array([1000.0]) clv = ((12 - 10) - (15 - 12)) / (15 - 10) # (2 - 3) / 5 = -0.2 expected = clv * 1000.0 result = AD(h, l, c, v) np.testing.assert_allclose(result[0], expected, rtol=1e-10) def test_monotone_rising_positive(self): # High CLV on rising data → AD should be non-negative cumulatively result = AD(SMALL_H, SMALL_L, SMALL_C, SMALL_V) assert np.all(np.isfinite(result)) def test_no_nan(self): result = AD(_HIGH, _LOW, _CLOSE, _VOL) assert np.all(np.isfinite(result)) def test_length(self): assert len(AD(_HIGH, _LOW, _CLOSE, _VOL)) == N # --------------------------------------------------------------------------- # ADOSC # --------------------------------------------------------------------------- class TestADOSC: def test_nan_warmup(self): result = ADOSC(_HIGH, _LOW, _CLOSE, _VOL, fastperiod=3, slowperiod=10) assert np.all(np.isnan(result[:9])) def test_length(self): assert len(ADOSC(_HIGH, _LOW, _CLOSE, _VOL, 3, 10)) == N def test_finite_after_warmup(self): result = ADOSC(_HIGH, _LOW, _CLOSE, _VOL, fastperiod=3, slowperiod=10) valid = result[~np.isnan(result)] assert np.all(np.isfinite(valid)) def test_known_values(self): result = ADOSC(SMALL_H, SMALL_L, SMALL_C, SMALL_V, fastperiod=2, slowperiod=3) valid = result[~np.isnan(result)] assert len(valid) > 0 assert np.all(np.isfinite(valid))