Files
discord-stock-bot/chart_generator.py
Michael Simard 1325863837 Switch to candlestick charts with 30-day daily data
- 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 <noreply@anthropic.com>
2025-12-26 12:14:15 -06:00

189 lines
6.6 KiB
Python

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
from typing import Dict, List, Optional
import logging
import io
logger = logging.getLogger(__name__)
class ChartGenerator:
"""Generates stock price charts using matplotlib."""
@staticmethod
def create_price_chart(
ticker: str,
candle_data: Dict[str, List],
company_name: Optional[str] = None
) -> Optional[io.BytesIO]:
"""
Create a price chart from candle data.
Args:
ticker: Stock ticker symbol
candle_data: Dictionary with timestamps, open, high, low, close, volume
company_name: Optional company name for title
Returns:
BytesIO buffer containing the chart image, or None if error
"""
try:
# Convert timestamps to datetime objects
dates = [datetime.fromtimestamp(ts) for ts in candle_data['timestamps']]
closes = candle_data['close']
# Create figure and axis
fig, ax = plt.subplots(figsize=(12, 6), facecolor='#1e1e1e')
ax.set_facecolor('#2d2d2d')
# Plot closing prices
ax.plot(dates, closes, color='#00d4ff', linewidth=2)
# Fill area under the line
ax.fill_between(dates, closes, alpha=0.3, color='#00d4ff')
# 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
ax.grid(True, alpha=0.2, color='#ffffff', linestyle='-', linewidth=0.5)
# Labels and title
title = f"{company_name} ({ticker})" if company_name else ticker
ax.set_title(title, color='#ffffff', fontsize=14, fontweight='bold', pad=20)
ax.set_xlabel('Date', color='#ffffff', fontsize=10)
ax.set_ylabel('Price ($)', color='#ffffff', fontsize=10)
# Tick colors
ax.tick_params(colors='#ffffff', which='both')
# Spine colors
for spine in ax.spines.values():
spine.set_color('#444444')
# Tight layout
plt.tight_layout()
# Save to buffer
buffer = io.BytesIO()
plt.savefig(buffer, format='png', facecolor='#1e1e1e', dpi=100)
buffer.seek(0)
plt.close(fig)
return buffer
except Exception as e:
logger.error(f"Error generating chart for {ticker}: {e}")
return None
@staticmethod
def create_candlestick_chart(
ticker: str,
candle_data: Dict[str, List],
company_name: Optional[str] = None
) -> Optional[io.BytesIO]:
"""
Create a candlestick chart from candle data.
Args:
ticker: Stock ticker symbol
candle_data: Dictionary with timestamps, open, high, low, close, volume
company_name: Optional company name for title
Returns:
BytesIO buffer containing the chart image, or None if error
"""
try:
# Convert timestamps to datetime objects
dates = [datetime.fromtimestamp(ts) for ts in candle_data['timestamps']]
opens = candle_data['open']
highs = candle_data['high']
lows = candle_data['low']
closes = candle_data['close']
# Create figure and axis
fig, ax = plt.subplots(figsize=(12, 6), facecolor='#1e1e1e')
ax.set_facecolor('#2d2d2d')
# Calculate bar width
width = 0.6
# Plot candlesticks
for i in range(len(dates)):
color = '#00ff88' if closes[i] >= opens[i] else '#ff4444'
# High-low line
ax.plot([dates[i], dates[i]], [lows[i], highs[i]], color=color, linewidth=1)
# Open-close rectangle
height = abs(closes[i] - opens[i])
bottom = min(opens[i], closes[i])
ax.bar(dates[i], height, width, bottom=bottom, color=color, alpha=0.8)
# 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
ax.grid(True, alpha=0.2, color='#ffffff', linestyle='-', linewidth=0.5)
# Labels and title
title = f"{company_name} ({ticker})" if company_name else ticker
ax.set_title(title, color='#ffffff', fontsize=14, fontweight='bold', pad=20)
ax.set_xlabel('Date', color='#ffffff', fontsize=10)
ax.set_ylabel('Price ($)', color='#ffffff', fontsize=10)
# Tick colors
ax.tick_params(colors='#ffffff', which='both')
# Spine colors
for spine in ax.spines.values():
spine.set_color('#444444')
# Tight layout
plt.tight_layout()
# Save to buffer
buffer = io.BytesIO()
plt.savefig(buffer, format='png', facecolor='#1e1e1e', dpi=100)
buffer.seek(0)
plt.close(fig)
return buffer
except Exception as e:
logger.error(f"Error generating candlestick chart for {ticker}: {e}")
return None