mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-03 16:12:44 +02:00
fix(yahoo): report the failed requests yfinance would answer as empty
- yfinance raises its failures instead of answering them with an empty result, so a statement request that timed out is unavailable, not "no data" - Yahoo answering that it has no such symbol (HTTP 404) stays an answer: no data
This commit is contained in:
@@ -199,3 +199,44 @@ def test_a_snapshot_with_no_rows_is_reported_not_raised(yahoo, frame):
|
||||
out = get_verified_market_snapshot.func("ZZZZ", DAY, trade_date=DAY)
|
||||
|
||||
assert out.startswith("NO_DATA_AVAILABLE"), out
|
||||
|
||||
|
||||
def _yahoo_request_raises(yahoo, exc):
|
||||
"""Every request yfinance makes raises ``exc``, below its own error handling."""
|
||||
from yfinance.data import YfData
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
raise exc
|
||||
|
||||
yahoo.setattr(YfData, "_make_request", request)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("call", _yahoo_calls())
|
||||
def test_a_request_that_yfinance_would_swallow_is_unavailable(yahoo, call):
|
||||
"""yfinance answers some failed requests with an empty result; an empty
|
||||
statement from a Yahoo whose host answers a probe read as "no data"."""
|
||||
_yahoo_request_raises(yahoo, TimeoutError("read timed out"))
|
||||
|
||||
with pytest.raises(VendorUnavailableError):
|
||||
call()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("call", [c for c in _yahoo_calls() if c.id in ("fundamentals", "stock_data", "insider")])
|
||||
def test_yahoo_answering_not_found_is_no_data(yahoo, call):
|
||||
"""Yahoo answers an unknown symbol with HTTP 404: an answer about the symbol, not an outage."""
|
||||
from curl_cffi.requests import Response
|
||||
from curl_cffi.requests.exceptions import HTTPError
|
||||
|
||||
from tradingagents.dataflows.errors import NoMarketDataError
|
||||
|
||||
not_found = Response()
|
||||
not_found.status_code = 404
|
||||
_yahoo_request_raises(yahoo, HTTPError("HTTP Error 404: ", response=not_found))
|
||||
|
||||
try:
|
||||
out = call()
|
||||
except NoMarketDataError:
|
||||
return
|
||||
assert out.startswith("No insider transactions"), out # an empty filing list is not an error
|
||||
|
||||
+2
-1
@@ -100,7 +100,8 @@ def _statement(ticker, freq, as_of_date, title, quarterly_attr, annual_attr) ->
|
||||
canonical = normalize_symbol(ticker)
|
||||
what = title.lower()
|
||||
attr = quarterly_attr if freq.lower() == "quarterly" else annual_attr
|
||||
data = filter_financials_by_date(yf_retry(lambda: getattr(yf.Ticker(canonical), attr)), as_of_date)
|
||||
data = yf_retry(lambda: getattr(yf.Ticker(canonical), attr))
|
||||
data = pd.DataFrame() if data is None else filter_financials_by_date(data, as_of_date)
|
||||
if data.empty:
|
||||
raise_for_empty(ticker, canonical, f"{what} data")
|
||||
return f"# {title} data for {canonical} ({freq})\n" + _PERIOD_END_VINTAGE + data.to_csv()
|
||||
|
||||
+3
-4
@@ -30,18 +30,17 @@ def get_YFin_data_online(
|
||||
|
||||
# Resolve broker/forex symbols to Yahoo's convention (XAUUSD+ -> GC=F).
|
||||
canonical = normalize_symbol(symbol)
|
||||
ticker = yf.Ticker(canonical)
|
||||
|
||||
# yfinance treats ``end`` as EXCLUSIVE, so it would drop the requested
|
||||
# end_date row (and the current day when end_date is today). Request one day
|
||||
# past end_date so the requested range is actually inclusive (#986/#987).
|
||||
end_inclusive = (end_dt + relativedelta(days=1)).strftime("%Y-%m-%d")
|
||||
data = yf_retry(lambda: ticker.history(start=start_date, end=end_inclusive))
|
||||
data = yf_retry(lambda: yf.Ticker(canonical).history(start=start_date, end=end_inclusive))
|
||||
|
||||
# Empty result means the symbol is unknown/delisted. Raise a typed error
|
||||
# instead of returning prose: the routing layer turns it into a single
|
||||
# unambiguous "no data" signal so the agent never fabricates a price.
|
||||
if data.empty:
|
||||
if data is None or data.empty:
|
||||
raise_for_empty(symbol, canonical, f"rows between {start_date} and {end_date}")
|
||||
|
||||
# Remove timezone info from index for cleaner output
|
||||
@@ -274,7 +273,7 @@ def get_closes(symbol: str, start_date: str, end_date: str) -> pd.Series:
|
||||
"""Daily closes from ``start_date`` up to, not including, ``end_date``."""
|
||||
canonical = normalize_symbol(symbol)
|
||||
history = yf_retry(lambda: yf.Ticker(canonical).history(start=start_date, end=end_date))
|
||||
return history["Close"] if "Close" in history else pd.Series(dtype=float)
|
||||
return history["Close"] if history is not None and "Close" in history else pd.Series(dtype=float)
|
||||
|
||||
|
||||
def get_stock_stats(
|
||||
|
||||
+17
-3
@@ -39,15 +39,25 @@ def raise_for_empty(symbol: str, canonical: str, what: str) -> None:
|
||||
raise NoMarketDataError(symbol, canonical, f"no {what}")
|
||||
|
||||
|
||||
# yfinance answers some failed requests with an empty result, which would read as
|
||||
# "no data" for a symbol nobody checked; raised, the failure is reported as one.
|
||||
yf.config.debug.hide_exceptions = False
|
||||
|
||||
|
||||
def _not_found(exc: Exception) -> bool:
|
||||
"""Whether Yahoo answered that it has no such symbol (HTTP 404)."""
|
||||
return getattr(getattr(exc, "response", None), "status_code", None) == 404
|
||||
|
||||
|
||||
def yf_retry(func, max_retries=3, base_delay=2.0):
|
||||
"""Execute a yfinance call with exponential backoff on rate limits.
|
||||
|
||||
yfinance raises YFRateLimitError on HTTP 429 responses but does not
|
||||
retry them internally, so this wrapper retries them. A rate limit that
|
||||
outlasts the retries, or any other exception, is raised as
|
||||
VendorUnavailableError: Yahoo answers an unknown symbol with an empty
|
||||
result, so a request that raised failed in transit and says nothing about
|
||||
the symbol. ``func`` should build its own Ticker and make the request
|
||||
VendorUnavailableError: it failed in transit and says nothing about the
|
||||
symbol. Yahoo answering that it has no such symbol (HTTP 404) 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.
|
||||
"""
|
||||
@@ -64,6 +74,8 @@ 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):
|
||||
return None
|
||||
raise VendorUnavailableError(f"Yahoo Finance request failed: {type(exc).__name__}") from exc
|
||||
|
||||
|
||||
@@ -258,6 +270,8 @@ def load_ohlcv(symbol: str, as_of_date: str, fill_gaps: bool = True) -> pd.DataF
|
||||
auto_adjust=True,
|
||||
actions=False,
|
||||
))
|
||||
if downloaded is None:
|
||||
raise_for_empty(symbol, canonical, "price rows")
|
||||
downloaded = _ensure_date_column(downloaded.reset_index())
|
||||
# Only cache real data — never persist an empty frame.
|
||||
if downloaded.empty or "Close" not in downloaded.columns:
|
||||
|
||||
Reference in New Issue
Block a user