실전 백테스트 - DAA 전략

 지난 포스팅까지 백테스트를 하기 위한 기본적인 코드를 익혀봤다면, 이번 포스팅부터는 여러 전략들을 백테스트하는 실전 내용을 다루려고 합니다. 앞서 공부했던 코드들을 대부분 사용하지만, 각 전략마다 로직을 구현해야 하는 부분이 존재하기 때문에, 그 부분을 중점적으로 다뤄보려고 합니다. 첫 번째로 다룰 전략은 DAA 전략입니다. 여러 번 요청이 왔었지만, 리밸런싱에 정신이 팔려 포스팅을 미루고만 있었네요😭

 

1.준비단계

 백테스트를 진행할 기간을 정하고, 종목을 선택해서 데이터를 가져오는 것부터 시작합니다. 백테스트 기간은 이왕이면 길게 산정하고 싶지만, 해당 ETF들의 상장일이 그리 길지 않아서 방법이 없습니다. 

import pandas_datareader as pdr
import pandas as pd
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import seaborn as sns
import math

# Jupyter Notebook에서 보기 편하게 하기 위한 설정
pd.options.display.float_format = '{:.4f}'.format
pd.set_option('display.max_columns', None)

start_day = datetime(2008,1,1) # 시작일
end_day = datetime(2021,4,6) # 종료일

# RU : Risky Universe
# CU : Cash Universe
# BU : Benchmark Universe
RU = ['SPY','IWM','QQQ','VGK','EWJ','VWO','VNQ','GSG','GLD','TLT','HYG','LQD'] 
CU = ['SHV','IEF','UST','BND']
BU = ['^GSPC','^IXIC','^KS11','^KQ11'] # S&P500 지수, 나스닥 지수, 코스피 지수, 코스닥 지수

# Yahoo Finance에서 데이터 가져오는 함수
def get_price_data(RU, CU, BU):
    df_RCU = pd.DataFrame(columns=RU+CU)
    df_BU = pd.DataFrame(columns=BU)
    
    for ticker in RU + CU:
        df_RCU[ticker] = pdr.get_data_yahoo(ticker, start_day - timedelta(days=365), end_day)['Adj Close']  
    
    for ticker in BU:
        df_BU[ticker] = pdr.get_data_yahoo(ticker, start_day - timedelta(days=365), end_day)['Adj Close']  
     
    return df_RCU, df_BU

 

 위의 코드에서 작성한 함수로 데이터를 불러오면 아래와 같이 결과를 확인할 수 있습니다. 데이터가 없는 종목도 있지만, 공격자산과 수비자산에 각 하나씩의 데이터만 존재해도 일단 소스코드가 작동은 하기 때문에 그대로 진행하겠습니다.

 

df_RCU, df_BU = get_price_data(RU, CU, BU)
df_RCU.head(5)

결과 DataFrame)

DAA 전략의 데이터. 빨간색이 공격자산, 파란색이 수비자산

 

2.모멘텀 지수 계산

 두 번째 단계는 각 자산 군의 모멘텀 지수를 계산하는 단계입니다. VAA 전략에서도 동일하게 계산하는 로직이며, 백테스트 코드에 대한 포스팅이니 모멘텀 지수 자체에 대한 설명은 생략하겠습니다.

 

# 모멘텀 지수 계산 함수
def get_momentum(x):
    temp_list = [0 for i in range(len(x.index))]
    momentum = pd.Series(temp_list, index=x.index)

    try:
        before1 = df_RCU[x.name-timedelta(days=35):x.name-timedelta(days=30)].iloc[-1][RU+CU]
        before3 = df_RCU[x.name-timedelta(days=95):x.name-timedelta(days=90)].iloc[-1][RU+CU]        
        before6 = df_RCU[x.name-timedelta(days=185):x.name-timedelta(days=180)].iloc[-1][RU+CU]        
        before12 = df_RCU[x.name-timedelta(days=370):x.name-timedelta(days=365)].iloc[-1][RU+CU]

        momentum = 12 * (x / before1 - 1) + 4 * (x / before3 - 1) + 2 * (x / before6 - 1) + (x / before12 - 1)
    except:
        pass

    return momentum
    
# 각 자산별 모멘텀 지수 계산
mom_col_list = [col+'_M' for col in df_RCU[RU+CU].columns]
df_RCU[mom_col_list] = df_RCU[RU+CU].apply(lambda x: get_momentum(x), axis=1)
df_RCU[mom_col_list]

결과 DataFrame)

계산된 모멘텀 지수

3.백테스트 기간 매월 말일 데이터 추출

매월 말일에 리밸런싱을 한다는 전제로 하여 월별 데이터만 추려냅니다.

# 백테스트할 기간 데이터 추출
df_RCU = df_RCU[start_day:end_day]

