최대낙폭(MDD)이란?
최대낙폭(Maximum Drawdown, MDD)은 투자 기간 중 고점 대비 최대 하락률을 의미합니다. 수익률만큼이나 중요한 리스크 지표로, 실제 투자에서 투자자가 견뎌야 하는 최악의 심리적·재정적 압박을 나타냅니다. 연 수익률 20%인 전략이라도 MDD가 -60%라면 대부분의 투자자가 중도 이탈합니다.
이 글에서는 파이썬으로 MDD를 정밀하게 분석하고, MDD를 제한하는 다양한 자동 리스크 관리 기법을 구현합니다.
MDD 계산과 분석
먼저 MDD를 계산하고, 낙폭의 깊이·기간·회복 시간을 분석하는 코드입니다.
import numpy as np
import pandas as pd
import yfinance as yf
data = yf.download('SPY', start='2015-01-01', end='2025-12-31')
prices = data['Close']
returns = prices.pct_change().dropna()
def analyze_drawdowns(prices, top_n=5):
"""낙폭 분석: 깊이, 기간, 회복 시간"""
cummax = prices.cummax()
drawdown = (prices - cummax) / cummax
# 낙폭 구간 식별
is_dd = drawdown = peak_price]
recovery_days = (recovery.index[0] - trough_date).days if len(recovery) > 0 else None
dd_list.append({
'peak_date': peak_date,
'trough_date': trough_date,
'max_drawdown': round(max_dd, 4),
'duration_days': duration,
'recovery_days': recovery_days
})
df = pd.DataFrame(dd_list).sort_values('max_drawdown')
return df.head(top_n), drawdown
top_dd, dd_series = analyze_drawdowns(prices)
print("=== 상위 5개 낙폭 ===")
print(top_dd.to_string(index=False))
print(f"n전체 MDD: {dd_series.min():.2%}")
MDD 수치만 보는 것이 아니라 회복 시간까지 분석하는 것이 핵심입니다. -30% 낙폭이라도 3개월 만에 회복하면 괜찮지만, 2년이 걸리면 전략의 실전 활용 가치가 크게 떨어집니다.
전략 1: MDD 기반 포지션 축소
현재 낙폭이 일정 수준을 넘으면 포지션을 단계적으로 줄이는 방식입니다. 가장 직관적이고 구현이 간단합니다.
def mdd_position_scaling(returns, thresholds=None):
"""
낙폭 수준에 따른 포지션 축소
thresholds: [(낙폭%, 포지션비율), ...]
"""
if thresholds is None:
thresholds = [
(-0.05, 1.0), # -5% 미만: 풀 포지션
(-0.10, 0.7), # -5%~-10%: 70%
(-0.15, 0.4), # -10%~-15%: 40%
(-0.20, 0.1), # -15%~-20%: 10%
(-1.00, 0.0), # -20% 초과: 전량 청산
]
equity = (1 + returns).cumprod()
cummax = equity.cummax()
current_dd = (equity - cummax) / cummax
positions = pd.Series(1.0, index=returns.index)
for dd_level, pos_size in thresholds:
positions[current_dd <= dd_level] = pos_size
# 1일 지연
positions = positions.shift(1).fillna(1.0)
strategy_returns = positions * returns
return strategy_returns, positions
mdd_ret, mdd_pos = mdd_position_scaling(returns)
# 비교
orig_equity = (1 + returns).cumprod()
strat_equity = (1 + mdd_ret).cumprod()
orig_mdd = ((orig_equity - orig_equity.cummax()) / orig_equity.cummax()).min()
strat_mdd = ((strat_equity - strat_equity.cummax()) / strat_equity.cummax()).min()
print(f"Buy&Hold MDD: {orig_mdd:.2%}")
print(f"MDD 관리 후 MDD: {strat_mdd:.2%}")
print(f"Buy&Hold 최종 수익: {orig_equity.iloc[-1] - 1:.2%}")
print(f"MDD 관리 후 최종 수익: {strat_equity.iloc[-1] - 1:.2%}")
전략 2: 트레일링 스탑 기반 청산
고점 대비 일정 비율 이상 하락하면 전량 청산하고, 시장이 회복된 후 재진입하는 방식입니다.
def trailing_stop_strategy(returns, stop_pct=-0.10,
reentry_days=10):
"""
트레일링 스탑 전략
- stop_pct: 고점 대비 청산 기준 (-10%)
- reentry_days: 청산 후 재진입 대기 일수
"""
equity = (1 + returns).cumprod()
position = pd.Series(1.0, index=returns.index)
in_market = True
wait_counter = 0
peak = equity.iloc[0]
for i in range(1, len(equity)):
if in_market:
peak = max(peak, equity.iloc[i-1])
dd = (equity.iloc[i-1] - peak) / peak
if dd = reentry_days:
in_market = True
peak = equity.iloc[i-1]
position.iloc[i] = 1
else:
position.iloc[i] = 0
strategy_returns = position.shift(1).fillna(1) * returns
return strategy_returns, position
trail_ret, trail_pos = trailing_stop_strategy(returns)
trail_equity = (1 + trail_ret).cumprod()
trail_mdd = ((trail_equity - trail_equity.cummax()) / trail_equity.cummax()).min()
print(f"트레일링 스탑 MDD: {trail_mdd:.2%}")
변동성 타겟팅 전략과 결합하면 변동성 급등 구간에서 포지션을 먼저 줄이고, MDD 한도 초과 시 추가 청산하는 이중 방어가 가능합니다.
전략 3: CPPI(일정 비율 포트폴리오 보험)
CPPI(Constant Proportion Portfolio Insurance)는 최소 보전 자산(Floor)을 설정하고, 쿠션(현재 자산 – Floor) 비율만큼만 위험 자산에 투자하는 기법입니다.
def cppi_strategy(returns, floor_pct=0.80, multiplier=3,
rf_rate=0.04):
"""
CPPI 전략
- floor_pct: 원금 대비 최소 보전 비율 (80%)
- multiplier: 쿠션 배수 (공격성)
- rf_rate: 무위험 수익률 (연, 안전자산 수익)
"""
n = len(returns)
rf_daily = (1 + rf_rate) ** (1/252) - 1
wealth = np.zeros(n + 1)
wealth[0] = 1.0
floor = floor_pct # 초기 자본의 80% 보전
positions = np.zeros(n)
for i in range(n):
cushion = max(wealth[i] - floor, 0)
risky_weight = min(multiplier * cushion / wealth[i], 1.0)
positions[i] = risky_weight
risky_return = returns.iloc[i]
safe_return = rf_daily
wealth[i+1] = wealth[i] * (
risky_weight * (1 + risky_return) +
(1 - risky_weight) * (1 + safe_return)
)
# Floor도 무위험 수익률로 성장
floor *= (1 + rf_daily)
cppi_returns = pd.Series(np.diff(wealth) / wealth[:-1],
index=returns.index)
return cppi_returns, pd.Series(positions, index=returns.index)
cppi_ret, cppi_pos = cppi_strategy(returns)
cppi_equity = (1 + cppi_ret).cumprod()
cppi_mdd = ((cppi_equity - cppi_equity.cummax()) / cppi_equity.cummax()).min()
print(f"CPPI MDD: {cppi_mdd:.2%}")
print(f"CPPI 최종 수익: {cppi_equity.iloc[-1] - 1:.2%}")
print(f"평균 위험자산 비중: {cppi_pos.mean():.1%}")
전략 비교 요약
| 전략 | 장점 | 단점 | 적합 대상 |
|---|---|---|---|
| MDD 포지션 축소 | 구현 간단, 직관적 | V자 반등 시 수익 감소 | 중장기 투자자 |
| 트레일링 스탑 | 큰 폭락 완전 회피 | 횡보장 잦은 청산 | 추세 추종 전략 |
| CPPI | 원금 보전 보장 | 상승장 수익 제한 | 보수적 투자자 |
실전 적용 핵심 규칙
- MDD 한도를 먼저 정하라: 전략 설계 전에 “최대 몇 퍼센트까지 감내할 수 있는가”를 결정하세요. 이것이 모든 파라미터의 기준점입니다.
- 회복 시간을 함께 고려하라: MDD -15%여도 회복에 1년 걸리면 심리적 부담이 큽니다. MDD × 회복 기간의 곱을 보조 지표로 활용하세요.
- 재진입 규칙이 핵심이다: 청산은 쉽지만 재진입 타이밍이 어렵습니다. 명확한 재진입 조건을 사전에 설정해야 합니다.
- 백테스트 MDD는 과소추정된다: 미래에는 더 큰 낙폭이 올 수 있습니다. 백테스트 MDD의 1.5~2배를 실전 예상치로 잡으세요.
- 다중 방어선을 구축하라: 변동성 타겟팅 + MDD 관리 + 자산 분산을 함께 적용하는 것이 가장 안정적입니다.
켈리 기준 자금 배분 전략과 MDD 관리를 결합하면 수익 극대화와 낙폭 제한이라는 두 가지 목표를 동시에 달성할 수 있습니다.
마무리
MDD 관리는 퀀트 전략에서 가장 중요한 리스크 관리 요소입니다. 아무리 높은 수익률을 기록해도, 중간에 -50% 낙폭을 경험하면 투자자는 전략을 포기합니다. 위 3가지 기법 중 자신의 투자 성향에 맞는 방식을 선택하고, 반드시 아웃오브샘플 검증을 거친 후 실전에 적용하세요. 수익보다 생존이 먼저입니다.
7) MDD 실시간 모니터링 코드
백테스트뿐 아니라 실시간 매매에서도 MDD를 추적해야 합니다. 기준선을 넘으면 자동으로 포지션을 줄이거나 매매를 중단하는 로직입니다.
import numpy as np
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class MDDMonitor:
"""실시간 MDD 모니터링 및 자동 대응"""
max_mdd_pct: float = 15.0 # MDD 허용치 (%)
warning_mdd_pct: float = 10.0 # 경고 기준 (%)
_peak: float = field(default=0.0, init=False)
_current_mdd: float = field(default=0.0, init=False)
def update(self, equity: float) -> dict:
"""잔고 업데이트 → MDD 계산 → 액션 반환"""
if equity > self._peak:
self._peak = equity
if self._peak > 0:
self._current_mdd = ((self._peak - equity) / self._peak) * 100
action = "normal"
if self._current_mdd >= self.max_mdd_pct:
action = "stop" # 매매 중단
elif self._current_mdd >= self.warning_mdd_pct:
action = "reduce" # 포지션 50% 축소
return {
"peak": self._peak,
"equity": equity,
"mdd_pct": round(self._current_mdd, 2),
"action": action,
"timestamp": datetime.now().isoformat(),
}
# 사용 예시
monitor = MDDMonitor(max_mdd_pct=15.0, warning_mdd_pct=10.0)
equities = [1000, 1050, 1100, 1080, 1020, 980, 950, 940, 960]
for eq in equities:
result = monitor.update(eq)
if result["action"] != "normal":
print(f"⚠️ {result['action'].upper()}: MDD {result['mdd_pct']}% (잔고 {eq})")
8) CPPI vs Trailing Stop vs 고정 비율: 전략 비교
| 전략 | 원리 | 장점 | 단점 |
|---|---|---|---|
| CPPI | 쿠션(잔고-바닥) × 승수 | 하방 보호 + 상방 참여 | 급락 시 갭 리스크 |
| Trailing Stop | 고점 대비 N% 하락 시 청산 | 추세 추종에 적합 | 횡보장에서 잦은 청산 |
| 고정 비율 | 항상 계좌의 N% 리스크 | 단순하고 일관적 | MDD 적응적 조절 불가 |
9) 관련 글
- 포지션 사이징 전략 — MDD 관리와 포지션 크기 결정은 동전의 양면입니다.
- 샤프·소르티노·칼마 비율 — MDD를 활용한 칼마 비율(Calmar Ratio) 계산법을 다룹니다.
- 자동매매 손절 전략 비교 — Trailing Stop을 포함한 5가지 손절 전략의 상세 비교입니다.
- 계좌 생존 규칙 7가지 — MDD 관리가 계좌 생존에 미치는 영향을 종합적으로 다룹니다.