Files
discord-stock-bot/chart_generator.py
Michael Simard 8792c03e64 Remove weekend gaps from candlestick charts
- Use index positions instead of datetime for x-axis
- All candles now evenly spaced (no gaps for weekends/holidays)
- Date labels still show actual dates at regular intervals
- Updated example chart to reflect changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 12:17:40 -06:00

202 lines
7.1 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')
# Use index positions to remove weekend gaps
x_pos = list(range(len(closes)))
# Plot closing prices
ax.plot(x_pos, closes, color='#00d4ff', linewidth=2)
# Fill area under the line
ax.fill_between(x_pos, closes, alpha=0.3, color='#00d4ff')
# Format x-axis with date labels (removing gaps)
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
# Select tick positions (show ~7-10 labels)
tick_spacing = max(1, len(dates) // 7)
tick_indices = list(range(0, len(dates), tick_spacing))
if tick_indices[-1] != len(dates) - 1:
tick_indices.append(len(dates) - 1)
if is_intraday:
# Show time for intraday data
tick_labels = [dates[i].strftime('%H:%M') for i in tick_indices]
else:
# Show date for multi-day data
tick_labels = [dates[i].strftime('%m/%d') for i in tick_indices]
ax.set_xticks(tick_indices)
ax.set_xticklabels(tick_labels, 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 using index positions (removes weekend gaps)
for i in range(len(dates)):
color = '#00ff88' if closes[i] >= opens[i] else '#ff4444'
# High-low line
ax.plot([i, 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(i, height, width, bottom=bottom, color=color, alpha=0.8)
# Format x-axis with date labels (removing gaps)
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
# Select tick positions (show ~7-10 labels)
tick_spacing = max(1, len(dates) // 7)
tick_indices = list(range(0, len(dates), tick_spacing))
if tick_indices[-1] != len(dates) - 1:
tick_indices.append(len(dates) - 1)
if is_intraday:
# Show time for intraday data
tick_labels = [dates[i].strftime('%H:%M') for i in tick_indices]
else:
# Show date for multi-day data
tick_labels = [dates[i].strftime('%m/%d') for i in tick_indices]
ax.set_xticks(tick_indices)
ax.set_xticklabels(tick_labels, 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