게리 안토나치의 듀얼 모멘텀 전략
게리 안토나치가 제안한 절대 모멘텀과 상대 모멘텀을 결합한 투자 전략
전략의 기본 원리
상승장이 아닌 시점에는 현금을 보유함으로써 위험을 최소화하고(절대모멘텀), 상승장인 시점에서는 가장 큰 추세를 가진 자산군을 사용함으로써 최대의 수익을 취함(상대 모멘텀)
- 모멘텀 : 특정 자산에 대하여 상승하는 가격이 더욱 상승하거나, 하락하는 자산 가격이 더욱 하락하는 경향을 말하며 주가 추세의 가속도를 측정해 주가의 변동 상황을 이해하는 하나의 방법
- 절대 모멘텀 : 투자 자산의 과거 12개월과 비요하여 절대적 상승세를 평가
- 상대 모멘텀 : 투자 자산 가운데 상대적으로 강세를 보이는 곳에 투자
파라미터
- 어떤 자산군(종목)을 편입할 것인가?
- 절대 모멘텀의 기준은 무엇인가? (지난 6개월 수익률이 +인지, 예금 금리 이상인지, 벤치마크 대비 outperform 했는지 등)
- 모멘텀 판단 기준 주기를 어떻게 잡을 것인가?
예시 - 국제 ETF 듀얼모멘텀
모멘텀 측정 주기 : 6개월
사용 자산군
- KODEX 미국 S&P500 선물 - 미국 지수
- TIGER 유로스탁스50 - 유럽 지수
- KODEX 일본 TOPIX100 - 일본 지수
- KODEX 200 - 한국 지수
매수
- 4개 자산군의 최근 6개월 수익률이 모두 - 이면 현금 보유 ⇒ 하락 추세(절대 모멘텀 없음), 하나라도 +이면 절대 모멘텀 있다고 판단하여 상대 모멘텀을 체크한다.
- 최근 6개월 수익률이 가장 높은 자산군을 매수 ⇒ 상대 모멘텀
매도
- 최근 6개월 수익률이 가장 높은 자산군이 변경되면 보유중인 자산군을 매도하고 해당 자산군을 매수
- 4개 자산군의 최근 6개월 수익률이 모두 -되면 매도 후 현금 보유
국제 ETF 듀얼모멘텀
데이터 준비
# 자산군 4개 종목
stock_list = [
["KODEX 미국S&P500선물(H)", "219480"],
["TIGER 유로스탁스50(합성 H)", "195930"],
["KODEX 일본TOPIX100", "101280"],
["KODEX 200", "069500"]
]
# 종가 데이터 가져오기
# 가장 짧은 날짜 종목 기준으로 시작일 설정
df_list = [fdr.DataReader(code, '2015-05-29')['Close'] for name, code in stock_list]
# 병합해서 데이터프레임으로 만들기
df = pd.concat(df_list, axis=1)
df.columns = [name for name, code in stock_list]
# 수익률 데이터 프레임으로 전환
수익률_측정기간 = 120 # 6개월
수익률_개월 = df.pct_change(periods=수익률_측정기간)
# 상대모멘텀, 절대모멘텀 columns 추가하기
df['상대모멘텀'] = 수익률_개월.idxmax(axis='columns')
df['절대모멘텀'] = 수익률_개월.max(axis='columns') > 0 # True & False
# 상대모멘텀과 절대모멘텀을 구할수 없는 앞에 개월간의 데이터(NaN)를 스킵
df = df[수익률_측정기간:]
# 시뮬레이션을 위한 날짜 변수와 날짜 인덱스 변수 만들기
date_list = df.index
date_i = np.arange(len(date_list))
- idxmax = 최댓값을 가지는 인덱스 레이블을 반환
- axis="columns" : 행 방향으로 탐색하여 열로 반환
듀얼 모멘텀 알고리즘
print(f"듀얼모멘텀 전략 Signal, 모멘텀 측정주기 : {수익률_측정기간}")
# 신호 데이터 수집
signal_i_list = []
signal_date_list = []
signal_return_list = []
#-------------------------------------------
포지션_보유유무 = False # 투자 상태 : 전체 현금 보유 시작
for i in date_i:
if i < 1: # 첫날의 이전날 데이터 없으므로
continue
# 전날과 당일 데이터 준비
prev_date = date_list[i-1]
now_date = date_list[i]
직전_상대모멘텀_종목 = df['상대모멘텀'].loc[prev_date]
당일_상대모멘텀_종목 = df['상대모멘텀'].loc[now_date]
직전_절대모멘텀 = df['절대모멘텀'].loc[prev_date]
당일_절대모멘텀 = df['절대모멘텀'].loc[now_date]
# -------------------------------------------------------
# 매수 신호 포착 시점
if 당일_절대모멘텀 == True and 직전_절대모멘텀 == False:
매수시_주가 = df[당일_상대모멘텀_종목].loc[now_date]
매수_종목 = 당일_상대모멘텀_종목
포지션_보유유무 = True
print(f'++++++++++++++++++++++++++++++++++++++{now_date} 절대모멘텀 발생+++++++++++++++++++++++++++++++++++')
# 매도 신호 포착 시점 : 수익률 계산, 신호 데이터 수집
elif 당일_절대모멘텀 == False and 직전_절대모멘텀 == True:
매도시_주가 = df[매수_종목].loc[now_date]
수익률 = (매도시_주가 / 매수시_주가) * 100 - 100
print(f" - {now_date} 매도 Signal 발생! 매수매도종목 {매수_종목} -> {매수_종목}, 매수주가 {매수시_주가:.0f} -> 매도주가 {매도시_주가:.0f} , 수익률 {수익률:.3f} %")
print(f'--------------------------------------{now_date} 절대모멘텀 상실----------------------------------------------')
signal_i_list.append(i)
signal_date_list.append(now_date)
signal_return_list.append(수익률)
포지션_보유유무 = False
# 매수 종목 보유 중에 상대 모멘텀 종목 변경됐을 경우
elif 당일_절대모멘텀 == True and 직전_상대모멘텀_종목 != 당일_상대모멘텀_종목:
if 포지션_보유유무 == True:
매도시_주가 = df[직전_상대모멘텀_종목].loc[now_date]
매도_종목 = 직전_상대모멘텀_종목
수익률 = (매도시_주가 / 매수시_주가) * 100 - 100
print(f" - {now_date} 매도 Signal 발생! {매수_종목} -> {매도_종목}, 매수주가 {매수시_주가:.0f} -> 매도주가 {매도시_주가:.0f} , 수익률 {수익률:.3f} %")
signal_i_list.append(i)
signal_date_list.append(now_date)
signal_return_list.append(수익률)
매수시_주가 = df[당일_상대모멘텀_종목].loc[now_date]
매수_종목 = 당일_상대모멘텀_종목
elif 포지션_보유유무 == False:
매수시_주가 = df[당일_상대모멘텀_종목].loc[now_date]
매수_종목 = 당일_상대모멘텀_종목
포지션_보유유무 = True
수익률
# 누적 수익률
듀얼_모멘텀_누적수익률 = np.array(signal_return_list) / 100 + 1
듀얼_모멘텀_누적수익률 = 듀얼_모멘텀_누적수익률.cumprod()
# 듀얼모멘텀 누적수익률 데이터프레임 만들기
듀얼모멘텀_기간_수익률 = pd.DataFrame(듀얼_모멘텀_누적수익률, index = signal_date_list, columns =['듀얼모멘텀'])
듀얼모멘텀_기간_수익률 - 1
# 수익률 비교
수익률_합 = sum(signal_return_list)
평균_수익률 = 수익률_합 / len(signal_return_list)
단순_기간_수익률 = ((df['KODEX 미국S&P500선물(H)'][-1] / df['KODEX 미국S&P500선물(H)'][0] +\
df['TIGER 유로스탁스50(합성 H)'][-1] / df['TIGER 유로스탁스50(합성 H)'][0] +\
df['KODEX 일본TOPIX100'][-1] / df['KODEX 일본TOPIX100'][0] + \
df['KODEX 200'][-1] / df['KODEX 200'][0]) / 4) * 100 - 100
print(f"전체 매매 수익률 합 {수익률_합:.2f}%, 매매 당 평균 수익률 {평균_수익률:.2f}%, "
f"단순 기간 수익률 {단순_기간_수익률:.2f}%, 듀얼모멘텀 기간 수익률 {((듀얼모멘텀_기간_수익률.iloc[-1].values[0]-1)*100):.2f}%")
# 전체 매매 수익률 합 -9.19%, 매매 당 평균 수익률 -0.06%, 단순 기간 수익률 62.01%, 듀얼모멘텀 기간 수익률 -17.60%
- df.cumprod(axis=None, skipna=True, args, kwargs)
- axis : 누적합/누적곱을 구할 축을 지정합니다.
- skipna : 결측치를 무시할지 여부 입니다
- 누적 수익률은 복리를 생각해야하므로 누적곱셈을 한다.
그래프
# 첫 날을 기준으로 상대 수익률 그려보기
df_norm = pd.DataFrame(columns = ['KODEX 미국S&P500선물(H)', 'TIGER 유로스탁스50(합성 H)', 'KODEX 일본TOPIX100', 'KODEX200'])
df_norm['KODEX 미국S&P500선물(H)'] = df['KODEX 미국S&P500선물(H)'] / df['KODEX 미국S&P500선물(H)'].iloc[0] - 1
df_norm['TIGER 유로스탁스50(합성 H)'] = df['TIGER 유로스탁스50(합성 H)'] / df['TIGER 유로스탁스50(합성 H)'].iloc[0] - 1
df_norm['KODEX 일본TOPIX100'] = df['KODEX 일본TOPIX100'] / df['KODEX 일본TOPIX100'].iloc[0] - 1
df_norm['KODEX200'] = df['KODEX 200'] / df['KODEX 200'].iloc[0] - 1
df_norm.plot(figsize=(20,6));
plt.title('상대 수익률');
# 듀얼모멘텀 상대수익률
df_dual = 듀얼모멘텀_기간_수익률 - 1
df_dual.plot(figsize=(20,6));
plt.title('상대 수익률');
결론
개별 주식을 단순 보유하는 것 만큼 안정적이면서 상승하는 수익률을 보여준다.
파라미터(모멘텀측정주기, 절대모멘텀과 상대모멘텀 정의, 자산군 설정)를 바꿔가며 효율적인 듀얼모멘텀 전략을 찾아가야 한다.
과거데이터가 좋게 나온다고해서 미래에도 그렇다는 보장이 없기때문에 왜 이런 현상이 발생했는지 생각해보는 것이 중요하다.
'퀀트' 카테고리의 다른 글
26. 해리 마코위츠의 포트폴리오 이론(분산투자의 필요성) (0) | 2023.01.16 |
---|---|
25. 레이 달리오의 올웨더 전략 (0) | 2023.01.15 |
23. 래리 윌리엄스의 변동성 돌파 전략 (0) | 2023.01.13 |
22. 켈리 베팅 (0) | 2023.01.12 |
21. 백테스팅 기본 예제 (0) | 2023.01.10 |