"""Polling scheduler — 시간대별 분기 + 휴장일 처리.""" from __future__ import annotations import json import logging from datetime import datetime, timedelta, time from pathlib import Path from zoneinfo import ZoneInfo logger = logging.getLogger(__name__) KST = ZoneInfo("Asia/Seoul") _HOLIDAYS_PATH = Path(__file__).parent / "holidays.json" _HOLIDAYS: set[str] = set(json.loads(_HOLIDAYS_PATH.read_text(encoding="utf-8"))) # Market windows (정규장) _PRE_OPEN = time(7, 0) _OPEN = time(9, 0) _CLOSE = time(15, 30) _POST_END = time(20, 0) # NXT windows (시간외) _NXT_PRE_END = time(23, 30) _NXT_POST_OPEN = time(4, 30) # 23:30 - 04:30 (dead zone) skip def _is_market_day(now: datetime) -> bool: """평일 + 휴장일 아닌 날.""" if now.weekday() >= 5: # Sat/Sun return False return now.strftime("%Y-%m-%d") not in _HOLIDAYS def _is_polling_window(now: datetime) -> bool: """폴링 윈도우: 07:00-23:30 + 04:30-07:00.""" t = now.time() return ( (_PRE_OPEN <= t < _NXT_PRE_END) or (_NXT_POST_OPEN <= t < _PRE_OPEN) ) def _next_interval(now: datetime) -> float: """다음 폴링까지 sleep 초수.""" if not _is_market_day(now): return _seconds_until_next_market_open(now) t = now.time() if _PRE_OPEN <= t < _OPEN: return 300.0 # 장전 5분 elif _OPEN <= t < _CLOSE: return 60.0 # 장중 1분 elif _CLOSE <= t < _POST_END: return 300.0 # 장후 5분 elif _POST_END <= t < _NXT_PRE_END: return 300.0 # NXT 야간 5분 elif _NXT_POST_OPEN <= t < _PRE_OPEN: return 300.0 # NXT 새벽 5분 else: # Dead zone (23:30-04:30) — wait until next 04:30 return _seconds_until_nxt_or_market_open(now) def _seconds_until_nxt_or_market_open(now: datetime) -> float: """다음 04:30 (NXT 새벽 start) 까지 초수. 휴장일은 다음 영업일 07:00.""" candidate = now.replace(hour=4, minute=30, second=0, microsecond=0) if candidate <= now: candidate += timedelta(days=1) for _ in range(14): if _is_market_day(candidate): return (candidate - now).total_seconds() candidate += timedelta(days=1) logger.warning("could not find next market day within 14 days") return 86400.0 def _is_post_close_trigger(now: datetime) -> bool: """16:00 KST ±1분 (post-close cycle 트리거). 평일/영업일만.""" if not _is_market_day(now): return False t = now.time() return time(16, 0) <= t < time(16, 1) def _seconds_until_next_market_open(now: datetime) -> float: """다음 영업일의 07:00 KST 까지 초수 (휴장일/주말용).""" candidate = now.replace(hour=7, minute=0, second=0, microsecond=0) if candidate <= now: candidate += timedelta(days=1) for _ in range(14): # safety bound (max 2 weeks of holidays) if _is_market_day(candidate): return (candidate - now).total_seconds() candidate += timedelta(days=1) logger.warning("could not find next market day within 14 days") return 86400.0