扩展指标
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
Integration tests using the synthetic OHLCV fixture in tests/fixtures/.
|
||||
|
||||
These tests verify that:
|
||||
- All major indicator categories produce finite output on realistic data.
|
||||
- Output lengths match the input length.
|
||||
- Error codes and suggestion hints are included in exception messages.
|
||||
- ferro_ta.indicators() and ferro_ta.info() work correctly.
|
||||
- Logging utilities (enable_debug, log_call, benchmark) work correctly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load the OHLCV fixture
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FIXTURE_PATH = Path(__file__).parent.parent / "fixtures" / "ohlcv_daily.csv"
|
||||
|
||||
|
||||
def _load_fixture() -> tuple[
|
||||
np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray
|
||||
]:
|
||||
"""Return (open, high, low, close, volume) as float64 arrays."""
|
||||
rows = []
|
||||
with open(FIXTURE_PATH, newline="") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rows.append(row)
|
||||
open_ = np.array([float(r["open"]) for r in rows])
|
||||
high = np.array([float(r["high"]) for r in rows])
|
||||
low = np.array([float(r["low"]) for r in rows])
|
||||
close = np.array([float(r["close"]) for r in rows])
|
||||
volume = np.array([float(r["volume"]) for r in rows])
|
||||
return open_, high, low, close, volume
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ohlcv():
|
||||
return _load_fixture()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixture sanity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_fixture_loads(ohlcv):
|
||||
o, h, l, c, v = ohlcv
|
||||
assert len(c) == 252
|
||||
assert np.all(h >= l)
|
||||
assert np.all(v > 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Overlap indicators on real OHLCV data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_sma_on_fixture(ohlcv):
|
||||
from ferro_ta import SMA
|
||||
|
||||
_, _, _, close, _ = ohlcv
|
||||
result = SMA(close, timeperiod=20)
|
||||
assert len(result) == len(close)
|
||||
# First 19 values should be NaN, rest finite
|
||||
assert np.all(np.isnan(result[:19]))
|
||||
assert np.all(np.isfinite(result[19:]))
|
||||
|
||||
|
||||
def test_ema_on_fixture(ohlcv):
|
||||
from ferro_ta import EMA
|
||||
|
||||
_, _, _, close, _ = ohlcv
|
||||
result = EMA(close, timeperiod=14)
|
||||
assert len(result) == len(close)
|
||||
assert np.all(np.isfinite(result[13:]))
|
||||
|
||||
|
||||
def test_bbands_on_fixture(ohlcv):
|
||||
from ferro_ta import BBANDS
|
||||
|
||||
_, _, _, close, _ = ohlcv
|
||||
upper, mid, lower = BBANDS(close, timeperiod=20)
|
||||
assert len(upper) == len(close)
|
||||
assert np.all(upper[19:] >= mid[19:])
|
||||
assert np.all(mid[19:] >= lower[19:])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Momentum indicators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_rsi_on_fixture(ohlcv):
|
||||
from ferro_ta import RSI
|
||||
|
||||
_, _, _, close, _ = ohlcv
|
||||
result = RSI(close, timeperiod=14)
|
||||
assert len(result) == len(close)
|
||||
valid = result[~np.isnan(result)]
|
||||
assert np.all(valid >= 0) and np.all(valid <= 100)
|
||||
|
||||
|
||||
def test_macd_on_fixture(ohlcv):
|
||||
from ferro_ta import MACD
|
||||
|
||||
_, _, _, close, _ = ohlcv
|
||||
macd, signal, hist = MACD(close)
|
||||
assert len(macd) == len(close)
|
||||
|
||||
|
||||
def test_adx_on_fixture(ohlcv):
|
||||
from ferro_ta import ADX
|
||||
|
||||
_, high, low, close, _ = ohlcv
|
||||
result = ADX(high, low, close, timeperiod=14)
|
||||
assert len(result) == len(close)
|
||||
|
||||
|
||||
def test_stoch_on_fixture(ohlcv):
|
||||
from ferro_ta import STOCH
|
||||
|
||||
_, high, low, close, _ = ohlcv
|
||||
slowk, slowd = STOCH(high, low, close)
|
||||
assert len(slowk) == len(close)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Volatility indicators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_atr_on_fixture(ohlcv):
|
||||
from ferro_ta import ATR
|
||||
|
||||
_, high, low, close, _ = ohlcv
|
||||
result = ATR(high, low, close, timeperiod=14)
|
||||
assert len(result) == len(close)
|
||||
valid = result[~np.isnan(result)]
|
||||
assert np.all(valid >= 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Volume indicators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_obv_on_fixture(ohlcv):
|
||||
from ferro_ta import OBV
|
||||
|
||||
_, _, _, close, volume = ohlcv
|
||||
result = OBV(close, volume)
|
||||
assert len(result) == len(close)
|
||||
assert np.all(np.isfinite(result))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error handling — error codes and suggestion hints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_value_error_has_code():
|
||||
from ferro_ta.core.exceptions import FerroTAValueError, check_timeperiod
|
||||
|
||||
with pytest.raises(FerroTAValueError) as exc_info:
|
||||
check_timeperiod(0, "timeperiod", minimum=1)
|
||||
exc = exc_info.value
|
||||
assert exc.code == "FTERR001"
|
||||
assert "FTERR001" in str(exc)
|
||||
assert exc.suggestion is not None
|
||||
assert "Suggestion" in str(exc)
|
||||
|
||||
|
||||
def test_input_error_length_mismatch_has_code():
|
||||
from ferro_ta.core.exceptions import FerroTAInputError, check_equal_length
|
||||
|
||||
with pytest.raises(FerroTAInputError) as exc_info:
|
||||
check_equal_length(open=np.array([1.0, 2.0]), close=np.array([1.0]))
|
||||
exc = exc_info.value
|
||||
assert exc.code == "FTERR004"
|
||||
assert "Suggestion" in str(exc)
|
||||
|
||||
|
||||
def test_input_error_too_short_has_code():
|
||||
from ferro_ta.core.exceptions import FerroTAInputError, check_min_length
|
||||
|
||||
with pytest.raises(FerroTAInputError) as exc_info:
|
||||
check_min_length(np.array([1.0]), 10, "close")
|
||||
exc = exc_info.value
|
||||
assert exc.code == "FTERR003"
|
||||
assert "Suggestion" in str(exc)
|
||||
|
||||
|
||||
def test_finite_check_error_has_code():
|
||||
from ferro_ta.core.exceptions import FerroTAInputError, check_finite
|
||||
|
||||
arr = np.array([1.0, float("nan"), 3.0])
|
||||
with pytest.raises(FerroTAInputError) as exc_info:
|
||||
check_finite(arr, "close")
|
||||
exc = exc_info.value
|
||||
assert exc.code == "FTERR005"
|
||||
assert "Suggestion" in str(exc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API discovery
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_indicators_returns_list():
|
||||
import ferro_ta
|
||||
|
||||
result = ferro_ta.indicators()
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 20
|
||||
names = [d["name"] for d in result]
|
||||
assert "SMA" in names
|
||||
assert "RSI" in names
|
||||
assert "ATR" in names
|
||||
|
||||
|
||||
def test_methods_returns_public_callables():
|
||||
import ferro_ta
|
||||
|
||||
result = ferro_ta.methods()
|
||||
assert isinstance(result, list)
|
||||
assert any(d["name"] == "SMA" and d["category"] == "top_level" for d in result)
|
||||
assert any(
|
||||
d["name"] == "option_price" and d["category"] == "options" for d in result
|
||||
)
|
||||
|
||||
|
||||
def test_about_reports_version_and_counts():
|
||||
import ferro_ta
|
||||
|
||||
meta = ferro_ta.about()
|
||||
assert meta["version"] == ferro_ta.__version__
|
||||
assert meta["indicator_count"] > 20
|
||||
assert meta["method_count"] >= meta["indicator_count"]
|
||||
assert "__version__" in meta["top_level_exports"]
|
||||
|
||||
|
||||
def test_indicators_filter_by_category():
|
||||
import ferro_ta
|
||||
|
||||
overlap = ferro_ta.indicators(category="overlap")
|
||||
assert all(d["category"] == "overlap" for d in overlap)
|
||||
assert any(d["name"] == "SMA" for d in overlap)
|
||||
|
||||
|
||||
def test_info_by_function():
|
||||
import ferro_ta
|
||||
|
||||
d = ferro_ta.info(ferro_ta.SMA)
|
||||
assert d["name"] == "SMA"
|
||||
assert "close" in d["params"]
|
||||
assert "timeperiod" in d["params"]
|
||||
assert isinstance(d["doc"], str)
|
||||
|
||||
|
||||
def test_info_by_string():
|
||||
import ferro_ta
|
||||
|
||||
d = ferro_ta.info("EMA")
|
||||
assert d["name"] == "EMA"
|
||||
|
||||
|
||||
def test_info_unknown_raises():
|
||||
import ferro_ta
|
||||
|
||||
with pytest.raises(ValueError, match="No indicator named"):
|
||||
ferro_ta.info("DOES_NOT_EXIST")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logging utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_logger_returns_logger():
|
||||
import ferro_ta
|
||||
|
||||
logger = ferro_ta.get_logger()
|
||||
assert isinstance(logger, logging.Logger)
|
||||
assert logger.name == "ferro_ta"
|
||||
|
||||
|
||||
def test_enable_disable_debug():
|
||||
import ferro_ta
|
||||
|
||||
ferro_ta.enable_debug()
|
||||
assert ferro_ta.get_logger().level == logging.DEBUG
|
||||
ferro_ta.disable_debug()
|
||||
assert ferro_ta.get_logger().level == logging.WARNING
|
||||
|
||||
|
||||
def test_debug_mode_context_manager():
|
||||
import ferro_ta
|
||||
|
||||
with ferro_ta.debug_mode() as logger:
|
||||
assert logger.level == logging.DEBUG
|
||||
# After context, should be restored
|
||||
assert ferro_ta.get_logger().level == logging.WARNING
|
||||
|
||||
|
||||
def test_log_call_returns_result(ohlcv):
|
||||
import ferro_ta
|
||||
from ferro_ta import SMA
|
||||
|
||||
_, _, _, close, _ = ohlcv
|
||||
result = ferro_ta.log_call(SMA, close, timeperiod=10)
|
||||
assert len(result) == len(close)
|
||||
|
||||
|
||||
def test_benchmark_returns_stats(ohlcv):
|
||||
import ferro_ta
|
||||
from ferro_ta import SMA
|
||||
|
||||
_, _, _, close, _ = ohlcv
|
||||
stats = ferro_ta.benchmark(SMA, close, timeperiod=10, n=5, warmup=1)
|
||||
assert "mean_ms" in stats
|
||||
assert stats["mean_ms"] > 0
|
||||
assert stats["n"] == 5
|
||||
|
||||
|
||||
def test_traced_decorator():
|
||||
import ferro_ta
|
||||
|
||||
@ferro_ta.traced
|
||||
def dummy(x):
|
||||
return x * 2
|
||||
|
||||
assert dummy(21) == 42
|
||||
Reference in New Issue
Block a user