
게리 안토나치가 제안한 전략으로 오리지널 듀얼 모멘텀에서 채권, 부동산, 불경기 자산 등을 추가해서 크게 4가지 자산으로 나누고 모멘텀을 적용한 전략이다.
포함 자산
매수 전략
매도 전략
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
# 데이터 다운로드
tickers = ['SPY', 'EFA', 'LQD','HYG', 'VNQ', 'REM', 'TLT', 'GLD', 'BIL']
start = '2007-05-01' # BIL은 2007년 5월 이전의 정보는 없음
end = '2023-06-01'
data = yf.download(tickers, start=start, end=end)['Adj Close']
# 월별 데이터셋 변환
monthly_data = data.resample('M').last()
print(monthly_data)

# 모멘텀 계산
momentum_12m = monthly_data.pct_change(periods=12) # 12개월 기준으로 수익률 계산
momentum_1m = monthly_data.pct_change(periods=1) # 1개월 기준으로 수익률 계산(월 1회 리밸런싱을 하기때문에 미리 계산해둠)
returns = pd.DataFrame(columns=['월 단위 수익률']) # 월별 수익률 저장할 데이터 프레임
cumprod = pd.DataFrame(columns=['누적 수익률']) # 누적 수익률을 저장할 데이터 프레임
strategy = monthly_data.copy()# 백테스팅 결과를 담을 데이터프레임
print("1개월 모멘텀")
print(momentum_1m) # 인덱스 1 이전의 값들은 자신 이전의 1개월 이전의 데이터를 찾을 수 없으므로 nan이다.
print("12개월 모멘텀")
print(momentum_12m)

# 월별 수익률 계산 함수
def calculate_momentum_1m(t,ticker):
return float(momentum_1m[[ticker]].iloc[t+1])+1 # 나중에 누적곱을 해줘야하기 때문에 +1를 더해줌
# 종합 듀얼 모멘텀 로직
# 달마다 투자하는 자산을 바꿔주기 때문에 리밸런싱이 되고 있다.
# t가 12부터 시작하는 이유는 12보다 작을 경우 return_12m의 값이 nan이기 때문
returns.loc[monthly_data.index[12], :] = 1 # 처음 시작 수익률은 1
for t in range(12,len(momentum_12m)-1):
total = 0
# 파트 1 로직, SPY와 EFA
if momentum_12m['SPY'].iloc[t] > momentum_12m['BIL'].iloc[t] or momentum_12m['EFA'].iloc[t] > momentum_12m['BIL'].iloc[t]:
# SPY가 클경우 SPY에 투자
if momentum_12m['SPY'].iloc[t] >= momentum_12m['EFA'].iloc[t]:
total_1 = calculate_momentum_1m(t,'SPY') # SPY의 1개월 수익률 할당
strategy.loc[monthly_data.index[t], ["파트 1"]] = 'SPY' # 구매한 종목 이름 저장
# EFA가 클경우 EFA에 투자
else:
total_1 = calculate_momentum_1m(t,'EFA') # EFA의 1개월 수익률 할당
strategy.loc[monthly_data.index[t], ["파트 1"]] = 'EFA' # 구매한 종목 이름 저장
else:
# SPY와 EFA 둘다 BIL보다 작으면 달러 현금에 투자 즉 수익률 변동이 없으므로 1할당
total_1 = 1
strategy.loc[monthly_data.index[t], ["파트 1"]] = '달러 현금 투자'
# 파트 2 로직, LQD와 HYG, 파트1과 로직은 동일
if momentum_12m['LQD'].iloc[t] > momentum_12m['BIL'].iloc[t] or momentum_12m['HYG'].iloc[t] > momentum_12m['BIL'].iloc[t]:
# LQD가 클경우 LQD에 투자
if momentum_12m['LQD'].iloc[t] >= momentum_12m['HYG'].iloc[t]:
total_2 = calculate_momentum_1m(t,'LQD')
strategy.loc[monthly_data.index[t], ["파트 2"]] = 'LQD'
# HYG가 클경우 HYG에 투자
else:
total_2 = calculate_momentum_1m(t,'HYG')
strategy.loc[monthly_data.index[t], ["파트 2"]] = 'HYG'
else:
# LQD와 HYG 둘다 BIL보다 작으면 달러 현금에 투자 즉 수익률 변동이 없으므로 1할당
total_2 = 1
strategy.loc[monthly_data.index[t], ["파트 2"]] = '달러 현금 투자'
# 파트 3 로직, VNQ와 REM, 파트1과 로직은 동일
if momentum_12m['VNQ'].iloc[t] > momentum_12m['BIL'].iloc[t] or momentum_12m['REM'].iloc[t] > momentum_12m['BIL'].iloc[t]:
# VNQ가 클경우 VNQ에 투자
if momentum_12m['VNQ'].iloc[t] >= momentum_12m['REM'].iloc[t]:
total_3 = calculate_momentum_1m(t,'VNQ')
strategy.loc[monthly_data.index[t], ["파트 3"]] = 'VNQ'
# REM가 클경우 REM에 투자
else:
total_3 = calculate_momentum_1m(t,'REM')
strategy.loc[monthly_data.index[t], ["파트 3"]] = 'REM'
else:
# VNQ와 REM 둘다 BIL보다 작으면 달러 현금에 투자 즉 수익률 변동이 없으므로 1할당
total_3 = 1
strategy.loc[monthly_data.index[t], ["파트 3"]] = '달러 현금 투자'
# 파트 4 로직, TLT와 GLD, 파트1과 로직은 동일
if momentum_12m['TLT'].iloc[t] > momentum_12m['BIL'].iloc[t] or momentum_12m['GLD'].iloc[t] > momentum_12m['BIL'].iloc[t]:
# TLT가 클경우 TLT에 투자
if momentum_12m['TLT'].iloc[t] >= momentum_12m['GLD'].iloc[t]:
total_4 = calculate_momentum_1m(t,'TLT')
strategy.loc[monthly_data.index[t], ["파트 4"]] = 'TLT'
# GLD가 클경우 GLD에 투자
else:
total_4 = calculate_momentum_1m(t,'GLD')
strategy.loc[monthly_data.index[t], ["파트 4"]] = 'GLD'
else:
# TLT와 GLD 둘다 BIL보다 작으면 달러 현금에 투자 즉 수익률 변동이 없으므로 1할당
total_4 = 1
strategy.loc[monthly_data.index[t], ["파트 4"]] = '달러 현금 투자'
#각 파트 별로 자산의 25% 배분
total = 0.25*total_1 + 0.25*total_2 + 0.25*total_3 + 0.25*total_4
returns.loc[monthly_data.index[t+1], :] = total
print("월단위 수익률")
print(returns)
print()
strategy["월단위 수익률"] = returns['월 단위 수익률']
print("누적 수익률")
cumprod['누적 수익률'] = (returns['월 단위 수익률']).cumprod(axis = 0) # 누적 수익률를 구하기 위함
print(cumprod)
strategy["누적 수익률"] = cumprod['누적 수익률']

