diff --git a/tests/test_yahoo_rate_limit.py b/tests/test_yahoo_rate_limit.py index 81a585fc8..ef61c3779 100644 --- a/tests/test_yahoo_rate_limit.py +++ b/tests/test_yahoo_rate_limit.py @@ -240,3 +240,45 @@ def test_yahoo_answering_not_found_is_no_data(yahoo, call): except NoMarketDataError: return assert out.startswith("No insider transactions"), out # an empty filing list is not an error + + +@pytest.mark.unit +@pytest.mark.parametrize("debug, reason, absent", [ + pytest.param(" (1d 2026-09-26 -> 2026-09-27)", None, True, id="a_chart_with_no_prices"), + pytest.param(" (1d 1970-01-01 -> 1970-02-01)", "Data doesn't exist for startDate = 18000, endDate = 2696400", + True, id="yahoo_says_the_data_does_not_exist"), + pytest.param(" (1d 2026-09-21 -> 2026-09-25)(Yahoo status_code = 503)", None, False, id="an_http_error"), + pytest.param(" (1d 2026-09-21 -> 2026-09-25)", "Service temporarily unavailable", False, + id="yahoo_reports_a_service_failure"), +]) +def test_only_yahoo_saying_a_window_has_no_prices_is_no_data(yahoo, debug, reason, absent): + """yfinance raises YFPricesMissingError both when Yahoo answered with no prices + and when its answer was an error; only the first is a fact about the symbol.""" + from yfinance.exceptions import YFPricesMissingError + + from tradingagents.dataflows.errors import NoMarketDataError + from tradingagents.dataflows.vendors.yahoo import market + + def no_prices(self, **kwargs): + raise YFPricesMissingError("AAPL", debug, yahoo_reason=reason) + + yahoo.setattr(yf.Ticker, "history", no_prices) + + with pytest.raises(NoMarketDataError if absent else VendorUnavailableError): + market.get_YFin_data_online("AAPL", "2026-09-21", "2026-09-25") + + +@pytest.mark.unit +def test_a_missing_time_zone_is_not_taken_for_an_answer(yahoo): + """yfinance reports a failed time-zone lookup the same way as a symbol without one.""" + from yfinance.exceptions import YFTzMissingError + + from tradingagents.dataflows.vendors.yahoo import market + + def no_tz(self, **kwargs): + raise YFTzMissingError("AAPL") + + yahoo.setattr(yf.Ticker, "history", no_tz) + + with pytest.raises(VendorUnavailableError): + market.get_YFin_data_online("AAPL", "2026-09-21", "2026-09-25") diff --git a/tradingagents/dataflows/vendors/yahoo/common.py b/tradingagents/dataflows/vendors/yahoo/common.py index 40b0a3bf0..7ab0afec2 100644 --- a/tradingagents/dataflows/vendors/yahoo/common.py +++ b/tradingagents/dataflows/vendors/yahoo/common.py @@ -4,7 +4,7 @@ import logging import time import yfinance as yf -from yfinance.exceptions import YFRateLimitError +from yfinance.exceptions import YFPricesMissingError, YFRateLimitError from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError from tradingagents.dataflows.net import vendor_reachable @@ -30,8 +30,19 @@ def raise_for_empty(symbol: str, canonical: str, what: str) -> None: yf.config.debug.hide_exceptions = False -def _not_found(exc: Exception) -> bool: - """Whether Yahoo answered that it has no such symbol (HTTP 404).""" +def _answered_empty(exc: Exception) -> bool: + """Whether Yahoo answered that it has nothing: no such symbol (HTTP 404), or a + price window with no prices in it. + + yfinance raises YFPricesMissingError for that answer and also for an answer + that was an error (an error status, or Yahoo describing a failure), so only + a chart with no prices, or Yahoo saying the data does not exist, counts. + A missing time zone is not an answer: yfinance reports a failed lookup the same way. + """ + if isinstance(exc, YFPricesMissingError): + reason = exc.yahoo_reason + return "status_code" not in (exc.debug_info or "") and ( + reason is None or reason.startswith("Data doesn't exist")) return getattr(getattr(exc, "response", None), "status_code", None) == 404 @@ -42,7 +53,7 @@ def yf_retry(func, max_retries=3, base_delay=2.0): retry them internally, so this wrapper retries them. A rate limit that outlasts the retries, or any other exception, is raised as VendorUnavailableError: it failed in transit and says nothing about the - symbol. Yahoo answering that it has no such symbol (HTTP 404) returns None, + symbol. Yahoo answering that it has nothing for the symbol returns None, an empty answer. ``func`` should build its own Ticker and make the request itself: a Ticker keeps a failed ``info`` fetch as done, so asking the same one again reads an empty profile. @@ -60,6 +71,6 @@ def yf_retry(func, max_retries=3, base_delay=2.0): f"Yahoo Finance rate limited after {max_retries} retries: {exc}" ) from exc except Exception as exc: - if _not_found(exc): + if _answered_empty(exc): return None raise VendorUnavailableError(f"Yahoo Finance request failed: {type(exc).__name__}") from exc