파이썬 최대낙폭 MDD 관리 전략

최대낙폭(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) 관련 글

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux