Files
Stock-trading-programming/app/risk/manager.py
T
whdwo798 edafeb7c79 [2026-05-19] 세션 분리 + L3→B안 전환 + /midday 장중 분석 추가
- L3 하드 중단 제거 → B안(연속 손절별 포지션 축소) 적용
  0회×1.0 / 1회×0.7 / 2회×0.5 / 3+회×0.3, 익절 시 한 단계 회복
- 아침·점심 세션 분리: 11:00 이후 midday_context.json 감지 시 점심 세션 자동 시작
  (12:00 고정 시작 제거 → 이벤트 기반)
- app/ai/midday.py: 장중 데이터 수집 스크립트 신규 작성
- .claude/commands/midday.md: /midday 슬래시 커맨드 신규 작성
- scripts/run_midday.ps1: 11:20 스케줄러 스크립트 신규 작성
- setup_scheduler.ps1: StockBot_Midday 태스크 추가
- CLAUDE.md: 전체 문서 업데이트

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:07:27 +09:00

113 lines
3.7 KiB
Python

"""
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
# ── 손실 기록 ──
# B안: 연속 손절 수 → 포지션 크기 배율
_CONSEC_MULT = {0: 1.0, 1: 0.7, 2: 0.5}
_CONSEC_MIN = 0.3 # 3회 이상 최소값
def get_consec_multiplier(self) -> float:
"""연속 손절 수에 따른 포지션 크기 배율 (B안)"""
return self._CONSEC_MULT.get(self.consec_loss, self._CONSEC_MIN)
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:
# 익절 시 한 단계만 회복 (0으로 리셋 아님)
self.consec_loss = max(0, self.consec_loss - 1)
self._check_limits()
def _check_limits(self):
"""L2/L4/L5 손실 한도 체크 (L3는 B안 포지션 축소로 대체)"""
# L2: 일일 누적 손실 -3%
if self.daily_pnl / self.init_cash < -DAILY_SL_PCT:
self._stop("L2", f"일일 손실 {self.daily_pnl/self.init_cash*100:.1f}% 도달")
# 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(),
}