""" MDD(Maximum Drawdown) 관리 - CLAUDE.md 섹션 6.3 - 포트폴리오 최고점 대비 하락률 추적 - -10% 돌파 시: 신규 진입 중단 + 기존 포지션 30% 축소 - -20% 돌파 시: 전체 포지션 50% 강제 축소 (레버리지/인버스 우선) """ import asyncio import logging from typing import Dict, Optional logger = logging.getLogger(__name__) class MDDManager: """포트폴리오 MDD 관리자 (싱글톤으로 사용)""" def __init__(self): self.peak_balance: float = 0.0 self.is_paused: bool = False self.reduction_applied: Optional[str] = None # None, "10pct", "20pct" self._lock = asyncio.Lock() # 스케줄러-sync 동시 접근 방지 async def update(self, current_balance: float) -> Dict: """MDD 업데이트 및 필요한 조치 반환 (thread-safe) Returns: {"action": "NONE"/"REDUCE_AND_PAUSE"/"FORCE_REDUCE", "drawdown": float, ...} """ async with self._lock: if current_balance > self.peak_balance: self.peak_balance = current_balance self.is_paused = False self.reduction_applied = None if self.peak_balance <= 0: return {"action": "NONE", "drawdown": 0.0} drawdown_pct = ((current_balance - self.peak_balance) / self.peak_balance) * 100 # -20% 돌파: 50% 강제 축소 if drawdown_pct <= -20 and self.reduction_applied != "20pct": self.is_paused = True self.reduction_applied = "20pct" logger.warning(f"MDD {drawdown_pct:.1f}% - 50% 강제 축소 + 신규진입 중단") return { "action": "FORCE_REDUCE", "drawdown": drawdown_pct, "reduce_pct": 0.50, "prioritize_leverage": True, } # -10% 돌파: 30% 축소 + 신규진입 중단 if drawdown_pct <= -10 and self.reduction_applied is None: self.is_paused = True self.reduction_applied = "10pct" logger.warning(f"MDD {drawdown_pct:.1f}% - 30% 축소 + 신규진입 중단") return { "action": "REDUCE_AND_PAUSE", "drawdown": drawdown_pct, "reduce_pct": 0.30, } return {"action": "NONE", "drawdown": drawdown_pct} async def can_open_new_position(self) -> bool: """신규 포지션 진입 가능 여부 (thread-safe)""" async with self._lock: return not self.is_paused def reset(self): """MDD 상태 리셋 (수동 복구 시) — 동기 메서드, 단독 호출 시에만 사용""" self.is_paused = False self.reduction_applied = None logger.info("MDD 관리자 리셋") # 싱글톤 인스턴스 mdd_manager = MDDManager()