- 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
199 lines
6.3 KiB
Python
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 {} |