galaxis-po/backend/app/services/position_sizing.py
머니페니 12d235a1f1 feat: add 9 new modules - notification alerts, trading journal, position sizing, pension allocation, drawdown monitoring, benchmark dashboard, tax simulation, correlation analysis, parameter optimizer
Phase 1:
- Real-time signal alerts (Discord/Telegram webhook)
- Trading journal with entry/exit tracking
- Position sizing calculator (Fixed/Kelly/ATR)

Phase 2:
- Pension asset allocation (DC/IRP 70% risk limit)
- Drawdown monitoring with SVG gauge
- Benchmark dashboard (portfolio vs KOSPI vs deposit)

Phase 3:
- Tax benefit simulation (Korean pension tax rules)
- Correlation matrix heatmap
- Parameter optimizer with grid search + overfit detection
2026-03-29 10:03:08 +09:00

108 lines
3.2 KiB
Python

"""
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}주."
),
}