fix(yahoo): read Yahoo's answer that a window has no prices as no data

- a chart with no prices, or Yahoo saying the data does not exist, is an empty answer; yfinance's same error for an error status or a service failure, and a missing time zone, stay unavailable
This commit is contained in:
Yijia-Xiao
2026-09-29 05:57:18 +00:00
parent bd9693f8ba
commit 2486470495
2 changed files with 58 additions and 5 deletions
+42
View File
@@ -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")
+16 -5
View File
@@ -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