diff --git a/README.md b/README.md index dc5b04a..1d5dd22 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # yfinance-plus šŸš€ -A drop-in replacement for `yfinance` with enhanced rate limiting, intelligent caching, and 401 error handling. Works exactly like the original yfinance but faster, more reliable, and with zero breaking changes. +A drop-in replacement for `yfinance` with improved rate limiting, intelligent caching, and 401 error handling. Works exactly like the original yfinance but faster, more reliable, and with zero breaking changes. [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) @@ -8,7 +8,7 @@ A drop-in replacement for `yfinance` with enhanced rate limiting, intelligent ca ## ✨ Key Features ### šŸŽÆ **100% Drop-in Replacement** -- Import as `import yfinance_enhanced as yf` +- Import as `import yfinance_plus as yf` - All yfinance functionality works identically - **99% API coverage** (97/98 features supported) - Zero code changes required @@ -55,7 +55,7 @@ pip install yfinance>=0.2.65 curl_cffi>=0.5.0 pandas>=1.3.0 numpy>=1.20.0 ```python # Simply replace your import - everything else stays the same! -import yfinance_enhanced as yf +import yfinance_plus as yf # All original yfinance functionality works identically ticker = yf.Ticker("AAPL") @@ -76,7 +76,7 @@ cache_info = yf.get_cache_info() ```python import time -import yfinance_enhanced as yf +import yfinance_plus as yf # First request (downloads and caches) start = time.time() @@ -167,7 +167,7 @@ $ yfp test ### How Smart Caching Works ```python -import yfinance_enhanced as yf +import yfinance_plus as yf # 1. First request caches data ticker = yf.Ticker("AAPL") @@ -200,7 +200,7 @@ data_1mo_from_3mo = ticker.history(period="1mo") # Extracted from 3mo cache ### Cache Management ```python -import yfinance_enhanced as yf +import yfinance_plus as yf # Check cache status cache_info = yf.get_cache_info() @@ -222,7 +222,7 @@ yf.set_config( ### Global Configuration ```python -import yfinance_enhanced as yf +import yfinance_plus as yf # Configure global settings yf.set_config( @@ -253,7 +253,7 @@ print(f"Max retries: {config.max_retries}") ## šŸ”„ Migration Guide -### From yfinance to yfinance-enhanced +### From yfinance to yfinance-plus Migration is **100% seamless** - just change your import: @@ -262,7 +262,7 @@ Migration is **100% seamless** - just change your import: import yfinance as yf # After -import yfinance_enhanced as yf +import yfinance_plus as yf # Everything else works identically! ticker = yf.Ticker("AAPL") @@ -274,7 +274,7 @@ info = tickers.tickers['MSFT'].info ### Common yfinance Patterns That Work ```python -import yfinance_enhanced as yf +import yfinance_plus as yf # All standard patterns work identically ticker = yf.Ticker("AAPL") @@ -308,7 +308,7 @@ for symbol in tickers.symbols: ```python # Automatic 401 error recovery -import yfinance_enhanced as yf +import yfinance_plus as yf ticker = yf.Ticker("AAPL") # If Yahoo returns 401, automatically: @@ -334,7 +334,7 @@ info = ticker.info # Just works! ### Performance Optimization ```python -import yfinance_enhanced as yf +import yfinance_plus as yf # Smart cache usage ticker = yf.Ticker("AAPL") @@ -354,7 +354,7 @@ hist_1mo = ticker.history(period="1mo") # ~0.001s, from cache ### 100% Compatible Functions -| Function | yfinance | yfinance-enhanced | Enhancements | +| Function | yfinance | yfinance-plus | Enhancements | |----------|----------|-------------------|--------------| | `Ticker(symbol)` | āœ… | āœ… | + Caching + Rate limiting | | `download(tickers)` | āœ… | āœ… | + Better error handling | @@ -462,7 +462,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## āš ļø Disclaimer -This project is for educational and research purposes. Please respect Yahoo Finance's terms of service and rate limits. The enhanced wrapper is designed to be more respectful of their servers while providing better reliability for legitimate use cases. +This project is for educational and research purposes. Please respect Yahoo Finance's terms of service and rate limits. The yfinance-plus wrapper is designed to be more respectful of their servers while providing better reliability for legitimate use cases. ## šŸ™ Acknowledgments diff --git a/setup.py b/setup.py index 1aad89b..f89ad05 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( long_description=read_readme(), long_description_content_type="text/markdown", url="https://gitea.yirugi.synology.me/yirugi/yfinance_plus.git", - py_modules=["yfinance_enhanced", "yfinance_enhanced_cli"], + py_modules=["yfinance_plus", "yfinance_plus_cli"], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -53,7 +53,7 @@ setup( }, entry_points={ "console_scripts": [ - "yfp=yfinance_enhanced_cli:main", + "yfp=yfinance_plus_cli:main", ], }, keywords="finance, yfinance, yahoo finance, stocks, market data, plus, rate limit, caching", diff --git a/yfinance_enhanced.py b/yfinance_enhanced.py deleted file mode 100644 index 3cefbe1..0000000 --- a/yfinance_enhanced.py +++ /dev/null @@ -1,592 +0,0 @@ -#!/usr/bin/env python3 -""" -Enhanced yfinance wrapper with improved rate limit handling and intelligent caching. -Uses dynamic proxy pattern to wrap all yfinance functionality automatically. -""" - -import os -import time -import random -import threading -import pickle -import hashlib -from pathlib import Path -from typing import Dict, Optional, Any, Union, List -from dataclasses import dataclass -from functools import wraps -import logging -from datetime import datetime, timedelta - -import yfinance as yf -from curl_cffi import requests -import pandas as pd - - -@dataclass -class RequestConfig: - """Configuration for enhanced requests""" - max_retries: int = 3 - base_delay: float = 1.0 - max_delay: float = 60.0 - jitter: bool = True - user_agents: list = None - enable_cache: bool = True - cache_dir: str = None - - def __post_init__(self): - if self.user_agents is None: - self.user_agents = [ - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0' - ] - if self.cache_dir is None: - self.cache_dir = os.path.expanduser('~/.yfinance_cache') - - -class HistoricalDataCache: - """Intelligent cache for historical data""" - - def __init__(self, cache_dir: str): - self.cache_dir = Path(cache_dir) - self.cache_dir.mkdir(parents=True, exist_ok=True) - self._lock = threading.Lock() - - def _get_cache_key(self, symbol: str, period: str, interval: str, start: str = None, end: str = None) -> str: - """Generate cache key for historical data request""" - key_data = f"{symbol}_{period}_{interval}_{start}_{end}" - return hashlib.md5(key_data.encode()).hexdigest() - - def _get_cache_file(self, cache_key: str) -> Path: - """Get cache file path""" - return self.cache_dir / f"{cache_key}.pkl" - - def _normalize_period_to_days(self, period: str) -> int: - """Convert period string to approximate days""" - period_map = { - '1d': 1, '2d': 2, '5d': 5, '1mo': 30, '3mo': 90, '6mo': 180, - '1y': 365, '2y': 730, '5y': 1825, '10y': 3650, 'ytd': 365, 'max': 36500 - } - return period_map.get(period.lower(), 30) - - def get(self, symbol: str, period: str, interval: str, start: str = None, end: str = None) -> Optional[pd.DataFrame]: - """Get cached historical data if available and valid""" - with self._lock: - try: - # First check exact match - cache_key = self._get_cache_key(symbol, period, interval, start, end) - cache_file = self._get_cache_file(cache_key) - - if cache_file.exists(): - with open(cache_file, 'rb') as f: - cache_data = pickle.load(f) - - # Check if cache is still valid (not older than 1 hour for recent data) - cache_time = datetime.fromtimestamp(cache_file.stat().st_mtime) - if datetime.now() - cache_time < timedelta(hours=1): - return cache_data['data'] - - # Look for longer period cache that might contain requested data - requested_days = self._normalize_period_to_days(period) - - for cache_file in self.cache_dir.glob(f"*.pkl"): - try: - with open(cache_file, 'rb') as f: - cache_data = pickle.load(f) - - # Check if this cache contains data for the same symbol and interval - if (cache_data['symbol'] == symbol and - cache_data['interval'] == interval and - cache_data['period_days'] >= requested_days): - - # Check cache validity - cache_time = datetime.fromtimestamp(cache_file.stat().st_mtime) - if datetime.now() - cache_time < timedelta(hours=1): - # Extract requested period from cached data - data = cache_data['data'] - if len(data) >= requested_days: - return data.tail(min(len(data), requested_days * 2)) # Buffer for weekends - except: - continue - - return None - - except Exception as e: - logging.warning(f"Cache read error: {e}") - return None - - def store(self, symbol: str, period: str, interval: str, data: pd.DataFrame, start: str = None, end: str = None): - """Store historical data in cache""" - with self._lock: - try: - cache_key = self._get_cache_key(symbol, period, interval, start, end) - cache_file = self._get_cache_file(cache_key) - - cache_data = { - 'symbol': symbol, - 'period': period, - 'period_days': self._normalize_period_to_days(period), - 'interval': interval, - 'start': start, - 'end': end, - 'data': data, - 'cached_at': datetime.now() - } - - with open(cache_file, 'wb') as f: - pickle.dump(cache_data, f) - - except Exception as e: - logging.warning(f"Cache store error: {e}") - - def clear(self): - """Clear all cached data""" - with self._lock: - for cache_file in self.cache_dir.glob("*.pkl"): - try: - cache_file.unlink() - except: - pass - - def get_cache_info(self) -> Dict[str, Any]: - """Get cache statistics""" - with self._lock: - cache_files = list(self.cache_dir.glob("*.pkl")) - total_size = sum(f.stat().st_size for f in cache_files) - - return { - 'cache_dir': str(self.cache_dir), - 'file_count': len(cache_files), - 'total_size_mb': total_size / (1024 * 1024), - 'files': [{ - 'name': f.name, - 'size_kb': f.stat().st_size / 1024, - 'modified': datetime.fromtimestamp(f.stat().st_mtime) - } for f in cache_files] - } - - -class EnhancedYFinance: - """Enhanced yfinance wrapper with better rate limit handling and caching""" - - def __init__(self, config: RequestConfig = None): - self.config = config or RequestConfig() - self._session = None - self._lock = threading.Lock() - self._last_request_time = 0 - self._request_count = 0 - self._session_created_time = time.time() - - # Setup caching - if self.config.enable_cache: - self._cache = HistoricalDataCache(self.config.cache_dir) - else: - self._cache = None - - # Setup logging - self.logger = logging.getLogger(__name__) - if not self.logger.handlers: - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - self.logger.setLevel(logging.WARNING) # Reduce log verbosity - - # Suppress yfinance HTTP error logs - yf_logger = logging.getLogger('yfinance') - yf_logger.setLevel(logging.CRITICAL) - - def _create_enhanced_session(self) -> requests.Session: - """Create a session with browser-like headers and behavior""" - session = requests.Session(impersonate="chrome") - - # Enhanced headers to mimic real browser behavior - browser_headers = { - 'User-Agent': random.choice(self.config.user_agents), - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Language': 'en-US,en;q=0.9,ko;q=0.8,ja;q=0.7', - 'Accept-Encoding': 'gzip, deflate, br', - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache', - 'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"macOS"', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'cross-site', - 'Sec-Fetch-User': '?1', - 'Upgrade-Insecure-Requests': '1', - 'Connection': 'keep-alive', - 'DNT': '1', - 'Origin': 'https://finance.yahoo.com', - 'Referer': 'https://finance.yahoo.com/', - } - - session.headers.update(browser_headers) - - # Add realistic Yahoo Finance cookies - current_time = int(time.time()) - session.cookies.update({ - 'A1': f'd=AQABBC{random.randint(100000, 999999)}YLoCIgEBwQJ7vgAB&S=AQAAAg', - 'A1S': f'd=AQABBC{random.randint(100000, 999999)}YLoCIgEBwQJ7vgAB&S=AQAAAg', - 'A3': f'd=AQABBC{random.randint(100000, 999999)}YLoCIgEBwQJ7vgAB&S=AQAAAg', - 'GUC': f'AQEBAQFm{random.randint(1000, 9999)}k0L', - 'B': f'c={random.randint(1000000, 9999999)}&b=3&s=4u', - 'cmp': f't={current_time}&j=0&u=1---', - 'EuConsent': 'CP-r9cAP-r9cAAOACKENAoEgAAAAAAAAACiQAAAAAAAA', - }) - - return session - - @property - def session(self) -> requests.Session: - """Get or create enhanced session""" - with self._lock: - if self._session is None or self._should_refresh_session(): - self._session = self._create_enhanced_session() - self._session_created_time = time.time() - self._request_count = 0 - self.logger.debug("Created new enhanced session") - return self._session - - def _should_refresh_session(self) -> bool: - """Check if session should be refreshed""" - session_age = time.time() - self._session_created_time - return (session_age > 600 or # 10 minutes - self._request_count > 50) # 50 requests - - def _rate_limit_delay(self): - """Apply intelligent rate limiting""" - with self._lock: - current_time = time.time() - time_since_last = current_time - self._last_request_time - - # Minimum delay based on request frequency - min_delay = 0.1 if self._request_count < 10 else 0.2 - - if time_since_last < min_delay: - delay = min_delay - time_since_last - if self.config.jitter: - delay += random.uniform(0, delay * 0.5) - time.sleep(delay) - - self._last_request_time = time.time() - self._request_count += 1 - - def _retry_with_backoff(self, func, *args, **kwargs): - """Execute function with exponential backoff retry""" - last_exception = None - - for attempt in range(self.config.max_retries + 1): - try: - self._rate_limit_delay() - return func(*args, **kwargs) - - except Exception as e: - last_exception = e - error_str = str(e).lower() - - # Check if it's a rate limit or 401 error - if ("rate limit" in error_str or "429" in error_str or - "401" in error_str or "unauthorized" in error_str): - - if attempt < self.config.max_retries: - delay = min( - self.config.base_delay * (2 ** attempt), - self.config.max_delay - ) - if self.config.jitter: - delay += random.uniform(0, delay * 0.3) - - if "401" in error_str: - self.logger.debug(f"401 error, refreshing session and retrying in {delay:.2f}s (attempt {attempt + 1})") - else: - self.logger.warning(f"Rate limit hit, retrying in {delay:.2f}s (attempt {attempt + 1})") - - time.sleep(delay) - - # Refresh session on auth/rate limit error - with self._lock: - self._session = None - continue - else: - # Non-rate-limit error, re-raise immediately - raise e - - # All retries exhausted - raise last_exception - - def get_ticker(self, symbol: str) -> 'EnhancedTicker': - """Get enhanced ticker with improved rate limiting""" - return EnhancedTicker(symbol, self) - - -class EnhancedTicker: - """ - Enhanced ticker wrapper using dynamic proxy pattern. - Automatically wraps ALL yfinance ticker functionality. - """ - - # Methods that should be cached - _CACHEABLE_METHODS = {'history'} - - # Methods that should NOT be cached (real-time or frequently changing data) - _NON_CACHEABLE_METHODS = {'news', 'get_news', 'fast_info'} - - def __init__(self, symbol: str, enhanced_yf: EnhancedYFinance): - self.symbol = symbol.upper() - self.enhanced_yf = enhanced_yf - self._yf_ticker = None - - @property - def yf_ticker(self): - """Get yfinance ticker with enhanced session""" - if self._yf_ticker is None: - self._yf_ticker = yf.Ticker(self.symbol, session=self.enhanced_yf.session) - return self._yf_ticker - - def __getattr__(self, name): - """ - Dynamic proxy: automatically wrap any yfinance method or property - """ - # Get the original attribute from yfinance ticker - original_attr = getattr(self.yf_ticker, name) - - # If it's a method/function, wrap it with enhanced functionality - if callable(original_attr): - if name == 'history': - # Special handling for history method with caching - return self._enhanced_history - elif name in self._NON_CACHEABLE_METHODS: - # Methods that should not be cached - return self._wrap_method_no_cache(original_attr) - else: - # All other methods get standard enhancement - return self._wrap_method(original_attr) - else: - # For properties, wrap them with retry logic - return self._wrap_property(name) - - def _enhanced_history(self, period="1mo", interval="1d", start=None, end=None, **kwargs): - """Enhanced history method with caching""" - # Check cache first for historical data (not real-time) - if (self.enhanced_yf._cache and - not kwargs.get('prepost', False)): # Don't cache extended hours data - - cached_data = self.enhanced_yf._cache.get( - self.symbol, period, interval, - str(start) if start else None, - str(end) if end else None - ) - - if cached_data is not None and not cached_data.empty: - self.enhanced_yf.logger.debug(f"Using cached data for {self.symbol}") - return cached_data - - # Fetch fresh data - def _get_history(): - return self.yf_ticker.history( - period=period, interval=interval, - start=start, end=end, **kwargs - ) - - data = self.enhanced_yf._retry_with_backoff(_get_history) - - # Cache the data if successful and caching is enabled - if (self.enhanced_yf._cache and - data is not None and not data.empty and - not kwargs.get('prepost', False)): - - self.enhanced_yf._cache.store( - self.symbol, period, interval, data, - str(start) if start else None, - str(end) if end else None - ) - - return data - - def _wrap_method(self, method): - """Wrap a method with enhanced error handling and rate limiting""" - @wraps(method) - def wrapped(*args, **kwargs): - def _call_method(): - return method(*args, **kwargs) - return self.enhanced_yf._retry_with_backoff(_call_method) - return wrapped - - def _wrap_method_no_cache(self, method): - """Wrap a method with enhanced error handling but no caching""" - @wraps(method) - def wrapped(*args, **kwargs): - def _call_method(): - return method(*args, **kwargs) - return self.enhanced_yf._retry_with_backoff(_call_method) - return wrapped - - def _wrap_property(self, name): - """Wrap a property with enhanced error handling""" - def _get_property(): - return getattr(self.yf_ticker, name) - return self.enhanced_yf._retry_with_backoff(_get_property) - - # Additional utility methods - def get_cache_info(self) -> Dict[str, Any]: - """Get cache information for this ticker""" - if self.enhanced_yf._cache: - return self.enhanced_yf._cache.get_cache_info() - return {'cache_enabled': False} - - def clear_cache(self): - """Clear cache for this ticker""" - if self.enhanced_yf._cache: - self.enhanced_yf._cache.clear() - - -def download(tickers: Union[str, list], - period: str = "1mo", - interval: str = "1d", - **kwargs) -> pd.DataFrame: - """Enhanced download function with better rate limiting""" - - config = RequestConfig( - max_retries=5, - base_delay=1.0, - max_delay=120.0 - ) - - enhanced_yf = EnhancedYFinance(config) - - def _download(): - return yf.download( - tickers=tickers, - period=period, - interval=interval, - session=enhanced_yf.session, - **kwargs - ) - - return enhanced_yf._retry_with_backoff(_download) - - -# Global configuration -_global_config = RequestConfig() - -def set_config(**kwargs): - """Set global configuration for enhanced yfinance""" - global _global_config - for key, value in kwargs.items(): - if hasattr(_global_config, key): - setattr(_global_config, key, value) - else: - raise ValueError(f"Unknown configuration option: {key}") - -def get_config() -> RequestConfig: - """Get current global configuration""" - return _global_config - -# Convenience functions with full yfinance API compatibility -def Ticker(symbol: str, session=None, proxy=None) -> EnhancedTicker: - """Create enhanced ticker (fully compatible with yfinance.Ticker)""" - config = RequestConfig( - max_retries=_global_config.max_retries, - base_delay=_global_config.base_delay, - max_delay=_global_config.max_delay, - jitter=_global_config.jitter, - user_agents=_global_config.user_agents, - enable_cache=_global_config.enable_cache, - cache_dir=_global_config.cache_dir - ) - - enhanced_yf = EnhancedYFinance(config) - - # Override session if provided (for compatibility) - if session is not None: - enhanced_yf._session = session - - return enhanced_yf.get_ticker(symbol) - -class EnhancedTickers: - """Enhanced Tickers class with full yfinance compatibility""" - - def __init__(self, tickers, session=None, proxy=None): - # Parse tickers like original yfinance - if isinstance(tickers, str): - tickers = tickers.replace(',', ' ').split() - elif not isinstance(tickers, list): - tickers = list(tickers) - - self.symbols = [ticker.upper() for ticker in tickers] - self.tickers = {symbol: Ticker(symbol, session, proxy) for symbol in self.symbols} - - # Store original yfinance Tickers for method delegation - self._yf_tickers = yf.Tickers(self.symbols, session=session) - - def __repr__(self): - return f"yfinance_enhanced.Tickers object <{','.join(self.symbols)}>" - - def __getattr__(self, name): - """Delegate any missing methods to original yfinance Tickers""" - return getattr(self._yf_tickers, name) - - def download(self, *args, **kwargs): - """Enhanced download for multiple tickers""" - return download(self.symbols, *args, **kwargs) - -# Additional yfinance compatibility functions -def Tickers(tickers, session=None, proxy=None) -> EnhancedTickers: - """Create enhanced tickers (fully compatible with yfinance.Tickers)""" - return EnhancedTickers(tickers, session, proxy) - -# Cache management functions -def clear_cache(): - """Clear all cached data""" - cache = HistoricalDataCache(_global_config.cache_dir) - cache.clear() - -def get_cache_info(): - """Get cache information""" - cache = HistoricalDataCache(_global_config.cache_dir) - return cache.get_cache_info() - -# Make the module work exactly like yfinance -from yfinance import __version__ - -# Export main classes and functions for full compatibility -__all__ = [ - 'Ticker', 'Tickers', 'download', 'set_config', 'get_config', - 'clear_cache', 'get_cache_info', 'RequestConfig', - 'EnhancedTicker', 'EnhancedTickers', 'EnhancedYFinance', 'HistoricalDataCache' -] - - -# Example usage -if __name__ == "__main__": - # Setup logging - logging.basicConfig(level=logging.INFO) - - # Create enhanced ticker - ticker = Ticker("AAPL") - - try: - # Test all yfinance functionality automatically - print("Testing enhanced yfinance with automatic wrapping...") - - # All these should work automatically via __getattr__ - print(f"Info: {ticker.info.get('longName', 'N/A')}") - print(f"History: {len(ticker.history(period='5d'))} rows") - print(f"Dividends: {len(ticker.dividends)} records") - print(f"Splits: {len(ticker.splits)} records") - print(f"Major holders: {ticker.major_holders.shape if hasattr(ticker.major_holders, 'shape') else 'Available'}") - print(f"Recommendations: {ticker.recommendations.shape if hasattr(ticker.recommendations, 'shape') else 'Available'}") - print(f"News: {len(ticker.news) if ticker.news else 0} articles") - - print("āœ… All yfinance functionality works automatically!") - - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() \ No newline at end of file diff --git a/yfinance_enhanced_cli.py b/yfinance_enhanced_cli.py deleted file mode 100644 index 9e6a0ba..0000000 --- a/yfinance_enhanced_cli.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env python3 -""" -Command Line Interface for yfinance-enhanced -Provides cache management and utility functions -""" - -import argparse -import sys -import json -import pickle -from pathlib import Path -from datetime import datetime - -try: - from yfinance_enhanced import get_cache_info, clear_cache, HistoricalDataCache, RequestConfig -except ImportError: - print("Error: yfinance_enhanced module not found. Please install it first.") - sys.exit(1) - - -def format_size(size_bytes): - """Format size in bytes to human readable format""" - if size_bytes == 0: - return "0 B" - - for unit in ['B', 'KB', 'MB', 'GB']: - if size_bytes < 1024.0: - return f"{size_bytes:.1f} {unit}" - size_bytes /= 1024.0 - return f"{size_bytes:.1f} TB" - - -def get_cache_file_details(cache_dir, filename): - """Load and analyze cache file to get details about stored data""" - try: - cache_file = Path(cache_dir) / filename - if not cache_file.exists(): - return None - - with open(cache_file, 'rb') as f: - cache_data = pickle.load(f) - - # Extract details from cache data - details = { - 'symbol': cache_data.get('symbol', 'Unknown'), - 'period': cache_data.get('period', 'Unknown'), - 'interval': cache_data.get('interval', '1d'), - 'rows': len(cache_data.get('data', [])), - 'date_range': None - } - - # Try to get date range from the data - data = cache_data.get('data') - if data is not None and hasattr(data, 'index') and len(data) > 0: - try: - start_date = data.index[0].strftime('%Y-%m-%d') - end_date = data.index[-1].strftime('%Y-%m-%d') - details['date_range'] = f"{start_date} to {end_date}" - except: - pass - - return details - - except Exception as e: - # If we can't read the cache file, return minimal info - return { - 'symbol': 'Error', - 'period': f'({str(e)[:20]}...)', - 'interval': '?', - 'rows': 0, - 'date_range': None - } - - -def cmd_cache_info(args): - """Show cache information""" - try: - info = get_cache_info() - - print("šŸ—‚ļø YFinance Plus Cache Information") - print("=" * 50) - print(f"Cache Directory: {info['cache_dir']}") - print(f"Number of Files: {info['file_count']}") - print(f"Total Size: {format_size(info['total_size_mb'] * 1024 * 1024)}") - - if info['file_count'] > 0: - print(f"\nšŸ“ Cache Files:") - print("-" * 100) - - sorted_files = sorted(info['files'], key=lambda x: x['modified'], reverse=True) - - for file_info in sorted_files[:args.limit if args.limit else len(sorted_files)]: - size_str = format_size(file_info['size_kb'] * 1024) - modified_str = file_info['modified'].strftime('%Y-%m-%d %H:%M:%S') - - # Try to load and analyze cache file content - cache_details = get_cache_file_details(info['cache_dir'], file_info['name']) - - print(f" šŸ“„ {file_info['name'][:32]:<32} {size_str:>8} {modified_str}") - if cache_details: - print(f" Symbol: {cache_details['symbol']:<8} Period: {cache_details['period']:<6} " - f"Interval: {cache_details['interval']:<4} Rows: {cache_details['rows']}") - if cache_details['date_range']: - print(f" Date Range: {cache_details['date_range']}") - else: - print(f" āš ļø Could not read cache file details") - print() - - if args.limit and len(sorted_files) > args.limit: - print(f" ... and {len(sorted_files) - args.limit} more files") - else: - print("\nšŸ’” No cache files found") - - except Exception as e: - print(f"āŒ Error getting cache info: {e}") - sys.exit(1) - - -def cmd_cache_clear(args): - """Clear cache""" - if not args.force: - try: - info = get_cache_info() - if info['file_count'] > 0: - response = input(f"āš ļø This will delete {info['file_count']} cache files " - f"({format_size(info['total_size_mb'] * 1024 * 1024)}). " - f"Continue? [y/N]: ") - if response.lower() not in ['y', 'yes']: - print("āŒ Operation cancelled") - return - except Exception: - pass - - try: - clear_cache() - print("āœ… Cache cleared successfully") - except Exception as e: - print(f"āŒ Error clearing cache: {e}") - sys.exit(1) - - -def cmd_config_show(args): - """Show current configuration""" - config = RequestConfig() - - print("āš™ļø YFinance Plus Configuration") - print("=" * 40) - print(f"Max Retries: {config.max_retries}") - print(f"Base Delay: {config.base_delay}s") - print(f"Max Delay: {config.max_delay}s") - print(f"Jitter Enabled: {config.jitter}") - print(f"Cache Enabled: {config.enable_cache}") - print(f"Cache Directory: {config.cache_dir}") - print(f"User Agents: {len(config.user_agents)} configured") - - -def cmd_test_connection(args): - """Test connection to Yahoo Finance""" - print("šŸ”— Testing connection to Yahoo Finance...") - - try: - # Import here to avoid circular imports - from yfinance_enhanced import Ticker - - print(" Creating test ticker...") - ticker = Ticker("AAPL") - - print(" Fetching basic info...") - info = ticker.info - company_name = info.get('longName', 'Unknown') - - print(" Fetching historical data...") - hist = ticker.history(period="5d") - - print(f"āœ… Connection test successful!") - print(f" Company: {company_name}") - print(f" Historical data points: {len(hist)}") - - if ticker.enhanced_yf._cache: - cache_info = ticker.get_cache_info() - print(f" Cache status: {cache_info['file_count']} files") - - except Exception as e: - print(f"āŒ Connection test failed: {e}") - sys.exit(1) - - -def cmd_benchmark(args): - """Run performance benchmark""" - print("šŸƒ Running performance benchmark...") - - try: - import time - from yfinance_enhanced import Ticker, download - - symbols = ["AAPL", "GOOGL", "MSFT", "TSLA", "NVDA"] - - # Test individual tickers - print(f" Testing individual tickers ({len(symbols)} symbols)...") - start_time = time.time() - - for symbol in symbols: - ticker = Ticker(symbol) - hist = ticker.history(period="1mo") - print(f" {symbol}: {len(hist)} data points") - - individual_time = time.time() - start_time - - # Test bulk download - print(f" Testing bulk download...") - start_time = time.time() - - data = download(symbols, period="1mo") - bulk_time = time.time() - start_time - - print(f"\nšŸ“Š Benchmark Results:") - print(f" Individual requests: {individual_time:.2f}s") - print(f" Bulk download: {bulk_time:.2f}s") - print(f" Bulk speedup: {individual_time/bulk_time:.2f}x") - print(f" Data shape: {data.shape}") - - except Exception as e: - print(f"āŒ Benchmark failed: {e}") - sys.exit(1) - - -def main(): - """Main CLI entry point""" - parser = argparse.ArgumentParser( - description="YFinance Plus CLI - Cache management and utilities", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - yfp cache info # Show cache information - yfp cache clear # Clear cache (with confirmation) - yfp cache clear --force # Clear cache without confirmation - yfp config show # Show current configuration - yfp test # Test connection to Yahoo Finance - yfp benchmark # Run performance benchmark - """ - ) - - subparsers = parser.add_subparsers(dest='command', help='Available commands') - - # Cache commands - cache_parser = subparsers.add_parser('cache', help='Cache management commands') - cache_subparsers = cache_parser.add_subparsers(dest='cache_command') - - # Cache info - info_parser = cache_subparsers.add_parser('info', help='Show cache information') - info_parser.add_argument('--limit', type=int, help='Limit number of files to show') - info_parser.set_defaults(func=cmd_cache_info) - - # Cache clear - clear_parser = cache_subparsers.add_parser('clear', help='Clear cache') - clear_parser.add_argument('--force', action='store_true', help='Clear without confirmation') - clear_parser.set_defaults(func=cmd_cache_clear) - - # Config commands - config_parser = subparsers.add_parser('config', help='Configuration commands') - config_subparsers = config_parser.add_subparsers(dest='config_command') - - # Config show - show_parser = config_subparsers.add_parser('show', help='Show current configuration') - show_parser.set_defaults(func=cmd_config_show) - - # Test command - test_parser = subparsers.add_parser('test', help='Test connection to Yahoo Finance') - test_parser.set_defaults(func=cmd_test_connection) - - # Benchmark command - benchmark_parser = subparsers.add_parser('benchmark', help='Run performance benchmark') - benchmark_parser.set_defaults(func=cmd_benchmark) - - # Parse arguments - args = parser.parse_args() - - # Handle no command - if not args.command: - parser.print_help() - return - - # Handle cache subcommands - if args.command == 'cache' and not args.cache_command: - cache_parser.print_help() - return - - # Handle config subcommands - if args.command == 'config' and not args.config_command: - config_parser.print_help() - return - - # Execute command - if hasattr(args, 'func'): - try: - args.func(args) - except KeyboardInterrupt: - print("\nāŒ Operation cancelled by user") - sys.exit(1) - else: - parser.print_help() - - -if __name__ == "__main__": - main() \ No newline at end of file