Implement custom 5-minute candlestick charts via Finnhub + QuickChart

Replace FinViz charts with custom-generated candlestick charts:
- Fetch 5-minute OHLC data from Finnhub API
- Generate candlestick chart images via QuickChart API
- Display last 50 candles with time-based x-axis
- Fallback to FinViz daily chart if intraday data unavailable

New FinnhubAPI methods:
- get_intraday_candles(): Fetch 5-min candle data
- generate_chart_url(): Create QuickChart URL from candle data

Chart specifications: 800x400px, Chart.js v3, candlestick type

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Simard
2025-12-03 19:31:18 -06:00
parent 30ebd85712
commit 6f6b6360ca
2 changed files with 109 additions and 5 deletions

13
bot.py
View File

@@ -169,7 +169,18 @@ class StockBot(commands.Bot):
embed.add_field(name="Change", value=change_str, inline=True) embed.add_field(name="Change", value=change_str, inline=True)
embed.add_field(name="Previous Close", value=f"${stock_data['previous_close']}", inline=True) embed.add_field(name="Previous Close", value=f"${stock_data['previous_close']}", inline=True)
# Add FinViz daily chart # Add 5-minute intraday chart from Finnhub + QuickChart
if hasattr(self.stock_api, 'get_intraday_candles'):
candles = self.stock_api.get_intraday_candles(ticker, resolution=5, days_back=1)
if candles:
chart_url = self.stock_api.generate_chart_url(ticker, candles)
embed.set_image(url=chart_url)
else:
# Fallback to FinViz daily chart if intraday data unavailable
chart_url = f"https://finviz.com/chart.ashx?t={ticker}&ty=c&ta=1&p=d&s=l"
embed.set_image(url=chart_url)
else:
# Fallback for non-Finnhub APIs
chart_url = f"https://finviz.com/chart.ashx?t={ticker}&ty=c&ta=1&p=d&s=l" chart_url = f"https://finviz.com/chart.ashx?t={ticker}&ty=c&ta=1&p=d&s=l"
embed.set_image(url=chart_url) embed.set_image(url=chart_url)

View File

@@ -1,9 +1,11 @@
import finnhub import finnhub
import logging import logging
from typing import Optional, Dict, Any from typing import Optional, Dict, Any, List
from .base import StockAPIBase from .base import StockAPIBase
from datetime import datetime from datetime import datetime, timedelta
import pytz import pytz
import urllib.parse
import json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -80,3 +82,94 @@ class FinnhubAPI(StockAPIBase):
except Exception as e: except Exception as e:
logger.error(f"Finnhub API unavailable: {e}") logger.error(f"Finnhub API unavailable: {e}")
return False return False
def get_intraday_candles(self, ticker: str, resolution: int = 5, days_back: int = 1) -> Optional[List[Dict[str, Any]]]:
"""
Retrieve intraday candlestick data from Finnhub.
Args:
ticker: Stock ticker symbol
resolution: Candle resolution in minutes (1, 5, 15, 30, 60)
days_back: Number of days of historical data to fetch
Returns:
List of candle dictionaries or None if unavailable
"""
try:
# Calculate time range (Unix timestamps)
now = datetime.now(pytz.timezone('America/New_York'))
end_time = int(now.timestamp())
start_time = int((now - timedelta(days=days_back)).timestamp())
# Fetch candle data
candles = self.client.stock_candles(ticker, resolution, start_time, end_time)
if not candles or candles.get('s') != 'ok':
logger.warning(f"No candle data available for {ticker}")
return None
# Format candles for chart consumption
formatted_candles = []
for i in range(len(candles['t'])):
formatted_candles.append({
'x': candles['t'][i] * 1000, # Convert to milliseconds
'o': round(candles['o'][i], 2),
'h': round(candles['h'][i], 2),
'l': round(candles['l'][i], 2),
'c': round(candles['c'][i], 2)
})
return formatted_candles
except Exception as e:
logger.error(f"Error fetching candles for {ticker}: {e}")
return None
def generate_chart_url(self, ticker: str, candles: List[Dict[str, Any]]) -> str:
"""
Generate QuickChart URL for candlestick chart.
Args:
ticker: Stock ticker symbol
candles: List of candle dictionaries with x, o, h, l, c keys
Returns:
QuickChart URL string
"""
# Limit to last 50 candles for readability
chart_data = candles[-50:] if len(candles) > 50 else candles
chart_config = {
'type': 'candlestick',
'data': {
'datasets': [{
'label': f'{ticker} 5-Minute',
'data': chart_data
}]
},
'options': {
'plugins': {
'title': {
'display': True,
'text': f'{ticker} - 5 Minute Chart'
}
},
'scales': {
'x': {
'type': 'time',
'time': {
'unit': 'minute',
'displayFormats': {
'minute': 'HH:mm'
}
}
}
}
}
}
# Encode config for URL
config_json = json.dumps(chart_config)
encoded_config = urllib.parse.quote(config_json)
return f"https://quickchart.io/chart?v=3&c={encoded_config}&width=800&height=400"