Files
discord-stock-bot/market_hours.py
Michael Simard 5964cadf94 Initial commit: Discord stock price bot with hourly PYPL updates
Implemented Discord bot for automated stock price tracking and reporting with the following features:

- Hourly PayPal (PYPL) stock price updates during NYSE market hours
- Custom branding for PYPL as "Steaks Stablecoin Value"
- Manual stock price queries via !stock and !price commands
- Multi-provider stock API support (Yahoo Finance and Finnhub)
- NYSE market hours detection with holiday awareness
- Discord embed formatting with color-coded price changes
- Docker containerization for consistent deployment
- Comprehensive documentation and deployment guides

Technical stack:
- Python 3.9+ with discord.py
- Finnhub API for stock price data
- APScheduler for hourly automated updates
- Docker support for local and Unraid deployment

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 21:54:11 -06:00

122 lines
4.1 KiB
Python

from datetime import datetime, time
import pytz
import logging
logger = logging.getLogger(__name__)
class MarketHours:
"""
Utility class for determining NYSE market hours.
Market is open 9:30 AM - 4:00 PM ET, Monday-Friday (excluding holidays).
"""
NYSE_TIMEZONE = pytz.timezone('America/New_York')
MARKET_OPEN = time(9, 30)
MARKET_CLOSE = time(16, 0)
# Major NYSE holidays (this is a simplified list - production systems would use a holiday calendar API)
HOLIDAYS_2024 = [
datetime(2024, 1, 1), # New Year's Day
datetime(2024, 1, 15), # MLK Day
datetime(2024, 2, 19), # Presidents Day
datetime(2024, 3, 29), # Good Friday
datetime(2024, 5, 27), # Memorial Day
datetime(2024, 6, 19), # Juneteenth
datetime(2024, 7, 4), # Independence Day
datetime(2024, 9, 2), # Labor Day
datetime(2024, 11, 28), # Thanksgiving
datetime(2024, 12, 25), # Christmas
]
HOLIDAYS_2025 = [
datetime(2025, 1, 1), # New Year's Day
datetime(2025, 1, 20), # MLK Day
datetime(2025, 2, 17), # Presidents Day
datetime(2025, 4, 18), # Good Friday
datetime(2025, 5, 26), # Memorial Day
datetime(2025, 6, 19), # Juneteenth
datetime(2025, 7, 4), # Independence Day
datetime(2025, 9, 1), # Labor Day
datetime(2025, 11, 27), # Thanksgiving
datetime(2025, 12, 25), # Christmas
]
@classmethod
def is_market_open(cls, check_time: datetime = None) -> bool:
"""
Determine if the NYSE is currently open.
Args:
check_time: Datetime to check (defaults to now)
Returns:
True if market is open, False otherwise
"""
if check_time is None:
check_time = datetime.now(cls.NYSE_TIMEZONE)
else:
check_time = check_time.astimezone(cls.NYSE_TIMEZONE)
# Check if weekend
if check_time.weekday() >= 5: # Saturday = 5, Sunday = 6
logger.debug("Market closed: Weekend")
return False
# Check if holiday
check_date = check_time.date()
all_holidays = cls.HOLIDAYS_2024 + cls.HOLIDAYS_2025
if any(holiday.date() == check_date for holiday in all_holidays):
logger.debug(f"Market closed: Holiday ({check_date})")
return False
# Check if within market hours
current_time = check_time.time()
if cls.MARKET_OPEN <= current_time < cls.MARKET_CLOSE:
return True
else:
logger.debug(f"Market closed: Outside trading hours ({current_time})")
return False
@classmethod
def get_next_market_open(cls, from_time: datetime = None) -> datetime:
"""
Calculate the next time the market will open.
Args:
from_time: Starting datetime (defaults to now)
Returns:
Datetime of next market open
"""
if from_time is None:
from_time = datetime.now(cls.NYSE_TIMEZONE)
else:
from_time = from_time.astimezone(cls.NYSE_TIMEZONE)
# Start checking from the next day at market open
next_day = from_time.replace(hour=cls.MARKET_OPEN.hour,
minute=cls.MARKET_OPEN.minute,
second=0,
microsecond=0)
# If we have not passed today's open time, check today first
if from_time.time() < cls.MARKET_OPEN:
next_day = next_day
else:
# Otherwise start from tomorrow
next_day = next_day.replace(day=next_day.day + 1)
# Find the next valid market day
max_iterations = 14 # Search up to 2 weeks ahead
for _ in range(max_iterations):
if cls.is_market_open(next_day):
return next_day
# Move to next day
next_day = next_day.replace(day=next_day.day + 1)
# Fallback (should not reach here under normal circumstances)
logger.warning("Could not determine next market open within 2 weeks")
return next_day