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
108 lines
3.2 KiB
Python
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}주."
|
|
),
|
|
}
|