""" Alpha Vantage financial API client. Stock and cryptocurrency quotes. https://www.alphavantage.co/documentation/ Requires API key (free tier available). """ import httpx import logging from datetime import datetime from typing import Optional from .base import FinancialProvider, StockQuote logger = logging.getLogger(__name__) class AlphaVantageProvider(FinancialProvider): """Alpha Vantage financial API implementation.""" BASE_URL = "https://www.alphavantage.co/query" def __init__(self, api_key: str, timeout: int = 10): """ Initialize Alpha Vantage client. Args: api_key: Alpha Vantage API key timeout: HTTP request timeout in seconds """ self.api_key = api_key self.timeout = timeout self._client: Optional[httpx.AsyncClient] = None @property def client(self) -> httpx.AsyncClient: """Lazy-initialize HTTP client.""" if self._client is None or self._client.is_closed: self._client = httpx.AsyncClient(timeout=self.timeout) return self._client async def close(self): """Close HTTP client.""" if self._client and not self._client.is_closed: await self._client.aclose() self._client = None async def get_quote(self, symbol: str) -> Optional[StockQuote]: """ Get current quote for a stock symbol. Args: symbol: Stock ticker symbol (e.g., "AAPL", "MSFT") Returns: StockQuote with current price info or None if not found """ try: response = await self.client.get( self.BASE_URL, params={ "function": "GLOBAL_QUOTE", "symbol": symbol.upper(), "apikey": self.api_key } ) response.raise_for_status() data = response.json() # Check for API errors if "Error Message" in data: logger.warning(f"Alpha Vantage error for {symbol}: {data['Error Message']}") return None if "Note" in data: # Rate limit warning logger.warning(f"Alpha Vantage rate limit: {data['Note']}") return None quote = data.get("Global Quote", {}) if not quote: logger.warning(f"No quote data for symbol: {symbol}") return None # Parse quote data price = float(quote.get("05. price", 0)) change = float(quote.get("09. change", 0)) change_percent_str = quote.get("10. change percent", "0%") change_percent = float(change_percent_str.rstrip('%')) return StockQuote( symbol=symbol.upper(), name=None, # Global Quote doesn't include company name price=price, currency="USD", # Alpha Vantage returns USD for US stocks change=change, change_percent=change_percent, timestamp=datetime.now() ) except httpx.HTTPError as e: logger.error(f"Alpha Vantage request failed for {symbol}: {e}") return None except (KeyError, ValueError) as e: logger.error(f"Failed to parse Alpha Vantage response for {symbol}: {e}") return None async def get_quotes(self, symbols: list[str]) -> list[StockQuote]: """ Get quotes for multiple stock symbols. Note: Alpha Vantage free tier has rate limits (5 calls/min, 500 calls/day). Consider using batch endpoints or caching for production use. Args: symbols: List of stock ticker symbols Returns: List of StockQuote objects (may be less than input if some fail) """ quotes = [] for symbol in symbols: quote = await self.get_quote(symbol) if quote: quotes.append(quote) return quotes async def get_crypto_quote( self, symbol: str, market: str = "USD" ) -> Optional[StockQuote]: """ Get current quote for a cryptocurrency. Args: symbol: Crypto symbol (e.g., "BTC", "ETH") market: Market currency (default: USD) Returns: StockQuote with current price info or None if not found """ try: response = await self.client.get( self.BASE_URL, params={ "function": "CURRENCY_EXCHANGE_RATE", "from_currency": symbol.upper(), "to_currency": market.upper(), "apikey": self.api_key } ) response.raise_for_status() data = response.json() # Check for API errors if "Error Message" in data: logger.warning(f"Alpha Vantage error for {symbol}: {data['Error Message']}") return None if "Note" in data: logger.warning(f"Alpha Vantage rate limit: {data['Note']}") return None rate_data = data.get("Realtime Currency Exchange Rate", {}) if not rate_data: logger.warning(f"No exchange rate data for: {symbol}/{market}") return None price = float(rate_data.get("5. Exchange Rate", 0)) return StockQuote( symbol=f"{symbol.upper()}/{market.upper()}", name=rate_data.get("2. From_Currency Name"), price=price, currency=market.upper(), change=None, # Exchange rate endpoint doesn't provide change change_percent=None, timestamp=datetime.now() ) except httpx.HTTPError as e: logger.error(f"Alpha Vantage crypto request failed for {symbol}: {e}") return None except (KeyError, ValueError) as e: logger.error(f"Failed to parse Alpha Vantage crypto response for {symbol}: {e}") return None async def search_symbol(self, keywords: str) -> list[dict]: """ Search for stock symbols by keywords. Args: keywords: Search keywords (company name or partial symbol) Returns: List of matching symbols with metadata """ try: response = await self.client.get( self.BASE_URL, params={ "function": "SYMBOL_SEARCH", "keywords": keywords, "apikey": self.api_key } ) response.raise_for_status() data = response.json() matches = data.get("bestMatches", []) return [ { "symbol": m.get("1. symbol"), "name": m.get("2. name"), "type": m.get("3. type"), "region": m.get("4. region"), "currency": m.get("8. currency"), } for m in matches ] except httpx.HTTPError as e: logger.error(f"Alpha Vantage search failed for '{keywords}': {e}") return []