import yfinance as yf import pandas as pd from typing import Dict, List, Optional from datetime import datetime import logging from ..base import BaseAPIClient from ...cache.yfinance_cache import YFinanceCacheManager class YFinanceClient(BaseAPIClient): """Yahoo Finance API client.""" def __init__(self, cache_manager: Optional[YFinanceCacheManager] = None): """Initialize yfinance client. Args: cache_manager: Optional cache manager instance """ super().__init__(None) # yfinance doesn't need API key self.cache_manager = cache_manager or YFinanceCacheManager() self.logger = logging.getLogger(self.__class__.__name__) def _get_ticker(self, symbol: str) -> Optional[yf.Ticker]: """Get yfinance Ticker object. Args: symbol: ETF ticker symbol Returns: yfinance Ticker object or None if invalid """ if not self._validate_symbol(symbol): return None return yf.Ticker(symbol) def _make_request(self, endpoint: str, params: Dict = None) -> Dict: """Make API request to yfinance. Args: endpoint: API endpoint params: Query parameters Returns: API response data """ # Check cache first if self.cache_manager: cached_data, is_valid = self.cache_manager.get(endpoint, params) if is_valid: return cached_data try: symbol = params.get('symbol') if params else None if not symbol: raise ValueError("Symbol is required") ticker = self._get_ticker(symbol) if not ticker: raise ValueError(f"Invalid symbol: {symbol}") # Get data based on endpoint if endpoint == "info": data = ticker.info elif endpoint == "holdings": data = ticker.get_holdings() elif endpoint == "history": period = params.get('period', '1y') data = ticker.history(period=period).to_dict('records') else: raise ValueError(f"Unknown endpoint: {endpoint}") # Cache the response if self.cache_manager: self.cache_manager.set(endpoint, data, params) return data except Exception as e: self.logger.error(f"yfinance API request failed: {str(e)}") return self._handle_error(e) def get_etf_profile(self, symbol: str) -> Dict: """Get ETF profile data. Args: symbol: ETF ticker symbol Returns: Dictionary containing ETF profile information """ if not self._validate_symbol(symbol): return self._handle_error(ValueError(f"Invalid symbol: {symbol}")) return self._make_request("info", {'symbol': symbol}) def get_etf_holdings(self, symbol: str) -> List[Dict]: """Get ETF holdings data. Args: symbol: ETF ticker symbol Returns: List of dictionaries containing holding information """ if not self._validate_symbol(symbol): return [self._handle_error(ValueError(f"Invalid symbol: {symbol}"))] data = self._make_request("holdings", {'symbol': symbol}) return data if isinstance(data, list) else [data] def get_historical_data(self, symbol: str, period: str = '1y') -> pd.DataFrame: """Get historical price data. Args: symbol: ETF ticker symbol period: Time period (e.g., '1d', '1w', '1m', '1y') Returns: DataFrame with historical price data """ if not self._validate_symbol(symbol): return pd.DataFrame() data = self._make_request("history", {'symbol': symbol, 'period': period}) if isinstance(data, dict) and data.get('error'): return pd.DataFrame() return pd.DataFrame(data) def get_dividend_history(self, symbol: str) -> pd.DataFrame: """Get dividend history. Args: symbol: ETF ticker symbol Returns: DataFrame with dividend history """ if not self._validate_symbol(symbol): return pd.DataFrame() try: ticker = self._get_ticker(symbol) if not ticker: return pd.DataFrame() dividends = ticker.dividends return pd.DataFrame(dividends).reset_index() except Exception as e: self.logger.error(f"Error getting dividend history: {str(e)}") return pd.DataFrame() def get_sector_weightings(self, symbol: str) -> Dict: """Get sector weightings. Args: symbol: ETF ticker symbol Returns: Dictionary with sector weightings """ if not self._validate_symbol(symbol): return self._handle_error(ValueError(f"Invalid symbol: {symbol}")) try: ticker = self._get_ticker(symbol) if not ticker: return self._handle_error(ValueError(f"Invalid symbol: {symbol}")) info = ticker.info return { 'sector_weightings': info.get('sectorWeights', {}), 'asset_allocation': info.get('assetAllocation', {}) } except Exception as e: self.logger.error(f"Error getting sector weightings: {str(e)}") return self._handle_error(e) def clear_cache(self) -> int: """Clear expired cache entries. Returns: Number of files cleared """ if self.cache_manager: return self.cache_manager.clear_expired() return 0 def get_cache_stats(self) -> Dict: """Get cache statistics. Returns: Dictionary with cache statistics """ if self.cache_manager: return self.cache_manager.get_stats() return {}