Add integration tests for yfinance and Alpha Vantage APIs (78 tests, all passing)

Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-15 14:13:02 +00:00
parent 79f3ce7edd
commit 9389cf1303
3 changed files with 1301 additions and 0 deletions

View File

@ -0,0 +1,518 @@
"""Integration tests for the Alpha Vantage data layer.
All HTTP requests are mocked so these tests run offline and without API-key or
rate-limit concerns. The mocks reproduce realistic Alpha Vantage response shapes
so that the code-under-test exercises every significant branch.
"""
import json
import pytest
from unittest.mock import patch, MagicMock
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
CSV_DAILY_ADJUSTED = (
"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"
"2024-01-03,181.00,184.00,180.00,183.00,183.00,48000000,0.0000,1.0\n"
)
RATE_LIMIT_JSON = json.dumps({
"Information": (
"Thank you for using Alpha Vantage! Our standard API rate limit is 25 requests "
"per day. Please subscribe to any of the premium plans at "
"https://www.alphavantage.co/premium/ to instantly remove all daily rate limits."
)
})
INVALID_KEY_JSON = json.dumps({
"Information": "Invalid API key. Please claim your free API key at https://www.alphavantage.co/support/"
})
CSV_SMA = (
"time,SMA\n"
"2024-01-05,182.50\n"
"2024-01-04,181.00\n"
"2024-01-03,179.50\n"
)
CSV_RSI = (
"time,RSI\n"
"2024-01-05,55.30\n"
"2024-01-04,53.10\n"
"2024-01-03,51.90\n"
)
OVERVIEW_JSON = json.dumps({
"Symbol": "AAPL",
"Name": "Apple Inc",
"Sector": "TECHNOLOGY",
"MarketCapitalization": "3000000000000",
"PERatio": "30.5",
"Beta": "1.2",
})
def _mock_response(text: str, status_code: int = 200):
"""Return a mock requests.Response with the given text body."""
resp = MagicMock()
resp.status_code = status_code
resp.text = text
resp.raise_for_status = MagicMock()
return resp
# ---------------------------------------------------------------------------
# AlphaVantageRateLimitError
# ---------------------------------------------------------------------------
class TestAlphaVantageRateLimitError:
"""Tests for the custom AlphaVantageRateLimitError exception class."""
def test_is_exception_subclass(self):
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
assert issubclass(AlphaVantageRateLimitError, Exception)
def test_can_be_raised_and_caught(self):
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
with pytest.raises(AlphaVantageRateLimitError, match="rate limit"):
raise AlphaVantageRateLimitError("rate limit exceeded")
# ---------------------------------------------------------------------------
# _make_api_request
# ---------------------------------------------------------------------------
class TestMakeApiRequest:
"""Tests for the internal _make_api_request helper."""
def test_returns_csv_text_on_success(self):
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(CSV_DAILY_ADJUSTED)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = _make_api_request("TIME_SERIES_DAILY_ADJUSTED",
{"symbol": "AAPL", "datatype": "csv"})
assert "timestamp" in result
assert "186.00" in result
def test_raises_rate_limit_error_on_information_field(self):
from tradingagents.dataflows.alpha_vantage_common import (
_make_api_request,
AlphaVantageRateLimitError,
)
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(RATE_LIMIT_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with pytest.raises(AlphaVantageRateLimitError):
_make_api_request("TIME_SERIES_DAILY_ADJUSTED", {"symbol": "AAPL"})
def test_raises_rate_limit_error_for_invalid_api_key(self):
from tradingagents.dataflows.alpha_vantage_common import (
_make_api_request,
AlphaVantageRateLimitError,
)
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(INVALID_KEY_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "invalid_key"}):
with pytest.raises(AlphaVantageRateLimitError):
_make_api_request("OVERVIEW", {"symbol": "AAPL"})
def test_missing_api_key_raises_value_error(self):
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
import os
env = {k: v for k, v in os.environ.items() if k != "ALPHA_VANTAGE_API_KEY"}
with patch.dict("os.environ", env, clear=True):
with pytest.raises(ValueError, match="ALPHA_VANTAGE_API_KEY"):
_make_api_request("OVERVIEW", {"symbol": "AAPL"})
def test_network_timeout_propagates(self):
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
side_effect=TimeoutError("connection timed out")):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with pytest.raises(TimeoutError):
_make_api_request("OVERVIEW", {"symbol": "AAPL"})
def test_http_error_propagates_via_raise_for_status(self):
"""HTTP 4xx/5xx raises an exception via response.raise_for_status()."""
import requests as _requests
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
bad_resp = _mock_response("", status_code=403)
bad_resp.raise_for_status.side_effect = _requests.HTTPError("403 Forbidden")
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=bad_resp):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with pytest.raises(_requests.HTTPError):
_make_api_request("OVERVIEW", {"symbol": "AAPL"})
# ---------------------------------------------------------------------------
# _filter_csv_by_date_range
# ---------------------------------------------------------------------------
class TestFilterCsvByDateRange:
"""Tests for the _filter_csv_by_date_range helper."""
def test_filters_rows_to_date_range(self):
from tradingagents.dataflows.alpha_vantage_common import _filter_csv_by_date_range
result = _filter_csv_by_date_range(CSV_DAILY_ADJUSTED, "2024-01-04", "2024-01-05")
assert "2024-01-03" not in result
assert "2024-01-04" in result
assert "2024-01-05" in result
def test_empty_input_returns_empty(self):
from tradingagents.dataflows.alpha_vantage_common import _filter_csv_by_date_range
assert _filter_csv_by_date_range("", "2024-01-01", "2024-01-31") == ""
def test_whitespace_only_input_returns_as_is(self):
from tradingagents.dataflows.alpha_vantage_common import _filter_csv_by_date_range
result = _filter_csv_by_date_range(" ", "2024-01-01", "2024-01-31")
assert result.strip() == ""
def test_all_rows_outside_range_returns_header_only(self):
from tradingagents.dataflows.alpha_vantage_common import _filter_csv_by_date_range
result = _filter_csv_by_date_range(CSV_DAILY_ADJUSTED, "2023-01-01", "2023-12-31")
lines = [l for l in result.strip().split("\n") if l]
# Only header row should remain
assert len(lines) == 1
assert "timestamp" in lines[0]
# ---------------------------------------------------------------------------
# format_datetime_for_api
# ---------------------------------------------------------------------------
class TestFormatDatetimeForApi:
"""Tests for format_datetime_for_api."""
def test_yyyy_mm_dd_is_converted(self):
from tradingagents.dataflows.alpha_vantage_common import format_datetime_for_api
result = format_datetime_for_api("2024-01-15")
assert result == "20240115T0000"
def test_already_formatted_string_is_returned_as_is(self):
from tradingagents.dataflows.alpha_vantage_common import format_datetime_for_api
result = format_datetime_for_api("20240115T1430")
assert result == "20240115T1430"
def test_datetime_object_is_converted(self):
from tradingagents.dataflows.alpha_vantage_common import format_datetime_for_api
from datetime import datetime
dt = datetime(2024, 1, 15, 14, 30)
result = format_datetime_for_api(dt)
assert result == "20240115T1430"
def test_unsupported_string_format_raises_value_error(self):
from tradingagents.dataflows.alpha_vantage_common import format_datetime_for_api
with pytest.raises(ValueError):
format_datetime_for_api("15-01-2024")
def test_unsupported_type_raises_value_error(self):
from tradingagents.dataflows.alpha_vantage_common import format_datetime_for_api
with pytest.raises(ValueError):
format_datetime_for_api(20240115)
# ---------------------------------------------------------------------------
# get_stock (alpha_vantage_stock)
# ---------------------------------------------------------------------------
class TestAlphaVantageGetStock:
"""Tests for the Alpha Vantage get_stock function."""
def test_returns_csv_for_recent_date_range(self):
"""Recent dates → compact outputsize; CSV data is filtered to range."""
from tradingagents.dataflows.alpha_vantage_stock import get_stock
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(CSV_DAILY_ADJUSTED)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_stock("AAPL", "2024-01-01", "2024-01-05")
assert isinstance(result, str)
def test_uses_full_outputsize_for_old_start_date(self):
"""Old start date (>100 days ago) → outputsize=full is selected."""
from tradingagents.dataflows.alpha_vantage_stock import get_stock
captured_params = {}
def capture_request(url, params):
captured_params.update(params)
return _mock_response(CSV_DAILY_ADJUSTED)
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
side_effect=capture_request):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
get_stock("AAPL", "2020-01-01", "2020-01-05")
assert captured_params.get("outputsize") == "full"
def test_rate_limit_error_propagates(self):
from tradingagents.dataflows.alpha_vantage_stock import get_stock
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(RATE_LIMIT_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with pytest.raises(AlphaVantageRateLimitError):
get_stock("AAPL", "2024-01-01", "2024-01-05")
# ---------------------------------------------------------------------------
# get_fundamentals / get_balance_sheet / get_cashflow / get_income_statement
# (alpha_vantage_fundamentals)
# ---------------------------------------------------------------------------
class TestAlphaVantageGetFundamentals:
"""Tests for Alpha Vantage get_fundamentals."""
def test_returns_json_string_on_success(self):
from tradingagents.dataflows.alpha_vantage_fundamentals import get_fundamentals
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(OVERVIEW_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_fundamentals("AAPL")
assert "Apple Inc" in result
assert "TECHNOLOGY" in result
def test_rate_limit_error_propagates(self):
from tradingagents.dataflows.alpha_vantage_fundamentals import get_fundamentals
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(RATE_LIMIT_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with pytest.raises(AlphaVantageRateLimitError):
get_fundamentals("AAPL")
class TestAlphaVantageGetBalanceSheet:
def test_returns_response_text_on_success(self):
from tradingagents.dataflows.alpha_vantage_fundamentals import get_balance_sheet
payload = json.dumps({"symbol": "AAPL", "annualReports": [], "quarterlyReports": []})
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(payload)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_balance_sheet("AAPL")
assert "AAPL" in result
class TestAlphaVantageGetCashflow:
def test_returns_response_text_on_success(self):
from tradingagents.dataflows.alpha_vantage_fundamentals import get_cashflow
payload = json.dumps({"symbol": "AAPL", "annualReports": [], "quarterlyReports": []})
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(payload)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_cashflow("AAPL")
assert "AAPL" in result
class TestAlphaVantageGetIncomeStatement:
def test_returns_response_text_on_success(self):
from tradingagents.dataflows.alpha_vantage_fundamentals import get_income_statement
payload = json.dumps({"symbol": "AAPL", "annualReports": [], "quarterlyReports": []})
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(payload)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_income_statement("AAPL")
assert "AAPL" in result
# ---------------------------------------------------------------------------
# get_news / get_global_news / get_insider_transactions (alpha_vantage_news)
# ---------------------------------------------------------------------------
NEWS_JSON = json.dumps({
"feed": [
{
"title": "Apple Hits Record High",
"url": "https://example.com/news/1",
"time_published": "20240105T150000",
"authors": ["John Doe"],
"summary": "Apple stock reached a new record.",
"overall_sentiment_label": "Bullish",
}
]
})
INSIDER_JSON = json.dumps({
"data": [
{
"executive": "Tim Cook",
"transactionDate": "2024-01-15",
"transactionType": "Sale",
"sharesTraded": "10000",
"sharePrice": "150.00",
}
]
})
class TestAlphaVantageGetNews:
def test_returns_news_response_on_success(self):
from tradingagents.dataflows.alpha_vantage_news import get_news
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(NEWS_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_news("AAPL", "2024-01-01", "2024-01-05")
assert "Apple Hits Record High" in result
def test_rate_limit_error_propagates(self):
from tradingagents.dataflows.alpha_vantage_news import get_news
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(RATE_LIMIT_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with pytest.raises(AlphaVantageRateLimitError):
get_news("AAPL", "2024-01-01", "2024-01-05")
class TestAlphaVantageGetGlobalNews:
def test_returns_global_news_response_on_success(self):
from tradingagents.dataflows.alpha_vantage_news import get_global_news
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(NEWS_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_global_news("2024-01-15", look_back_days=7)
assert isinstance(result, str)
def test_look_back_days_affects_time_from_param(self):
"""The time_from parameter should reflect the look_back_days offset."""
from tradingagents.dataflows.alpha_vantage_news import get_global_news
captured_params = {}
def capture(url, params):
captured_params.update(params)
return _mock_response(NEWS_JSON)
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
side_effect=capture):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
get_global_news("2024-01-15", look_back_days=7)
# time_from should be 7 days before 2024-01-15 → 2024-01-08
assert "20240108T0000" in captured_params.get("time_from", "")
class TestAlphaVantageGetInsiderTransactions:
def test_returns_insider_data_on_success(self):
from tradingagents.dataflows.alpha_vantage_news import get_insider_transactions
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(INSIDER_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_insider_transactions("AAPL")
assert "Tim Cook" in result
def test_rate_limit_error_propagates(self):
from tradingagents.dataflows.alpha_vantage_news import get_insider_transactions
from tradingagents.dataflows.alpha_vantage_common import AlphaVantageRateLimitError
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(RATE_LIMIT_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with pytest.raises(AlphaVantageRateLimitError):
get_insider_transactions("AAPL")
# ---------------------------------------------------------------------------
# get_indicator (alpha_vantage_indicator)
# ---------------------------------------------------------------------------
class TestAlphaVantageGetIndicator:
"""Tests for the Alpha Vantage get_indicator function."""
def test_rsi_returns_formatted_string_on_success(self):
from tradingagents.dataflows.alpha_vantage_indicator import get_indicator
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(CSV_RSI)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_indicator(
"AAPL", "rsi", "2024-01-05", look_back_days=5
)
assert isinstance(result, str)
assert "RSI" in result.upper()
def test_sma_50_returns_formatted_string_on_success(self):
from tradingagents.dataflows.alpha_vantage_indicator import get_indicator
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(CSV_SMA)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_indicator(
"AAPL", "close_50_sma", "2024-01-05", look_back_days=5
)
assert isinstance(result, str)
assert "SMA" in result.upper()
def test_unsupported_indicator_raises_value_error(self):
from tradingagents.dataflows.alpha_vantage_indicator import get_indicator
with pytest.raises(ValueError, match="not supported"):
get_indicator("AAPL", "unsupported_indicator", "2024-01-05", look_back_days=5)
def test_rate_limit_error_surfaces_as_error_string(self):
"""Rate limit errors during indicator fetch result in an error string (not a raise)."""
from tradingagents.dataflows.alpha_vantage_indicator import get_indicator
with patch("tradingagents.dataflows.alpha_vantage_common.requests.get",
return_value=_mock_response(RATE_LIMIT_JSON)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_indicator("AAPL", "rsi", "2024-01-05", look_back_days=5)
assert "Error" in result or "rate limit" in result.lower()
def test_vwma_returns_informational_message(self):
"""VWMA is not directly available; a descriptive message is returned."""
from tradingagents.dataflows.alpha_vantage_indicator import get_indicator
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
result = get_indicator("AAPL", "vwma", "2024-01-05", look_back_days=5)
assert "VWMA" in result
assert "not directly available" in result.lower() or "Volume Weighted" in result

View File

@ -0,0 +1,378 @@
"""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.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)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
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)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
# 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.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
with patch(
"tradingagents.dataflows.y_finance.yf.Ticker",
side_effect=ConnectionError("network unavailable"),
):
with pytest.raises(RuntimeError, match="No available vendor"):
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)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
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)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
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)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
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)):
with patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
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 patch.dict("os.environ", {"ALPHA_VANTAGE_API_KEY": "demo"}):
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"]

View File

@ -0,0 +1,405 @@
"""Integration tests for the yfinance data layer.
All external network calls are mocked so these tests run offline and without
rate-limit concerns. The mocks reproduce realistic yfinance return shapes so
that the code-under-test (y_finance.py) exercises every branch that matters.
"""
import pytest
import pandas as pd
from unittest.mock import patch, MagicMock, PropertyMock
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_ohlcv_df(start="2024-01-02", periods=5):
"""Return a minimal OHLCV DataFrame with a timezone-aware DatetimeIndex."""
idx = pd.date_range(start, periods=periods, freq="B", tz="America/New_York")
return pd.DataFrame(
{
"Open": [150.0, 151.0, 152.0, 153.0, 154.0][:periods],
"High": [155.0, 156.0, 157.0, 158.0, 159.0][:periods],
"Low": [148.0, 149.0, 150.0, 151.0, 152.0][:periods],
"Close": [152.0, 153.0, 154.0, 155.0, 156.0][:periods],
"Volume": [1_000_000] * periods,
},
index=idx,
)
# ---------------------------------------------------------------------------
# get_YFin_data_online
# ---------------------------------------------------------------------------
class TestGetYFinDataOnline:
"""Tests for get_YFin_data_online."""
def test_returns_csv_string_on_success(self):
"""Valid symbol and date range returns a CSV-formatted string with header."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
df = _make_ohlcv_df()
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_YFin_data_online("AAPL", "2024-01-02", "2024-01-08")
assert isinstance(result, str)
assert "# Stock data for AAPL" in result
assert "# Total records:" in result
assert "Close" in result # CSV column header
def test_symbol_is_uppercased(self):
"""Symbol is normalised to upper-case regardless of how it is supplied."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
df = _make_ohlcv_df()
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker) as mock_cls:
get_YFin_data_online("aapl", "2024-01-02", "2024-01-08")
mock_cls.assert_called_once_with("AAPL")
def test_empty_dataframe_returns_no_data_message(self):
"""When yfinance returns an empty DataFrame a clear message is returned."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
mock_ticker = MagicMock()
mock_ticker.history.return_value = pd.DataFrame()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_YFin_data_online("INVALID", "2024-01-02", "2024-01-08")
assert "No data found" in result
assert "INVALID" in result
def test_invalid_date_format_raises_value_error(self):
"""Malformed date strings raise ValueError before any network call is made."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
with pytest.raises(ValueError):
get_YFin_data_online("AAPL", "2024/01/02", "2024-01-08")
def test_timezone_stripped_from_index(self):
"""Timezone info is removed from the index for cleaner output."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
df = _make_ohlcv_df()
assert df.index.tz is not None # pre-condition
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_YFin_data_online("AAPL", "2024-01-02", "2024-01-08")
# Timezone strings like "+00:00" or "UTC" should not appear in the CSV portion
csv_lines = result.split("\n")
data_lines = [l for l in csv_lines if l and not l.startswith("#")]
for line in data_lines:
assert "+00:00" not in line
assert "UTC" not in line
def test_numeric_columns_are_rounded(self):
"""OHLC values in the returned CSV are rounded to 2 decimal places."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
idx = pd.date_range("2024-01-02", periods=1, freq="B", tz="UTC")
df = pd.DataFrame(
{"Open": [150.123456], "High": [155.987654], "Low": [148.0], "Close": [152.999999], "Volume": [1_000_000]},
index=idx,
)
mock_ticker = MagicMock()
mock_ticker.history.return_value = df
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_YFin_data_online("AAPL", "2024-01-02", "2024-01-02")
assert "150.12" in result
assert "155.99" in result
def test_network_timeout_propagates(self):
"""A TimeoutError from yfinance propagates to the caller."""
from tradingagents.dataflows.y_finance import get_YFin_data_online
mock_ticker = MagicMock()
mock_ticker.history.side_effect = TimeoutError("request timed out")
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
with pytest.raises(TimeoutError):
get_YFin_data_online("AAPL", "2024-01-02", "2024-01-08")
# ---------------------------------------------------------------------------
# get_fundamentals
# ---------------------------------------------------------------------------
class TestGetFundamentals:
"""Tests for the yfinance get_fundamentals function."""
def test_returns_fundamentals_string_on_success(self):
"""When info is populated, fundamentals are returned as a formatted string."""
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,
"fiftyTwoWeekHigh": 200.0,
"fiftyTwoWeekLow": 150.0,
}
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 "# Company Fundamentals for AAPL" in result
assert "Apple Inc." in result
assert "Technology" in result
def test_empty_info_returns_no_data_message(self):
"""Empty info dict returns a clear 'no data' message."""
from tradingagents.dataflows.y_finance import get_fundamentals
mock_ticker = MagicMock()
type(mock_ticker).info = PropertyMock(return_value={})
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_fundamentals("AAPL")
assert "No fundamentals data" in result
def test_exception_returns_error_string(self):
"""An exception from yfinance yields a safe error string (not a raise)."""
from tradingagents.dataflows.y_finance import get_fundamentals
mock_ticker = MagicMock()
type(mock_ticker).info = PropertyMock(side_effect=ConnectionError("network error"))
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_fundamentals("AAPL")
assert "Error" in result
assert "AAPL" in result
# ---------------------------------------------------------------------------
# get_balance_sheet
# ---------------------------------------------------------------------------
class TestGetBalanceSheet:
"""Tests for yfinance get_balance_sheet."""
def _mock_balance_df(self):
return pd.DataFrame(
{"2023-12-31": [1_000_000], "2022-12-31": [900_000]},
index=["Total Assets"],
)
def test_quarterly_balance_sheet_success(self):
from tradingagents.dataflows.y_finance import get_balance_sheet
mock_ticker = MagicMock()
mock_ticker.quarterly_balance_sheet = self._mock_balance_df()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_balance_sheet("AAPL", freq="quarterly")
assert "# Balance Sheet data for AAPL (quarterly)" in result
assert "Total Assets" in result
def test_annual_balance_sheet_success(self):
from tradingagents.dataflows.y_finance import get_balance_sheet
mock_ticker = MagicMock()
mock_ticker.balance_sheet = self._mock_balance_df()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_balance_sheet("AAPL", freq="annual")
assert "# Balance Sheet data for AAPL (annual)" in result
def test_empty_dataframe_returns_no_data_message(self):
from tradingagents.dataflows.y_finance import get_balance_sheet
mock_ticker = MagicMock()
mock_ticker.quarterly_balance_sheet = pd.DataFrame()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_balance_sheet("AAPL")
assert "No balance sheet data" in result
def test_exception_returns_error_string(self):
from tradingagents.dataflows.y_finance import get_balance_sheet
mock_ticker = MagicMock()
type(mock_ticker).quarterly_balance_sheet = PropertyMock(
side_effect=ConnectionError("network error")
)
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_balance_sheet("AAPL")
assert "Error" in result
# ---------------------------------------------------------------------------
# get_cashflow
# ---------------------------------------------------------------------------
class TestGetCashflow:
"""Tests for yfinance get_cashflow."""
def _mock_cashflow_df(self):
return pd.DataFrame(
{"2023-12-31": [500_000]},
index=["Free Cash Flow"],
)
def test_quarterly_cashflow_success(self):
from tradingagents.dataflows.y_finance import get_cashflow
mock_ticker = MagicMock()
mock_ticker.quarterly_cashflow = self._mock_cashflow_df()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_cashflow("AAPL", freq="quarterly")
assert "# Cash Flow data for AAPL (quarterly)" in result
assert "Free Cash Flow" in result
def test_empty_dataframe_returns_no_data_message(self):
from tradingagents.dataflows.y_finance import get_cashflow
mock_ticker = MagicMock()
mock_ticker.quarterly_cashflow = pd.DataFrame()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_cashflow("AAPL")
assert "No cash flow data" in result
def test_exception_returns_error_string(self):
from tradingagents.dataflows.y_finance import get_cashflow
mock_ticker = MagicMock()
type(mock_ticker).quarterly_cashflow = PropertyMock(
side_effect=ConnectionError("network error")
)
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_cashflow("AAPL")
assert "Error" in result
# ---------------------------------------------------------------------------
# get_income_statement
# ---------------------------------------------------------------------------
class TestGetIncomeStatement:
"""Tests for yfinance get_income_statement."""
def _mock_income_df(self):
return pd.DataFrame(
{"2023-12-31": [400_000]},
index=["Total Revenue"],
)
def test_quarterly_income_statement_success(self):
from tradingagents.dataflows.y_finance import get_income_statement
mock_ticker = MagicMock()
mock_ticker.quarterly_income_stmt = self._mock_income_df()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_income_statement("AAPL", freq="quarterly")
assert "# Income Statement data for AAPL (quarterly)" in result
assert "Total Revenue" in result
def test_empty_dataframe_returns_no_data_message(self):
from tradingagents.dataflows.y_finance import get_income_statement
mock_ticker = MagicMock()
mock_ticker.quarterly_income_stmt = pd.DataFrame()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_income_statement("AAPL")
assert "No income statement data" in result
# ---------------------------------------------------------------------------
# get_insider_transactions
# ---------------------------------------------------------------------------
class TestGetInsiderTransactions:
"""Tests for yfinance get_insider_transactions."""
def _mock_insider_df(self):
return pd.DataFrame(
{
"Date": ["2024-01-15"],
"Insider": ["Tim Cook"],
"Transaction": ["Sale"],
"Shares": [10000],
"Value": [1_500_000],
}
)
def test_returns_csv_string_with_header(self):
from tradingagents.dataflows.y_finance import get_insider_transactions
mock_ticker = MagicMock()
mock_ticker.insider_transactions = self._mock_insider_df()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_insider_transactions("AAPL")
assert "# Insider Transactions data for AAPL" in result
assert "Tim Cook" in result
def test_none_data_returns_no_data_message(self):
from tradingagents.dataflows.y_finance import get_insider_transactions
mock_ticker = MagicMock()
mock_ticker.insider_transactions = None
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_insider_transactions("AAPL")
assert "No insider transactions data" in result
def test_empty_dataframe_returns_no_data_message(self):
from tradingagents.dataflows.y_finance import get_insider_transactions
mock_ticker = MagicMock()
mock_ticker.insider_transactions = pd.DataFrame()
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_insider_transactions("AAPL")
assert "No insider transactions data" in result
def test_exception_returns_error_string(self):
from tradingagents.dataflows.y_finance import get_insider_transactions
mock_ticker = MagicMock()
type(mock_ticker).insider_transactions = PropertyMock(
side_effect=ConnectionError("network error")
)
with patch("tradingagents.dataflows.y_finance.yf.Ticker", return_value=mock_ticker):
result = get_insider_transactions("AAPL")
assert "Error" in result