feat(task-watcher): mode.py — 시간대+휴장일 판정 (SP-10)
current_mode(now, holidays): 비휴장 평일 07:00–16:30 → trading, 그 외 free. fetch_holidays(): NAS /api/stock/holidays 조회 (실패 시 빈 set = free 안전). TRADING_START/END env로 윈도우 조정. idle 감지 생략 (박재오 결정). 6 tests (평일 장중/장전/장후, 주말, 휴장, 경계). Plan-B-Infra Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
57
services/task-watcher/mode.py
Normal file
57
services/task-watcher/mode.py
Normal file
@@ -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()
|
||||||
0
services/task-watcher/tests/__init__.py
Normal file
0
services/task-watcher/tests/__init__.py
Normal file
44
services/task-watcher/tests/test_mode.py
Normal file
44
services/task-watcher/tests/test_mode.py
Normal file
@@ -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"
|
||||||
Reference in New Issue
Block a user