"""미국 시장 스케줄 (us_market_schedule.py) 테스트 테스트 시나리오: 1. is_us_market_open() — 정규장 시간 판별 (다양한 시각 모킹) 2. is_us_premarket() — 프리마켓 시간 판별 3. is_us_aftermarket() — 애프터마켓 시간 판별 4. is_us_holiday() — 공휴일 판별 5. is_dst() — 서머타임(DST) 감지 """ import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import pytest from datetime import datetime, time, date, timedelta from unittest.mock import patch, MagicMock import pytz from app.services.us_market_schedule import ( is_us_market_open, is_us_premarket, is_us_aftermarket, is_us_holiday, is_dst, get_us_market_status, get_next_us_market_open, ET, MARKET_OPEN, MARKET_CLOSE, PREMARKET_OPEN, AFTERMARKET_CLOSE, ) # ─ 헬퍼 ─────────────────────────────────────────────────────────────────────── def _make_et_datetime(year, month, day, hour, minute, weekday=None): """US/Eastern timezone-aware datetime 생성. weekday 파라미터는 무시 (실제 날짜의 요일 사용). """ dt = datetime(year, month, day, hour, minute, 0) return ET.localize(dt) # ─ is_us_market_open 테스트 ──────────────────────────────────────────────────── class TestIsUsMarketOpen: """미국 정규장 운영 시간 테스트""" @patch("app.services.us_market_schedule._now_et") def test_market_open_during_regular_hours(self, mock_now): """정규장 시간 (10:30 ET 수요일) → True""" # 2026-03-11 수요일 10:30 ET mock_now.return_value = _make_et_datetime(2026, 3, 11, 10, 30) assert is_us_market_open() is True @patch("app.services.us_market_schedule._now_et") def test_market_open_at_opening(self, mock_now): """정확히 09:30 ET → True""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 9, 30) assert is_us_market_open() is True @patch("app.services.us_market_schedule._now_et") def test_market_closed_at_closing(self, mock_now): """정확히 16:00 ET → False (16:00은 폐장)""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 16, 0) assert is_us_market_open() is False @patch("app.services.us_market_schedule._now_et") def test_market_closed_before_open(self, mock_now): """09:00 ET (개장 전) → False""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 9, 0) assert is_us_market_open() is False @patch("app.services.us_market_schedule._now_et") def test_market_closed_after_hours(self, mock_now): """18:00 ET (폐장 후) → False""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 18, 0) assert is_us_market_open() is False @patch("app.services.us_market_schedule._now_et") def test_market_closed_on_saturday(self, mock_now): """토요일 → False""" # 2026-03-14 토요일 mock_now.return_value = _make_et_datetime(2026, 3, 14, 10, 30) assert is_us_market_open() is False @patch("app.services.us_market_schedule._now_et") def test_market_closed_on_sunday(self, mock_now): """일요일 → False""" # 2026-03-15 일요일 mock_now.return_value = _make_et_datetime(2026, 3, 15, 10, 30) assert is_us_market_open() is False @patch("app.services.us_market_schedule._now_et") @patch("app.services.us_market_schedule.is_us_holiday", return_value=True) def test_market_closed_on_holiday(self, mock_holiday, mock_now): """공휴일 → False""" mock_now.return_value = _make_et_datetime(2026, 1, 1, 10, 30) assert is_us_market_open() is False # ─ is_us_premarket 테스트 ────────────────────────────────────────────────────── class TestIsUsPremarket: """프리마켓 시간 테스트""" @patch("app.services.us_market_schedule._now_et") def test_premarket_at_4am(self, mock_now): """04:00 ET → 프리마켓 시작""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 4, 0) assert is_us_premarket() is True @patch("app.services.us_market_schedule._now_et") def test_premarket_at_7am(self, mock_now): """07:00 ET → 프리마켓 중""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 7, 0) assert is_us_premarket() is True @patch("app.services.us_market_schedule._now_et") def test_premarket_ends_at_market_open(self, mock_now): """09:30 ET → 프리마켓 종료 (정규장 시작)""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 9, 30) assert is_us_premarket() is False @patch("app.services.us_market_schedule._now_et") def test_not_premarket_at_3am(self, mock_now): """03:00 ET → 프리마켓 전""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 3, 0) assert is_us_premarket() is False @patch("app.services.us_market_schedule._now_et") def test_not_premarket_on_weekend(self, mock_now): """주말 프리마켓 없음""" mock_now.return_value = _make_et_datetime(2026, 3, 14, 7, 0) # 토요일 assert is_us_premarket() is False # ─ is_us_aftermarket 테스트 ──────────────────────────────────────────────────── class TestIsUsAftermarket: """애프터마켓 시간 테스트""" @patch("app.services.us_market_schedule._now_et") def test_aftermarket_at_4pm(self, mock_now): """16:00 ET → 애프터마켓 시작""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 16, 0) assert is_us_aftermarket() is True @patch("app.services.us_market_schedule._now_et") def test_aftermarket_at_6pm(self, mock_now): """18:00 ET → 애프터마켓 중""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 18, 0) assert is_us_aftermarket() is True @patch("app.services.us_market_schedule._now_et") def test_aftermarket_ends_at_8pm(self, mock_now): """20:00 ET → 애프터마켓 종료""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 20, 0) assert is_us_aftermarket() is False @patch("app.services.us_market_schedule._now_et") def test_not_aftermarket_during_regular(self, mock_now): """정규장 시간에는 애프터마켓 아님""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 14, 0) assert is_us_aftermarket() is False @patch("app.services.us_market_schedule._now_et") def test_not_aftermarket_on_weekend(self, mock_now): """주말 애프터마켓 없음""" mock_now.return_value = _make_et_datetime(2026, 3, 14, 17, 0) # 토요일 assert is_us_aftermarket() is False # ─ is_us_holiday 테스트 ─────────────────────────────────────────────────────── class TestIsUsHoliday: """미국 공휴일 테스트""" def test_new_years_day_2026(self): """2026년 새해 첫날 — 휴장""" assert is_us_holiday(datetime(2026, 1, 1)) is True def test_christmas_2026(self): """2026년 크리스마스 — 휴장""" assert is_us_holiday(datetime(2026, 12, 25)) is True def test_independence_day_observed_2026(self): """2026년 독립기념일 (관측일) — 휴장""" assert is_us_holiday(datetime(2026, 7, 3)) is True def test_thanksgiving_2026(self): """2026년 추수감사절 — 휴장""" assert is_us_holiday(datetime(2026, 11, 26)) is True def test_regular_weekday_not_holiday(self): """일반 평일 — 휴장 아님""" assert is_us_holiday(datetime(2026, 3, 10)) is False def test_date_object_input(self): """date 객체 입력도 지원""" assert is_us_holiday(date(2026, 1, 1)) is True assert is_us_holiday(date(2026, 3, 10)) is False # ─ is_dst 테스트 ────────────────────────────────────────────────────────────── class TestIsDst: """서머타임(DST) 감지 테스트""" @patch("app.services.us_market_schedule._now_et") def test_dst_in_summer(self, mock_now): """여름 (7월) → DST 적용""" summer_dt = ET.localize(datetime(2026, 7, 15, 12, 0)) mock_now.return_value = summer_dt assert is_dst() is True @patch("app.services.us_market_schedule._now_et") def test_no_dst_in_winter(self, mock_now): """겨울 (1월) → DST 미적용""" winter_dt = ET.localize(datetime(2026, 1, 15, 12, 0)) mock_now.return_value = winter_dt assert is_dst() is False @patch("app.services.us_market_schedule._now_et") def test_dst_in_april(self, mock_now): """4월 → DST 적용""" april_dt = ET.localize(datetime(2026, 4, 15, 12, 0)) mock_now.return_value = april_dt assert is_dst() is True @patch("app.services.us_market_schedule._now_et") def test_no_dst_in_december(self, mock_now): """12월 → DST 미적용""" dec_dt = ET.localize(datetime(2026, 12, 15, 12, 0)) mock_now.return_value = dec_dt assert is_dst() is False # ─ get_us_market_status 테스트 ──────────────────────────────────────────────── class TestGetUsMarketStatus: """시장 상태 요약 테스트""" @patch("app.services.us_market_schedule._now_et") def test_status_open(self, mock_now): """정규장 시간 → status='OPEN'""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 10, 30) status = get_us_market_status() assert status["status"] == "OPEN" assert "is_dst" in status assert "current_et" in status assert "next_open" in status @patch("app.services.us_market_schedule._now_et") def test_status_premarket(self, mock_now): """프리마켓 시간 → status='PREMARKET'""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 6, 0) status = get_us_market_status() assert status["status"] == "PREMARKET" @patch("app.services.us_market_schedule._now_et") def test_status_aftermarket(self, mock_now): """애프터마켓 시간 → status='AFTERMARKET'""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 17, 0) status = get_us_market_status() assert status["status"] == "AFTERMARKET" @patch("app.services.us_market_schedule._now_et") def test_status_closed(self, mock_now): """폐장 시간 → status='CLOSED'""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 22, 0) status = get_us_market_status() assert status["status"] == "CLOSED" @patch("app.services.us_market_schedule._now_et") def test_status_weekend_closed(self, mock_now): """주말 → status='CLOSED'""" mock_now.return_value = _make_et_datetime(2026, 3, 14, 10, 30) # 토요일 status = get_us_market_status() assert status["status"] == "CLOSED" # ─ 시간 경계 테스트 ────────────────────────────────────────────────────────── class TestTimeBoundaries: """시간 경계 조건 테스트""" @patch("app.services.us_market_schedule._now_et") def test_one_minute_before_open(self, mock_now): """09:29 ET → 프리마켓""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 9, 29) assert is_us_market_open() is False assert is_us_premarket() is True @patch("app.services.us_market_schedule._now_et") def test_one_minute_before_close(self, mock_now): """15:59 ET → 정규장""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 15, 59) assert is_us_market_open() is True @patch("app.services.us_market_schedule._now_et") def test_premarket_start_boundary(self, mock_now): """03:59 ET → 프리마켓 전""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 3, 59) assert is_us_premarket() is False @patch("app.services.us_market_schedule._now_et") def test_aftermarket_end_boundary(self, mock_now): """19:59 ET → 애프터마켓""" mock_now.return_value = _make_et_datetime(2026, 3, 11, 19, 59) assert is_us_aftermarket() is True if __name__ == "__main__": pytest.main([__file__, "-v"])