source push

This commit is contained in:
2026-02-22 18:05:14 +09:00
parent 899a3c6543
commit b827f03d56
19 changed files with 6986 additions and 0 deletions

270
risk_manager.py Normal file
View File

@@ -0,0 +1,270 @@
"""
변동성 기반 리스크 관리 모듈
- 예수금 비율 기반 비중 조절
- ATR/변동성 역가중 (Volatility Inverse Weighting)
- 종목별 안전 매수 금액 계산
"""
import logging
import pandas as pd
import numpy as np
from typing import List, Dict
logger = logging.getLogger("RiskManager")
class RiskManager:
"""
[예수금 연동 리스크 관리 모듈]
- 고정 금액이 아닌 '현재 예수금' 대비 퍼센트로 투자금 계산
- 변동성 큰 종목 = 적게 매수, 변동성 작은 종목 = 많이 매수
"""
def __init__(
self,
risk_pct_per_trade: float = 0.02, # 1회 거래 시 허용 손실 비율 (2%)
max_position_pct: float = 0.20, # 종목당 최대 비중 (20%)
min_position_amount: int = 50000, # 최소 매수 금액 (5만원)
use_kelly: bool = False, # 켈리 공식 사용 여부
kelly_multiplier: float = 0.5 # 켈리 배율 (0.5 = 하프 켈리)
):
"""
Args:
risk_pct_per_trade: 1회 매매 시 허용할 손실 비율 (기본 0.02 = 2%)
max_position_pct: 종목당 최대 투자 비율 (기본 0.20 = 20%)
min_position_amount: 최소 매수 금액 (너무 작은 주문 방지)
use_kelly: 켈리 공식 활성화 여부
kelly_multiplier: 켈리 배율 (0.5 = 하프켈리, 0.25 = 쿼터켈리)
"""
self.risk_pct = risk_pct_per_trade
self.max_pos_pct = max_position_pct
self.min_amount = min_position_amount
self.use_kelly = use_kelly
self.kelly_mult = kelly_multiplier
logger.info(
f"💰 RiskManager 초기화: 리스크{self.risk_pct*100}% | "
f"최대비중{self.max_pos_pct*100}% | 켈리{'ON' if use_kelly else 'OFF'}"
)
def calculate_volatility_atr(self, df: pd.DataFrame, period: int = 14) -> float:
"""
ATR(Average True Range) 기반 변동성 계산
Args:
df: OHLC 데이터프레임 (컬럼: open, high, low, close)
period: ATR 계산 기간 (기본 14)
Returns:
현재가 대비 ATR 비율 (%)
"""
try:
if df is None or len(df) < period:
logger.warning(f"⚠️ ATR 계산: 데이터 부족 -> 기본값 3.0% 리턴")
return 3.0
# True Range 계산
df = df.copy()
df['tr'] = np.maximum(
df['high'] - df['low'],
np.maximum(
np.abs(df['high'] - df['close'].shift()),
np.abs(df['low'] - df['close'].shift())
)
)
# ATR = TR의 이동평균
atr = df['tr'].rolling(window=period).mean().iloc[-1]
current_price = df['close'].iloc[-1]
if current_price == 0:
return 3.0
# 현재가 대비 ATR 비율 (%)
volatility_pct = (atr / current_price) * 100
# 최소값 보정 (0.5% 미만은 0.5%로)
return max(round(volatility_pct, 2), 0.5)
except Exception as e:
logger.error(f"❌ ATR 계산 에러: {e}")
return 3.0
def calculate_volatility_simple(self, candles: List[Dict], period: int = 10) -> float:
"""
간단한 변동성 계산 (고저차 비율 평균)
Args:
candles: 캔들 데이터 리스트 [{'high': , 'low': , 'close': }, ...]
period: 계산 기간 (기본 10일)
Returns:
변동성 (%)
"""
try:
if not candles or len(candles) < period:
return 3.0
recent = candles[-period:]
volatility_sum = 0.0
for candle in recent:
high = float(candle.get('high', 0))
low = float(candle.get('low', 0))
close = float(candle.get('close', 1))
if close == 0:
continue
# (고가 - 저가) / 종가
volatility_sum += (high - low) / close
avg_vol = (volatility_sum / len(recent)) * 100
return max(round(avg_vol, 2), 0.5)
except Exception as e:
logger.error(f"❌ 변동성 계산 에러: {e}")
return 3.0
def get_position_size(
self,
stock_name: str,
current_balance: float,
volatility_pct: float = None,
df: pd.DataFrame = None,
kelly_fraction: float = None,
size_class: str = None,
) -> int:
"""
[메인 함수] 종목별 안전 매수 금액 계산
Args:
stock_name: 종목명 (로깅용)
current_balance: 현재 예수금
volatility_pct: 변동성 (직접 제공 시)
df: OHLC 데이터프레임 (ATR 계산용)
kelly_fraction: 켈리 비율 (DB에서 계산한 값)
size_class: 대/중/소형 구분 ("", "", "")
Returns:
추천 매수 금액 (원)
"""
try:
if current_balance <= 0:
logger.warning(f"⚠️ [{stock_name}] 예수금 0원 이하")
return 0
# 1. 변동성 계산
if volatility_pct is None:
if df is not None and not df.empty:
volatility_pct = self.calculate_volatility_atr(df)
else:
logger.warning(f"⚠️ [{stock_name}] 변동성 데이터 없음 -> 기본값 3.0%")
volatility_pct = 3.0
# 2. 기본 리스크 금액 계산 (예수금 * 리스크 비율)
# 예: 1,000만 원 * 2% = 20만 원 (이 종목에서 최대 20만원까지 잃을 수 있음)
allowable_risk = current_balance * self.risk_pct
# 3. 켈리 공식 적용 (쿼터 켈리/하프 켈리)
if self.use_kelly:
if kelly_fraction is not None and kelly_fraction > 0:
# DB에서 계산한 켈리 비율 사용 (하프 켈리)
base_position_pct = kelly_fraction * self.kelly_mult
allowable_risk = current_balance * base_position_pct
logger.info(f"🎲 [{stock_name}] 켈리 적용(DB): {base_position_pct*100:.1f}% (켈리={kelly_fraction*100:.1f}% × 배율={self.kelly_mult})")
else:
# 켈리 비율이 없으면 쿼터 켈리 배율을 기본값으로 사용
# 예: 리스크 1% × 쿼터 켈리 0.25 = 0.25% (더 보수적)
base_position_pct = self.risk_pct * self.kelly_mult
allowable_risk = current_balance * base_position_pct
logger.info(f"🎲 [{stock_name}] 켈리 적용(기본): {base_position_pct*100:.1f}% (리스크={self.risk_pct*100}% × 쿼터켈리={self.kelly_mult})")
# 4. 변동성 역가중으로 매수 금액 계산
# 공식: (허용 손실액) / (변동성 비율)
# 변동성 5% -> 20만 / 0.05 = 400만원 매수
# 변동성 10% -> 20만 / 0.10 = 200만원 매수
risk_ratio = volatility_pct / 100.0
calculated_amount = allowable_risk / risk_ratio
# 5. 상한선 체크 (종목당 최대 비중)
max_limit = current_balance * self.max_pos_pct
if calculated_amount > max_limit:
final_amount = max_limit
note = f"(최대{self.max_pos_pct*100:.0f}% 제한)"
else:
final_amount = calculated_amount
note = "(변동성기반)"
# 5-2. 대형/소형 구간별 조정 (소형주는 포지션 축소)
if size_class == "":
final_amount = int(final_amount * 0.7)
note = "(소형주 70%)"
elif size_class == "":
final_amount = int(final_amount * 0.85)
note = "(중형주 85%)"
elif size_class == "":
pass # 대형주는 기존과 동일
# 6. 하한선 체크 (너무 작은 금액은 거래 안함)
if final_amount < self.min_amount:
logger.info(
f"🚫 [{stock_name}] 계산 금액({final_amount:,.0f}원) < "
f"최소금액({self.min_amount:,.0f}원) -> 매수 보류"
)
return 0
logger.info(
f"💰 [{stock_name}] 예수금:{current_balance:,.0f}원 | "
f"변동성:{volatility_pct:.2f}% | 추천:{int(final_amount):,.0f}{note}"
)
return int(final_amount)
except Exception as e:
logger.error(f"❌ [{stock_name}] 포지션 사이즈 계산 실패: {e}")
return 0
def calculate_quantity(
self,
current_price: float,
target_amount: int,
fee_rate: float = 0.00015
) -> int:
"""
목표 금액을 현재가와 수수료 고려하여 수량으로 변환
Args:
current_price: 현재 주가
target_amount: 목표 투자 금액
fee_rate: 수수료율 (기본 0.015%)
Returns:
매수 가능 수량 (주)
"""
try:
if current_price <= 0 or target_amount <= 0:
return 0
# 수수료 고려 실제 매수 가능 금액
# 총비용 = 가격 * 수량 * (1 + 수수료율)
# 수량 = 목표금액 / (가격 * (1 + 수수료율))
max_buy_amount = target_amount / (1 + fee_rate)
quantity = int(max_buy_amount / current_price)
if quantity < 1:
return 0
# 예상 총 비용 계산 (확인용)
expected_cost = quantity * current_price * (1 + fee_rate)
logger.debug(
f"💵 수량 계산: {current_price:,.0f}× {quantity}주 = "
f"예상비용 {expected_cost:,.0f}원 (목표: {target_amount:,.0f}원)"
)
return quantity
except Exception as e:
logger.error(f"❌ 수량 계산 에러: {e}")
return 0