""" risk/manager.py 리스크 매니저 L1~L5 기획서 v2.1 기준 """ import logging from app.config import ( SL_PCT, DAILY_SL_PCT, CONSEC_LOSS, AI_RISK_SL_MAP, POS_SIZE_PCT, MAX_POSITIONS ) logger = logging.getLogger(__name__) class RiskManager: def __init__(self, init_cash: float): self.init_cash = init_cash self.daily_pnl = 0.0 self.weekly_pnl = 0.0 self.monthly_pnl = 0.0 self.consec_loss = 0 self.trading_stopped = False self.stop_reason = "" self.risk_level = "보통" def set_risk_level(self, level: str): """AI 판단 결과로 risk_level 설정""" self.risk_level = level def get_sl_pct(self) -> float: """현재 risk_level에 따른 손절 비율 반환""" return AI_RISK_SL_MAP.get(self.risk_level, SL_PCT) def get_pos_size(self, cash: float, multiplier: float = 1.0) -> float: """포지션 사이즈 계산 (AI multiplier 반영)""" return cash * POS_SIZE_PCT * multiplier # ── 손실 기록 ── def record_trade(self, pnl: float): """매매 결과 기록 및 손실 한도 체크""" self.daily_pnl += pnl self.weekly_pnl += pnl self.monthly_pnl += pnl if pnl < 0: self.consec_loss += 1 else: self.consec_loss = 0 self._check_limits() def _check_limits(self): """L1~L5 손실 한도 체크""" # L2: 일일 누적 손실 -3% if self.daily_pnl / self.init_cash < -DAILY_SL_PCT: self._stop("L2", f"일일 손실 {self.daily_pnl/self.init_cash*100:.1f}% 도달") # L3: 연속 손절 3회 if self.consec_loss >= CONSEC_LOSS: self._stop("L3", f"{CONSEC_LOSS}연속 손절 발생") # L4: 주간 누적 -7% if self.weekly_pnl / self.init_cash < -0.07: self._stop("L4", f"주간 손실 {self.weekly_pnl/self.init_cash*100:.1f}%") # L5: 월간 누적 -15% if self.monthly_pnl / self.init_cash < -0.15: self._stop("L5", f"월간 손실 {self.monthly_pnl/self.init_cash*100:.1f}%") def _stop(self, level: str, reason: str): self.trading_stopped = True self.stop_reason = f"{level}: {reason}" logger.warning(f"매매 중단 - {self.stop_reason}") # ── 상태 조회 ── def can_trade(self) -> bool: return not self.trading_stopped def can_add_position(self, current_positions: int) -> bool: return (not self.trading_stopped and current_positions < MAX_POSITIONS) def reset_daily(self): """매일 장 시작 전 일일 손익 초기화""" self.daily_pnl = 0.0 self.consec_loss = 0 self.trading_stopped = False self.stop_reason = "" def reset_weekly(self): self.weekly_pnl = 0.0 def reset_monthly(self): self.monthly_pnl = 0.0 def status(self) -> dict: return { "trading_stopped": self.trading_stopped, "stop_reason" : self.stop_reason, "daily_pnl" : self.daily_pnl, "weekly_pnl" : self.weekly_pnl, "monthly_pnl" : self.monthly_pnl, "consec_loss" : self.consec_loss, "risk_level" : self.risk_level, "sl_pct" : self.get_sl_pct(), }