# 매월 말일 데이터만 추출(리밸런싱에 사용)
df_RCU = df_RCU.resample(rule='M').last()
df_RCU.head(5)

결과 DataFrame)

4.DAA 전략 기준에 맞춰 자산 선택

 다음 단계는 백테스트에서 가장 중요한 종목 선정에 해당하는 부분입니다. 전체적으로 VAA 전략과 유사하기는 하지만, 공격 자산 중 상위 2개의 자산군을 선정한다는 것이 가장 큰 차이점입니다. 

# DAA 전략 기준에 맞춰 자산 선택
def select_asset(x):
    asset = pd.Series([0,0,0,0], index=['ASSET1','PRICE1','ASSET2','PRICE2'])
    momentum1 = None
    momentum2 = None
    
    # DAA 전략
    # 카나리아 자산군이 모두 0이상이면, 공격 자산 중 상위 2개 모멘텀 자산 선정
    # 'SPY','IWM','QQQ','VGK','EWJ','VWO','VNQ','GSG','GLD','TLT','HYG','LQD'
    if x['VWO_M'] > 0 and x['BND_M'] > 0:
        momentum_sort = x[['SPY_M','IWM_M','QQQ_M','VGK_M','EWJ_M','VWO_M','VNQ_M','GSG_M','GLD_M','TLT_M','HYG_M','LQD_M']].sort_values(ascending=False)
        momentum1 = momentum_sort[0]
        momentum2 = momentum_sort[1]

        asset['ASSET1'] = x[x == momentum1].index[0][:3]
        asset['PRICE1'] = x[asset['ASSET1']] 
        asset['ASSET2'] = x[x == momentum2].index[0][:3]
        asset['PRICE2'] = x[asset['ASSET2']]
    
    # 카나리아 자산군 중 하나라도 0이하라면, 방어 자산 중 최고 모멘텀 자산 선정
    # 'SHV','IEF','UST'
    else :
        momentum1 = max(x['SHV_M'],x['IEF_M'],x['UST_M'])
        
        asset['ASSET1'] = x[x == momentum1].index[0][:3]
        asset['PRICE1'] = x[asset['ASSET1']] 
        asset['ASSET2'] = x[x == momentum1].index[0][:3]
        asset['PRICE2'] = x[asset['ASSET2']]        
    
    return asset

 VAA 전략에서 max() 함수를 쓴 부분을 변경하였습니다. Series의 모멘텀 지수들을 내림차순으로 정렬하여, 가장 큰 값을 ASSET1, 두 번째 값을 ASSET2로 지정하였습니다. 이후 로직을 공통적으로 적용시키기 위해 하나의 자산만 선정하는 수비 자산의 경우에도 ASSET1과 ASSET2에 같은 값으로 넣어주었습니다.

 

# 매월 선택할 자산과 가격
df_RCU[['ASSET1','PRICE1','ASSET2','PRICE2']] = df_RCU.apply(lambda x: select_asset(x), axis=1)
df_RCU[['ASSET1','PRICE1','ASSET2','PRICE2']].tail(5)

결과 DataFrame)

월별 선정 자산과 가격

 

5.자산별 수익률 계산

# 각 자산별 수익률 계산
profit_col_list = [col+'_P' for col in df_RCU[RU+CU].columns]
df_RCU[profit_col_list] = df_RCU[RU+CU].pct_change()
df_RCU[profit_col_list].tail(5)

결과 DataFrame)

각 자산군별 수익률 계산 결과

6.DAA 전략의 수익률

 5번에서 전체 자산군의 월별 수익률을 계산하였습니다. 이제 DAA 전략에서 선정된 자산의 수익을 계산해보겠습니다. 두 개의 자산을 선택하기 때문에 각 자산군의 수익률의 평균으로 DAA 전략의 월별 수익률을 계산합니다. 수비자산의 경우에도 ASSET1과 ASSET2에 동일하게 데이터를 입력해두었기 때문에 동일하게 계산해도 됩니다.

# 매월 수익률 & 누적 수익률 계산
df_RCU['PROFIT'] = 0
df_RCU['PROFIT_ACC'] = 0
df_RCU['LOG_PROFIT'] = 0
df_RCU['LOG_PROFIT_ACC'] = 0

for i in range(len(df_RCU)):
    profit = 0
    log_profit = 0
        
    if i != 0:
        profit = (df_RCU[df_RCU.iloc[i-1]['ASSET1'] + '_P'].iloc[i] + df_RCU[df_RCU.iloc[i-1]['ASSET2'] + '_P'].iloc[i]) / 2
        log_profit = math.log(profit+1)
    
    df_RCU.loc[df_RCU.index[i], 'PROFIT'] = profit
    df_RCU.loc[df_RCU.index[i], 'PROFIT_ACC'] = (1+df_RCU.loc[df_RCU.index[i-1], 'PROFIT_ACC'])*(1+profit)-1
    df_RCU.loc[df_RCU.index[i], 'LOG_PROFIT'] = log_profit
    df_RCU.loc[df_RCU.index[i], 'LOG_PROFIT_ACC'] = df_RCU.loc[df_RCU.index[i-1], 'LOG_PROFIT_ACC'] + log_profit
    
