diff --git a/signal_v2/scheduler.py b/signal_v2/scheduler.py index 952cabe..23265c5 100644 --- a/signal_v2/scheduler.py +++ b/signal_v2/scheduler.py @@ -12,12 +12,17 @@ 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 +# 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: """평일 + 휴장일 아닌 날.""" @@ -27,8 +32,12 @@ def _is_market_day(now: datetime) -> bool: def _is_polling_window(now: datetime) -> bool: - """현재 시각이 폴링 윈도우 (07:00-20:00) 안인가.""" - return _PRE_OPEN <= now.time() < _POST_END + """폴링 윈도우: 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: @@ -38,17 +47,37 @@ def _next_interval(now: datetime) -> float: t = now.time() if _PRE_OPEN <= t < _OPEN: - return 300.0 + return 300.0 # 장전 5분 elif _OPEN <= t < _CLOSE: - return 60.0 + return 60.0 # 장중 1분 elif _CLOSE <= t < _POST_END: - return 300.0 + 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: - return _seconds_until_next_market_open(now) + # 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 _seconds_until_next_market_open(now: datetime) -> float: - """다음 영업일의 07:00 KST 까지 초수.""" + """다음 영업일의 07:00 KST 까지 초수 (휴장일/주말용).""" candidate = now.replace(hour=7, minute=0, second=0, microsecond=0) if candidate <= now: candidate += timedelta(days=1) @@ -58,5 +87,5 @@ def _seconds_until_next_market_open(now: datetime) -> float: return (candidate - now).total_seconds() candidate += timedelta(days=1) - logger.warning("could not find next market day within 14 days; using +1 day") + logger.warning("could not find next market day within 14 days") return 86400.0 diff --git a/signal_v2/tests/test_scheduler.py b/signal_v2/tests/test_scheduler.py index 4f7ff0a..245ce3d 100644 --- a/signal_v2/tests/test_scheduler.py +++ b/signal_v2/tests/test_scheduler.py @@ -26,10 +26,10 @@ def test_next_interval_post_market_5min(): def test_next_interval_overnight_skip_to_next_morning(): - now = _kst(2026, 5, 18, 22, 0) # Monday 22:00 + now = _kst(2026, 5, 18, 2, 30) # Monday 02:30 (dead zone, not NXT window) interval = _next_interval(now) - # Next polling: Tuesday 07:00 (9 hours away) - assert 9 * 3600 - 60 < interval < 9 * 3600 + 60 + # Dead zone 23:30-04:30 → next 04:30 is ~2h away + assert 2 * 3600 - 60 < interval < 2 * 3600 + 60 def test_next_interval_holiday_skip(): @@ -54,8 +54,28 @@ def test_next_interval_at_market_close_boundary(): def test_next_interval_at_polling_window_end_boundary(): - """20:00:00 정확 second → overnight skip (다음 영업일 07:00 까지).""" - now = _kst(2026, 5, 18, 20, 0) # Monday 20:00:00 + """23:30:00 정확 second → dead zone skip (다음 04:30 까지).""" + now = _kst(2026, 5, 18, 23, 30) # Monday 23:30:00 (NXT_PRE_END boundary) interval = _next_interval(now) - # Next: Tuesday 07:00 — 11h away - assert 11 * 3600 - 60 < interval < 11 * 3600 + 60 + # Dead zone 23:30-04:30 → next 04:30 is ~5h away + assert 5 * 3600 - 60 < interval < 5 * 3600 + 60 + + +def test_next_interval_nxt_evening_5min(): + """22:00 평일 (NXT 야간) → 300 (5분).""" + now = _kst(2026, 5, 18, 22, 0) + assert _next_interval(now) == 300 + + +def test_next_interval_nxt_dawn_5min(): + """05:30 평일 (NXT 새벽) → 300 (5분).""" + now = _kst(2026, 5, 18, 5, 30) + assert _next_interval(now) == 300 + + +def test_next_interval_dead_zone_skip(): + """02:00 평일 (dead zone 23:30-04:30) → 다음 04:30 까지 (~9000s).""" + now = _kst(2026, 5, 18, 2, 0) + interval = _next_interval(now) + # 02:00 → 04:30 = 2.5h = 9000s + assert 9000 - 60 < interval < 9000 + 60