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>
This commit is contained in:
2026-03-26 16:05:41 +09:00
parent 739a8a2e5d
commit de7921d270
149 changed files with 133353 additions and 3338 deletions

View File

@@ -0,0 +1,286 @@
"""시장 이벤트 플레이북 유닛 테스트."""
import sys
import os
from datetime import date, timedelta
from unittest.mock import patch
import pytest
# backend를 sys.path에 추가
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.services.market_playbook import (
MarketEventType,
EventPhase,
PlaybookAction,
get_upcoming_events,
get_today_playbook,
check_realtime_events,
trigger_circuit_breaker,
clear_circuit_breaker,
trigger_vi,
is_vi_active,
is_circuit_breaker_active,
apply_playbook_to_regime,
is_daytrading_allowed_by_playbook,
is_entry_allowed_by_playbook,
get_position_multiplier_from_playbook,
_get_nth_weekday_of_month,
_get_kr_options_expiry_dates,
_get_quad_witching_dates,
_get_us_employment_dates,
_classify_phase,
_is_earnings_season,
_is_window_dressing_period,
_is_tax_loss_selling_period,
_realtime_state,
_playbook_cache,
)
class TestNthWeekday:
"""n번째 요일 계산 테스트."""
def test_second_thursday_jan_2026(self):
# 2026-01-01은 목요일 -> 둘째 목요일 = 01-08
d = _get_nth_weekday_of_month(2026, 1, 3, 2)
assert d == date(2026, 1, 8)
def test_third_friday_march_2026(self):
# 쿼드러플 위칭: 2026-03 셋째 금요일
d = _get_nth_weekday_of_month(2026, 3, 4, 3)
assert d.weekday() == 4 # 금요일
assert 15 <= d.day <= 21 # 셋째 주
def test_first_friday_jan_2026(self):
d = _get_nth_weekday_of_month(2026, 1, 4, 1)
assert d.weekday() == 4
assert d.day <= 7
class TestRuleBasedDates:
"""규칙 기반 날짜 계산 테스트."""
def test_kr_options_expiry_12_months(self):
dates = _get_kr_options_expiry_dates(2026)
assert len(dates) == 12
for d in dates:
assert d.weekday() == 3 # 모두 목요일
def test_quad_witching_4_dates(self):
dates = _get_quad_witching_dates(2026)
assert len(dates) == 4
for d in dates:
assert d.weekday() == 4 # 모두 금요일
assert d.month in (3, 6, 9, 12)
def test_us_employment_12_months(self):
dates = _get_us_employment_dates(2026)
assert len(dates) == 12
for d in dates:
assert d.weekday() == 4 # 모두 금요일
assert d.day <= 7 # 첫째 주
class TestPhaseClassification:
"""이벤트 단계 분류 테스트."""
def test_day_of(self):
today = date(2026, 1, 28)
assert _classify_phase(today, date(2026, 1, 28)) == EventPhase.DAY_OF
def test_pre_d1(self):
today = date(2026, 1, 27)
assert _classify_phase(today, date(2026, 1, 28), pre_days=2) == EventPhase.PRE
def test_pre_d2(self):
today = date(2026, 1, 26)
assert _classify_phase(today, date(2026, 1, 28), pre_days=2) == EventPhase.PRE
def test_post_d1(self):
today = date(2026, 1, 29)
assert _classify_phase(today, date(2026, 1, 28), post_days=1) == EventPhase.POST
def test_none_too_far(self):
today = date(2026, 1, 20)
assert _classify_phase(today, date(2026, 1, 28), pre_days=2) == EventPhase.NONE
class TestSeasonalEvents:
"""시즌 이벤트 판별 테스트."""
def test_earnings_season_jan(self):
assert _is_earnings_season(date(2026, 1, 15)) is True
def test_earnings_season_jan_early(self):
assert _is_earnings_season(date(2026, 1, 5)) is False
def test_earnings_season_feb(self):
assert _is_earnings_season(date(2026, 2, 15)) is False
def test_window_dressing_march(self):
assert _is_window_dressing_period(date(2026, 3, 26)) is True
assert _is_window_dressing_period(date(2026, 3, 20)) is False
def test_tax_loss_selling(self):
assert _is_tax_loss_selling_period(date(2026, 12, 15)) is True
assert _is_tax_loss_selling_period(date(2026, 11, 15)) is False
class TestCircuitBreaker:
"""서킷브레이커 실시간 이벤트 테스트."""
def setup_method(self):
_realtime_state.circuit_breaker_active = False
_realtime_state.circuit_breaker_triggered_at = None
_realtime_state.vi_stocks.clear()
_realtime_state.alerts.clear()
_playbook_cache["result"] = None
_playbook_cache["date"] = None
def test_trigger_and_check(self):
assert not is_circuit_breaker_active()
trigger_circuit_breaker("test")
assert is_circuit_breaker_active()
def test_clear(self):
trigger_circuit_breaker("test")
clear_circuit_breaker()
assert not is_circuit_breaker_active()
def test_auto_expire(self):
import time as _time
trigger_circuit_breaker("test")
# 만료 시간을 과거로 설정
_realtime_state.circuit_breaker_triggered_at = _time.monotonic() - 2000
assert not is_circuit_breaker_active()
class TestVI:
"""VI(변동성 완화장치) 테스트."""
def setup_method(self):
_realtime_state.vi_stocks.clear()
_realtime_state.alerts.clear()
def test_trigger_and_check(self):
assert not is_vi_active("005930")
trigger_vi("005930")
assert is_vi_active("005930")
def test_auto_expire(self):
import time as _time
trigger_vi("005930")
# 쿨다운 만료 시뮬레이션
_realtime_state.vi_stocks["005930"] = _time.monotonic() - 700
assert not is_vi_active("005930")
class TestPlaybookAction:
"""PlaybookAction 직렬화 테스트."""
def test_to_dict(self):
action = PlaybookAction(
position_multiplier=0.5,
entry_allowed=False,
daytrading_allowed=False,
reason="FOMC 당일",
)
d = action.to_dict()
assert d["position_multiplier"] == 0.5
assert d["entry_allowed"] is False
assert d["daytrading_allowed"] is False
assert "FOMC" in d["reason"]
class TestApplyPlaybookToRegime:
"""regime 결과에 플레이북 오버레이 테스트."""
def setup_method(self):
_realtime_state.circuit_breaker_active = False
_realtime_state.circuit_breaker_triggered_at = None
_realtime_state.vi_stocks.clear()
_playbook_cache["result"] = None
_playbook_cache["date"] = None
def test_no_events_passthrough(self):
"""이벤트 없으면 원본 multiplier 유지."""
regime = {"regime": "bullish", "position_multiplier": 1.0, "max_trades": 5}
# 이벤트 없는 날짜로 패치
with patch("app.services.market_playbook.get_today_playbook") as mock_pb:
mock_pb.return_value = PlaybookAction(
position_multiplier=1.0,
entry_allowed=True,
daytrading_allowed=True,
reason="이벤트 없음",
)
result = apply_playbook_to_regime(regime)
assert result["position_multiplier"] == 1.0
assert result["regime"] == "bullish"
def test_fomc_blocks_entry(self):
"""FOMC 당일 entry_allowed=False -> regime='event_blocked'."""
regime = {"regime": "bullish", "position_multiplier": 1.0, "max_trades": 5, "min_score": 1}
with patch("app.services.market_playbook.get_today_playbook") as mock_pb:
mock_pb.return_value = PlaybookAction(
position_multiplier=0.0,
entry_allowed=False,
daytrading_allowed=False,
reason="FOMC 당일",
)
result = apply_playbook_to_regime(regime)
assert result["regime"] == "event_blocked"
assert result["max_trades"] == 0
assert result["position_multiplier"] == 0.0
def test_multiplier_stacks(self):
"""bear(0.5) * playbook(0.5) = 0.25."""
regime = {"regime": "bearish", "position_multiplier": 0.5, "max_trades": 3}
with patch("app.services.market_playbook.get_today_playbook") as mock_pb:
mock_pb.return_value = PlaybookAction(
position_multiplier=0.5,
entry_allowed=True,
daytrading_allowed=True,
reason="옵션 만기 D-1",
)
result = apply_playbook_to_regime(regime)
assert result["position_multiplier"] == 0.25
def test_circuit_breaker_overrides_all(self):
"""서킷브레이커 발동 시 모든 매매 차단."""
trigger_circuit_breaker("test")
regime = {"regime": "bullish", "position_multiplier": 1.0, "max_trades": 5, "min_score": 1}
result = apply_playbook_to_regime(regime)
assert result["regime"] == "event_blocked"
assert result["position_multiplier"] == 0.0
# 정리
clear_circuit_breaker()
class TestHelperFunctions:
"""통합 헬퍼 함수 테스트."""
def setup_method(self):
_realtime_state.circuit_breaker_active = False
_realtime_state.vi_stocks.clear()
_playbook_cache["result"] = None
_playbook_cache["date"] = None
def test_is_daytrading_allowed_default(self):
"""기본 상태에서 데이트레이딩 허용."""
# 이벤트 없는 날이면 True
result = is_daytrading_allowed_by_playbook()
assert isinstance(result, bool)
def test_is_entry_allowed_default(self):
result = is_entry_allowed_by_playbook()
assert isinstance(result, bool)
def test_get_position_multiplier_default(self):
result = get_position_multiplier_from_playbook()
assert 0.0 <= result <= 1.0
if __name__ == "__main__":
pytest.main([__file__, "-v"])