퀀트 투자 입문 ⑤ — 첫 백테스트: 절대 모멘텀을 코드 30줄로
앞의 네 글에서 준비를 마쳤습니다. 데이터를 받는 법, 수익률을 계산하는 법, 백테스트가 무엇인지, 모멘텀이 무엇인지. 이제 그것들을 하나로 합쳐 실제로 돌아가는 백테스트를 만듭니다. 코스피 지수 하나에 절대 모멘텀 규칙을 적용하고, 그냥 들고 있기(buy & hold)와 비교합니다.
검증할 규칙
가장 단순한 형태로 정합니다.
매월 말, 코스피의 최근 12개월 수익률이 플러스면 다음 달 한 달간 코스피를 보유한다. 마이너스면 현금으로 대피한다(수익률 0).
백테스트 4단계 중 1단계(규칙 정하기)가 이걸로 끝났습니다. 애매한 단어가 하나도 없다는 점을 확인해 보세요. “12개월 수익률 > 0”은 누가 계산해도 같은 답이 나옵니다.
2단계: 데이터 준비
월 단위 규칙이므로 월말 종가만 있으면 됩니다.
import FinanceDataReader as fdr
import pandas as pd
# 코스피 지수 일별 종가 → 월말 종가
kospi = fdr.DataReader('KS11', '2005-01-01', '2026-08-31')['Close']
monthly = kospi.resample('ME').last()
print(monthly.tail())
print(f"월 데이터 개수: {len(monthly)}")
resample('ME')는 월말(Month End) 기준으로 묶으라는 뜻이고, .last()로 그달의 마지막 값을 취합니다. 2005년부터 잡은 이유는 2008년 금융위기와 2020년 코로나 폭락을 모두 포함시키기 위해서입니다. 앞 글에서 말한 “최소한 큰 하락장을 한 번 이상 포함하라”는 조건입니다. 아래 표의 숫자와 맞추려고 종료일은 2026년 8월 31일로 고정했습니다. 종료일을 빼면 실행한 날까지의 데이터가 들어가고, 아직 끝나지 않은 달이 마지막 행으로 섞입니다(FinanceDataReader 글 참고).
3단계: 규칙을 기계적으로 적용
여기가 백테스트의 심장입니다. 코드는 다섯 줄이지만 한 줄 한 줄이 중요합니다.
# 월별 수익률
returns = monthly.pct_change()
# 신호: 최근 12개월 수익률이 플러스인가?
momentum = monthly.pct_change(12)
signal = (momentum > 0).astype(int) # 1 = 보유, 0 = 현금
# 핵심: 이번 달 말에 관측한 신호는 '다음 달'에 적용된다
position = signal.shift(1)
# 전략의 월별 수익률
strategy_returns = position * returns
shift(1)이 없으면 백테스트는 무효입니다
position = signal.shift(1) 이 한 줄이 이 코드 전체에서 가장 중요합니다.
signal은 그달 말 종가를 봐야 계산됩니다. 8월 31일 종가를 알아야 8월의 12개월 모멘텀을 알 수 있죠. 그런데 returns의 8월 값은 8월 한 달 동안의 수익률입니다. 이 둘을 그냥 곱하면 어떻게 될까요? “8월이 끝난 뒤에 알게 된 정보로, 이미 지나간 8월에 투자한다”는 시간여행이 됩니다.
shift(1)은 신호를 한 칸 뒤로 미뤄서 “8월 말에 본 신호 → 9월 수익률에 적용”으로 맞춰줍니다. 이것이 앞 글에서 말한 미래 참조 편향(look-ahead bias)을 막는 장치입니다. 이 한 줄을 빼면 백테스트 성과가 마법처럼 좋아지는데, 그건 실력이 아니라 버그입니다.
시험 삼아 shift(1)을 지우고 돌려보세요. 성과가 얼마나 부풀려지는지 직접 보면 이 개념을 평생 잊지 않게 됩니다.
4단계: 성과 평가
이제 buy & hold와 나란히 놓고 비교합니다.
result = pd.DataFrame({
'Buy & Hold': returns,
'Momentum': strategy_returns
}).dropna()
cum = (1 + result).cumprod()
# 연평균 복리 수익률 (CAGR)
n_years = len(result) / 12
cagr = cum.iloc[-1] ** (1 / n_years) - 1
# 최대 낙폭 (MDD): 고점 대비 얼마나 깊이 떨어졌나
def max_drawdown(cum_series):
peak = cum_series.cummax()
return ((cum_series / peak) - 1).min()
mdd = cum.apply(max_drawdown)
print(pd.DataFrame({'CAGR': cagr, 'MDD': mdd}).round(4))
cum.plot(figsize=(12, 5), title='Absolute Momentum vs Buy & Hold (KOSPI)')
여기서 처음 등장한 지표가 MDD(Maximum Drawdown, 최대 낙폭)입니다. 전 고점 대비 가장 깊이 떨어졌던 폭으로, “이 전략을 들고 있었다면 최악의 순간에 계좌가 얼마나 녹아 있었나”를 뜻합니다. 수익률만큼, 어쩌면 수익률보다 중요한 숫자입니다. CAGR이 아무리 좋아도 MDD가 -60%인 전략은 실제로는 대부분의 사람이 중간에 포기하기 때문입니다.
실제로 돌려본 결과
위 코드를 실제 코스피 지수 월말 종가(2005년 1월~2026년 8월, 260개월, FinanceDataReader)로 돌린 결과입니다. 비용은 아직 반영하지 않았습니다. 또 데이터가 2005년 1월부터 시작하므로 첫 12개월(2005년 2월~2006년 1월)은 신호에 필요한 12개월 수익률을 계산할 수 없고, 위 코드는 이 기간을 신호 0, 즉 현금으로 처리합니다. 그 사이 코스피가 약 50% 올랐기 때문에 아래 CAGR 격차(3.7%p)의 절반가량은 이 출발 조건에서 생긴 것입니다. 표와 차트를 만든 전체 코드는 run_series_backtests.py에 있고, 입문 ⑧까지의 결과도 이 파일로 다시 계산할 수 있습니다. 다른 글의 코드는 코드 페이지에 모아 두었습니다.
| Buy & Hold | 절대 모멘텀(12개월) | |
|---|---|---|
| CAGR | 9.66% | 5.91% |
| 연 변동성 | 22.5% | 18.2% |
| MDD | -48.5% | -30.5% |
| 최장 침체 기간 | 71개월 | 115개월 |

