Initial commit: Discord stock bot with hourly PYPL updates

Functional Discord bot with automated hourly stock price updates during NYSE trading hours. Supports manual queries for any ticker via prefix commands.

🤖 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-02 22:25:37 -06:00
commit 3b6f0cbe4a
15 changed files with 1321 additions and 0 deletions

229
bot.py Normal file
View File

@@ -0,0 +1,229 @@
import discord
from discord.ext import commands
import logging
from datetime import datetime
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
import pytz
from config import Config
from stock_api import YahooFinanceAPI, FinnhubAPI
from market_hours import MarketHours
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class StockBot(commands.Bot):
"""Discord bot for stock price tracking and reporting."""
def __init__(self):
intents = discord.Intents.default()
intents.message_content = True
super().__init__(command_prefix=Config.COMMAND_PREFIX, intents=intents)
# Initialize stock API based on configuration
if Config.STOCK_API_PROVIDER == 'finnhub':
self.stock_api = FinnhubAPI(Config.FINNHUB_API_KEY)
logger.info("Using Finnhub API for stock data")
else:
self.stock_api = YahooFinanceAPI()
logger.info("Using Yahoo Finance API for stock data")
self.scheduler = AsyncIOScheduler(timezone=pytz.timezone('America/New_York'))
self.target_channel_id = int(Config.CHANNEL_ID)
self.primary_ticker = Config.PRIMARY_TICKER
async def setup_hook(self):
"""Initialize the bot and set up scheduled tasks."""
logger.info("Bot setup initiated")
# Schedule hourly updates during market hours
# Run every hour on the hour, but only post if market is open
self.scheduler.add_job(
self.send_hourly_update,
CronTrigger(minute=0, timezone=MarketHours.NYSE_TIMEZONE),
id='hourly_update',
name='Hourly Stock Price Update'
)
self.scheduler.start()
logger.info("Scheduler started for hourly updates")
async def on_ready(self):
"""Called when bot successfully connects to Discord."""
logger.info(f'Bot connected as {self.user.name} (ID: {self.user.id})')
logger.info(f'Monitoring ticker: {self.primary_ticker}')
logger.info(f'Target channel ID: {self.target_channel_id}')
# Verify channel exists
channel = self.get_channel(self.target_channel_id)
if channel:
logger.info(f'Target channel found: #{channel.name}')
else:
logger.error(f'Could not find channel with ID {self.target_channel_id}')
async def send_hourly_update(self):
"""Send hourly stock price update if market is open."""
if not MarketHours.is_market_open():
logger.info("Market is closed, skipping hourly update")
return
logger.info(f"Sending hourly update for {self.primary_ticker}")
await self.send_stock_update(self.primary_ticker, self.target_channel_id)
async def send_stock_update(self, ticker: str, channel_id: int):
"""
Fetch stock data and send formatted embed to specified channel.
Args:
ticker: Stock ticker symbol
channel_id: Discord channel ID to send message to
"""
channel = self.get_channel(channel_id)
if not channel:
logger.error(f"Could not find channel {channel_id}")
return
stock_data = self.stock_api.get_stock_price(ticker)
if not stock_data:
await channel.send(f"Unable to retrieve data for ticker: {ticker}")
return
embed = self.create_stock_embed(stock_data)
await channel.send(embed=embed)
logger.info(f"Sent stock update for {ticker} to channel {channel_id}")
def create_stock_embed(self, stock_data: dict) -> discord.Embed:
"""
Create a formatted Discord embed for stock price data.
Args:
stock_data: Dictionary containing stock information
Returns:
Discord embed object
"""
ticker = stock_data['ticker']
current_price = stock_data['current_price']
change_dollar = stock_data['change_dollar']
change_percent = stock_data['change_percent']
# Determine color based on price movement
if change_dollar > 0:
color = discord.Color.green()
change_emoji = "📈"
elif change_dollar < 0:
color = discord.Color.red()
change_emoji = "📉"
else:
color = discord.Color.blue()
change_emoji = "➡️"
# Format change string
change_sign = "+" if change_dollar >= 0 else ""
change_str = f"{change_sign}${change_dollar} ({change_sign}{change_percent}%)"
# Custom title for PYPL ticker
if ticker == "PYPL":
title = f"{change_emoji} Steaks Stablecoin Value"
else:
title = f"{change_emoji} {ticker} Stock Price"
# Create embed
embed = discord.Embed(
title=title,
description=f"**${current_price}**",
color=color,
timestamp=datetime.now(pytz.timezone('America/New_York'))
)
embed.add_field(name="Change", value=change_str, inline=True)
embed.add_field(name="Previous Close", value=f"${stock_data['previous_close']}", inline=True)
market_status = "🟢 Market Open" if stock_data['market_open'] else "🔴 Market Closed"
embed.set_footer(text=f"{market_status}")
return embed
# Create bot instance
bot = StockBot()
@bot.command(name='stock', aliases=['price'])
async def get_stock_price(ctx, ticker: str = None):
"""
Manually query stock price for any ticker.
Usage: !stock AAPL or !price TSLA
"""
if not ticker:
await ctx.send("Please provide a ticker symbol. Usage: `!stock AAPL`")
return
ticker = ticker.upper()
logger.info(f"Manual stock query for {ticker} by {ctx.author}")
stock_data = bot.stock_api.get_stock_price(ticker)
if not stock_data:
await ctx.send(f"Unable to retrieve data for ticker: {ticker}")
return
embed = bot.create_stock_embed(stock_data)
await ctx.send(embed=embed)
@bot.command(name='ping')
async def ping(ctx):
"""Check if bot is responsive."""
await ctx.send(f'Pong! Latency: {round(bot.latency * 1000)}ms')
@bot.command(name='market')
async def market_status(ctx):
"""Check if market is currently open."""
is_open = MarketHours.is_market_open()
status = "🟢 Market is currently OPEN" if is_open else "🔴 Market is currently CLOSED"
embed = discord.Embed(
title="NYSE Market Status",
description=status,
color=discord.Color.green() if is_open else discord.Color.red(),
timestamp=datetime.now(pytz.timezone('America/New_York'))
)
embed.add_field(
name="Trading Hours",
value="Monday-Friday\n9:30 AM - 4:00 PM ET",
inline=False
)
if not is_open:
next_open = MarketHours.get_next_market_open()
embed.add_field(
name="Next Open",
value=next_open.strftime("%A, %B %d at %I:%M %p ET"),
inline=False
)
await ctx.send(embed=embed)
def main():
"""Main entry point for the bot."""
if not Config.validate():
logger.error("Configuration validation failed")
return
logger.info("Starting bot...")
bot.run(Config.DISCORD_TOKEN)
if __name__ == '__main__':
main()