refactor(phase3): Create realtime module core structure
This commit is contained in:
145
backend/app/realtime/market_scheduler.py
Normal file
145
backend/app/realtime/market_scheduler.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user