차트에는 선이 하나 더 있습니다. ‘Momentum (net)’은 다음 글에서 다룰 편도 0.3% 거래 비용을 반영한 선이고, 위 표의 숫자는 비용을 반영하지 않은 ‘Momentum (gross)’ 기준입니다.
솔직히 말하면 예상보다 모멘텀 쪽이 훨씬 초라합니다. 연 수익률에서 3.7%p를 내줬고, 21년 반 동안 누적으로는 Buy & Hold가 약 7.3배, 모멘텀은 약 3.5배입니다. 절반도 안 됩니다.
대신 약속한 것은 지켰습니다. 2008년 금융위기에서 Buy & Hold가 -48.5%까지 빠질 때 모멘텀의 낙폭은 -20% 안팎에서 멈췄습니다. 표의 MDD -30.5%는 그때가 아니라 2021년 고점 이후 2024년 말까지 이어진 낙폭입니다. 변동성도 4%p 낮습니다. “큰 하락장을 피한다”는 목적 자체는 달성한 겁니다.
그런데 표에서 가장 놀란 숫자는 최장 침체 기간입니다. 모멘텀이 115개월, 거의 10년입니다. Buy & Hold의 71개월보다 훨씬 깁니다. 낙폭이 얕은 전략이 왜 물속에 더 오래 있었을까요? 아래 낙폭 그래프를 보면 이유가 보입니다.

