## 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>
329 lines
13 KiB
Python
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"])
|