"""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) 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-20:00) 안인가.""" return _PRE_OPEN <= now.time() < _POST_END 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 elif _OPEN <= t < _CLOSE: return 60.0 elif _CLOSE <= t < _POST_END: return 300.0 else: return _seconds_until_next_market_open(now) 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; using +1 day") return 86400.0