""" Position sizing module. Supports three methods: - fixed_ratio: Equal allocation (quant.md default, 30% cash reserve) - kelly_criterion: Kelly criterion (conservative 1/4 Kelly) - atr_based: ATR-based volatility sizing """ def fixed_ratio( capital: float, num_positions: int, cash_ratio: float = 0.3, ) -> dict: """Equal allocation across positions with cash reserve. Per quant.md: 5-10 positions, 30% cash, max loss per position -3%. """ if capital <= 0: raise ValueError("capital must be positive") if num_positions <= 0: raise ValueError("num_positions must be positive") if cash_ratio < 0 or cash_ratio >= 1.0: raise ValueError("cash_ratio must be in [0, 1)") investable = capital * (1 - cash_ratio) position_size = investable / num_positions risk_amount = position_size * 0.03 # -3% max loss per position (quant.md) return { "method": "fixed", "position_size": position_size, "shares": 0, # needs price to calculate "risk_amount": risk_amount, "notes": ( f"균등 분배: 투자금 {investable:,.0f}원을 {num_positions}개 종목에 배분. " f"종목당 최대 손실 -3% = {risk_amount:,.0f}원." ), } def kelly_criterion( win_rate: float, avg_win: float, avg_loss: float, fraction: float = 0.25, ) -> dict: """Kelly criterion position sizing (default: conservative 1/4 Kelly). Kelly% = W - (1-W)/R where W=win_rate, R=avg_win/avg_loss """ if win_rate <= 0 or win_rate > 1.0: raise ValueError("win_rate must be in (0, 1]") if avg_win <= 0: raise ValueError("avg_win must be positive") if avg_loss <= 0: raise ValueError("avg_loss must be positive") if fraction <= 0 or fraction > 1.0: raise ValueError("fraction must be in (0, 1]") win_loss_ratio = avg_win / avg_loss kelly_pct = win_rate - (1 - win_rate) / win_loss_ratio position_size = max(kelly_pct * fraction, 0.0) return { "method": "kelly", "position_size": position_size, "shares": 0, "risk_amount": position_size * avg_loss, "notes": ( f"켈리 기준: Full Kelly = {kelly_pct:.2%}, " f"{fraction:.0%} Kelly = {position_size:.2%}. " f"승률 {win_rate:.0%}, 평균 수익 {avg_win:.1%}, 평균 손실 {avg_loss:.1%}." ), } def atr_based( capital: float, atr: float, risk_pct: float = 0.02, ) -> dict: """ATR-based volatility position sizing. Shares = (Capital * Risk%) / ATR """ if capital <= 0: raise ValueError("capital must be positive") if atr <= 0: raise ValueError("atr must be positive") if risk_pct <= 0 or risk_pct > 1.0: raise ValueError("risk_pct must be in (0, 1]") risk_amount = capital * risk_pct shares = int(risk_amount / atr) return { "method": "atr", "position_size": risk_amount, "shares": shares, "risk_amount": risk_amount, "notes": ( f"ATR 사이징: 자본금의 {risk_pct:.1%} = {risk_amount:,.0f}원 위험 허용. " f"ATR {atr:,.0f} 기준 {shares}주." ), }