first vibe coding
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
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(),
|
||||
}
|
||||
Reference in New Issue
Block a user