듀얼 모멘텀 백테스트 — 18년 실측, 코로나는 못 피했다
게리 안토나치의 듀얼 모멘텀(GEM)은 이 구간에서 "수익을 늘린 전략"이 아니라 "낙폭을 줄인 전략"이었습니다. SPY·EFA·AGG·BIL의 배당 재투자 가격으로 2008-06-02 ~ 2026-08-31을 파이썬으로 직접 돌리면 CAGR 8.62%(SPY 보유 11.85%), MDD −33.72%(SPY −50.70%)입니다. 그런데 그 −33.72%는 2020년 코로나 폭락을 그대로 맞은 값입니다 — 월 1회 신호는 한 달 안에 끝나는 폭락을 못 피합니다. 또 룩백을 3·6·9·12개월로 바꾸면 CAGR이 6.53%~10.09%, 리밸런싱일을 바꾸면 7.79%~9.76%로 흔들립니다. 숫자 하나보다 이 흔들림의 폭이 자동화 전에 알아야 할 정보입니다. (비용·세금·환율 미반영, 과거≠미래)
목차
- GEM 규칙 — 세 줄이면 끝난다
- 18년 실측 결과
- 함정 ① 월 1회 신호는 한 달짜리 폭락을 못 피한다
- 함정 ② 방어 자산도 떨어진다 — 2022년 AGG −13%
- 함정 ③ 룩백 기간을 고르는 순간 스누핑이 시작된다
- 함정 ④ 리밸런싱 날짜 운
- 재현 코드 전체
- 자동매매로 옮길 때 — KIS API·국내 ETF·세금
- 자주 묻는 질문
1. GEM 규칙 — 세 줄이면 끝난다
듀얼 모멘텀은 절대 모멘텀(주식이 현금보다 나았나)과 상대 모멘텀(미국과 미국 외 중 어느 쪽이 나았나)을 겹친 규칙입니다. 안토나치가 Dual Momentum Investing(2014)에서 제시한 GEM(Global Equities Momentum)은 월말마다 아래만 판정합니다.
매월 말, 최근 12개월 수익률을 비교한다
1) SPY(미국 주식) 12M 수익률 > BIL(단기 국채) 12M 수익률 ? ← 절대 모멘텀
예 → SPY 와 EFA(미국 외 선진국) 중 12M 수익률이 큰 쪽 100% ← 상대 모멘텀
아니오 → AGG(미국 종합채권) 100%
2) 다음 달 첫 거래일부터 그 자산을 한 달 보유
규칙이 단순해서 판단이 끼어들 틈이 없고, 한 달에 한 번만 돌면 되므로 리밸런싱형 자동매매의 입문 전략으로 자주 거론됩니다. 문제는 "단순하다"와 "검증됐다"가 다른 말이라는 점입니다. 그래서 직접 돌렸습니다.
2. 18년 실측 결과
조건 — 데이터: FinanceDataReader의 Adj Close(배당 재투자 반영, USD). 신호: 각 월 마지막 거래일 종가 기준 12개월 수익률. 체결: 신호 다음 거래일 수익률부터 반영(미래참조 방지). 첫 신호 2008-05-30, 성과 구간 2008-06-02 ~ 2026-08-31. 비교: SPY 보유, SPY 60 / AGG 40(일간 리밸런싱 근사). 매매비용·세금·환율 미반영.
| 전략 | CAGR | MDD | 연 변동성 | 샤프(rf=0) | 1 → 최종 |
|---|---|---|---|---|---|
| GEM 12M | 8.62% | −33.72% | 15.86% | 0.60 | 4.54배 |
| SPY 보유 | 11.85% | −50.70% | 19.74% | 0.67 | 7.64배 |
| SPY 60 / AGG 40 | 8.66% | −32.27% | 12.09% | 0.75 | 4.53배 |
세 가지가 보입니다. ⑴ GEM은 SPY보다 CAGR이 3.2%p 낮았습니다. ⑵ 대신 MDD는 −50.70%에서 −33.72%로 줄었습니다. ⑶ 그런데 아무 판단 없는 60/40 고정 비중이 CAGR·MDD 모두 GEM과 거의 같고 변동성은 더 낮았습니다. 이 구간에서 GEM의 신호가 60/40보다 나은 무언가를 더했다고 말하기 어렵다는 뜻입니다.
219개월 동안 보유 자산은 SPY 143개월 · EFA 39개월 · AGG 37개월이었고, 자산을 바꾼 횟수는 33회(연 약 1.8회)였습니다. 2008-06부터 2009-08까지 15개월 연속 AGG에 있어서 금융위기 하락(SPY 2008년 6~12월 −33.8%)을 피한 대신 GEM은 +6.1%였고, 이것이 MDD 차이의 대부분을 만듭니다.
3. 함정 ① 월 1회 신호는 한 달짜리 폭락을 못 피한다
GEM의 MDD −33.72%가 언제 났는지 찾아보면 고점 2020-02-19, 저점 2020-03-23입니다. SPY의 같은 구간 낙폭과 소수점까지 같습니다. 신호 기록을 보면 이유가 바로 나옵니다.
신호일 SPY 12M EFA 12M BIL 12M → 다음 달 보유
2020-02-28 +8.32% +0.05% +1.91% SPY (3월 폭락을 SPY로 통과)
2020-03-31 −6.89% −14.85% +1.94% AGG (저점 03-23 이후에야 전환)
2020-04-30 +0.81% −12.46% +1.72% AGG (반등 한 달을 채권으로)
2020-05-29 +11.60% −3.34% +1.52% SPY
2월 말 시점의 12개월 수익률은 여전히 +8.32%였습니다. 폭락은 3월 한 달 안에 시작해 끝났고, 신호는 그 달이 끝나야 바뀝니다. 그 결과 GEM은 하락은 전부 맞고, 반등 초입은 채권으로 놓치는 최악의 조합을 겪었습니다. GEM 계좌가 2020-02-19 고점을 회복한 날은 2021-01-20입니다.
자동매매 관점의 결론 — "추세추종이니 폭락은 피한다"는 기대는 폭락이 신호 주기보다 길 때만 맞습니다. 2008년(1년 넘게 이어진 하락)은 피했고 2020년(23거래일 하락)은 못 피했습니다. 한 달 안의 급락이 걱정이라면 GEM 규칙을 고치기보다 계좌 단위 안전장치를 별도 층으로 두는 편이 규칙을 오염시키지 않습니다.
4. 함정 ② 방어 자산도 떨어진다 — 2022년 AGG −13%
GEM은 주식이 현금보다 못하면 AGG로 피합니다. 그런데 2022년은 금리가 급하게 오르면서 채권이 같이 무너진 해였습니다. 연간 수익률로 보면:
| 연도 | GEM | SPY | 무엇이 있었나 |
|---|---|---|---|
| 2008(6~12월) | +6.1% | −33.8% | 15개월 연속 AGG — 전략이 설계대로 작동 |
| 2020 | +2.6% | +18.3% | 3월 폭락 통과 + 4월 반등 누락 |
| 2022 | −16.9% | −18.2% | 5월부터 AGG 전환, 그 AGG가 연간 −13.02% |
| 2023 | +7.4% | +26.2% | 2023-05 신호까지 AGG 유지 → 회복장 절반을 채권으로 |
같은 2022년에 BIL(단기 국채)은 +1.4%였습니다. 절대 모멘텀의 비교 기준(BIL)과 실제 피난처(AGG)가 다르다는 GEM의 설계가, 금리 상승기에는 "현금보다 나쁜 곳으로 피하는" 결과를 만들 수 있다는 뜻입니다. 이 차이를 알고 피난처를 BIL로 바꾸는 변형을 쓸지, 원 규칙을 지킬지는 백테스트를 보기 전에 정해야 합니다. 결과를 보고 고르면 다음 절의 함정에 그대로 빠집니다.
5. 함정 ③ 룩백 기간을 고르는 순간 스누핑이 시작된다
규칙은 그대로 두고 룩백만 바꿔 같은 구간을 돌렸습니다.
| 룩백 | CAGR | MDD | 샤프 | 자산 교체 |
|---|---|---|---|---|
| 3개월 | 10.09% | −20.72% | 0.80 | 70회 |
| 6개월 | 6.53% | −35.37% | 0.50 | 54회 |
| 9개월 | 6.90% | −33.72% | 0.51 | 48회 |
| 12개월(원 규칙) | 8.62% | −33.72% | 0.60 | 33회 |
3개월이 모든 지표에서 가장 좋아 보입니다. 하지만 3 → 6 → 9 → 12로 가면서 결과가 좋아졌다 나빠졌다 다시 좋아지는, 경향 없는 모양입니다. 인접한 값끼리 결과가 이렇게 튀면 "3개월이 좋다"는 이 구간의 우연일 가능성이 큽니다. 게다가 3개월은 교체가 70회로 12개월의 2배가 넘어, 매매비용·세금을 넣으면 격차가 줄어듭니다.
여기서 3개월을 채택하는 것이 전형적인 데이터 스누핑입니다. 네 개를 돌려 가장 좋은 것을 고른 순간, 그 숫자는 더 이상 표본 외 성과의 추정치가 아닙니다. 원 저자가 이 데이터를 보기 전에 정한 12개월을 쓰고, 이 표는 "파라미터에 따라 ±2%p 가까이 흔들리는 전략"이라는 불확실성의 크기로 읽는 편이 정직합니다.
6. 함정 ④ 리밸런싱 날짜 운
월말이 아니라 매월 k번째 거래일에 신호를 내도록 바꿔 봤습니다. 룩백은 12개월 그대로입니다.
| 신호일 | CAGR | MDD | 샤프 |
|---|---|---|---|
| 매월 1번째 거래일 | 9.31% | −33.72% | 0.65 |
| 매월 5번째 거래일 | 9.76% | −33.72% | 0.67 |
| 매월 10번째 거래일 | 9.16% | −26.67% | 0.65 |
| 매월 15번째 거래일 | 7.79% | −31.98% | 0.56 |
| 매월 마지막 거래일 | 8.62% | −33.72% | 0.60 |
규칙이 같아도 날짜만으로 CAGR이 7.79%~9.76%, 약 2%p 벌어집니다. 10번째 거래일에만 MDD가 −26.67%로 작은 것도 전략의 실력이 아니라 2020년 3월 폭락과 신호일이 우연히 맞물린 결과입니다. 이것을 흔히 리밸런싱 타이밍 운이라고 부릅니다. 실무에서는 날짜를 하나로 고정하되, 백테스트 보고서에는 이 범위를 같이 적어 두는 것이 좋습니다. 자동화한 뒤 실전 결과가 백테스트보다 2%p 낮게 나와도 고장이 아니라 이 범위 안일 수 있기 때문입니다.
7. 재현 코드 전체
위 숫자는 아래 코드로 재현됩니다. pandas와 FinanceDataReader만 필요합니다. 기준일을 ASOF로 고정해 두었기 때문에 실행 시점이 달라도 같은 기간을 계산합니다(최신까지 보려면 ASOF = None).
import numpy as np, pandas as pd
import FinanceDataReader as fdr
ASOF = "2026-08-31" # 기준일 고정 — 이 글의 숫자를 재현하려면 그대로 둔다
TICKERS = ["SPY", "EFA", "AGG", "BIL"]
px = pd.concat({t: fdr.DataReader(t, "2007-06-01")["Adj Close"]
for t in TICKERS}, axis=1).dropna()
px = px[:ASOF] if ASOF else px # 받은 뒤 자른다 — 캐시를 쓰든 새로 받든 같은 구간
ret = px.pct_change().fillna(0)
def gem(lookback=12, day_k=-1, start="2008-06-01"):
# 매월 k번째 거래일 = 신호일 (day_k=-1 이면 월말)
g = px.groupby([px.index.year, px.index.month])
dates = pd.DatetimeIndex([grp.index[day_k] if len(grp) > abs(day_k)
else grp.index[-1] for _, grp in g])
hold = pd.Series(index=px.index, dtype=object)
for i in range(lookback, len(dates)):
r = px.loc[dates[i]] / px.loc[dates[i - lookback]] - 1
if r["SPY"] > r["BIL"]: # 절대 모멘텀
pick = "SPY" if r["SPY"] >= r["EFA"] else "EFA" # 상대 모멘텀
else:
pick = "AGG"
pos = px.index.get_loc(dates[i]) + 1 # 다음 거래일부터 보유
hold.iloc[pos:] = pick
hold = hold[start:].dropna()
r = pd.Series([ret.at[d, a] for d, a in hold.items()], index=hold.index)
return (1 + r).cumprod()
def stats(eq):
d = eq.pct_change().dropna()
yrs = (eq.index[-1] - eq.index[0]).days / 365.25
cagr = (eq.iloc[-1] / eq.iloc[0]) ** (1 / yrs) - 1
mdd = (eq / eq.cummax() - 1).min()
return round(cagr, 4), round(mdd, 4)
print(stats(gem())) # (0.0862, -0.3372)
for lb in (3, 6, 9, 12):
print(lb, stats(gem(lookback=lb)))
코드에서 가장 중요한 한 줄은 pos = ... + 1입니다. 신호를 계산한 그날 종가의 수익률까지 새 자산으로 먹게 두면, 장 마감 후에야 알 수 있는 값을 장중에 알았다고 가정하는 미래참조가 됩니다. 성과 지표를 quantstats로 뽑을 때는 무위험수익률(rf)과 연율화 기준을 위 stats()와 맞춰야 숫자가 일치합니다.
8. 자동매매로 옮길 때 — KIS API·국내 ETF·세금
신호: 월봉 한 번이면 된다
한국투자증권(KIS) Open API의 해외주식 기간별시세 HHDFS76240000(/uapi/overseas-price/v1/quotations/dailyprice)은 GUBN이 0 일 · 1 주 · 2 월입니다. 월봉에 MODP=1(수정주가)을 넣으면 13개월치가 한 번에 오므로 종목당 1회 호출로 12개월 수익률이 나옵니다.
params = {
"AUTH": "", "EXCD": "AMS", # 거래소 코드 — 첫 호출에서 빈 응답이면 코드부터 의심
"SYMB": "SPY",
"GUBN": "2", # 0:일 1:주 2:월
"BYMD": "", # 공란 = 최근부터
"MODP": "1", # 수정주가 반영
}
headers["tr_id"] = "HHDFS76240000"
# 응답 output2[i]["xymd"], output2[i]["clos"] → 최근 월말 종가 vs 12개월 전 월말 종가
⚠️ 월봉의 최신 행은 아직 끝나지 않은 이번 달일 수 있습니다. 월초에 돌리면 직전 월말 종가가 두 번째 행에 있으니, xymd를 보고 "완결된 월말"만 쓰도록 걸러야 백테스트와 같은 신호가 나옵니다. 파라미터 전체와 BYMD 연속조회는 HHDFS76240000 글, 거래소 코드 매핑은 해외 현재가 조회 글에 있습니다.
주문: 매수·매도 TR이 다르다
자산을 바꾸는 달에는 보유분을 TTTT1006U(미국 매도)로 팔고 TTTT1002U(미국 매수)로 삽니다. 매도 대금이 결제되기 전에 매수 가능 금액이 얼마로 잡히는지, 원화로 바로 사려면 통합증거금이 필요한지는 계좌 설정에 달려 있습니다 — 각각 미국주식 주문 글과 통합증거금 TTTC0869R 글에서 다룹니다. 교체가 연 2회 안팎이라 레이트리밋 걱정은 거의 없지만, 교체일 하루에 매도 → 체결 확인 → 매수의 순서를 봇이 확실히 지키게 하는 것이 구현의 핵심입니다.
국내 상장 ETF로 바꾸면 달라지는 것
- 검증 기간이 짧아진다 — 예컨대
TIGER 미국S&P500(360750)은 첫 거래일이 2020-08-07이라 그 ETF만으로는 6년 남짓만 돌릴 수 있습니다. 2008년 같은 긴 하락장이 표본에 없습니다. - 원화 수익률에 환율이 섞인다 — USD 기준 백테스트와 원화 계좌의 결과는 같은 달에도 다를 수 있습니다.
- 세금 구조가 다르다 — 해외 상장 ETF 매매차익은 연 250만 원 기본공제 후 22% 양도소득세, 국내 상장 해외 ETF 매매차익은 15.4% 배당소득세 원천징수(금융소득종합과세 대상)로 알려져 있습니다. 한 달 단위로 갈아타는 전략은 이 차이가 매년 누적되므로 국세청·증권사 공식 안내로 현재 기준을 확인하고 설계하십시오. 연금계좌에서 돌릴 경우의 제약은 별도로 확인해야 합니다.
정리 — 듀얼 모멘텀을 자동화할 때 가장 먼저 합의할 것은 코드가 아니라 기대치입니다. 이 구간의 실측은 "낙폭을 줄이는 대가로 상승장 일부를 포기하는 규칙"이었고, 한 달짜리 폭락은 못 피했으며, 파라미터·날짜에 따라 ±2%p가 흔들렸습니다. 이것을 받아들일 수 있다면 규칙이 단순한 만큼 봇은 오래 갑니다. 알고랩의 리밸런싱봇도 같은 이유로 "예측"이 아니라 "정해 둔 규칙을 매달 지키는 것"에 초점을 둡니다.
9. 자주 묻는 질문
듀얼 모멘텀은 백테스트에서 S&P500보다 수익이 높았나요?
이 글의 조건(2008-06-02 ~ 2026-08-31, 12개월 룩백, 배당 재투자)에서는 아니었습니다. GEM CAGR 8.62% vs SPY 11.85%. 대신 MDD는 −33.72% vs −50.70%로 작았습니다. 비용·세금·환율 미반영이며 과거 결과는 미래 성과를 보장하지 않습니다.
듀얼 모멘텀이 2020년 코로나 폭락을 피했나요?
못 피했습니다. 2020-02-28 신호에서 SPY 12개월 수익률이 +8.32%라 3월을 SPY로 보유했고, AGG 전환은 저점(03-23)이 지난 03-31 신호였습니다. 월 1회 신호는 한 달 안의 폭락을 구조적으로 못 피합니다.
룩백 기간은 12개월이 정답인가요?
근거는 없습니다. 3·6·9·12개월의 CAGR이 10.09%·6.53%·6.90%·8.62%로 경향 없이 튑니다. 가장 좋은 값을 고르면 스누핑이 되므로, 미리 정한 12개월을 쓰고 흔들림의 폭을 불확실성으로 받아들이는 편이 안전합니다.
한국투자증권 API로 듀얼 모멘텀을 자동화할 수 있나요?
미국 ETF 직접 매매라면 가능합니다. 신호는 HHDFS76240000에 GUBN=2(월봉)·MODP=1로 1회 호출, 주문은 TTTT1002U(매수)·TTTT1006U(매도)입니다. 해외주식 거래 신청·환전(또는 통합증거금)·거래소 코드는 계좌마다 먼저 확인해야 합니다.
국내 상장 ETF로 바꿔서 돌려도 결과가 같나요?
같다고 볼 수 없습니다. 검증 가능 기간이 짧고(예: 360750은 2020-08-07 첫 거래), 원화 수익률에 환율이 섞이며, 매매차익 과세 방식이 해외 상장 ETF와 다릅니다. 세금 기준은 공식 자료로 확인하십시오.
고지 — 알고랩(퀀트웍스)은 투자자문업·투자일임업을 영위하지 않으며, 고객의 규칙을 코드로 옮기는 도구 제공만 합니다. 이 글은 특정 종목·ETF나 매매 전략을 추천하지 않고 수익을 보장하지 않습니다. 듀얼 모멘텀은 공개된 학술·저술 개념이며, 본문 수치는 2026-08-31까지의 FinanceDataReader Adj Close(USD)로 계산한 것으로 매매비용·세금·환율·슬리피지를 반영하지 않았습니다. 데이터 출처·기준일이 달라지면 값이 달라집니다. 과거 데이터로 계산한 어떤 결과도 미래 성과를 보장하지 않습니다. API 사양과 세법은 변경될 수 있으니 한국투자증권 KIS Developers와 국세청 공식 안내를 직접 확인하십시오.