diff --git a/.env.example b/.env.example index d4143bf..eedbeb4 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ COMMAND_PREFIX=! PRIMARY_TICKER=PYPL STOCK_API_PROVIDER=finnhub FINNHUB_API_KEY=your_finnhub_api_key_here +POLYGON_API_KEY=your_polygon_api_key_here # Scheduling Configuration UPDATE_INTERVAL_HOURS=1 diff --git a/bot.py b/bot.py index 63919e4..7024e33 100644 --- a/bot.py +++ b/bot.py @@ -11,6 +11,7 @@ from stock_api import YahooFinanceAPI, FinnhubAPI from market_hours import MarketHours from crypto_api import CoinGeckoAPI from chart_generator import ChartGenerator +from polygon_api import PolygonAPI # Configure logging @@ -37,9 +38,13 @@ class StockBot(commands.Bot): self.stock_api = YahooFinanceAPI() logger.info("Using Yahoo Finance API for stock data") - # Always initialize Yahoo Finance for chart data (free historical data) - self.yahoo_api = YahooFinanceAPI() - logger.info("Using Yahoo Finance API for chart data") + # Initialize Polygon.io for chart data (free historical data) + if Config.POLYGON_API_KEY: + self.chart_api = PolygonAPI(Config.POLYGON_API_KEY) + logger.info("Using Polygon.io API for chart data") + else: + logger.warning("POLYGON_API_KEY not set, charts will use FinViz fallback") + self.chart_api = None self.scheduler = AsyncIOScheduler(timezone=pytz.timezone('America/New_York')) self.target_channel_ids = [int(id) for id in Config.CHANNEL_IDS] @@ -258,17 +263,18 @@ class StockBot(commands.Bot): await channel.send(f"Unable to retrieve data for ticker: {ticker}") return - # Generate chart using Yahoo Finance historical data + # Generate chart using Polygon.io historical data chart_file = None - candle_data = self.yahoo_api.get_candles(ticker, days=30) - if candle_data: - chart_buffer = ChartGenerator.create_price_chart( - ticker, - candle_data, - stock_data.get('company_name') - ) - if chart_buffer: - chart_file = discord.File(chart_buffer, filename="chart.png") + if self.chart_api: + candle_data = self.chart_api.get_candles(ticker, days=30) + if candle_data: + chart_buffer = ChartGenerator.create_price_chart( + ticker, + candle_data, + stock_data.get('company_name') + ) + if chart_buffer: + chart_file = discord.File(chart_buffer, filename="chart.png") # Create embed with appropriate chart reference embed = self.create_stock_embed(stock_data, has_chart_attachment=bool(chart_file)) @@ -553,17 +559,18 @@ async def get_stock_price(ctx, ticker: str = None): await ctx.send(f"Unable to retrieve data for ticker: {ticker}") return - # Generate chart using Yahoo Finance historical data + # Generate chart using Polygon.io historical data chart_file = None - candle_data = bot.yahoo_api.get_candles(ticker, days=30) - if candle_data: - chart_buffer = ChartGenerator.create_price_chart( - ticker, - candle_data, - stock_data.get('company_name') - ) - if chart_buffer: - chart_file = discord.File(chart_buffer, filename="chart.png") + if bot.chart_api: + candle_data = bot.chart_api.get_candles(ticker, days=30) + if candle_data: + chart_buffer = ChartGenerator.create_price_chart( + ticker, + candle_data, + stock_data.get('company_name') + ) + if chart_buffer: + chart_file = discord.File(chart_buffer, filename="chart.png") # Create embed with appropriate chart reference embed = bot.create_stock_embed(stock_data, has_chart_attachment=bool(chart_file)) diff --git a/config.py b/config.py index 1da4c49..036a016 100644 --- a/config.py +++ b/config.py @@ -19,6 +19,7 @@ class Config: PRIMARY_TICKER = os.getenv('PRIMARY_TICKER', 'PYPL') STOCK_API_PROVIDER = os.getenv('STOCK_API_PROVIDER', 'finnhub') # 'yahoo' or 'finnhub' FINNHUB_API_KEY = os.getenv('FINNHUB_API_KEY') + POLYGON_API_KEY = os.getenv('POLYGON_API_KEY') # Earnings calendar configuration EARNINGS_MIN_MARKET_CAP_BILLIONS = float(os.getenv('EARNINGS_MIN_MARKET_CAP_BILLIONS', '10')) diff --git a/polygon_api.py b/polygon_api.py new file mode 100644 index 0000000..f068925 --- /dev/null +++ b/polygon_api.py @@ -0,0 +1,115 @@ +"""Polygon.io API client for historical stock data.""" +import requests +import logging +from typing import Optional, Dict, List +from datetime import datetime, timedelta + +logger = logging.getLogger(__name__) + + +class PolygonAPI: + """Polygon.io implementation for historical stock data.""" + + def __init__(self, api_key: str): + """ + Initialize Polygon API client. + + Args: + api_key: Polygon.io API key + """ + self.api_key = api_key + self.base_url = "https://api.polygon.io" + + def get_candles(self, ticker: str, days: int = 30, resolution: str = 'D') -> Optional[Dict[str, List]]: + """ + Retrieve historical candlestick data from Polygon.io. + + Args: + ticker: Stock ticker symbol + days: Number of days of historical data to fetch + resolution: Candle resolution (ignored, always daily) + + Returns: + Dictionary with OHLCV data or None if unavailable + """ + try: + # Calculate date range + end_date = datetime.now() + start_date = end_date - timedelta(days=days) + + # Format dates as YYYY-MM-DD + from_date = start_date.strftime('%Y-%m-%d') + to_date = end_date.strftime('%Y-%m-%d') + + # Build API URL + url = f"{self.base_url}/v2/aggs/ticker/{ticker}/range/1/day/{from_date}/{to_date}" + + # Make request + params = { + 'adjusted': 'true', + 'sort': 'asc', + 'apiKey': self.api_key + } + + logger.info(f"Fetching Polygon data for {ticker} from {from_date} to {to_date}") + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + + data = response.json() + + # Check if we got results + if data.get('status') != 'OK' or not data.get('results'): + logger.warning(f"No candle data available for ticker: {ticker}") + return None + + results = data['results'] + + # Convert to expected format + timestamps = [] + opens = [] + highs = [] + lows = [] + closes = [] + volumes = [] + + for candle in results: + # Polygon returns timestamp in milliseconds + timestamps.append(int(candle['t'] / 1000)) + opens.append(float(candle['o'])) + highs.append(float(candle['h'])) + lows.append(float(candle['l'])) + closes.append(float(candle['c'])) + volumes.append(int(candle['v'])) + + logger.info(f"Successfully fetched {len(timestamps)} candles for {ticker}") + + return { + 'timestamps': timestamps, + 'open': opens, + 'high': highs, + 'low': lows, + 'close': closes, + 'volume': volumes + } + + except requests.exceptions.RequestException as e: + logger.error(f"HTTP error fetching candle data for {ticker}: {e}") + return None + except Exception as e: + logger.error(f"Error fetching candle data for {ticker}: {e}") + return None + + def is_available(self) -> bool: + """ + Check if Polygon API is accessible. + + Returns: + True if accessible, False otherwise + """ + try: + # Test with a simple request + test_data = self.get_candles("AAPL", days=5) + return test_data is not None + except Exception as e: + logger.error(f"Polygon API unavailable: {e}") + return False diff --git a/test_polygon.py b/test_polygon.py new file mode 100644 index 0000000..0190007 --- /dev/null +++ b/test_polygon.py @@ -0,0 +1,69 @@ +"""Test script for Polygon.io API integration.""" +import logging +from polygon_api import PolygonAPI +from chart_generator import ChartGenerator +import os +from dotenv import load_dotenv + +load_dotenv() + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +def test_polygon_integration(): + """Test Polygon.io API and chart generation.""" + + api_key = os.getenv('POLYGON_API_KEY') + if not api_key: + logger.error("POLYGON_API_KEY not set in .env file") + return False + + logger.info("Initializing Polygon API") + api = PolygonAPI(api_key) + + # Test tickers + tickers = ["AAPL", "TSLA"] + + for ticker in tickers: + logger.info(f"\nTesting {ticker}...") + + # Fetch candle data + logger.info(f"Fetching candle data for {ticker}") + candle_data = api.get_candles(ticker, days=30) + + if not candle_data: + logger.error(f"Failed to fetch candle data for {ticker}") + continue + + logger.info(f"Successfully fetched {len(candle_data['timestamps'])} candles") + logger.info(f"Price range: ${min(candle_data['low']):.2f} - ${max(candle_data['high']):.2f}") + + # Generate chart + logger.info(f"Generating chart for {ticker}") + chart = ChartGenerator.create_price_chart(ticker, candle_data, f"{ticker} Test") + + if not chart: + logger.error(f"Failed to generate chart for {ticker}") + continue + + # Save chart + filename = f"/tmp/{ticker}_polygon_chart.png" + with open(filename, "wb") as f: + f.write(chart.getvalue()) + logger.info(f"Chart saved: {filename} ({len(chart.getvalue())} bytes)") + + logger.info("\nPolygon.io integration test completed successfully!") + return True + + +if __name__ == "__main__": + try: + success = test_polygon_integration() + exit(0 if success else 1) + except Exception as e: + logger.error(f"Test failed: {e}", exc_info=True) + exit(1)