ETF_Suite_Portal/ETF_Portal/api/fmp/client.py

214 lines
6.5 KiB
Python

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 {}