mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 15:02:39 +03:00
fix(dataflows): report a Yahoo rate limit as a rate limit (#1387)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user