Files
2026-07-09 05:08:16 +08:00

184 lines
5.5 KiB
Python

"""Unit tests for ferro_ta.indicators.cycle"""
import numpy as np
from ferro_ta.indicators.cycle import (
HT_DCPERIOD,
HT_DCPHASE,
HT_PHASOR,
HT_SINE,
HT_TRENDLINE,
HT_TRENDMODE,
)
# ---------------------------------------------------------------------------
# Shared fixtures — cycle indicators need at least ~64 bars for valid output
# ---------------------------------------------------------------------------
N = 200
t = np.linspace(0, 10 * np.pi, N)
SINE_CLOSE = 100 + 10 * np.sin(t) # clean sine wave
def _warmup_end(arr):
"""Return index of first non-NaN value (or N if all NaN)."""
valid = np.where(~np.isnan(arr.astype(float)))[0]
return valid[0] if len(valid) else N
# ---------------------------------------------------------------------------
# HT_DCPERIOD
# ---------------------------------------------------------------------------
class TestHT_DCPERIOD:
def test_length(self):
result = HT_DCPERIOD(SINE_CLOSE)
assert len(result) == N
def test_nan_warmup(self):
result = HT_DCPERIOD(SINE_CLOSE)
w = _warmup_end(result)
assert w > 0
assert np.all(np.isnan(result[:w]))
def test_valid_finite(self):
result = HT_DCPERIOD(SINE_CLOSE)
w = _warmup_end(result)
assert np.all(np.isfinite(result[w:]))
def test_sine_period_reasonable(self):
# Our sine has period = 2*pi in t; with N=200 and t in [0,10*pi]
# the true period in samples = 200 / (10*pi / (2*pi)) = 200/5 = 40
result = HT_DCPERIOD(SINE_CLOSE)
valid = result[~np.isnan(result)]
# HT_DCPERIOD should detect a period in a reasonable range [6, 100]
assert np.any((valid > 6) & (valid < 100))
# ---------------------------------------------------------------------------
# HT_DCPHASE
# ---------------------------------------------------------------------------
class TestHT_DCPHASE:
def test_length(self):
assert len(HT_DCPHASE(SINE_CLOSE)) == N
def test_nan_warmup(self):
result = HT_DCPHASE(SINE_CLOSE)
w = _warmup_end(result)
assert w > 0
def test_valid_finite(self):
result = HT_DCPHASE(SINE_CLOSE)
w = _warmup_end(result)
assert np.all(np.isfinite(result[w:]))
# ---------------------------------------------------------------------------
# HT_PHASOR
# ---------------------------------------------------------------------------
class TestHT_PHASOR:
def test_returns_two_arrays(self):
result = HT_PHASOR(SINE_CLOSE)
assert isinstance(result, tuple) and len(result) == 2
def test_length(self):
inphase, quadrature = HT_PHASOR(SINE_CLOSE)
assert len(inphase) == len(quadrature) == N
def test_nan_warmup(self):
inphase, quadrature = HT_PHASOR(SINE_CLOSE)
w = _warmup_end(inphase)
assert w > 0
def test_valid_finite(self):
inphase, quadrature = HT_PHASOR(SINE_CLOSE)
wi = _warmup_end(inphase)
wq = _warmup_end(quadrature)
assert np.all(np.isfinite(inphase[wi:]))
assert np.all(np.isfinite(quadrature[wq:]))
# ---------------------------------------------------------------------------
# HT_SINE
# ---------------------------------------------------------------------------
class TestHT_SINE:
def test_returns_two_arrays(self):
result = HT_SINE(SINE_CLOSE)
assert isinstance(result, tuple) and len(result) == 2
def test_length(self):
sine, leadsine = HT_SINE(SINE_CLOSE)
assert len(sine) == len(leadsine) == N
def test_nan_warmup(self):
sine, leadsine = HT_SINE(SINE_CLOSE)
w = _warmup_end(sine)
assert w > 0
def test_valid_finite(self):
sine, leadsine = HT_SINE(SINE_CLOSE)
ws = _warmup_end(sine)
wl = _warmup_end(leadsine)
assert np.all(np.isfinite(sine[ws:]))
assert np.all(np.isfinite(leadsine[wl:]))
def test_values_in_sine_range(self):
# Sine values should be in [-1, 1] roughly
sine, leadsine = HT_SINE(SINE_CLOSE)
valid = sine[~np.isnan(sine)]
assert np.all(valid >= -1.5) and np.all(valid <= 1.5)
# ---------------------------------------------------------------------------
# HT_TRENDLINE
# ---------------------------------------------------------------------------
class TestHT_TRENDLINE:
def test_length(self):
assert len(HT_TRENDLINE(SINE_CLOSE)) == N
def test_nan_warmup(self):
result = HT_TRENDLINE(SINE_CLOSE)
w = _warmup_end(result)
assert w > 0
def test_valid_finite(self):
result = HT_TRENDLINE(SINE_CLOSE)
w = _warmup_end(result)
assert np.all(np.isfinite(result[w:]))
def test_smooth_trendline(self):
# Trendline should be smoother than raw close
result = HT_TRENDLINE(SINE_CLOSE)
w = _warmup_end(result)
raw_std = np.std(np.diff(SINE_CLOSE[w:]))
trend_std = np.std(np.diff(result[w:]))
assert trend_std < raw_std
# ---------------------------------------------------------------------------
# HT_TRENDMODE
# ---------------------------------------------------------------------------
class TestHT_TRENDMODE:
def test_length(self):
assert len(HT_TRENDMODE(SINE_CLOSE)) == N
def test_values_binary(self):
result = HT_TRENDMODE(SINE_CLOSE)
assert np.all(np.isin(result, [0, 1]))
def test_nan_warmup_as_zero(self):
# HT_TRENDMODE returns integers (no NaN); warmup bars should be 0
result = HT_TRENDMODE(SINE_CLOSE)
assert np.all(np.isfinite(result.astype(float)))