refactor(data): unify vendor errors under a VendorError hierarchy

Every condition where a vendor cannot return usable data now derives from a
single VendorError base (errors.py): NoMarketDataError, VendorRateLimitError,
and VendorNotConfiguredError (still a ValueError for back-compat). Vendor-named
errors subclass the generic bases, and the router catches the base types, so a
new vendor needs no new except clause. Not-configured now has explicit
try-next-vendor handling instead of falling through the generic catch-all. The
number of error types tracks the number of distinct router reactions, not the
number of causes.
This commit is contained in:
Yijia-Xiao committed 2026-06-14 07:10:15 +00:00
1 parent db059034a2
commit 7df18fc912
7 files changed
+226 -64

No files matched your search

+34 -26
View File
@@ -1,35 +1,34 @@
import logging
from typing import Annotated
# Import from vendor-specific modules
from .y_finance import (
get_YFin_data_online,
get_stock_stats_indicators_window,
get_fundamentals as get_yfinance_fundamentals,
get_balance_sheet as get_yfinance_balance_sheet,
get_cashflow as get_yfinance_cashflow,
get_income_statement as get_yfinance_income_statement,
get_insider_transactions as get_yfinance_insider_transactions,
)
from .yfinance_news import get_news_yfinance, get_global_news_yfinance
from .alpha_vantage import (
get_stock as get_alpha_vantage_stock,
get_indicator as get_alpha_vantage_indicator,
get_fundamentals as get_alpha_vantage_fundamentals,
get_balance_sheet as get_alpha_vantage_balance_sheet,
get_cashflow as get_alpha_vantage_cashflow,
get_fundamentals as get_alpha_vantage_fundamentals,
get_global_news as get_alpha_vantage_global_news,
get_income_statement as get_alpha_vantage_income_statement,
get_indicator as get_alpha_vantage_indicator,
get_insider_transactions as get_alpha_vantage_insider_transactions,
get_news as get_alpha_vantage_news,
get_global_news as get_alpha_vantage_global_news,
get_stock as get_alpha_vantage_stock,
)
from .config import get_config
from .errors import (
NoMarketDataError,
VendorNotConfiguredError,
VendorRateLimitError,
)
from .alpha_vantage_common import AlphaVantageRateLimitError
from .fred import get_macro_data as get_fred_macro_data
from .polymarket import get_prediction_markets as get_polymarket_prediction_markets
from .symbol_utils import NoMarketDataError
# Configuration and routing logic
from .config import get_config
from .y_finance import (
get_balance_sheet as get_yfinance_balance_sheet,
get_cashflow as get_yfinance_cashflow,
get_fundamentals as get_yfinance_fundamentals,
get_income_statement as get_yfinance_income_statement,
get_insider_transactions as get_yfinance_insider_transactions,
get_stock_stats_indicators_window,
get_YFin_data_online,
)
from .yfinance_news import get_global_news_yfinance, get_news_yfinance
logger = logging.getLogger(__name__)
@@ -194,9 +193,14 @@ def route_to_vendor(method: str, *args, **kwargs):
try:
return impl_func(*args, **kwargs)
except AlphaVantageRateLimitError:
except VendorRateLimitError:
logger.warning("Vendor %r rate-limited for %s; trying next vendor.", vendor, method)
continue
except VendorNotConfiguredError as e:
logger.warning("Vendor %r not configured for %s; trying next vendor.", vendor, method)
if first_error is None:
first_error = e # Surface it if no other vendor can serve the call.
continue
except NoMarketDataError as e:
last_no_data = e # No data here; another configured vendor may have it
continue
@@ -224,10 +228,14 @@ def route_to_vendor(method: str, *args, **kwargs):
sym = last_no_data.symbol
canonical = last_no_data.canonical
resolved = "" if canonical == sym else f" (resolved to '{canonical}')"
# Surface the typed error's detail (e.g. "latest row is 2025-06-11 ...
# stale") so the agent sees the specific reason — invalid symbol, no
# coverage, or stale data — not just a generic "unavailable".
reason = f" ({last_no_data.detail})" if last_no_data.detail else ""
return (
f"NO_DATA_AVAILABLE: No market data found for '{sym}'{resolved} from "
f"any configured vendor. The symbol may be invalid, delisted, or not "
f"covered by Yahoo Finance / Alpha Vantage. Do not estimate or "
f"NO_DATA_AVAILABLE: No usable market data for '{sym}'{resolved} from "
f"any configured vendor{reason}. The symbol may be invalid, delisted, "
f"not covered, or the vendor returned stale data. Do not estimate or "
f"fabricate values — report that data is unavailable for this symbol."
)
@@ -236,4 +244,4 @@ def route_to_vendor(method: str, *args, **kwargs):
if first_error is not None:
raise first_error
raise RuntimeError(f"No available vendor for '{method}'")
raise RuntimeError(f"No available vendor for '{method}'")