146 lines
4.9 KiB
Python
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()
|