""" Market Scheduler 장 시간 관리 및 스케줄링 """ from enum import Enum from datetime import datetime, time from typing import Optional, Callable, Awaitable import logging import asyncio import pytz logger = logging.getLogger(__name__) class MarketStatus(Enum): """장 상태""" PRE_MARKET = "PRE_MARKET" # 장 전 (08:00~09:00) MARKET_OPEN = "MARKET_OPEN" # 정규장 (09:00~15:30) AFTER_MARKET = "AFTER_MARKET" # 장 후 (15:30~18:00) MARKET_CLOSED = "MARKET_CLOSED" # 휴장 (18:00~08:00) class MarketScheduler: """ 장 시간 관리 및 스케줄 실행 - 현재 장 상태 파악 - 장 시작/종료 콜백 실행 - 다음 장 시작까지 대기 """ # 한국 시간 기준 PRE_MARKET_START = time(8, 0) MARKET_OPEN = time(9, 0) MARKET_CLOSE = time(15, 30) AFTER_MARKET_END = time(18, 0) def __init__(self): self.kst = pytz.timezone("Asia/Seoul") self.current_status: Optional[MarketStatus] = None self.on_market_open: Optional[Callable[[], Awaitable[None]]] = None self.on_market_close: Optional[Callable[[], Awaitable[None]]] = None logger.info("📅 MarketScheduler 초기화") def get_current_status(self) -> MarketStatus: """현재 장 상태 조회""" now = datetime.now(self.kst).time() if self.PRE_MARKET_START <= now < self.MARKET_OPEN: return MarketStatus.PRE_MARKET elif self.MARKET_OPEN <= now < self.MARKET_CLOSE: return MarketStatus.MARKET_OPEN elif self.MARKET_CLOSE <= now < self.AFTER_MARKET_END: return MarketStatus.AFTER_MARKET else: return MarketStatus.MARKET_CLOSED def is_market_open(self) -> bool: """현재 정규장 개장 여부""" return self.get_current_status() == MarketStatus.MARKET_OPEN def register_callbacks( self, on_open: Optional[Callable[[], Awaitable[None]]] = None, on_close: Optional[Callable[[], Awaitable[None]]] = None ): """장 시작/종료 콜백 등록""" if on_open: self.on_market_open = on_open logger.info("✅ 장 시작 콜백 등록") if on_close: self.on_market_close = on_close logger.info("✅ 장 종료 콜백 등록") async def run(self): """ 스케줄러 실행 (무한 루프) 장 상태 변화 감지 및 콜백 실행 """ logger.info("🚀 MarketScheduler 시작") while True: try: status = self.get_current_status() # 상태 변화 감지 if status != self.current_status: logger.info(f"📊 장 상태 변경: {self.current_status} → {status}") # 장 시작 if status == MarketStatus.MARKET_OPEN and self.on_market_open: logger.info("🔔 장 시작 콜백 실행") await self.on_market_open() # 장 종료 elif self.current_status == MarketStatus.MARKET_OPEN and status == MarketStatus.AFTER_MARKET: if self.on_market_close: logger.info("🔕 장 종료 콜백 실행") await self.on_market_close() self.current_status = status # 1분마다 상태 체크 await asyncio.sleep(60) except Exception as e: logger.error(f"❌ 스케줄러 오류: {e}", exc_info=True) await asyncio.sleep(60) def get_seconds_until_market_open(self) -> Optional[int]: """다음 장 시작까지 남은 시간 (초)""" now = datetime.now(self.kst) today = now.date() # 오늘 장 시작 시간 market_open_today = datetime.combine(today, self.MARKET_OPEN, tzinfo=self.kst) if now.time() < self.MARKET_OPEN: # 오늘 장 시작 전 return int((market_open_today - now).total_seconds()) else: # 내일 장 시작 대기 from datetime import timedelta market_open_tomorrow = market_open_today + timedelta(days=1) return int((market_open_tomorrow - now).total_seconds()) def get_seconds_until_market_close(self) -> Optional[int]: """장 종료까지 남은 시간 (초)""" now = datetime.now(self.kst) today = now.date() market_close_today = datetime.combine(today, self.MARKET_CLOSE, tzinfo=self.kst) if now.time() < self.MARKET_CLOSE: return int((market_close_today - now).total_seconds()) else: return 0 # 싱글톤 market_scheduler = MarketScheduler()