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>
This commit is contained in:
12
bot.py
12
bot.py
@@ -263,12 +263,12 @@ 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 Polygon.io historical data
|
# Generate chart using Polygon.io historical data (daily candlesticks)
|
||||||
chart_file = None
|
chart_file = None
|
||||||
if self.chart_api:
|
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:
|
if candle_data:
|
||||||
chart_buffer = ChartGenerator.create_price_chart(
|
chart_buffer = ChartGenerator.create_candlestick_chart(
|
||||||
ticker,
|
ticker,
|
||||||
candle_data,
|
candle_data,
|
||||||
stock_data.get('company_name')
|
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}")
|
await ctx.send(f"Unable to retrieve data for ticker: {ticker}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Generate chart using Polygon.io historical data
|
# Generate chart using Polygon.io historical data (daily candlesticks)
|
||||||
chart_file = None
|
chart_file = None
|
||||||
if bot.chart_api:
|
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:
|
if candle_data:
|
||||||
chart_buffer = ChartGenerator.create_price_chart(
|
chart_buffer = ChartGenerator.create_candlestick_chart(
|
||||||
ticker,
|
ticker,
|
||||||
candle_data,
|
candle_data,
|
||||||
stock_data.get('company_name')
|
stock_data.get('company_name')
|
||||||
|
|||||||
@@ -45,9 +45,22 @@ class ChartGenerator:
|
|||||||
# Fill area under the line
|
# Fill area under the line
|
||||||
ax.fill_between(dates, closes, alpha=0.3, color='#00d4ff')
|
ax.fill_between(dates, closes, alpha=0.3, color='#00d4ff')
|
||||||
|
|
||||||
# Format x-axis
|
# 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_formatter(mdates.DateFormatter('%m/%d'))
|
||||||
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
|
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
|
||||||
|
|
||||||
plt.xticks(rotation=45, ha='right')
|
plt.xticks(rotation=45, ha='right')
|
||||||
|
|
||||||
# Grid styling
|
# Grid styling
|
||||||
@@ -125,9 +138,22 @@ class ChartGenerator:
|
|||||||
bottom = min(opens[i], closes[i])
|
bottom = min(opens[i], closes[i])
|
||||||
ax.bar(dates[i], height, width, bottom=bottom, color=color, alpha=0.8)
|
ax.bar(dates[i], height, width, bottom=bottom, color=color, alpha=0.8)
|
||||||
|
|
||||||
# Format x-axis
|
# 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_formatter(mdates.DateFormatter('%m/%d'))
|
||||||
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
|
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
|
||||||
|
|
||||||
plt.xticks(rotation=45, ha='right')
|
plt.xticks(rotation=45, ha='right')
|
||||||
|
|
||||||
# Grid styling
|
# Grid styling
|
||||||
|
|||||||
@@ -26,14 +26,35 @@ class PolygonAPI:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Stock ticker symbol
|
ticker: Stock ticker symbol
|
||||||
days: Number of days of historical data to fetch
|
days: Number of days of historical data to fetch (ignored for intraday)
|
||||||
resolution: Candle resolution (ignored, always daily)
|
resolution: Candle resolution ('D' for daily, '5min' requires paid tier)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with OHLCV data or None if unavailable
|
Dictionary with OHLCV data or None if unavailable
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Calculate date range (end yesterday to avoid missing today's data)
|
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)
|
||||||
|
|
||||||
|
# Set to today at 9:30 AM ET
|
||||||
|
market_open = now_et.replace(hour=9, minute=30, second=0, microsecond=0)
|
||||||
|
|
||||||
|
# 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)
|
end_date = datetime.now() - timedelta(days=1)
|
||||||
start_date = end_date - timedelta(days=days)
|
start_date = end_date - timedelta(days=days)
|
||||||
|
|
||||||
@@ -41,7 +62,7 @@ class PolygonAPI:
|
|||||||
from_date = start_date.strftime('%Y-%m-%d')
|
from_date = start_date.strftime('%Y-%m-%d')
|
||||||
to_date = end_date.strftime('%Y-%m-%d')
|
to_date = end_date.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
# Build API URL
|
# Build API URL for daily bars
|
||||||
url = f"{self.base_url}/v2/aggs/ticker/{ticker}/range/1/day/{from_date}/{to_date}"
|
url = f"{self.base_url}/v2/aggs/ticker/{ticker}/range/1/day/{from_date}/{to_date}"
|
||||||
|
|
||||||
# Make request
|
# Make request
|
||||||
|
|||||||
69
test_candlestick.py
Normal file
69
test_candlestick.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user