# 누적 수익률 그래프 출력
plt.figure(figsize=(10,5))
plt.plot(cumprod)
plt.title('Portfolio Value over Time')
plt.show()

rolling_max = cumprod.cummax() # 월별로 누적 최대값을 저장
strategy["rolling max"] = rolling_max
# 롤링 맥스 그래프 출력
plt.figure(figsize=(10,5))
plt.plot(rolling_max)
plt.title('Rolling max over Time')
plt.show()

# CAGR 계산
# len(monthly_data)에서 12를 빼준 이유는 우리가 투자를 시작한 지점은 처음 데이터가
# 나온 2007-05-31의 1년 뒤인 2008-05-31이기 때문이다.(실투자 기간으로 잡음)
# 1년 뒤에 투자를 시작한 이유는 우리가 12개월 모멘텀을 사용함으로
# 12개월 모멘텀을 구하기 위해서는 최초데이터에서 최소한 12개월이 지나야한다.
cagr = cumprod['누적 수익률'].iloc[-1] ** (1 / ((len(monthly_data)-12) / 12)) - 1
# MDD 계산
daily_drawdown = cumprod/rolling_max - 1.0 # 누적 수익률에 롤링맥스 값 나누고 떨어진 값을 표현하기 위해 - 1.0을 더함
strategy["daily drawdown"] = daily_drawdown
max_daily_drawdown = daily_drawdown.cummin()
print("Cumulative Return: ", float(cumprod.iloc[-1]))
print("CAGR: ", cagr)
print("MDD: ", float(max_daily_drawdown.iloc[-1]))
# Plot MDD 그래프 출력
plt.figure(figsize=(10,5))
plt.plot(daily_drawdown)
plt.plot(max_daily_drawdown)
plt.title('Maximum Drawdown')
plt.show()
strategy["Cumulative Return"] = float(cumprod.iloc[-1])
strategy["cagr"] = cagr
strategy["MDD"] = float(max_daily_drawdown.iloc[-1])

# 기타 부가 성능 평가 요인
# 정규화된 ETF 가격 그래프 출력
normalize_monthly_data = monthly_data/monthly_data.iloc[0]
plt.figure(figsize=(10,5))
plt.plot(normalize_monthly_data,label = normalize_monthly_data.columns)
plt.legend(loc = "upper left")
plt.title('ETF Price')
plt.show()

# 엑셀 파일 다운로드
from google.colab import files
strategy.to_excel('백테스팅 결과 값.xlsx',index = True)
files.download('백테스팅 결과 값.xlsx')
momentum_12m.to_excel('12개월 모멘텀 값.xlsx',index = True)
files.download('12개월 모멘텀 값.xlsx')
momentum_1m.to_excel('1개월 모멘텀 값.xlsx',index = True)
files.download('1개월 모멘텀 값.xlsx')