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,270 @@
"""환율 리스크 관리 모듈 (FX Risk Manager)
미국 주식 투자 시 환율 변동 리스크를 관리.
[관리 항목]
- USD 익스포저 모니터링 (전체 자산 대비 USD 비중)
- 환율 변동 임팩트 계산
- 환율 급변 시 경고
[규칙]
- USD 익스포저 최대 30% (설정 가능)
- 환율 변동 +-2% 이상 시 신규 미국 주식 매수 경고
- 환율 하락 시 (원화 강세) 미국 주식 매수 유리 -> 비중 확대 허용
"""
import logging
from typing import Dict, List, Optional
logger = logging.getLogger(__name__)
# ── 기본 설정값 ─────────────────────────────────────────────────────────────────
DEFAULT_MAX_USD_EXPOSURE_PCT = 30.0 # USD 익스포저 최대 30%
DEFAULT_FX_LOW_THRESHOLD = 1.0 # +-1% = low
DEFAULT_FX_MEDIUM_THRESHOLD = 2.0 # +-2% = medium (warning)
DEFAULT_FX_HIGH_THRESHOLD = 3.0 # +-3%+ = high (block)
class FxRiskManager:
"""환율 리스크 관리자 (싱글톤으로 사용)"""
def __init__(
self,
max_usd_exposure_pct: float = DEFAULT_MAX_USD_EXPOSURE_PCT,
fx_low_threshold: float = DEFAULT_FX_LOW_THRESHOLD,
fx_medium_threshold: float = DEFAULT_FX_MEDIUM_THRESHOLD,
fx_high_threshold: float = DEFAULT_FX_HIGH_THRESHOLD,
):
self.max_usd_exposure_pct = max_usd_exposure_pct
self.fx_low_threshold = fx_low_threshold
self.fx_medium_threshold = fx_medium_threshold
self.fx_high_threshold = fx_high_threshold
def calculate_usd_exposure(
self,
portfolio_items: List[Dict],
exchange_rate: float,
) -> Dict:
"""USD 익스포저 계산
Args:
portfolio_items: 포트폴리오 종목 리스트
각 항목: {"ticker": str, "market": "US"/"KR",
"quantity": int, "current_price": float,
"currency": "USD"/"KRW"}
exchange_rate: 현재 USD/KRW 환율 (예: 1350.0)
Returns:
{"usd_value": float, "krw_equivalent": float,
"exposure_pct": float, "total_assets_krw": float}
"""
total_usd_value = 0.0
total_krw_value = 0.0
for item in portfolio_items:
qty = item.get("quantity", 0)
price = item.get("current_price", 0.0)
currency = item.get("currency", "KRW")
if currency == "USD":
item_usd = qty * price
total_usd_value += item_usd
total_krw_value += item_usd * exchange_rate
else:
total_krw_value += qty * price
krw_equivalent = total_usd_value * exchange_rate
total_assets_krw = total_krw_value
exposure_pct = (krw_equivalent / total_assets_krw * 100) if total_assets_krw > 0 else 0.0
result = {
"usd_value": round(total_usd_value, 2),
"krw_equivalent": round(krw_equivalent, 0),
"exposure_pct": round(exposure_pct, 2),
"total_assets_krw": round(total_assets_krw, 0),
}
logger.debug(
f"[FX_EXPOSURE] USD ${total_usd_value:,.2f} = "
f"KRW {krw_equivalent:,.0f} ({exposure_pct:.1f}%)"
)
return result
def check_fx_risk(
self,
current_rate: float,
avg_rate_30d: Optional[float] = None,
) -> Dict:
"""환율 변동 리스크 평가
Args:
current_rate: 현재 USD/KRW 환율
avg_rate_30d: 30일 평균 환율 (없으면 리스크 판단 불가 -> low 반환)
Returns:
{"fx_change_pct": float, "risk_level": "low"/"medium"/"high",
"recommendation": str}
"""
if avg_rate_30d is None or avg_rate_30d <= 0:
return {
"fx_change_pct": 0.0,
"risk_level": "low",
"recommendation": "30일 평균 환율 데이터 없음 - 기본 허용",
}
fx_change_pct = ((current_rate - avg_rate_30d) / avg_rate_30d) * 100
abs_change = abs(fx_change_pct)
# 리스크 레벨 판단
if abs_change >= self.fx_high_threshold:
risk_level = "high"
if fx_change_pct > 0:
recommendation = (
f"환율 급등 ({fx_change_pct:+.1f}%) - "
"신규 미국 주식 매수 차단 권고 (원화 약세)"
)
else:
recommendation = (
f"환율 급락 ({fx_change_pct:+.1f}%) - "
"신규 미국 주식 매수 차단 권고 (환율 불안정)"
)
elif abs_change >= self.fx_medium_threshold:
risk_level = "medium"
if fx_change_pct > 0:
recommendation = (
f"환율 상승 ({fx_change_pct:+.1f}%) - "
"미국 주식 매수 시 주의 (환전 비용 증가)"
)
else:
recommendation = (
f"환율 하락 ({fx_change_pct:+.1f}%) - "
"미국 주식 매수 유리한 구간 (원화 강세)"
)
else:
risk_level = "low"
recommendation = f"환율 안정 ({fx_change_pct:+.1f}%) - 정상 매매 가능"
result = {
"fx_change_pct": round(fx_change_pct, 2),
"risk_level": risk_level,
"recommendation": recommendation,
}
if risk_level != "low":
logger.info(
f"[FX_RISK] {risk_level.upper()}: "
f"현재 {current_rate:.1f} vs 30일평균 {avg_rate_30d:.1f} "
f"({fx_change_pct:+.1f}%)"
)
return result
def should_allow_us_buy(
self,
total_assets_krw: float,
current_us_value_krw: float,
new_buy_krw: float,
fx_risk_level: str,
) -> Dict:
"""미국 주식 신규 매수 허용 여부 판단
Args:
total_assets_krw: 전체 자산 (KRW)
current_us_value_krw: 현재 미국 주식 보유분 (KRW 환산)
new_buy_krw: 신규 매수 금액 (KRW 환산)
fx_risk_level: 환율 리스크 레벨 ("low"/"medium"/"high")
Returns:
{"allowed": bool, "reason": str, "current_exposure_pct": float}
"""
if total_assets_krw <= 0:
return {
"allowed": False,
"reason": "전체 자산 정보 없음",
"current_exposure_pct": 0.0,
}
current_exposure_pct = (current_us_value_krw / total_assets_krw) * 100
new_exposure_pct = ((current_us_value_krw + new_buy_krw) / total_assets_krw) * 100
# 1) 환율 리스크 high -> 차단
if fx_risk_level == "high":
reason = (
f"환율 리스크 HIGH - 신규 미국 주식 매수 차단 "
f"(현재 익스포저: {current_exposure_pct:.1f}%)"
)
logger.warning(f"[FX_BLOCK] {reason}")
return {
"allowed": False,
"reason": reason,
"current_exposure_pct": round(current_exposure_pct, 2),
}
# 2) 최대 USD 익스포저 초과 체크
if new_exposure_pct > self.max_usd_exposure_pct:
reason = (
f"USD 익스포저 한도 초과: "
f"매수 후 {new_exposure_pct:.1f}% > 최대 {self.max_usd_exposure_pct:.1f}% "
f"(현재 {current_exposure_pct:.1f}%)"
)
logger.info(f"[FX_LIMIT] {reason}")
return {
"allowed": False,
"reason": reason,
"current_exposure_pct": round(current_exposure_pct, 2),
}
# 3) 허용
reason = (
f"매수 허용: 익스포저 {current_exposure_pct:.1f}% -> {new_exposure_pct:.1f}% "
f"(한도 {self.max_usd_exposure_pct:.1f}%, FX리스크 {fx_risk_level})"
)
return {
"allowed": True,
"reason": reason,
"current_exposure_pct": round(current_exposure_pct, 2),
}
def calculate_fx_impact(
self,
entry_rate: float,
current_rate: float,
usd_value: float,
) -> Dict:
"""환율 변동으로 인한 손익 계산
Args:
entry_rate: 매수 시점 USD/KRW 환율
current_rate: 현재 USD/KRW 환율
usd_value: 보유 USD 금액
Returns:
{"fx_profit_loss_krw": float, "fx_impact_pct": float}
"""
if entry_rate <= 0:
return {"fx_profit_loss_krw": 0.0, "fx_impact_pct": 0.0}
# 환율 변동에 의한 KRW 손익
krw_at_entry = usd_value * entry_rate
krw_at_current = usd_value * current_rate
fx_profit_loss_krw = krw_at_current - krw_at_entry
# 환율 변동률
fx_impact_pct = ((current_rate - entry_rate) / entry_rate) * 100
result = {
"fx_profit_loss_krw": round(fx_profit_loss_krw, 0),
"fx_impact_pct": round(fx_impact_pct, 2),
}
if abs(fx_impact_pct) >= 1.0:
logger.info(
f"[FX_IMPACT] USD ${usd_value:,.2f}: "
f"환율 {entry_rate:.1f} -> {current_rate:.1f} "
f"({fx_impact_pct:+.2f}%) = KRW {fx_profit_loss_krw:+,.0f}"
)
return result
# 싱글톤 인스턴스
fx_risk_manager = FxRiskManager()