diff --git a/services/task-watcher/mode.py b/services/task-watcher/mode.py new file mode 100644 index 0000000..075ac65 --- /dev/null +++ b/services/task-watcher/mode.py @@ -0,0 +1,57 @@ +"""시간대 + 휴장일 기반 모드 판정 (idle 감지 생략 — 박재오 결정 2026-05-22). + +trading: 비휴장 평일 07:00–16:30 (장중) → queue:paused SET +free: 그 외 (장 전/후, 주말, 휴장) → queue:paused DEL +""" +from __future__ import annotations + +import datetime as dt +import logging +import os +from typing import Set +from zoneinfo import ZoneInfo + +import httpx + +logger = logging.getLogger(__name__) + +KST = ZoneInfo("Asia/Seoul") +STOCK_BASE_URL = os.getenv("STOCK_BASE_URL", "http://192.168.45.54:18500") + +# 트레이딩 윈도우 (HH:MM, KST). .env로 조정 가능. +TRADING_START = os.getenv("TRADING_START", "07:00") +TRADING_END = os.getenv("TRADING_END", "16:30") + + +def _parse_hhmm(s: str) -> dt.time: + hh, mm = s.split(":") + return dt.time(int(hh), int(mm)) + + +def current_mode(now: dt.datetime, holidays: Set[str]) -> str: + """now(KST aware) + holidays(ISO date set) → 'trading' | 'free'.""" + # 주말 (토=5, 일=6) + if now.weekday() >= 5: + return "free" + # 휴장일 + if now.date().isoformat() in holidays: + return "free" + # 트레이딩 윈도우 [start, end) + start = _parse_hhmm(TRADING_START) + end = _parse_hhmm(TRADING_END) + t = now.timetz().replace(tzinfo=None) + if start <= t < end: + return "trading" + return "free" + + +def fetch_holidays() -> Set[str]: + """NAS stock /api/stock/holidays 조회. 실패 시 빈 set (안전 — free로 판정).""" + try: + r = httpx.get(f"{STOCK_BASE_URL}/api/stock/holidays", timeout=10.0) + if r.status_code == 200: + return set(r.json().get("holidays", [])) + logger.warning("holidays fetch returned %d", r.status_code) + except Exception: + logger.exception("holidays fetch 실패") + return set() diff --git a/services/task-watcher/tests/__init__.py b/services/task-watcher/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/task-watcher/tests/test_mode.py b/services/task-watcher/tests/test_mode.py new file mode 100644 index 0000000..42d2995 --- /dev/null +++ b/services/task-watcher/tests/test_mode.py @@ -0,0 +1,44 @@ +"""current_mode — 시간대 + 휴장일 판정 (순수 함수).""" +import datetime as dt +from zoneinfo import ZoneInfo + +from mode import current_mode + +KST = ZoneInfo("Asia/Seoul") +HOLIDAYS = {"2026-05-25"} # 가상 휴장일 (월요일) + + +def _kst(y, m, d, hh, mm): + return dt.datetime(y, m, d, hh, mm, tzinfo=KST) + + +def test_weekday_trading_hours_is_trading(): + # 2026-05-22 금요일 10:00 — 트레이딩 시간대 + assert current_mode(_kst(2026, 5, 22, 10, 0), HOLIDAYS) == "trading" + + +def test_weekday_before_open_is_free(): + # 평일 06:00 — 장 전 + assert current_mode(_kst(2026, 5, 22, 6, 0), HOLIDAYS) == "free" + + +def test_weekday_after_close_is_free(): + # 평일 17:00 — 장 마감 후 + assert current_mode(_kst(2026, 5, 22, 17, 0), HOLIDAYS) == "free" + + +def test_weekend_is_free(): + # 2026-05-23 토요일 10:00 + assert current_mode(_kst(2026, 5, 23, 10, 0), HOLIDAYS) == "free" + + +def test_holiday_weekday_is_free(): + # 2026-05-25 월요일이지만 휴장일 → 트레이딩 시간대라도 free + assert current_mode(_kst(2026, 5, 25, 10, 0), HOLIDAYS) == "free" + + +def test_trading_boundary_inclusive_start_exclusive_end(): + # 07:00 정각 = 트레이딩 시작, 16:30 정각 = 마감 (16:30은 free) + assert current_mode(_kst(2026, 5, 22, 7, 0), HOLIDAYS) == "trading" + assert current_mode(_kst(2026, 5, 22, 16, 29), HOLIDAYS) == "trading" + assert current_mode(_kst(2026, 5, 22, 16, 30), HOLIDAYS) == "free"