""" 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