diff --git a/tests/test_alpha_vantage_integration.py b/tests/test_alpha_vantage_integration.py new file mode 100644 index 00000000..327376a9 --- /dev/null +++ b/tests/test_alpha_vantage_integration.py @@ -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 diff --git a/tests/test_e2e_api_integration.py b/tests/test_e2e_api_integration.py new file mode 100644 index 00000000..51fef3c9 --- /dev/null +++ b/tests/test_e2e_api_integration.py @@ -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"] diff --git a/tests/test_yfinance_integration.py b/tests/test_yfinance_integration.py new file mode 100644 index 00000000..78bc88f3 --- /dev/null +++ b/tests/test_yfinance_integration.py @@ -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