6 분 분량

지금까지 전략 성과를 볼 때 CAGR과 MDD 두 가지만 썼습니다. 오늘은 이 도구함을 제대로 채웁니다. 수익률 하나만 보고 전략을 고르는 것은 자동차를 최고 속도만 보고 고르는 것과 같습니다. 브레이크와 안전벨트를 함께 봐야 합니다. 퀀트 투자란 무엇인가에서 말한 “검증 가능성”이 실제로는 이 지표들을 본다는 뜻입니다.

변동성 — 얼마나 출렁이는가

변동성(volatility)은 수익률의 표준편차입니다. 평균에서 얼마나 흩어져 있는지를 재는 값이고, 관례적으로 연 단위로 환산해서 씁니다.

import numpy as np

# 월별 수익률이라면 √12, 일별이라면 √252를 곱한다
vol_annual = monthly_returns.std() * np.sqrt(12)

여기서 np.sqrt가 붙는 이유를 짚고 갑니다. 분산(표준편차의 제곱)은 기간에 비례해 커지므로, 표준편차는 기간의 제곱근에 비례합니다. 그래서 월별 변동성을 연율화할 때는 12가 아니라 √12를 곱합니다. 이 규칙을 모르면 변동성을 3.5배 과대평가하게 됩니다.

변동성의 한계도 분명합니다. 위로 튀는 것과 아래로 빠지는 것을 똑같이 위험으로 취급한다는 점입니다. 갑자기 +20% 오른 달과 -20% 빠진 달이 변동성에는 동일하게 기여합니다. 투자자에게 이 둘은 전혀 같지 않죠. 이 문제를 보완한 지표가 뒤에 나올 소르티노 비율입니다.

MDD와 회복 기간 — 얼마나 아팠고, 얼마나 오래 아팠는가

MDD(최대 낙폭)는 첫 백테스트 글에서 다뤘습니다. 전 고점 대비 가장 깊이 떨어진 폭입니다. 여기에 하나를 더 봐야 합니다. 회복 기간(time to recovery), 즉 전 고점을 되찾는 데 걸린 시간입니다.

-30%까지 빠졌다가 6개월 만에 회복한 전략과, -25%까지 빠졌지만 4년간 회복하지 못한 전략이 있다면, 후자가 훨씬 견디기 어렵습니다. 사람이 전략을 포기하는 것은 낙폭의 깊이보다 부진이 지속되는 시간 때문인 경우가 많습니다.

def drawdown_stats(cum):
    """낙폭 시계열, MDD, 최장 침체 기간(개월)을 반환"""
    peak = cum.cummax()
    dd = cum / peak - 1

    # 고점을 회복하지 못한 구간의 연속 길이 중 최댓값
    under = dd < 0
    longest, run = 0, 0
    for flag in under:
        run = run + 1 if flag else 0
        longest = max(longest, run)

    return dd, dd.min(), longest

샤프비율 — 위험 한 단위당 얼마를 벌었나

샤프비율(Sharpe ratio)은 가장 널리 쓰이는 위험조정 수익률 지표입니다.

샤프비율 = (전략 수익률 - 무위험 수익률) / 변동성

“위험을 1만큼 감수해서 초과수익을 얼마나 얻었나”를 재는 값입니다. 연 10%를 변동성 20%로 번 전략보다, 연 8%를 변동성 10%로 번 전략이 더 효율적이라는 판단을 가능하게 해줍니다.

def sharpe(returns, rf_annual=0.03, periods=12):
    """rf_annual: 연 무위험수익률 가정치. 실제 값으로 교체할 것"""
    rf_period = (1 + rf_annual) ** (1 / periods) - 1
    excess = returns - rf_period
    if excess.std() == 0:
        return float('nan')
    return excess.mean() / excess.std() * np.sqrt(periods)

해석의 대략적인 기준은 이렇습니다. 다만 자산군과 기간에 따라 통용되는 수준이 다르므로 절대적인 잣대로 쓰면 안 됩니다.

샤프비율 통상적인 평가
1 미만 평범
1 ~ 2 양호
2 이상 매우 우수 — 다만 백테스트에서 나왔다면 오류를 먼저 의심

마지막 줄이 중요합니다. 개인이 만든 백테스트에서 샤프 3이 나왔다면 축하할 일이 아니라 미래 참조나 비용 누락을 찾아야 할 일입니다.

샤프비율의 함정도 알아둬야 합니다. 분모가 변동성이므로, 위로 크게 튄 달도 아래로 떨어진 달과 똑같이 위험으로 계산됩니다. 또 수익률 분포가 정규분포에 가깝다는 가정을 깔고 있어서, 평소엔 잔잔하다가 가끔 크게 터지는 전략(옵션 매도 같은)은 실제 위험보다 좋아 보이게 됩니다.

