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_income_statement as get_alpha_vantage_income_statement, 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, ) from .alpha_vantage_common import AlphaVantageRateLimitError # Market provider registry from .market_registry import registry as market_registry, MARKET_METHOD_MAP # Auto-discover and register market providers from . import markets # noqa: F401 # Configuration and routing logic from .config import get_config # Tools organized by category TOOLS_CATEGORIES = { "core_stock_apis": { "description": "OHLCV stock price data", "tools": [ "get_stock_data" ] }, "technical_indicators": { "description": "Technical analysis indicators", "tools": [ "get_indicators" ] }, "fundamental_data": { "description": "Company fundamentals", "tools": [ "get_fundamentals", "get_balance_sheet", "get_cashflow", "get_income_statement" ] }, "news_data": { "description": "News and insider data", "tools": [ "get_news", "get_global_news", "get_insider_transactions", ] } } VENDOR_LIST = [ "yfinance", "alpha_vantage", ] # Mapping of methods to their vendor-specific implementations VENDOR_METHODS = { # core_stock_apis "get_stock_data": { "alpha_vantage": get_alpha_vantage_stock, "yfinance": get_YFin_data_online, }, # technical_indicators "get_indicators": { "alpha_vantage": get_alpha_vantage_indicator, "yfinance": get_stock_stats_indicators_window, }, # fundamental_data "get_fundamentals": { "alpha_vantage": get_alpha_vantage_fundamentals, "yfinance": get_yfinance_fundamentals, }, "get_balance_sheet": { "alpha_vantage": get_alpha_vantage_balance_sheet, "yfinance": get_yfinance_balance_sheet, }, "get_cashflow": { "alpha_vantage": get_alpha_vantage_cashflow, "yfinance": get_yfinance_cashflow, }, "get_income_statement": { "alpha_vantage": get_alpha_vantage_income_statement, "yfinance": get_yfinance_income_statement, }, # news_data "get_news": { "alpha_vantage": get_alpha_vantage_news, "yfinance": get_news_yfinance, }, "get_global_news": { "yfinance": get_global_news_yfinance, "alpha_vantage": get_alpha_vantage_global_news, }, "get_insider_transactions": { "alpha_vantage": get_alpha_vantage_insider_transactions, "yfinance": get_yfinance_insider_transactions, }, } def _extract_ticker(method: str, args: tuple, kwargs: dict) -> str: """Extract the ticker/symbol from method arguments. Most data methods take ticker/symbol as the first positional argument. get_global_news is the exception — it has no ticker argument. """ # Methods that don't have a ticker argument no_ticker_methods = {"get_global_news"} if method in no_ticker_methods: return None # First positional arg is the ticker/symbol for all other methods if args: return args[0] # Check common keyword names for key in ("symbol", "ticker"): if key in kwargs: return kwargs[key] return None def get_category_for_method(method: str) -> str: """Get the category that contains the specified method.""" for category, info in TOOLS_CATEGORIES.items(): if method in info["tools"]: return category raise ValueError(f"Method '{method}' not found in any category") def get_vendor(category: str, method: str = None) -> str: """Get the configured vendor for a data category or specific tool method. Tool-level configuration takes precedence over category-level. """ config = get_config() # Check tool-level configuration first (if method provided) if method: tool_vendors = config.get("tool_vendors", {}) if method in tool_vendors: return tool_vendors[method] # Fall back to category-level configuration return config.get("data_vendors", {}).get(category, "default") def route_to_vendor(method: str, *args, **kwargs): """Route method calls to appropriate vendor implementation with fallback support. First checks if the ticker belongs to a registered non-US market (VN, TH, etc.). If so, delegates directly to that market's provider. Otherwise, uses the existing vendor routing (yfinance/alpha_vantage). """ # --- Market provider check --- ticker = _extract_ticker(method, args, kwargs) market_code = None if ticker: market_code = market_registry.detect_market(ticker) elif method == "get_global_news": # For global news with no ticker, check config for explicit market config = get_config() market_setting = config.get("market", "auto") if market_setting not in ("auto", "US"): market_code = market_setting # If a market provider handles this method, delegate to it if market_code and method in MARKET_METHOD_MAP: return market_registry.call_market_method(market_code, method, *args, **kwargs) # --- Default vendor routing (US / yfinance / alpha_vantage) --- category = get_category_for_method(method) vendor_config = get_vendor(category, method) primary_vendors = [v.strip() for v in vendor_config.split(',')] if method not in VENDOR_METHODS: raise ValueError(f"Method '{method}' not supported") # Build fallback chain: primary vendors first, then remaining available vendors all_available_vendors = list(VENDOR_METHODS[method].keys()) fallback_vendors = primary_vendors.copy() for vendor in all_available_vendors: if vendor not in fallback_vendors: fallback_vendors.append(vendor) for vendor in fallback_vendors: if vendor not in VENDOR_METHODS[method]: continue vendor_impl = VENDOR_METHODS[method][vendor] impl_func = vendor_impl[0] if isinstance(vendor_impl, list) else vendor_impl try: return impl_func(*args, **kwargs) except AlphaVantageRateLimitError: continue # Only rate limits trigger fallback raise RuntimeError(f"No available vendor for '{method}'")