# 수익률에 100을 곱해서 백분율로 표기   
df_RCU[['PROFIT', 'PROFIT_ACC', 'LOG_PROFIT','LOG_PROFIT_ACC']] = df_RCU[['PROFIT', 'PROFIT_ACC', 'LOG_PROFIT','LOG_PROFIT_ACC']] * 100
df_RCU[profit_col_list] = df_RCU[profit_col_list] * 100    

df_RCU[['PROFIT','PROFIT_ACC','LOG_PROFIT','LOG_PROFIT_ACC']].tail(10)

결과 DataFrame)

DAA 전략에 따라 선택한 자산군의 월별 수익률

 

7.MDD 계산

 다음은 MDD를 계산하는 로직입니다. 각 월별 낙폭을 계산하고, 전체 중 최대 낙폭을 구하는 방식입니다.

# 가상의 잔고 필드와 낙폭 필드 추가
col_list_BAL = ['DAA_BAL']
col_list_DD = ['DAA_DD']

df_RCU[col_list_BAL] = (1+df_RCU['PROFIT']/100).cumprod()
df_RCU[col_list_DD] = -(df_RCU[col_list_BAL].cummax() - df_RCU[col_list_BAL]) / df_RCU[col_list_BAL].cummax()

# 백분율을 %로 전환
df_RCU[col_list_BAL+col_list_DD] = df_RCU[col_list_BAL+col_list_DD] * 100

df_RCU[col_list_DD].min()

 

8.전체 정보 조회

 실제 백테스트에 대한 코드는 끝이 났지만, 결과를 더 깔끔하게 확인하는 코드도 함께 올립니다. 중요한 부분은 아니지만, 한 번 코드를 익혀놓으면 편하게 결과를 확인하실 수 있을 거에요😋

total_month = len(df_RCU)
risky_month = len(df_RCU[df_RCU['ASSET1'].isin(RU)])
cash_month = len(df_RCU[df_RCU['ASSET1'].isin(CU)])
profit_month = len(df_RCU[df_RCU['PROFIT'] >= 0])
loss_month = len(df_RCU[df_RCU['PROFIT'] < 0])

print(total_month, "개월 중 Risky 자산 보유 :", risky_month, "개월")
print(total_month, "개월 중 Cash 자산 보유 :", cash_month, "개월")
print(total_month, "개월 중 수익 월 :", profit_month, "개월")
print(total_month, "개월 중 손실 월 :", loss_month, "개월")

"""
출력 결과 예시
160 개월 중 Risky 자산 보유 : 83 개월
160 개월 중 Cash 자산 보유 : 77 개월
160 개월 중 수익 월 : 107 개월
160 개월 중 손실 월 : 53 개월
"""
# CAGR = (EV / BV)^ (1 / n) - 1
# BV : 초기값
# EV : 종료값 = (1+누적수익률) * BV
# n : 기간 수(연)
CAGR = ((1+df_RCU['PROFIT_ACC'][-1]/100)**(1/(total_month/12)))-1
print('연복리 수익률(CAGR) : ', round(CAGR*100,2))
print('최대 낙폭(MDD) : ', round(df_RCU[col_list_DD].min()[0],2))

"""
출력결과 예시
연복리 수익률(CAGR) :  12.16
최대 낙폭(MDD) :  -15.35
"""

 

9.그래프 조회

 위에서 구한 결과를 그래프로 출력해서 결과를 확인할 수도 있습니다. 첫 번째는 수익률 그래프 출력입니다.

plt.figure(figsize=(15,5))
sns.lineplot(data=df_RCU, x=df_RCU.index, y=df_RCU['LOG_PROFIT_ACC'])

DAA 전략 수익률 그래프

 두 번째는 MDD 그래프입니다.

plt.figure(figsize=(15,5))
sns.lineplot(data=df_RCU, x=df_RCU.index, y=df_RCU['DAA_DD'])

 

 전략 백테스트의 경우 저도 전략 검증을 우선적으로 생각하는 터라 코드가 많이 지저분하고, 알아보기 힘든 경향이 있습니다. 양해 부탁드리며, 이해가 되지 않는 부분이 있다면 댓글로 알려주세요! 소스코드 수정 or 보충설명을 최대한 반영하겠습니다.

 

 

 


공감댓글, 공유는 큰 힘이 됩니다!

도움이 되셨다면 널리널리 알려주세요😉

 

 

댓글()