214 lines
6.5 KiB
Python
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 {} |