Files
auto-stock/backend/app/realtime/market_scheduler.py

146 lines
4.9 KiB
Python

"""
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()