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