Bot enhancements: - Add scheduled update at market close (4:00 PM ET on trading days) - Implement is_trading_day() method to check weekdays/holidays - Market close update sends PYPL price at end of trading day Deployment changes: - Update all scripts to use Docker Compose Manager plugin path - New path: /boot/config/plugins/compose.manager/projects/discord-stock-bot - Container now integrates with Unraid Docker UI Compose section - Update documentation with plugin integration details 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
5.0 KiB
Python
150 lines
5.0 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 is_trading_day(cls, check_time: datetime = None) -> bool:
|
|
"""
|
|
Determine if the given date is a trading day (weekday, not a holiday).
|
|
|
|
Args:
|
|
check_time: Datetime to check (defaults to now)
|
|
|
|
Returns:
|
|
True if it's a trading day, 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
|
|
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):
|
|
return False
|
|
|
|
return True
|
|
|
|
@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
|