diff --git a/tests/test_no_data_handling.py b/tests/test_no_data_handling.py index 355bc76bf..20e1adb7b 100644 --- a/tests/test_no_data_handling.py +++ b/tests/test_no_data_handling.py @@ -95,11 +95,11 @@ if __name__ == "__main__": @pytest.mark.unit def test_an_unreachable_yahoo_is_not_reported_as_a_symbol_without_insider_data(): - from tradingagents.dataflows.errors import VendorRateLimitError + from tradingagents.dataflows.errors import VendorUnavailableError from tradingagents.dataflows.vendors.yahoo import fundamentals ticker = type("T", (), {"insider_transactions": pd.DataFrame()})() with mock.patch.object(fundamentals.yf, "Ticker", return_value=ticker), \ mock.patch.object(fundamentals, "vendor_reachable", return_value=False), \ - pytest.raises(VendorRateLimitError): + pytest.raises(VendorUnavailableError): fundamentals.get_insider_transactions("AAPL", as_of_date="2026-09-21") diff --git a/tests/test_sec_edgar.py b/tests/test_sec_edgar.py index d1729ce51..848fa5be0 100644 --- a/tests/test_sec_edgar.py +++ b/tests/test_sec_edgar.py @@ -144,13 +144,13 @@ def test_a_throttle_lets_the_next_vendor_try(monkeypatch): """SEC throttles by refusing the request; the router then tries yfinance.""" import requests - from tradingagents.dataflows.errors import VendorRateLimitError + from tradingagents.dataflows.errors import VendorUnavailableError def _throttled(*a, **k): raise requests.HTTPError(response=mock.Mock(status_code=429)) monkeypatch.setattr(sec_edgar.requests, "get", _throttled) - with pytest.raises(VendorRateLimitError): + with pytest.raises(VendorUnavailableError): _REAL_FETCH("https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json") diff --git a/tests/test_undated_tools_as_of.py b/tests/test_undated_tools_as_of.py index ea3450083..6af796ae6 100644 --- a/tests/test_undated_tools_as_of.py +++ b/tests/test_undated_tools_as_of.py @@ -196,14 +196,14 @@ def test_an_unreachable_vendor_is_not_reported_as_a_missing_symbol(monkeypatch): company has no balance sheet, when the truth is we could not ask.""" import pandas as pd - from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError + from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError from tradingagents.dataflows.vendors.yahoo import ohlcv empty = mock.Mock(quarterly_balance_sheet=pd.DataFrame(), balance_sheet=pd.DataFrame()) monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: empty) monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: False) - with pytest.raises(VendorRateLimitError, match="unreachable"): + with pytest.raises(VendorUnavailableError, match="unreachable"): yahoo_fundamentals.get_balance_sheet("AAPL", "annual", "2026-09-01") monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True) @@ -216,10 +216,10 @@ def test_every_vendor_unavailable_says_so_rather_than_crashing(monkeypatch): """A throttled or unreachable chain used to raise RuntimeError('No available vendor'), which ends the run, and never said the vendor was the problem.""" from tradingagents.dataflows import router - from tradingagents.dataflows.errors import VendorRateLimitError + from tradingagents.dataflows.errors import VendorUnavailableError def _down(*a, **k): - raise VendorRateLimitError("Yahoo Finance is unreachable") + raise VendorUnavailableError("Yahoo Finance is unreachable") monkeypatch.setitem(router.VENDOR_METHODS["get_balance_sheet"], "yfinance", _down) @@ -235,13 +235,13 @@ def test_the_price_path_also_tells_an_outage_from_an_unknown_symbol(monkeypatch) delisted symbol either.""" import pandas as pd - from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError + from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError from tradingagents.dataflows.vendors.yahoo import ohlcv monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: mock.Mock(history=lambda **k: pd.DataFrame())) monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: False) - with pytest.raises(VendorRateLimitError, match="unreachable"): + with pytest.raises(VendorUnavailableError, match="unreachable"): yahoo_market.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10") monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True) diff --git a/tests/test_vendor_errors.py b/tests/test_vendor_errors.py index 51e2f15fd..0f65da1f3 100644 --- a/tests/test_vendor_errors.py +++ b/tests/test_vendor_errors.py @@ -16,7 +16,7 @@ from tradingagents.dataflows.errors import ( NoMarketDataError, VendorError, VendorNotConfiguredError, - VendorRateLimitError, + VendorUnavailableError, ) from tradingagents.dataflows.vendors.alpha_vantage.common import ( AlphaVantageNotConfiguredError, @@ -28,7 +28,7 @@ from tradingagents.dataflows.vendors.fred import FredNotConfiguredError @pytest.mark.unit class HierarchyTests(unittest.TestCase): def test_all_conditions_derive_from_vendor_error(self): - for cls in (NoMarketDataError, VendorRateLimitError, VendorNotConfiguredError): + for cls in (NoMarketDataError, VendorUnavailableError, VendorNotConfiguredError): self.assertTrue(issubclass(cls, VendorError)) def test_not_configured_is_still_a_value_error(self): @@ -36,7 +36,7 @@ class HierarchyTests(unittest.TestCase): self.assertTrue(issubclass(VendorNotConfiguredError, ValueError)) def test_vendor_named_errors_subclass_the_generic_bases(self): - self.assertTrue(issubclass(AlphaVantageRateLimitError, VendorRateLimitError)) + self.assertTrue(issubclass(AlphaVantageRateLimitError, VendorUnavailableError)) self.assertTrue(issubclass(AlphaVantageNotConfiguredError, VendorNotConfiguredError)) self.assertTrue(issubclass(FredNotConfiguredError, VendorNotConfiguredError)) # ... and therefore still ValueErrors diff --git a/tests/test_yahoo_rate_limit.py b/tests/test_yahoo_rate_limit.py index c441ef1b1..a24f1d179 100644 --- a/tests/test_yahoo_rate_limit.py +++ b/tests/test_yahoo_rate_limit.py @@ -21,7 +21,7 @@ from tradingagents.agents.tools import ( ) from tradingagents.dataflows import router from tradingagents.dataflows.config import set_config -from tradingagents.dataflows.errors import VendorRateLimitError +from tradingagents.dataflows.errors import VendorUnavailableError from tradingagents.dataflows.vendors.yahoo import fundamentals, ohlcv DAY = "2026-09-18" @@ -84,7 +84,7 @@ def test_the_indicator_path_retries_a_rate_limit(yahoo): def test_yfinance_raises_the_rate_limit_from_history(yahoo): yahoo.setattr(YfData, "_make_request", _rate_limited) - with pytest.raises(VendorRateLimitError, match="rate limited"): + with pytest.raises(VendorUnavailableError, match="rate limited"): ohlcv.load_ohlcv("AAPL", DAY) @@ -122,7 +122,7 @@ def test_a_price_request_that_raises_is_unavailable_whether_or_not_yahoo_answers yahoo.setattr(yf.Ticker, "history", refused) for reachable in (False, True): yahoo.setattr(ohlcv, "vendor_reachable", lambda url, _r=reachable: _r) - with pytest.raises(VendorRateLimitError, match="request failed"): + with pytest.raises(VendorUnavailableError, match="request failed"): ohlcv.load_ohlcv("AAPL", DAY) @@ -136,7 +136,7 @@ def test_a_price_request_that_fails_is_unavailable_even_when_yahoo_answers(monke monkeypatch.setattr(ohlcv.yf, "Ticker", lambda s: type("T", (), {"history": lambda self, **k: timed_out(**k)})()) monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True) - with pytest.raises(VendorRateLimitError): + with pytest.raises(VendorUnavailableError): ohlcv.load_ohlcv("NVDA", "2026-09-23") @@ -174,5 +174,5 @@ def test_a_yahoo_request_that_fails_is_unavailable_not_absent(yahoo, call): yahoo.setattr(yf, "Ticker", _Unreachable) yahoo.setattr(yf, "Search", _Unreachable) - with pytest.raises(VendorRateLimitError): + with pytest.raises(VendorUnavailableError): call() diff --git a/tradingagents/agents/tools.py b/tradingagents/agents/tools.py index 872df4481..ef4949164 100644 --- a/tradingagents/agents/tools.py +++ b/tradingagents/agents/tools.py @@ -10,7 +10,7 @@ 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.errors import VendorRateLimitError +from tradingagents.dataflows.errors import VendorUnavailableError from tradingagents.dataflows.router import route_to_vendor, vendor_unavailable from tradingagents.dataflows.vendors.yahoo.snapshot import build_verified_market_snapshot @@ -85,7 +85,7 @@ def get_verified_market_snapshot( # 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: + except VendorUnavailableError as exc: return vendor_unavailable("get_verified_market_snapshot", exc) diff --git a/tradingagents/dataflows/errors.py b/tradingagents/dataflows/errors.py index 2ce6a0c46..3ea1c2a82 100644 --- a/tradingagents/dataflows/errors.py +++ b/tradingagents/dataflows/errors.py @@ -7,7 +7,7 @@ these (or a thin vendor-named subclass) and needs no new ``except`` clause. VendorError ├── NoMarketDataError no usable rows (empty result OR stale data) - ├── VendorRateLimitError transient throttle -> skip to next vendor + ├── VendorUnavailableError throttled or failed request -> skip to next vendor └── VendorNotConfiguredError missing API key/config -> vendor unavailable The number of types is the number of distinct router reactions, not the number @@ -43,8 +43,11 @@ class NoMarketDataError(VendorError): super().__init__(msg) -class VendorRateLimitError(VendorError): - """A vendor throttled the request; the router skips to the next vendor.""" +class VendorUnavailableError(VendorError): + """A vendor could not answer: it throttled or failed the request. + + Says nothing about the instrument; the router skips to the next vendor. + """ class VendorNotConfiguredError(VendorError, ValueError): diff --git a/tradingagents/dataflows/router.py b/tradingagents/dataflows/router.py index 72f95e5cf..f8e2b3c2d 100644 --- a/tradingagents/dataflows/router.py +++ b/tradingagents/dataflows/router.py @@ -4,7 +4,7 @@ from tradingagents.dataflows.config import get_config from tradingagents.dataflows.errors import ( NoMarketDataError, VendorNotConfiguredError, - VendorRateLimitError, + VendorUnavailableError, ) from tradingagents.dataflows.vendors.alpha_vantage import ( get_balance_sheet as get_alpha_vantage_balance_sheet, @@ -173,7 +173,7 @@ 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: +def vendor_unavailable(method: str, error: VendorUnavailableError) -> str: """What a call returns when every vendor was throttled or unreachable.""" return ( f"DATA_UNAVAILABLE: no configured vendor could serve {method} right now " @@ -210,7 +210,7 @@ def route_to_vendor(method: str, *args, **kwargs): vendor_chain = all_available_vendors last_no_data: NoMarketDataError | None = None - last_unavailable: VendorRateLimitError | None = None + last_unavailable: VendorUnavailableError | None = None first_error: Exception | None = None for vendor in vendor_chain: vendor_impl = VENDOR_METHODS[method][vendor] @@ -218,7 +218,7 @@ def route_to_vendor(method: str, *args, **kwargs): try: return impl_func(*args, **kwargs) - except VendorRateLimitError as e: + except VendorUnavailableError as e: logger.warning("Vendor %r unavailable for %s: %s; trying next vendor.", vendor, method, e) # Kept so an all-unavailable chain can say the vendor was the # problem, rather than reporting nothing about the symbol. diff --git a/tradingagents/dataflows/vendors/alpha_vantage/common.py b/tradingagents/dataflows/vendors/alpha_vantage/common.py index 39ffb1d81..d8e47045f 100644 --- a/tradingagents/dataflows/vendors/alpha_vantage/common.py +++ b/tradingagents/dataflows/vendors/alpha_vantage/common.py @@ -5,7 +5,7 @@ from io import StringIO import pandas as pd -from tradingagents.dataflows.errors import VendorNotConfiguredError, VendorRateLimitError +from tradingagents.dataflows.errors import VendorNotConfiguredError, VendorUnavailableError from tradingagents.dataflows.net import get_scrubbed API_BASE_URL = "https://www.alphavantage.co/query" @@ -62,7 +62,7 @@ def format_datetime_for_api(date_input, end_of_day: bool = False) -> str: raise ValueError(f"Date must be string or datetime object, got {type(date_input)}") -class AlphaVantageRateLimitError(VendorRateLimitError): +class AlphaVantageRateLimitError(VendorUnavailableError): """Raised when the Alpha Vantage API rate limit is exceeded.""" pass diff --git a/tradingagents/dataflows/vendors/sec_edgar.py b/tradingagents/dataflows/vendors/sec_edgar.py index f9ccbadf2..240da0d6b 100644 --- a/tradingagents/dataflows/vendors/sec_edgar.py +++ b/tradingagents/dataflows/vendors/sec_edgar.py @@ -28,7 +28,7 @@ import requests from tradingagents import __version__ from tradingagents.dataflows.config import get_config -from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError +from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError from tradingagents.dataflows.files import replace_file logger = logging.getLogger(__name__) @@ -109,9 +109,9 @@ def _fetch_json(url: str) -> dict: status = getattr(getattr(exc, "response", None), "status_code", None) # Every failure here is "this vendor cannot serve it now", so the router # moves on instead of seeing a transport exception it has no rule for. - raise VendorRateLimitError(f"SEC EDGAR request failed ({status or type(exc).__name__})") from exc + raise VendorUnavailableError(f"SEC EDGAR request failed ({status or type(exc).__name__})") from exc except ValueError as exc: - raise VendorRateLimitError("SEC EDGAR returned an unreadable response") from exc + raise VendorUnavailableError("SEC EDGAR returned an unreadable response") from exc def _cached_json(url: str, name: str) -> dict: diff --git a/tradingagents/dataflows/vendors/yahoo/fundamentals.py b/tradingagents/dataflows/vendors/yahoo/fundamentals.py index 243c87704..98e3dacc8 100644 --- a/tradingagents/dataflows/vendors/yahoo/fundamentals.py +++ b/tradingagents/dataflows/vendors/yahoo/fundamentals.py @@ -4,7 +4,7 @@ import pandas as pd import yfinance as yf from tradingagents.dataflows.date_window import withhold_live_profile -from tradingagents.dataflows.errors import VendorRateLimitError +from tradingagents.dataflows.errors import VendorUnavailableError from tradingagents.dataflows.net import vendor_reachable from tradingagents.dataflows.symbols import normalize_symbol from tradingagents.dataflows.vendors.yahoo.ohlcv import ( @@ -151,7 +151,7 @@ def get_insider_transactions( # so report it plainly rather than treating the symbol as invalid. if data is None or data.empty: if not vendor_reachable(YAHOO_HOST): - raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved") + raise VendorUnavailableError("Yahoo Finance is unreachable; insider filings were not retrieved") return f"No insider transactions reported for symbol '{canonical}'" if as_of_date: diff --git a/tradingagents/dataflows/vendors/yahoo/ohlcv.py b/tradingagents/dataflows/vendors/yahoo/ohlcv.py index b9b0a710e..b5b886c0a 100644 --- a/tradingagents/dataflows/vendors/yahoo/ohlcv.py +++ b/tradingagents/dataflows/vendors/yahoo/ohlcv.py @@ -7,7 +7,7 @@ import yfinance as yf from yfinance.exceptions import YFRateLimitError from tradingagents.dataflows.config import get_config -from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError +from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError from tradingagents.dataflows.files import replace_file from tradingagents.dataflows.net import vendor_reachable from tradingagents.dataflows.symbols import normalize_symbol, safe_ticker_component @@ -35,7 +35,7 @@ def raise_for_empty(symbol: str, canonical: str, what: str) -> None: without this a Yahoo outage reads as "this symbol has no {what}". """ if not vendor_reachable(YAHOO_HOST): - raise VendorRateLimitError(f"Yahoo Finance is unreachable; no {what} was retrieved") + raise VendorUnavailableError(f"Yahoo Finance is unreachable; no {what} was retrieved") raise NoMarketDataError(symbol, canonical, f"no {what}") @@ -45,7 +45,7 @@ def yf_retry(func, max_retries=3, base_delay=2.0): 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 - VendorRateLimitError: Yahoo answers an unknown symbol with an empty + 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 itself: a Ticker keeps a failed ``info`` fetch as done, so asking the same @@ -60,11 +60,11 @@ def yf_retry(func, max_retries=3, base_delay=2.0): logger.warning(f"Yahoo Finance rate limited, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries})") time.sleep(delay) else: - raise VendorRateLimitError( + raise VendorUnavailableError( f"Yahoo Finance rate limited after {max_retries} retries: {exc}" ) from exc except Exception as exc: - raise VendorRateLimitError(f"Yahoo Finance request failed: {type(exc).__name__}") from exc + raise VendorUnavailableError(f"Yahoo Finance request failed: {type(exc).__name__}") from exc def _ensure_date_column(data: pd.DataFrame) -> pd.DataFrame: