import requests import pandas as pd from typing import Dict, List, Optional from datetime import datetime import logging from ..base import BaseAPIClient from ...cache.fmp_cache import FMPCacheManager class FMPClient(BaseAPIClient): """Financial Modeling Prep API client.""" BASE_URL = "https://financialmodelingprep.com/api/v3" def __init__(self, api_key: str, cache_manager: Optional[FMPCacheManager] = None): """Initialize FMP client. Args: api_key: FMP API key cache_manager: Optional cache manager instance """ super().__init__(api_key) self.cache_manager = cache_manager or FMPCacheManager() self.logger = logging.getLogger(self.__class__.__name__) def _validate_symbol(self, symbol: str) -> bool: """Validate ETF symbol format. Args: symbol: ETF ticker symbol Returns: True if valid, False otherwise """ return bool(symbol and isinstance(symbol, str) and symbol.isupper()) def _make_request(self, endpoint: str, params: Dict = None) -> Dict: """Make API request to FMP. Args: endpoint: API endpoint params: Query parameters Returns: API response data """ # Prepare request url = f"{self.BASE_URL}/{endpoint}" params = params or {} params['apikey'] = self.api_key try: response = requests.get(url, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: self.logger.error(f"FMP 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 with ETF profile data """ if not self._validate_symbol(symbol): return self._handle_error(ValueError(f"Invalid symbol: {symbol}")) # Try cache first if self.cache_manager: is_valid, cached_data = self.cache_manager.load('fmp', symbol, 'profile') if is_valid: return cached_data # Fetch from API data = self._make_request(f"etf/profile/{symbol}") # Cache the response if self.cache_manager and data: self.cache_manager.save('fmp', symbol, 'profile', data) return data def get_etf_holdings(self, symbol: str) -> List[Dict]: """Get ETF holdings. Args: symbol: ETF ticker symbol Returns: List of holdings """ if not self._validate_symbol(symbol): return [] # Try cache first if self.cache_manager: is_valid, cached_data = self.cache_manager.load('fmp', symbol, 'holdings') if is_valid: return cached_data # Fetch from API data = self._make_request(f"etf/holdings/{symbol}") # Cache the response if self.cache_manager and data: self.cache_manager.save('fmp', symbol, 'holdings', data) return data def get_etf_historical_data(self, symbol: str, timeframe: str = '1d') -> pd.DataFrame: """Get ETF historical data. Args: symbol: ETF ticker symbol timeframe: Timeframe for historical data Returns: DataFrame with historical data """ if not self._validate_symbol(symbol): return pd.DataFrame() # Try cache first if self.cache_manager: is_valid, cached_data = self.cache_manager.load('fmp', symbol, f'historical_{timeframe}') if is_valid: return pd.DataFrame(cached_data) # Fetch from API data = self._make_request(f"etf/historical-price/{symbol}", {'timeframe': timeframe}) # Cache the response if self.cache_manager and data: self.cache_manager.save('fmp', symbol, f'historical_{timeframe}', data) 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 cache first if self.cache_manager: is_valid, cached_data = self.cache_manager.load('fmp', symbol, 'dividend_history') if is_valid: return pd.DataFrame(cached_data) # Fetch from API data = self._make_request(f"etf/dividend/{symbol}") # Cache the response if self.cache_manager and data: self.cache_manager.save('fmp', symbol, 'dividend_history', data) return pd.DataFrame(data) 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 cache first if self.cache_manager: is_valid, cached_data = self.cache_manager.load('fmp', symbol, 'sector_weightings') if is_valid: return cached_data # Fetch from API data = self._make_request(f"etf/sector-weightings/{symbol}") # Cache the response if self.cache_manager and data: self.cache_manager.save('fmp', symbol, 'sector_weightings', data) return data 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 {}