fix(dataflows): report a Yahoo rate limit as a rate limit (#1387)

This commit is contained in:
Chaoqi
2026-09-24 11:45:39 -07:00
committed by GitHub
parent ecd3404213
commit 6ac7c6f017
10 changed files with 193 additions and 31 deletions
+7 -2
View File
@@ -10,7 +10,8 @@ from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.date_window import as_of, as_of_window
from tradingagents.dataflows.router import route_to_vendor
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.router import route_to_vendor, vendor_unavailable
from tradingagents.dataflows.vendors.yahoo.snapshot import build_verified_market_snapshot
@@ -83,7 +84,11 @@ def get_verified_market_snapshot(
price levels, Bollinger bands, RSI, MACD, moving averages, support /
resistance, or historical comparisons, and treat it as the source of truth.
"""
return build_verified_market_snapshot(symbol, as_of(curr_date, trade_date), look_back_days)
# An exception out of a tool would end the run.
try:
return build_verified_market_snapshot(symbol, as_of(curr_date, trade_date), look_back_days)
except VendorRateLimitError as exc:
return vendor_unavailable("get_verified_market_snapshot", exc)
@tool
+10 -5
View File
@@ -181,6 +181,15 @@ def get_vendor(category: str, method: str = None) -> str:
return config.get("data_vendors", {}).get(category, "default")
def vendor_unavailable(method: str, error: VendorRateLimitError) -> str:
"""What a call returns when every vendor was throttled or unreachable."""
return (
f"DATA_UNAVAILABLE: no configured vendor could serve {method} right now "
f"({error}). This says nothing about the instrument; report the "
f"data as unavailable and do not estimate or fabricate values."
)
def route_to_vendor(method: str, *args, **kwargs):
"""Route method calls to appropriate vendor implementation with fallback support."""
category = get_category_for_method(method)
@@ -273,11 +282,7 @@ def route_to_vendor(method: str, *args, **kwargs):
# Every vendor was throttled or unreachable: that is a fact about the
# vendors, not about the instrument, and it must not end the run.
if last_unavailable is not None:
return (
f"DATA_UNAVAILABLE: no configured vendor could serve {method} right now "
f"({last_unavailable}). This says nothing about the instrument; report the "
f"data as unavailable and do not estimate or fabricate values."
)
return vendor_unavailable(method, last_unavailable)
if first_error is not None:
if category in OPTIONAL_CATEGORIES:
+1 -2
View File
@@ -33,8 +33,7 @@ def get_fundamentals(
return withheld
try:
ticker_obj = yf.Ticker(canonical)
info = yf_retry(lambda: ticker_obj.info)
info = yf_retry(lambda: yf.Ticker(canonical).info)
if not info:
raise_for_empty(ticker, canonical, "fundamentals")
+5 -1
View File
@@ -8,7 +8,7 @@ from dateutil.relativedelta import relativedelta
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.date_window import coverage_gap, in_window
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError, VendorError
from tradingagents.dataflows.symbols import normalize_symbol
from tradingagents.dataflows.vendors.yahoo.ohlcv import yf_retry
@@ -114,6 +114,8 @@ def get_news_yfinance(
return f"## {ticker}{resolved} News, from {start_date} to {end_date}:\n\n{news_str}"
except VendorError:
raise
except Exception as e:
raise NoMarketDataError(ticker, ticker, f"news unavailable: {e}") from e
@@ -192,5 +194,7 @@ def get_global_news_yfinance(
return f"## Global Market News, from {start_date} to {curr_date}:\n\n{news_str}"
except VendorError:
raise
except Exception as e:
raise NoMarketDataError("global news", "global news", f"unavailable: {e}") from e
+25 -11
View File
@@ -43,18 +43,24 @@ def yf_retry(func, max_retries=3, base_delay=2.0):
yfinance raises YFRateLimitError on HTTP 429 responses but does not
retry them internally. This wrapper adds retry logic specifically
for rate limits. Other exceptions propagate immediately.
for rate limits. Other exceptions propagate immediately. A rate limit
that outlasts the retries is raised as VendorRateLimitError, so the
router reports a throttled vendor rather than a symbol with no data.
``func`` should build its own Ticker: a Ticker keeps a failed ``info``
fetch as done, so asking the same one again reads an empty profile.
"""
for attempt in range(max_retries + 1):
try:
return func()
except YFRateLimitError:
except YFRateLimitError as exc:
if attempt < max_retries:
delay = base_delay * (2 ** attempt)
logger.warning(f"Yahoo Finance rate limited, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(delay)
else:
raise
raise VendorRateLimitError(
f"Yahoo Finance rate limited after {max_retries} retries: {exc}"
) from exc
def _ensure_date_column(data: pd.DataFrame) -> pd.DataFrame:
@@ -240,14 +246,22 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
data = cached
if data is None:
downloaded = yf_retry(lambda: yf.download(
canonical,
start=start_str,
end=end_str,
multi_level_index=False,
progress=False,
auto_adjust=True,
))
# yf.download catches every error, a rate limit included, and returns
# an empty frame. Ticker.history raises the rate limit, so it is retried.
try:
downloaded = yf_retry(lambda: yf.Ticker(canonical).history(
start=start_str,
end=end_str,
auto_adjust=True,
actions=False,
))
except VendorRateLimitError:
raise
except Exception as exc:
# Any other failure is an outage or an unknown symbol, which
# raise_for_empty tells apart by whether Yahoo answers at all.
logger.warning("Yahoo Finance price request for %s failed: %s", canonical, exc)
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: