216 lines
8.9 KiB
Python
216 lines
8.9 KiB
Python
"""Live-data integration tests for the stockstats utilities.
|
|
|
|
These tests call the real yfinance API and therefore require network access.
|
|
They are marked with ``integration`` and are excluded from the default test
|
|
run (which uses ``--ignore=tests/integration``).
|
|
|
|
Run them explicitly with:
|
|
|
|
python -m pytest tests/integration/test_stockstats_live.py -v --override-ini="addopts="
|
|
|
|
The tests validate the fix for the "Invalid number of return arguments after
|
|
parsing column name: 'Date'" error that occurred because stockstats.wrap()
|
|
promotes the lowercase ``date`` column to the DataFrame index, so the old
|
|
``df["Date"]`` access caused stockstats to try to parse "Date" as an indicator
|
|
name. The fix uses ``df.index.strftime("%Y-%m-%d")`` instead.
|
|
"""
|
|
|
|
import pytest
|
|
import pandas as pd
|
|
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
# A well-known trading day we can use for assertions
|
|
_TEST_DATE = "2025-01-02"
|
|
_TEST_TICKER = "AAPL"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# StockstatsUtils.get_stock_stats
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestStockstatsUtilsLive:
|
|
"""Live tests for StockstatsUtils.get_stock_stats against real yfinance data."""
|
|
|
|
def test_close_50_sma_returns_numeric(self):
|
|
"""close_50_sma indicator returns a numeric value for a known trading day."""
|
|
from tradingagents.dataflows.stockstats_utils import StockstatsUtils
|
|
|
|
result = StockstatsUtils.get_stock_stats(_TEST_TICKER, "close_50_sma", _TEST_DATE)
|
|
|
|
assert result != "N/A: Not a trading day (weekend or holiday)", (
|
|
f"Expected a numeric value for {_TEST_DATE}, got N/A (check if it's a holiday)"
|
|
)
|
|
# Should be a finite float-like value
|
|
assert float(result) > 0, f"close_50_sma should be positive, got: {result}"
|
|
|
|
def test_rsi_returns_value_in_valid_range(self):
|
|
"""RSI indicator returns a value in [0, 100] for a known trading day."""
|
|
from tradingagents.dataflows.stockstats_utils import StockstatsUtils
|
|
|
|
result = StockstatsUtils.get_stock_stats(_TEST_TICKER, "rsi", _TEST_DATE)
|
|
|
|
assert result != "N/A: Not a trading day (weekend or holiday)", (
|
|
f"Expected numeric RSI for {_TEST_DATE}"
|
|
)
|
|
rsi = float(result)
|
|
assert 0.0 <= rsi <= 100.0, f"RSI must be in [0, 100], got: {rsi}"
|
|
|
|
def test_macd_returns_numeric(self):
|
|
"""MACD indicator returns a numeric value for a known trading day."""
|
|
from tradingagents.dataflows.stockstats_utils import StockstatsUtils
|
|
|
|
result = StockstatsUtils.get_stock_stats(_TEST_TICKER, "macd", _TEST_DATE)
|
|
|
|
assert result != "N/A: Not a trading day (weekend or holiday)"
|
|
# MACD can be positive or negative — just confirm it's a valid float
|
|
float(result) # raises ValueError if not numeric
|
|
|
|
def test_weekend_returns_na(self):
|
|
"""A weekend date returns the N/A holiday/weekend message."""
|
|
from tradingagents.dataflows.stockstats_utils import StockstatsUtils
|
|
|
|
# 2025-01-04 is a Saturday
|
|
result = StockstatsUtils.get_stock_stats(_TEST_TICKER, "close_50_sma", "2025-01-04")
|
|
|
|
assert result == "N/A: Not a trading day (weekend or holiday)", (
|
|
f"Expected N/A for Saturday 2025-01-04, got: {result}"
|
|
)
|
|
|
|
def test_no_date_column_error(self):
|
|
"""Calling get_stock_stats must NOT raise the 'Date' column parsing error."""
|
|
from tradingagents.dataflows.stockstats_utils import StockstatsUtils
|
|
|
|
# This previously raised: Invalid number of return arguments after
|
|
# parsing column name: 'Date'
|
|
try:
|
|
StockstatsUtils.get_stock_stats(_TEST_TICKER, "close_50_sma", _TEST_DATE)
|
|
except Exception as e:
|
|
if "Invalid number of return arguments" in str(e) and "Date" in str(e):
|
|
pytest.fail(
|
|
"Regression: stockstats is still trying to parse 'Date' as an "
|
|
f"indicator. Error: {e}"
|
|
)
|
|
raise # re-raise unexpected errors
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _get_stock_stats_bulk
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetStockStatsBulkLive:
|
|
"""Live tests for _get_stock_stats_bulk against real yfinance data."""
|
|
|
|
def test_returns_dict_with_date_keys(self):
|
|
"""Bulk method returns a non-empty dict with YYYY-MM-DD string keys."""
|
|
from tradingagents.dataflows.y_finance import _get_stock_stats_bulk
|
|
|
|
result = _get_stock_stats_bulk(_TEST_TICKER, "rsi", _TEST_DATE)
|
|
|
|
assert isinstance(result, dict), "Expected dict from _get_stock_stats_bulk"
|
|
assert len(result) > 0, "Expected non-empty result dict"
|
|
|
|
# Keys should all be YYYY-MM-DD strings
|
|
for key in list(result.keys())[:5]:
|
|
pd.Timestamp(key) # raises if not parseable
|
|
|
|
def test_trading_day_has_numeric_value(self):
|
|
"""A known trading day has a numeric (non-N/A) value in the bulk result."""
|
|
from tradingagents.dataflows.y_finance import _get_stock_stats_bulk
|
|
|
|
result = _get_stock_stats_bulk(_TEST_TICKER, "rsi", _TEST_DATE)
|
|
|
|
assert _TEST_DATE in result, (
|
|
f"Expected {_TEST_DATE} in bulk result dict. Keys sample: {list(result.keys())[:5]}"
|
|
)
|
|
value = result[_TEST_DATE]
|
|
assert value != "N/A", (
|
|
f"Expected numeric RSI for {_TEST_DATE}, got N/A (check if it's a holiday)"
|
|
)
|
|
float(value) # should be convertible to float
|
|
|
|
def test_no_date_column_parsing_error(self):
|
|
"""Bulk method must not raise the 'Date' column parsing error (regression guard)."""
|
|
from tradingagents.dataflows.y_finance import _get_stock_stats_bulk
|
|
|
|
try:
|
|
_get_stock_stats_bulk(_TEST_TICKER, "close_50_sma", _TEST_DATE)
|
|
except Exception as e:
|
|
if "Invalid number of return arguments" in str(e) and "Date" in str(e):
|
|
pytest.fail(
|
|
"Regression: _get_stock_stats_bulk still hits the 'Date' indicator "
|
|
f"parsing error. Error: {e}"
|
|
)
|
|
raise
|
|
|
|
def test_multiple_indicators_all_work(self):
|
|
"""All supported indicators can be computed without error."""
|
|
from tradingagents.dataflows.y_finance import _get_stock_stats_bulk
|
|
|
|
indicators = [
|
|
"close_50_sma",
|
|
"close_200_sma",
|
|
"close_10_ema",
|
|
"macd",
|
|
"macds",
|
|
"macdh",
|
|
"rsi",
|
|
"boll",
|
|
"boll_ub",
|
|
"boll_lb",
|
|
"atr",
|
|
]
|
|
|
|
for indicator in indicators:
|
|
try:
|
|
result = _get_stock_stats_bulk(_TEST_TICKER, indicator, _TEST_DATE)
|
|
assert isinstance(result, dict), f"{indicator}: expected dict"
|
|
assert len(result) > 0, f"{indicator}: expected non-empty dict"
|
|
except Exception as e:
|
|
pytest.fail(f"Indicator '{indicator}' raised an unexpected error: {e}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_stock_stats_indicators_window (end-to-end with live data)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetStockStatsIndicatorsWindowLive:
|
|
"""Live end-to-end tests for get_stock_stats_indicators_window."""
|
|
|
|
def test_rsi_window_returns_formatted_string(self):
|
|
"""Window function returns a multi-line string with RSI values over a date range."""
|
|
from tradingagents.dataflows.y_finance import get_stock_stats_indicators_window
|
|
|
|
result = get_stock_stats_indicators_window(_TEST_TICKER, "rsi", _TEST_DATE, look_back_days=5)
|
|
|
|
assert isinstance(result, str)
|
|
assert "rsi" in result.lower()
|
|
assert _TEST_DATE in result
|
|
# Should have date: value lines
|
|
lines = [l for l in result.split("\n") if ":" in l and "-" in l]
|
|
assert len(lines) > 0, "Expected date:value lines in result"
|
|
|
|
def test_close_50_sma_window_contains_numeric_values(self):
|
|
"""50-day SMA window result contains actual numeric price values."""
|
|
from tradingagents.dataflows.y_finance import get_stock_stats_indicators_window
|
|
|
|
result = get_stock_stats_indicators_window(
|
|
_TEST_TICKER, "close_50_sma", _TEST_DATE, look_back_days=10
|
|
)
|
|
|
|
assert isinstance(result, str)
|
|
# At least some lines should have numeric values (not all N/A)
|
|
value_lines = [l for l in result.split("\n") if ":" in l and l.strip().startswith("20")]
|
|
numeric_values = []
|
|
for line in value_lines:
|
|
try:
|
|
val = line.split(":", 1)[1].strip()
|
|
numeric_values.append(float(val))
|
|
except (ValueError, IndexError):
|
|
pass # N/A lines are expected for weekends
|
|
|
|
assert len(numeric_values) > 0, (
|
|
"Expected at least some numeric 50-SMA values in the 10-day window"
|
|
)
|