"""미국 시장 스케줄 관리 - 미국 정규장: 09:30~16:00 ET (한국시간 23:30~06:00, 서머타임 시 22:30~05:00) - 프리마켓: 04:00~09:30 ET - 애프터마켓: 16:00~20:00 ET - 미국 공휴일 관리 """ import logging from datetime import datetime, time, timedelta from typing import Optional import pytz logger = logging.getLogger(__name__) # ── 타임존 ──────────────────────────────────────────────────────────────────── ET = pytz.timezone("US/Eastern") KST = pytz.timezone("Asia/Seoul") # ── 미국 시장 시간 (Eastern Time) ───────────────────────────────────────────── PREMARKET_OPEN = time(4, 0) # 04:00 ET MARKET_OPEN = time(9, 30) # 09:30 ET MARKET_CLOSE = time(16, 0) # 16:00 ET AFTERMARKET_CLOSE = time(20, 0) # 20:00 ET # ── 2026년 미국 주식시장 휴장일 ────────────────────────────────────────────── # NYSE/NASDAQ 공식 휴장일 (반장 제외) US_HOLIDAYS_2026 = { # New Year's Day datetime(2026, 1, 1).date(), # Martin Luther King Jr. Day (3rd Monday of January) datetime(2026, 1, 19).date(), # Presidents' Day (3rd Monday of February) datetime(2026, 2, 16).date(), # Good Friday datetime(2026, 4, 3).date(), # Memorial Day (last Monday of May) datetime(2026, 5, 25).date(), # Juneteenth National Independence Day datetime(2026, 6, 19).date(), # Independence Day (observed) datetime(2026, 7, 3).date(), # Labor Day (1st Monday of September) datetime(2026, 9, 7).date(), # Thanksgiving Day (4th Thursday of November) datetime(2026, 11, 26).date(), # Christmas Day datetime(2026, 12, 25).date(), } # 추가 연도 지원을 위한 동적 휴일 계산 US_HOLIDAYS_2025 = { datetime(2025, 1, 1).date(), datetime(2025, 1, 20).date(), datetime(2025, 2, 17).date(), datetime(2025, 4, 18).date(), datetime(2025, 5, 26).date(), datetime(2025, 6, 19).date(), datetime(2025, 7, 4).date(), datetime(2025, 9, 1).date(), datetime(2025, 11, 27).date(), datetime(2025, 12, 25).date(), } # ── 2027년 미국 주식시장 휴장일 ────────────────────────────────────────────── # NYSE/NASDAQ 공식 휴장일 (반장 제외) US_HOLIDAYS_2027 = { # New Year's Day (Friday) datetime(2027, 1, 1).date(), # Martin Luther King Jr. Day (3rd Monday of January) datetime(2027, 1, 18).date(), # Presidents' Day (3rd Monday of February) datetime(2027, 2, 15).date(), # Good Friday (Easter 2027-03-28 기준) datetime(2027, 3, 26).date(), # Memorial Day (last Monday of May) datetime(2027, 5, 31).date(), # Juneteenth (June 19 = Saturday → Friday June 18 observed) datetime(2027, 6, 18).date(), # Independence Day (July 4 = Sunday → Monday July 5 observed) datetime(2027, 7, 5).date(), # Labor Day (1st Monday of September) datetime(2027, 9, 6).date(), # Thanksgiving Day (4th Thursday of November) datetime(2027, 11, 25).date(), # Christmas Day (December 25 = Saturday → Friday December 24 observed) datetime(2027, 12, 24).date(), } _ALL_HOLIDAYS = US_HOLIDAYS_2025 | US_HOLIDAYS_2026 | US_HOLIDAYS_2027 def _get_holidays_set(): """holidays 패키지가 있으면 사용, 없으면 하드코딩 세트 반환.""" try: import holidays as holidays_pkg return holidays_pkg.US(years=[2025, 2026, 2027, 2028], categories={"public"}) except ImportError: return None def _now_et() -> datetime: """현재 시각을 US/Eastern 기준으로 반환 (timezone-aware).""" return datetime.now(ET) def is_dst() -> bool: """현재 미국이 서머타임(DST) 기간인지 확인. DST 기간: 3월 둘째 일요일 ~ 11월 첫째 일요일 DST 기간에는 한국과의 시차가 13시간 (비DST: 14시간). Returns: True if 현재 DST 적용 중 """ now = _now_et() # pytz의 dst()는 DST 오프셋을 반환 (0이면 비DST) return bool(now.dst()) def is_us_holiday(date: Optional[datetime] = None) -> bool: """해당 날짜가 미국 주식시장 휴장일인지 확인. Args: date: 확인할 날짜. None이면 현재 ET 날짜 사용. datetime 또는 date 객체 모두 허용. Returns: True if 휴장일 """ if date is None: check_date = _now_et().date() elif isinstance(date, datetime): check_date = date.date() else: check_date = date # holidays 패키지 시도 holidays_cal = _get_holidays_set() if holidays_cal is not None: return check_date in holidays_cal # 하드코딩 fallback return check_date in _ALL_HOLIDAYS def is_us_market_open() -> bool: """미국 정규장이 현재 열려있는지 확인. 조건: - 평일 (월~금) - 휴장일 아님 - 09:30 ET <= 현재시각 < 16:00 ET Returns: True if 정규 거래시간 """ now = _now_et() # 주말 체크 (토: 5, 일: 6) if now.weekday() >= 5: return False # 휴장일 체크 if is_us_holiday(now): return False current_time = now.time() return MARKET_OPEN <= current_time < MARKET_CLOSE def is_us_premarket() -> bool: """미국 프리마켓 시간인지 확인. 프리마켓: 04:00 ~ 09:30 ET (평일, 비휴장일) Returns: True if 프리마켓 시간 """ now = _now_et() if now.weekday() >= 5: return False if is_us_holiday(now): return False current_time = now.time() return PREMARKET_OPEN <= current_time < MARKET_OPEN def is_us_aftermarket() -> bool: """미국 애프터마켓 시간인지 확인. 애프터마켓: 16:00 ~ 20:00 ET (평일, 비휴장일) Returns: True if 애프터마켓 시간 """ now = _now_et() if now.weekday() >= 5: return False if is_us_holiday(now): return False current_time = now.time() return MARKET_CLOSE <= current_time < AFTERMARKET_CLOSE def get_next_us_market_open() -> datetime: """다음 미국 정규장 개장 시각 반환 (timezone-aware, US/Eastern). 현재 정규장이 열려있으면 오늘 개장 시각을 반환. 그렇지 않으면 다음 거래일의 09:30 ET을 반환. Returns: 다음 개장 시각 (datetime, US/Eastern timezone) """ now = _now_et() today_open = now.replace(hour=9, minute=30, second=0, microsecond=0) # 오늘이 거래일이고 아직 개장 전이면 → 오늘 09:30 if now.weekday() < 5 and not is_us_holiday(now) and now.time() < MARKET_OPEN: return ET.localize(today_open.replace(tzinfo=None)) if today_open.tzinfo is None else today_open # 오늘이 거래일이고 정규장 중이면 → 오늘 09:30 if is_us_market_open(): return ET.localize(today_open.replace(tzinfo=None)) if today_open.tzinfo is None else today_open # 다음 거래일 탐색 (최대 10일 — 연휴 대비) next_day = now + timedelta(days=1) for _ in range(10): next_day = next_day.replace(hour=9, minute=30, second=0, microsecond=0) if next_day.weekday() < 5 and not is_us_holiday(next_day): if next_day.tzinfo is None: return ET.localize(next_day) return next_day next_day += timedelta(days=1) # fallback: 내일 09:30 (이론적으로 도달 불가) fallback = (now + timedelta(days=1)).replace(hour=9, minute=30, second=0, microsecond=0) return ET.localize(fallback.replace(tzinfo=None)) if fallback.tzinfo is None else fallback def get_us_market_status() -> dict: """현재 미국 시장 상태 요약 반환. Returns: dict with keys: - status: 'OPEN', 'PREMARKET', 'AFTERMARKET', 'CLOSED' - is_dst: bool - current_et: str (현재 ET 시각) - next_open: str (다음 개장 시각) """ now = _now_et() if is_us_market_open(): status = "OPEN" elif is_us_premarket(): status = "PREMARKET" elif is_us_aftermarket(): status = "AFTERMARKET" else: status = "CLOSED" return { "status": status, "is_dst": is_dst(), "current_et": now.strftime("%Y-%m-%d %H:%M:%S %Z"), "next_open": get_next_us_market_open().strftime("%Y-%m-%d %H:%M:%S %Z"), }