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:
parent
79f3ce7edd
commit
9389cf1303
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue