From 13258638373d0e37f6132cf8c0e5d6b422557ea9 Mon Sep 17 00:00:00 2001 From: Michael Simard Date: Fri, 26 Dec 2025 12:14:15 -0600 Subject: [PATCH] Switch to candlestick charts with 30-day daily data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace line charts with candlestick charts for better visualization - Green candles for up days, red for down days - Daily resolution (30 days) - free tier compatible - Auto-detect intraday vs daily data for proper time/date formatting - Note: 5-min intraday requires paid Polygon tier ($199/month) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- bot.py | 12 ++++---- chart_generator.py | 38 +++++++++++++++++++++---- polygon_api.py | 41 ++++++++++++++++++++------- test_candlestick.py | 69 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 test_candlestick.py diff --git a/bot.py b/bot.py index 7024e33..9d153a3 100644 --- a/bot.py +++ b/bot.py @@ -263,12 +263,12 @@ class StockBot(commands.Bot): await channel.send(f"Unable to retrieve data for ticker: {ticker}") return - # Generate chart using Polygon.io historical data + # Generate chart using Polygon.io historical data (daily candlesticks) chart_file = None if self.chart_api: - candle_data = self.chart_api.get_candles(ticker, days=30) + candle_data = self.chart_api.get_candles(ticker, days=30, resolution='D') if candle_data: - chart_buffer = ChartGenerator.create_price_chart( + chart_buffer = ChartGenerator.create_candlestick_chart( ticker, candle_data, stock_data.get('company_name') @@ -559,12 +559,12 @@ async def get_stock_price(ctx, ticker: str = None): await ctx.send(f"Unable to retrieve data for ticker: {ticker}") return - # Generate chart using Polygon.io historical data + # Generate chart using Polygon.io historical data (daily candlesticks) chart_file = None if bot.chart_api: - candle_data = bot.chart_api.get_candles(ticker, days=30) + candle_data = bot.chart_api.get_candles(ticker, days=30, resolution='D') if candle_data: - chart_buffer = ChartGenerator.create_price_chart( + chart_buffer = ChartGenerator.create_candlestick_chart( ticker, candle_data, stock_data.get('company_name') diff --git a/chart_generator.py b/chart_generator.py index 312d990..5654d0e 100644 --- a/chart_generator.py +++ b/chart_generator.py @@ -45,9 +45,22 @@ class ChartGenerator: # Fill area under the line ax.fill_between(dates, closes, alpha=0.3, color='#00d4ff') - # Format x-axis - ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) - ax.xaxis.set_major_locator(mdates.AutoDateLocator()) + # Format x-axis (detect if intraday or multi-day) + if len(dates) > 0: + # Check if all data is from the same day + first_date = dates[0].date() + last_date = dates[-1].date() + is_intraday = first_date == last_date + + if is_intraday: + # Show time for intraday data + ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) + ax.xaxis.set_major_locator(mdates.HourLocator(interval=1)) + else: + # Show date for multi-day data + ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) + ax.xaxis.set_major_locator(mdates.AutoDateLocator()) + plt.xticks(rotation=45, ha='right') # Grid styling @@ -125,9 +138,22 @@ class ChartGenerator: bottom = min(opens[i], closes[i]) ax.bar(dates[i], height, width, bottom=bottom, color=color, alpha=0.8) - # Format x-axis - ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) - ax.xaxis.set_major_locator(mdates.AutoDateLocator()) + # Format x-axis (detect if intraday or multi-day) + if len(dates) > 0: + # Check if all data is from the same day + first_date = dates[0].date() + last_date = dates[-1].date() + is_intraday = first_date == last_date + + if is_intraday: + # Show time for intraday data + ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) + ax.xaxis.set_major_locator(mdates.HourLocator(interval=1)) + else: + # Show date for multi-day data + ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) + ax.xaxis.set_major_locator(mdates.AutoDateLocator()) + plt.xticks(rotation=45, ha='right') # Grid styling diff --git a/polygon_api.py b/polygon_api.py index ca9274f..66012be 100644 --- a/polygon_api.py +++ b/polygon_api.py @@ -26,23 +26,44 @@ class PolygonAPI: Args: ticker: Stock ticker symbol - days: Number of days of historical data to fetch - resolution: Candle resolution (ignored, always daily) + days: Number of days of historical data to fetch (ignored for intraday) + resolution: Candle resolution ('D' for daily, '5min' requires paid tier) Returns: Dictionary with OHLCV data or None if unavailable """ try: - # Calculate date range (end yesterday to avoid missing today's data) - end_date = datetime.now() - timedelta(days=1) - start_date = end_date - timedelta(days=days) + if resolution == '5min': + # For 5-minute bars, get current day data + # Start at market open (9:30 AM ET) + import pytz + et_tz = pytz.timezone('America/New_York') + now_et = datetime.now(et_tz) - # Format dates as YYYY-MM-DD - from_date = start_date.strftime('%Y-%m-%d') - to_date = end_date.strftime('%Y-%m-%d') + # Set to today at 9:30 AM ET + market_open = now_et.replace(hour=9, minute=30, second=0, microsecond=0) - # Build API URL - url = f"{self.base_url}/v2/aggs/ticker/{ticker}/range/1/day/{from_date}/{to_date}" + # If current time is before market open, use yesterday + if now_et < market_open: + market_open = market_open - timedelta(days=1) + now_et = market_open.replace(hour=16, minute=0) # Use previous day's close + + from_date = market_open.strftime('%Y-%m-%d') + to_date = now_et.strftime('%Y-%m-%d') + + # Build API URL for 5-minute bars + url = f"{self.base_url}/v2/aggs/ticker/{ticker}/range/5/minute/{from_date}/{to_date}" + else: + # Daily bars - Calculate date range (end yesterday to avoid missing today's data) + end_date = datetime.now() - timedelta(days=1) + 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 for daily bars + url = f"{self.base_url}/v2/aggs/ticker/{ticker}/range/1/day/{from_date}/{to_date}" # Make request params = { diff --git a/test_candlestick.py b/test_candlestick.py new file mode 100644 index 0000000..48fa26d --- /dev/null +++ b/test_candlestick.py @@ -0,0 +1,69 @@ +"""Test script for candlestick chart generation.""" +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_candlestick_charts(): + """Test candlestick chart generation with Polygon data.""" + + 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 candlestick chart for {ticker}...") + + # Fetch candle data + logger.info(f"Fetching 30-day candle data for {ticker}") + candle_data = api.get_candles(ticker, days=30, resolution='D') + + 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 candlestick chart + logger.info(f"Generating candlestick chart for {ticker}") + chart = ChartGenerator.create_candlestick_chart(ticker, candle_data, f"{ticker}") + + if not chart: + logger.error(f"Failed to generate chart for {ticker}") + continue + + # Save chart + filename = f"/tmp/{ticker}_candlestick_30d.png" + with open(filename, "wb") as f: + f.write(chart.getvalue()) + logger.info(f"Chart saved: {filename} ({len(chart.getvalue())} bytes)") + + logger.info("\nCandlestick chart test completed successfully!") + return True + + +if __name__ == "__main__": + try: + success = test_candlestick_charts() + exit(0 if success else 1) + except Exception as e: + logger.error(f"Test failed: {e}", exc_info=True) + exit(1)