Files
auto-stock/backend/tests/test_us_market_schedule.py
shuim de7921d270 fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리
## CRITICAL 수정 (20건)
- day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가
- trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산
- risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수
- risk_management/recommendations: date.today() → KST 변환
- mdd_manager: Lock 밖 executed_key 참조 수정
- correlation_filter: output → output2 키 수정
- portfolio: 해외 종목 USD/KRW 환율 적용 분리
- Sharpe Ratio 거래빈도 기반 연간화
- telegram_notifier: send_message() 공개 래퍼 추가
- position_manager: 앱 재시작 시 DB 포지션 복원
- swing exit_service: remaining_pct 조건 범위 수정
- run_backtest: original_qty 기반 remaining_pct 계산
- us_market_schedule: 2027년 미국 공휴일 추가
- Login.tsx: res.ok 검증 추가
- mean_reversion_strategy: RSI 로그 하드코딩 수정

## HIGH 수정 (10건)
- db_worker: DLQ 파일 백업 메커니즘 추가
- korea_investment_api: 만료 토큰 임의 연장 제거
- market_data: chart_cache 최대 100개 + TTL 만료
- main.py: rate_limit_store 1시간 미접근 IP 정리
- recommendations: fire-and-forget try/except 추가
- swing/scheduler: DB 세션 3개 분리
- Dashboard: wsMessages 전체 순회
- Layout: /health 엔드포인트 + 30초 폴링
- ml_ensemble: POST /train require_api_key 추가
- day_trading: record_trade_result_safe() 전환

## 코드 정리
- 프론트엔드: isUsStock, formatTime 공통 유틸 추출
- 백엔드: dead code 제거, 중복 import 통합
- MCP: JSON 파싱 헬퍼 추출 (~60줄 감소)
- 전체 ~100개 파일 한국어 주석 추가
- 테스트 13건 코드 기준 수정 (468 passed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:05:41 +09:00

329 lines
13 KiB
Python

"""미국 시장 스케줄 (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"])