TradingAgents/tests/unit/test_e2e_api_integration.py

372 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""End-to-end integration tests combining the Y Finance and Alpha Vantage data layers.
These tests validate the full pipeline from the vendor-routing layer
(interface.route_to_vendor) through data retrieval to formatted output, using
mocks so that no real network calls are made.
"""
import json
import pytest
import pandas as pd
from unittest.mock import patch, MagicMock, PropertyMock
# ---------------------------------------------------------------------------
# Shared mock data
# ---------------------------------------------------------------------------
_OHLCV_CSV_AV = (
"timestamp,open,high,low,close,adjusted_close,volume,dividend_amount,split_coefficient\n"
"2024-01-05,185.00,187.50,184.20,186.00,186.00,50000000,0.0000,1.0\n"
"2024-01-04,183.00,186.00,182.50,185.00,185.00,45000000,0.0000,1.0\n"
)
_OVERVIEW_JSON = json.dumps({
"Symbol": "AAPL",
"Name": "Apple Inc",
"Sector": "TECHNOLOGY",
"MarketCapitalization": "3000000000000",
"PERatio": "30.5",
})
_NEWS_JSON = json.dumps({
"feed": [
{
"title": "Apple Hits Record High",
"url": "https://example.com/news/1",
"time_published": "20240105T150000",
"summary": "Apple stock reached a new record.",
"overall_sentiment_label": "Bullish",
}
]
})
_RATE_LIMIT_JSON = json.dumps({
"Information": (
"Thank you for using Alpha Vantage! Our standard API rate limit is 25 requests per day."
)
})
def _mock_av_response(text: str):
resp = MagicMock()
resp.status_code = 200
resp.text = text
resp.raise_for_status = MagicMock()
return resp
def _make_yf_ohlcv_df():
idx = pd.date_range("2024-01-04", periods=2, freq="B", tz="America/New_York")
return pd.DataFrame(
{"Open": [183.0, 185.0], "High": [186.0, 187.5], "Low": [182.5, 184.2],
"Close": [185.0, 186.0], "Volume": [45_000_000, 50_000_000]},
index=idx,
)
# ---------------------------------------------------------------------------
# Vendor-routing layer tests
# ---------------------------------------------------------------------------
class TestRouteToVendor:
"""Tests for interface.route_to_vendor."""
def test_routes_stock_data_to_yfinance_by_default(self):
"""With default config (yfinance), get_stock_data is routed to yfinance."""
from tradingagents.dataflows.interface import route_to_vendor
df = _make_yf_ohlcv_df()
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = route_to_vendor("get_stock_data", "AAPL", "2024-01-04", "2024-01-05")
assert isinstance(result, str)
assert "AAPL" in result
def test_routes_stock_data_to_alpha_vantage_when_configured(self):
"""When the vendor is overridden to alpha_vantage, the AV implementation is called."""
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.config import get_config
original_config = get_config()
patched_config = {
**original_config,
"data_vendors": {**original_config.get("data_vendors", {}), "core_stock_apis": "alpha_vantage"},
}
with patch("tradingagents.dataflows.interface.get_config", return_value=patched_config):
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_OHLCV_CSV_AV)):
result = route_to_vendor("get_stock_data", "AAPL", "2024-01-04", "2024-01-05")
assert isinstance(result, str)
def test_fallback_to_yfinance_when_alpha_vantage_rate_limited(self):
"""When AV hits a rate limit, the router falls back to yfinance automatically."""
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.config import get_config
original_config = get_config()
patched_config = {
**original_config,
"data_vendors": {**original_config.get("data_vendors", {}), "core_stock_apis": "alpha_vantage"},
}
df = _make_yf_ohlcv_df()
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.interface.get_config", return_value=patched_config):
# AV returns a rate-limit response
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_RATE_LIMIT_JSON)):
# yfinance is the fallback
with patch("tradingagents.dataflows.y_finance.yf.Ticker",
return_value=mock_ticker):
result = route_to_vendor(
"get_stock_data", "AAPL", "2024-01-04", "2024-01-05"
)
assert isinstance(result, str)
assert "AAPL" in result
def test_raises_runtime_error_when_all_vendors_fail(self):
"""When every vendor fails, a RuntimeError is raised."""
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
original_config = get_config()
patched_config = {
**original_config,
"data_vendors": {**original_config.get("data_vendors", {}), "core_stock_apis": "alpha_vantage"},
}
with patch("tradingagents.dataflows.interface.get_config", return_value=patched_config):
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_RATE_LIMIT_JSON)):
with patch(
"tradingagents.dataflows.y_finance.yf.Ticker",
side_effect=ConnectionError("network unavailable"),
):
with pytest.raises(RuntimeError, match="All vendors failed for"):
route_to_vendor("get_stock_data", "AAPL", "2024-01-04", "2024-01-05")
def test_unknown_method_raises_value_error(self):
from tradingagents.dataflows.interface import route_to_vendor
with pytest.raises(ValueError):
route_to_vendor("nonexistent_method", "AAPL")
# ---------------------------------------------------------------------------
# Full pipeline: fetch → process → output
# ---------------------------------------------------------------------------
class TestFullPipeline:
"""End-to-end tests that walk through the complete data retrieval pipeline."""
def test_yfinance_stock_data_pipeline(self):
"""Fetch OHLCV data via yfinance, verify the formatted CSV output."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
df = _make_yf_ohlcv_df()
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
raw = get_YFin_data_online("AAPL", "2024-01-04", "2024-01-05")
# Response structure checks
assert raw.startswith("# Stock data for AAPL")
assert "# Total records: 2" in raw
assert "Close" in raw # CSV column
assert "186.0" in raw # rounded close price
def test_alpha_vantage_stock_data_pipeline(self):
"""Fetch OHLCV data via Alpha Vantage, verify the CSV output is filtered."""
from tradingagents.dataflows.alpha_vantage_stock import get_stock
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_OHLCV_CSV_AV)):
result = get_stock("AAPL", "2024-01-04", "2024-01-05")
assert isinstance(result, str)
# pandas may reformat "185.00" → "185.0"; check for the numeric value
assert "185.0" in result or "186.0" in result
def test_yfinance_fundamentals_pipeline(self):
"""Fetch company fundamentals via yfinance, verify key fields appear."""
from tradingagents.dataflows.y_finance import get_fundamentals
mock_info = {
"longName": "Apple Inc.",
"sector": "Technology",
"industry": "Consumer Electronics",
"marketCap": 3_000_000_000_000,
"trailingPE": 30.5,
"beta": 1.2,
}
mock_ticker = MagicMock()
type(mock_ticker).info = PropertyMock(return_value=mock_info)
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_fundamentals("AAPL")
assert "Apple Inc." in result
assert "Technology" in result
assert "30.5" in result
def test_alpha_vantage_fundamentals_pipeline(self):
"""Fetch company overview via Alpha Vantage, verify key fields appear."""
from tradingagents.dataflows.alpha_vantage_fundamentals import get_fundamentals
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_OVERVIEW_JSON)):
result = get_fundamentals("AAPL")
assert "Apple Inc" in result
assert "TECHNOLOGY" in result
def test_yfinance_news_pipeline(self):
"""Fetch news via yfinance and verify basic response structure."""
from tradingagents.dataflows.yfinance_news import get_news_yfinance
mock_search = MagicMock()
mock_search.news = [
{
"title": "Apple Earnings Beat Expectations",
"publisher": "Reuters",
"link": "https://example.com",
"providerPublishTime": 1704499200,
"summary": "Apple reports Q1 earnings above estimates.",
}
]
with patch("tradingagents.dataflows.yfinance_news.yf.Search", return_value=mock_search):
result = get_news_yfinance("AAPL", "2024-01-01", "2024-01-05")
assert isinstance(result, str)
def test_alpha_vantage_news_pipeline(self):
"""Fetch ticker news via Alpha Vantage and verify basic response structure."""
from tradingagents.dataflows.alpha_vantage_news import get_news
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_NEWS_JSON)):
result = get_news("AAPL", "2024-01-01", "2024-01-05")
assert "Apple Hits Record High" in result
def test_combined_yfinance_and_alpha_vantage_workflow(self):
"""
Simulates a multi-source workflow:
1. Fetch stock price data from yfinance.
2. Fetch company fundamentals from Alpha Vantage.
3. Verify both results contain expected data and can be used together.
"""
from tradingagents.dataflows.y_finance import get_YFin_data_online
from tradingagents.dataflows.alpha_vantage_fundamentals import get_fundamentals
# --- Step 1: yfinance price data ---
df = _make_yf_ohlcv_df()
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
price_data = get_YFin_data_online("AAPL", "2024-01-04", "2024-01-05")
# --- Step 2: Alpha Vantage fundamentals ---
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_OVERVIEW_JSON)):
fundamentals = get_fundamentals("AAPL")
# --- Assertions ---
assert isinstance(price_data, str)
assert isinstance(fundamentals, str)
# Price data should reference the ticker
assert "AAPL" in price_data
# Fundamentals should contain company info
assert "Apple Inc" in fundamentals
# Both contain data a real application could merge them here
combined_report = price_data + "\n\n" + fundamentals
assert "AAPL" in combined_report
assert "Apple Inc" in combined_report
def test_error_handling_in_combined_workflow(self):
"""
When Alpha Vantage fails with a rate-limit error, the workflow can
continue with yfinance data alone the error is surfaced rather than
silently swallowed.
"""
from tradingagents.dataflows.y_finance import get_YFin_data_online
from tradingagents.dataflows.alpha_vantage_fundamentals import get_fundamentals
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
# yfinance succeeds
df = _make_yf_ohlcv_df()
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
price_data = get_YFin_data_online("AAPL", "2024-01-04", "2024-01-05")
assert isinstance(price_data, str)
assert "AAPL" in price_data
# Alpha Vantage rate-limits
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_av_response(_RATE_LIMIT_JSON)):
with pytest.raises(AlphaVantageRateLimitError):
get_fundamentals("AAPL")
# ---------------------------------------------------------------------------
# Vendor configuration and method routing
# ---------------------------------------------------------------------------
class TestVendorConfiguration:
"""Tests for vendor configuration helpers in the interface module."""
def test_get_category_for_method_core_stock_apis(self):
from tradingagents.dataflows.interface import get_category_for_method
assert get_category_for_method("get_stock_data") == "core_stock_apis"
def test_get_category_for_method_fundamental_data(self):
from tradingagents.dataflows.interface import get_category_for_method
assert get_category_for_method("get_fundamentals") == "fundamental_data"
def test_get_category_for_method_news_data(self):
from tradingagents.dataflows.interface import get_category_for_method
assert get_category_for_method("get_news") == "news_data"
def test_get_category_for_unknown_method_raises_value_error(self):
from tradingagents.dataflows.interface import get_category_for_method
with pytest.raises(ValueError, match="not found"):
get_category_for_method("nonexistent_method")
def test_vendor_methods_contains_both_vendors_for_stock_data(self):
"""Both yfinance and alpha_vantage implementations are registered."""
from tradingagents.dataflows.interface import VENDOR_METHODS
assert "get_stock_data" in VENDOR_METHODS
assert "yfinance" in VENDOR_METHODS["get_stock_data"]
assert "alpha_vantage" in VENDOR_METHODS["get_stock_data"]
def test_vendor_methods_contains_both_vendors_for_news(self):
from tradingagents.dataflows.interface import VENDOR_METHODS
assert "get_news" in VENDOR_METHODS
assert "yfinance" in VENDOR_METHODS["get_news"]
assert "alpha_vantage" in VENDOR_METHODS["get_news"]