Replace Yahoo Finance with Polygon.io for chart data
- Add Polygon.io API client for reliable historical data access - Update bot to use Polygon instead of Yahoo Finance for charts - Add POLYGON_API_KEY to config and environment example - Polygon free tier: 5 API calls/minute, more reliable than Yahoo - Fallback to FinViz chart URL when Polygon unavailable 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ COMMAND_PREFIX=!
|
|||||||
PRIMARY_TICKER=PYPL
|
PRIMARY_TICKER=PYPL
|
||||||
STOCK_API_PROVIDER=finnhub
|
STOCK_API_PROVIDER=finnhub
|
||||||
FINNHUB_API_KEY=your_finnhub_api_key_here
|
FINNHUB_API_KEY=your_finnhub_api_key_here
|
||||||
|
POLYGON_API_KEY=your_polygon_api_key_here
|
||||||
|
|
||||||
# Scheduling Configuration
|
# Scheduling Configuration
|
||||||
UPDATE_INTERVAL_HOURS=1
|
UPDATE_INTERVAL_HOURS=1
|
||||||
|
|||||||
21
bot.py
21
bot.py
@@ -11,6 +11,7 @@ from stock_api import YahooFinanceAPI, FinnhubAPI
|
|||||||
from market_hours import MarketHours
|
from market_hours import MarketHours
|
||||||
from crypto_api import CoinGeckoAPI
|
from crypto_api import CoinGeckoAPI
|
||||||
from chart_generator import ChartGenerator
|
from chart_generator import ChartGenerator
|
||||||
|
from polygon_api import PolygonAPI
|
||||||
|
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -37,9 +38,13 @@ class StockBot(commands.Bot):
|
|||||||
self.stock_api = YahooFinanceAPI()
|
self.stock_api = YahooFinanceAPI()
|
||||||
logger.info("Using Yahoo Finance API for stock data")
|
logger.info("Using Yahoo Finance API for stock data")
|
||||||
|
|
||||||
# Always initialize Yahoo Finance for chart data (free historical data)
|
# Initialize Polygon.io for chart data (free historical data)
|
||||||
self.yahoo_api = YahooFinanceAPI()
|
if Config.POLYGON_API_KEY:
|
||||||
logger.info("Using Yahoo Finance API for chart data")
|
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.scheduler = AsyncIOScheduler(timezone=pytz.timezone('America/New_York'))
|
||||||
self.target_channel_ids = [int(id) for id in Config.CHANNEL_IDS]
|
self.target_channel_ids = [int(id) for id in Config.CHANNEL_IDS]
|
||||||
@@ -258,9 +263,10 @@ class StockBot(commands.Bot):
|
|||||||
await channel.send(f"Unable to retrieve data for ticker: {ticker}")
|
await channel.send(f"Unable to retrieve data for ticker: {ticker}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Generate chart using Yahoo Finance historical data
|
# Generate chart using Polygon.io historical data
|
||||||
chart_file = None
|
chart_file = None
|
||||||
candle_data = self.yahoo_api.get_candles(ticker, days=30)
|
if self.chart_api:
|
||||||
|
candle_data = self.chart_api.get_candles(ticker, days=30)
|
||||||
if candle_data:
|
if candle_data:
|
||||||
chart_buffer = ChartGenerator.create_price_chart(
|
chart_buffer = ChartGenerator.create_price_chart(
|
||||||
ticker,
|
ticker,
|
||||||
@@ -553,9 +559,10 @@ async def get_stock_price(ctx, ticker: str = None):
|
|||||||
await ctx.send(f"Unable to retrieve data for ticker: {ticker}")
|
await ctx.send(f"Unable to retrieve data for ticker: {ticker}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Generate chart using Yahoo Finance historical data
|
# Generate chart using Polygon.io historical data
|
||||||
chart_file = None
|
chart_file = None
|
||||||
candle_data = bot.yahoo_api.get_candles(ticker, days=30)
|
if bot.chart_api:
|
||||||
|
candle_data = bot.chart_api.get_candles(ticker, days=30)
|
||||||
if candle_data:
|
if candle_data:
|
||||||
chart_buffer = ChartGenerator.create_price_chart(
|
chart_buffer = ChartGenerator.create_price_chart(
|
||||||
ticker,
|
ticker,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Config:
|
|||||||
PRIMARY_TICKER = os.getenv('PRIMARY_TICKER', 'PYPL')
|
PRIMARY_TICKER = os.getenv('PRIMARY_TICKER', 'PYPL')
|
||||||
STOCK_API_PROVIDER = os.getenv('STOCK_API_PROVIDER', 'finnhub') # 'yahoo' or 'finnhub'
|
STOCK_API_PROVIDER = os.getenv('STOCK_API_PROVIDER', 'finnhub') # 'yahoo' or 'finnhub'
|
||||||
FINNHUB_API_KEY = os.getenv('FINNHUB_API_KEY')
|
FINNHUB_API_KEY = os.getenv('FINNHUB_API_KEY')
|
||||||
|
POLYGON_API_KEY = os.getenv('POLYGON_API_KEY')
|
||||||
|
|
||||||
# Earnings calendar configuration
|
# Earnings calendar configuration
|
||||||
EARNINGS_MIN_MARKET_CAP_BILLIONS = float(os.getenv('EARNINGS_MIN_MARKET_CAP_BILLIONS', '10'))
|
EARNINGS_MIN_MARKET_CAP_BILLIONS = float(os.getenv('EARNINGS_MIN_MARKET_CAP_BILLIONS', '10'))
|
||||||
|
|||||||
115
polygon_api.py
Normal file
115
polygon_api.py
Normal file
@@ -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
|
||||||
69
test_polygon.py
Normal file
69
test_polygon.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user