ETF_Suite_Portal/ETF_Portal/api/yfinance/client.py
Pascal c462342d44 feat: Add API management system with caching support
- Add base API client and cache manager classes

- Implement FMP and YFinance specific clients and cache managers

- Add API factory for managing multiple data providers

- Add test suite for API configuration and caching

- Add logging configuration for API operations
2025-05-27 14:07:32 +02:00

199 lines
6.3 KiB
Python

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