소르티노와 칼마 — 다른 각도에서 보기

소르티노 비율(Sortino ratio)은 샤프비율의 분모를 “전체 변동성” 대신 하락 변동성으로 바꾼 것입니다. 기준 수익률(여기서는 무위험수익률)에 못 미친 달의 부족분만 제곱해 전체 기간으로 평균 내고 제곱근을 취한 하방 편차를 위험으로 씁니다.

def sortino(returns, rf_annual=0.03, periods=12):
    rf_period = (1 + rf_annual) ** (1 / periods) - 1
    excess = returns - rf_period
    downside = np.sqrt((np.minimum(excess, 0) ** 2).mean())   # 하방 편차: 기준에 못 미친 부족분만 제곱해 평균 → 제곱근
    if downside == 0 or np.isnan(downside):
        return float('nan')
    return excess.mean() / downside * np.sqrt(periods)

칼마 비율(Calmar ratio)은 훨씬 직관적입니다. CAGR을 MDD의 절댓값으로 나눈 값으로, “내가 견뎌야 했던 최악의 고통 대비 얼마를 벌었나”를 뜻합니다. 칼마 0.5라면 연평균 수익률이 최대 낙폭의 절반이라는 의미입니다.

def calmar(cagr, mdd):
    return cagr / abs(mdd) if mdd != 0 else float('nan')

실전 감각으로는 칼마 비율이 가장 와닿는 지표라고 생각합니다. 샤프비율은 통계적으로 정교하지만, 실제로 계좌를 들고 버티는 사람에게는 “얼마나 깊이 파였나” 대비 수익이 더 피부에 닿습니다.

한 번에 계산하는 성과 요약 함수

지금까지의 지표를 하나로 묶습니다. 앞으로 백테스트를 만들 때마다 이 함수를 재사용하면 됩니다.

import pandas as pd
import numpy as np

def performance_summary(returns, rf_annual=0.03, periods=12):
    r = returns.dropna()
    cum = (1 + r).cumprod()
    n_years = len(r) / periods

    cagr = cum.iloc[-1] ** (1 / n_years) - 1
    vol = r.std() * np.sqrt(periods)
    dd, mdd, longest = drawdown_stats(cum)

    return pd.Series({
        'CAGR': cagr,
        'Volatility': vol,
        'MDD': mdd,
        'Sharpe': sharpe(r, rf_annual, periods),
        'Sortino': sortino(r, rf_annual, periods),
        'Calmar': cagr / abs(mdd) if mdd != 0 else np.nan,
        'Longest DD (기간)': longest,
    })

# 사용 예: 첫 백테스트(입문 ⑤)의 모멘텀 전략과 buy & hold 비교
# summary = pd.DataFrame({
#     'Buy & Hold': performance_summary(returns),
#     'Momentum':   performance_summary(strategy_returns),
# })
# print(summary.round(3))

무위험 수익률(rf_annual)은 시기에 따라 달라지므로 기본값 3%는 어디까지나 가정치입니다. 비교하려는 기간의 실제 단기 국채나 예금 금리 수준으로 바꿔 넣으셔야 합니다.

실제 결과로 읽어보기

위 함수를 지금까지의 코스피 절대 모멘텀 백테스트(2005년 1월~2026년 8월, 비용 0.3% 반영, 무위험수익률 3% 가정)에 적용한 결과입니다. 계산 코드는 run_series_backtests.py에 있습니다.

  Buy & Hold 모멘텀 (비용 반영)
CAGR 9.66% 5.37%
연 변동성 22.5% 18.2%
MDD -48.5% -31.5%
샤프비율 0.39 0.21
소르티노 비율 0.63 0.37
칼마 비율 0.199 0.171
최장 침체 기간 71개월 115개월

이 표가 “지표를 여러 개 봐야 한다”는 말의 뜻을 그대로 보여줍니다.

샤프비율로 보면 모멘텀은 명백한 패자입니다(0.21 vs 0.39). 변동성을 줄이긴 했지만 수익률을 그보다 훨씬 많이 잃었기 때문입니다.

칼마 비율로 보면 거의 비슷합니다(0.171 vs 0.199). 최대 낙폭을 -48.5%에서 -31.5%로 줄인 것이 수익률 손실을 상당히 상쇄합니다. “최악의 순간에 얼마나 아팠나” 대비로는 두 전략이 비슷한 효율이라는 뜻입니다. 2008년을 -48%로 겪을 자신이 없는 사람에게는 이 지표가 더 중요할 수 있습니다.