이 낙폭 그래프의 모멘텀 선은 비용을 반영한 ‘Momentum (net)’이라 최저점(2024년 12월, -31.5%)이 표의 -30.5%보다 조금 깊습니다.
모멘텀의 낙폭 선에 수평으로 멈춘 구간들이 있습니다. 현금으로 대피해 있는 기간입니다. 그 사이 시장은 반등해서 Buy & Hold의 낙폭은 회복되는데, 모멘텀은 현금에 앉아 있으니 낙폭이 그 자리에 얼어 있습니다. 2011년부터 2017년까지 코스피가 박스권에서 오르내리던 시기에 모멘텀은 샀다 팔았다를 반복하며 계속 물속에 있었습니다. 이것이 앞 글에서 말한 휩쏘의 실제 모습입니다.
누적 수익률 그래프 오른쪽 끝에서 두 선의 간격이 크게 벌어져 보이는 것은 세로축이 선형 눈금이라, 같은 비율 차이도 값이 커질수록 넓게 그려지기 때문입니다. 두 곡선의 비율(Buy & Hold ÷ 모멘텀)로 보면 2025년 이후에 커진 것은 2024년 말 1.89배에서 2025년 5월 2.12배까지가 전부입니다. 모멘텀은 2025년 1월과 4~5월 상승을 현금으로 놓쳤고, 6월에 다시 들어간 뒤로는 Buy & Hold와 같은 포지션이었습니다(모멘텀 누적 1.4배 → 2026년 5월 4.3배 → 8월 3.5배).
격차의 큰 몫은 그보다 앞에서 생겼습니다. 출발 조건으로 첫 12개월 동안 1.5배가 된 비율은 2008년 폭락을 피하면서 2009년 2월에 거의 1배까지 좁혀졌지만, 2009년 3~8월 반등을 현금으로 놓치면서 1.49배로 되돌아갔습니다. 그 뒤로는 하락을 피해 좁혀지고 반등을 놓쳐 벌어지기를 되풀이하다 2024년 말 1.89배가 됐습니다. 모멘텀의 구조적 약점인 “급격한 방향 전환에 늦다”는 2025년만이 아니라 2009년에도 그대로 나타났습니다.
이 결과는 코스피라는 하나의 시장, 21년이라는 하나의 표본에서 나온 것입니다. 다른 시장이나 다른 기간이라면 다를 수 있습니다. 다만 “절대 모멘텀은 수익을 올려주는 전략”이 아니라 “수익의 일부를 내주고 최악의 낙폭을 사는 보험”이라는 성격은 분명하게 드러났다고 봅니다. 표의 격차는 연 3.7%p지만, 출발 조건의 영향을 뺀 2006년 2월~2026년 8월만 보면 CAGR은 8.00% 대 6.21%로 보험료는 연 1.8%p 안팎입니다. 이 값이 비싼지 싼지는 사람마다 답이 다를 겁니다.
결과를 읽는 법
위 결과를 볼 때 던져야 할 질문을 정리합니다.
- CAGR이 buy & hold보다 높은가? 낮아도 실패가 아닙니다. 다음 질문을 함께 봐야 합니다.
- MDD가 얼마나 줄었는가? 절대 모멘텀의 존재 이유는 수익 극대화가 아니라 큰 하락장 회피입니다. CAGR이 비슷한데 MDD가 절반이면 훌륭한 결과입니다.
- 누적 수익률 곡선의 모양은? 2008년과 2020년 구간에서 두 선이 어떻게 갈라지는지 보세요. 모멘텀 전략이 폭락을 피했다면 그 지점에서 Buy & Hold 선만 꺾이고 모멘텀 선은 평평하게 버팁니다. 이 백테스트에서는 Buy & Hold가 위에 있었기 때문에 그 구간에서 두 선의 간격이 오히려 좁혀졌습니다.
- 반대로 어디서 손해를 봤는가? 급반등 구간(2009년, 2020년 4~5월)에서는 현금에 앉아 있느라 상승을 놓쳤을 겁니다. 앞 글에서 말한 모멘텀의 구조적 약점이 실제 데이터에서 어떻게 나타나는지 확인하는 순간입니다. 가장 많이 오른 날들이 폭락 한가운데에 몰려 있어 하락장에서 빠져 있으면 놓치기 쉽다는 점은 코스피 최고의 날 10일을 놓치면에서 일별 데이터로 따로 확인해 봤습니다.
이 백테스트에 아직 없는 것들
축하하지 말고 의심하라고 했었죠. 지금 이 30줄에는 현실이 여럿 빠져 있습니다.
- 거래 비용 — 매수·매도 수수료와 세금, 슬리피지가 전혀 없습니다. 신호가 자주 바뀌는 구간에서는 이 비용이 성과를 상당히 갉아먹습니다.
- 현금의 수익률 — 대피했을 때 수익률을 0으로 뒀지만, 실제로는 예금이나 단기 채권에 넣어 이자를 받습니다. 이건 오히려 성과를 과소평가한 부분입니다.
- 지수는 살 수 없다 — 코스피 지수 자체는 매매할 수 없고, 실제로는 ETF를 삽니다. ETF는 운용보수가 있고 지수를 완벽히 따라가지도 않습니다.
- 파라미터 12개월은 어디서 왔나 — 제가 그냥 정했습니다. 6개월, 9개월로 바꾸면 결과가 달라지고, “가장 성과 좋은 개월 수”를 찾아 헤매는 순간 과최적화가 시작됩니다.
특히 4번은 다음 단계로 넘어가기 전에 반드시 짚어야 할 주제입니다.
정리
- 절대 모멘텀 백테스트의 뼈대는
pct_change→ 신호 →shift(1)→ 곱하기, 네 단계다. shift(1)은 미래 참조를 막는 장치이고, 이 한 줄이 백테스트의 유효성을 결정한다.- 성과는 CAGR만이 아니라 MDD와 곡선의 모양까지 함께, 반드시 buy & hold와 비교해서 읽는다.
- 첫 백테스트에는 거래 비용, 파라미터 선택 문제 등 현실이 빠져 있다.
앞에서 꼽은 빠진 것 가운데 1번 거래 비용과 4번 파라미터 문제를 이어지는 두 글에서 다룹니다. 거래 비용을 반영하면 성과가 얼마나 달라지는지, 그리고 파라미터를 바꿔가며 최고 성과를 찾는 것이 왜 위험한지를 같은 코드로 직접 확인합니다.
2026-09-28 수정: 2008년 금융위기 때 모멘텀의 낙폭을 표의 MDD(-30.5%)로 잘못 적었던 문장을 -20% 안팎으로 바로잡고, 첫 12개월이 현금으로 처리되는 출발 조건 설명을 더했으며, 누적 배수(약 7.3배, 1.4배→4.3배)를 다시 계산한 값으로, 급등 구간의 시작을 2025년으로 고쳤습니다.
2026-10-22 수정: 표의 수치가 그대로 재현되도록 코드에 종료일(2026-08-31)을 넣고, ‘2025년 이후 격차가 결정적으로 벌어졌다’는 서술과 ‘폭락을 피한 지점에서 두 선이 벌어진다’는 서술을 두 곡선의 비율로 다시 따져 고쳤으며, 결론의 보험료를 출발 조건을 뺀 기간 기준(연 1.8%p 안팎)으로 바로잡고, 차트의 ‘Momentum (net)’ 선이 비용을 반영한 선이라는 설명을 더했습니다.
이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.