[주식 전략 백테스팅] 종합 듀얼 모멘텀 전략

ZERO·2023년 12월 25일
0

Stock Backtesting

목록 보기
1/1
post-thumbnail

종합 듀얼 모멘텀 전략이란?

게리 안토나치가 제안한 전략으로 오리지널 듀얼 모멘텀에서 채권, 부동산, 불경기 자산 등을 추가해서 크게 4가지 자산으로 나누고 모멘텀을 적용한 전략이다.

전략 설명

포함 자산

  • 포트폴리오를 4개 파트로 나눔(각 파트에 자산의 25% 배분)
  • 파트 1: 주식 - 미국 주식 SPY 또는 해외 주식 EFA
  • 파트 2: 채권 - 미국 회사채 LQD 또는 미국 하이일드 채권 HYG
  • 파트 3: 부동산 - 부동산 리츠 VNQ 또는 모기지 리츠 REM
  • 파트 4: 불경기 - 미국 장기채 TLT 또는 금 GLD

매수 전략

  • 매월 말 각 파트 별 2개 자산의 최근 12개월 모멘텀을 계산
  • 둘 중 수익이 더 높은 ETF에 투자 (상대 모멘텀)
  • 두 ETF 수익 모두가 BIL(미국 초단기채)수익보다 낮으면 달러 현금에 투자 (절대 모멘텀)

매도 전략

  • 월 1회 리밸러싱

구현 코드

필요한 패키지 임포트

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()

MDD 그래프

# 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 가격 그래프 출력(기타 성능 평가 요인)

# 기타 부가 성능 평가 요인
# 정규화된 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')

0개의 댓글