최장 침체 기간으로 보면 모멘텀이 오히려 나쁩니다(115개월 vs 71개월). 낙폭은 얕지만 물속에 훨씬 오래 있었습니다. 현금으로 대피한 동안 시장이 회복해 버리면, 모멘텀 전략의 계좌는 전 고점을 되찾지 못한 채 시간만 흐릅니다. 이 숫자는 CAGR·MDD·샤프 어디에도 나타나지 않습니다.

같은 백테스트가 어떤 지표로는 “비슷한 효율”, 어떤 지표로는 “명백한 패자”, 어떤 지표로는 “더 나쁨”으로 읽힙니다. 지표 하나를 골라 결론을 내리면 그 순간 나머지가 말하는 것을 놓칩니다.

이 표에는 모멘텀에 불리한 조건이 두 가지 들어 있습니다. 첫째, 첫 백테스트에서 밝혔듯 첫 12개월(2005년 2월~2006년 1월)은 신호를 계산할 수 없어 모멘텀이 현금으로 있었고, 그 사이 코스피는 약 50% 올랐습니다. 출발 조건의 영향을 빼고 2006년 2월~2026년 8월만 보면 Buy & Hold와 모멘텀이 각각 CAGR 8.00%와 5.64%, 샤프 0.32와 0.23, 칼마 0.165와 0.179입니다. 둘째, 샤프·소르티노는 연 3%를 월로 환산한 무위험수익률을 매달 빼는데, 모멘텀이 현금으로 있던 달(전체의 약 40%)은 수익률을 0으로 두었습니다. 현금에도 연 3%를 주면 모멘텀은 CAGR 6.64%, 샤프 0.28, 칼마 0.235가 됩니다. 어느 쪽으로 맞춰도 샤프는 Buy & Hold가 앞서지만, 칼마는 모멘텀이 앞섭니다. 출발 시점과 현금 가정 하나로도 지표의 순위가 바뀔 수 있습니다.

지표를 읽을 때의 원칙

  • 한 지표만 보지 않습니다. CAGR만 보면 위험을 놓치고, 샤프만 보면 낙폭의 깊이를 놓칩니다. 최소한 CAGR, MDD, 샤프 세 개는 함께 봅니다.
  • 비교 대상과 나란히 놓습니다. 샤프 0.8이 좋은지 나쁜지는 같은 기간 buy & hold의 샤프가 얼마였는지를 봐야 압니다.
  • 너무 좋은 숫자는 의심합니다. 앞 글의 교훈이 여기서도 그대로 적용됩니다.
  • 가정치를 명시합니다. 무위험 수익률, 연율화 기준(12/252), 거래 비용을 무엇으로 놓았는지 적어두지 않으면 나중에 자기 결과도 재현하지 못합니다. 데이터가 끝나는 날짜도 마찬가지여서, 같은 코드가 그 날짜만 달라 다른 숫자를 낸 사례를 삽질 기록 — 같은 코드가 다른 숫자를 낸 날에 정리했습니다.

정리

  • 변동성은 √기간으로 연율화하며, 상승과 하락을 구분하지 못한다는 한계가 있다.
  • MDD는 깊이를, 회복 기간은 지속 시간을 재며 둘 다 봐야 한다.
  • 샤프는 위험 한 단위당 초과수익이고, 소르티노는 하락 위험만, 칼마는 최대 낙폭 대비 수익을 본다.
  • 지표는 항상 여러 개를 벤치마크와 나란히 놓고 읽는다.

다음 글에서는 백테스트를 조용히 망가뜨리는 미래 참조 편향(look-ahead bias)을 본격적으로 해부합니다. shift(1)을 빼먹는 것 말고도 미래 정보가 새어 들어오는 경로가 여럿 있는데, 재무제표 발표 시차와 지수 편입 시점처럼 알아채기 어려운 사례를 중심으로 다루겠습니다.

2026-09-28 수정: 변동성 연율화 코드 주석을 “12를 곱한다”에서 “√12를 곱한다”로 고치고, 위로 튄 달이 샤프비율을 나쁘게 만든다고 쓴 문장과 칼마 0.5의 해석 문장을 바로잡았습니다.

2026-10-22 수정: 소르티노 비율의 하방 편차를 초과수익이 음수인 달끼리의 표준편차가 아니라 표준 정의(기준 미달분의 제곱평균 제곱근)로 고쳐(본문 코드와 계산 코드 run_series_backtests.py 모두) 표의 값을 다시 계산하고(0.59·0.36 → 0.63·0.37), 표의 출발 시점과 현금 수익률 가정이 모멘텀에 불리하게 작용한다는 설명을 더했습니다.

참고 자료

  • Sharpe, W. F. (1966). Mutual Fund Performance. Journal of Business, 39(1).
  • Sortino, F. A., & Price, L. N. (1994). Performance Measurement in a Downside Risk Framework. Journal of Investing, 3(3).

이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.

최종 수정: