- Add chart_generator.py with price and candlestick chart support - Implement Yahoo Finance candle data fetching for free historical data - Update bot to generate and attach charts to stock embeds - Add matplotlib dependency to requirements.txt - Configure dual API approach: Finnhub for quotes, Yahoo for charts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
import yfinance as yf
|
|
import logging
|
|
from typing import Optional, Dict, Any, List
|
|
from .base import StockAPIBase
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class YahooFinanceAPI(StockAPIBase):
|
|
"""Yahoo Finance implementation of stock price provider."""
|
|
|
|
def get_stock_price(self, ticker: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Retrieve stock price data from Yahoo Finance.
|
|
|
|
Args:
|
|
ticker: Stock ticker symbol
|
|
|
|
Returns:
|
|
Dictionary with stock data or None if unavailable
|
|
"""
|
|
try:
|
|
stock = yf.Ticker(ticker)
|
|
|
|
# Get company name from info
|
|
company_name = None
|
|
try:
|
|
info = stock.info
|
|
company_name = info.get('longName') or info.get('shortName') or ticker.upper()
|
|
except Exception as e:
|
|
logger.warning(f"Could not fetch company name for {ticker}: {e}")
|
|
company_name = ticker.upper()
|
|
|
|
# Use history() method instead of info to avoid rate limiting
|
|
# Get last 2 days of data to calculate change
|
|
hist = stock.history(period="2d")
|
|
|
|
if hist.empty or len(hist) < 1:
|
|
logger.warning(f"No historical data available for ticker: {ticker}")
|
|
return None
|
|
|
|
# Get most recent price data
|
|
current_price = float(hist['Close'].iloc[-1])
|
|
|
|
# Get previous close (either from previous day or use current data)
|
|
if len(hist) >= 2:
|
|
previous_close = float(hist['Close'].iloc[-2])
|
|
else:
|
|
previous_close = float(hist['Open'].iloc[-1])
|
|
|
|
change_dollar = current_price - previous_close
|
|
change_percent = (change_dollar / previous_close) * 100
|
|
|
|
# Market is considered open if we have today's data
|
|
market_open = True # Simplified - actual market status requires additional API call
|
|
|
|
return {
|
|
'ticker': ticker.upper(),
|
|
'company_name': company_name,
|
|
'current_price': round(current_price, 2),
|
|
'previous_close': round(previous_close, 2),
|
|
'change_dollar': round(change_dollar, 2),
|
|
'change_percent': round(change_percent, 2),
|
|
'market_open': market_open
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching data for {ticker}: {e}")
|
|
return None
|
|
|
|
def is_available(self) -> bool:
|
|
"""
|
|
Check if Yahoo Finance API is accessible.
|
|
|
|
Returns:
|
|
True if accessible, False otherwise
|
|
"""
|
|
try:
|
|
test_stock = yf.Ticker("AAPL")
|
|
info = test_stock.info
|
|
return info is not None and len(info) > 0
|
|
except Exception as e:
|
|
logger.error(f"Yahoo Finance API unavailable: {e}")
|
|
return False
|
|
|
|
def get_candles(self, ticker: str, days: int = 30, resolution: str = 'D') -> Optional[Dict[str, List]]:
|
|
"""
|
|
Retrieve historical candlestick data from Yahoo Finance.
|
|
|
|
Args:
|
|
ticker: Stock ticker symbol
|
|
days: Number of days of historical data to fetch
|
|
resolution: Candle resolution (ignored, always daily for Yahoo Finance)
|
|
|
|
Returns:
|
|
Dictionary with OHLCV data or None if unavailable
|
|
"""
|
|
try:
|
|
stock = yf.Ticker(ticker)
|
|
|
|
# Fetch historical data
|
|
hist = stock.history(period=f"{days}d")
|
|
|
|
if hist.empty:
|
|
logger.warning(f"No candle data available for ticker: {ticker}")
|
|
return None
|
|
|
|
# Convert to the format expected by chart generator
|
|
timestamps = [int(date.timestamp()) for date in hist.index]
|
|
opens = hist['Open'].tolist()
|
|
highs = hist['High'].tolist()
|
|
lows = hist['Low'].tolist()
|
|
closes = hist['Close'].tolist()
|
|
volumes = hist['Volume'].tolist()
|
|
|
|
return {
|
|
'timestamps': timestamps,
|
|
'open': opens,
|
|
'high': highs,
|
|
'low': lows,
|
|
'close': closes,
|
|
'volume': volumes
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching candle data for {ticker}: {e}")
|
|
return None
|