[Backtest] DAA (Defensive Asset Allocation)


개요

동적 자산배분 전략인 DAA (Defensive Asset Allocation) 전략을 구현합니다.

DAA 전략은 폭락 신호를 미리 주는 카나리아 자산군을 이용해 VAA 전략을 개선한 전략입니다. VAA와 비슷한 수준으로 하락 폭을 낮추면서 방어 자산에 머무는 시간은 줄여보려는 시도를 합니다. VAA 전략은 논문에 따르면 1970년부터 테스트할 경우 60% 기간에서 방어 자산에 머무른다 합니다. 방어 자산은 단기 혹은 중기 채권이니 하락 폭이 낮은 것이 당연합니다. 그런데, 금리가 계속 낮아져서 더 내려갈 곳이 없어보이니 방어 자산인 채권 쪽에 머무는 기간이 짧을수록 좋을 것이고, DAA 전략은 카나리아 자산군을 가지고 그 시도를 합니다.

공격 자산으로 SPY (S&P 500), IWM (Russell 2000), QQQ (NASDAQ 100), VGK (Europe), EWJ (Japan), VWO (Emerging), VNQ (US REITs), GSG (Commodities), GLD (Gold), TLT (US 20+ Year Treasury), HYG (High Yield), LQD (Investment Grade Corporate Bond)를 사용합니다. 방어 자산으로 SHY (US 1-3 Year Treasury), IEF (US 7-10 Year Treasury), UST (Leveraged US 7-10 Year Treasury)를 사용합니다. 카나리아로는 VWO (Emerging), BND (US Bond)를 사용합니다.

방어 자산에서 레버리지 채권을 사용한다는 것과 카나리아의 작동 원리에 대한 명확한 설명이 없는 것이 상당히 마음에 걸리고 과최적화 요소가 있다는 생각이 듭니다. VAA는 그래도 직접 사용하려면 할 수도 있다는 생각이 들었는데, DAA는 과최적화 문제와 설명되지 않는 전략의 원리 때문에 전략의 성과가 괜찮을 경우 카나리아의 경고 기능만 참고하는 것이 더 나을 것 같습니다.

논문에서 제시하는 카나리아의 효과는 다음 그림과 같습니다. 카나리아 2개 모두가 음수 모멘텀이 되면 다음 달 S&P 500 수익률이 연 환산 기준 -14%이고, 그 때에도 S&P 500이 수익을 낼 가능성이 50%에 가까우니 카나리아가 모두 음수 모멘텀이라는 것은 큰 손실을 예견한다고 할 수 있습니다. 90년이 넘는 기간 동안의 결과이니 아주 말이 안 되는 것은 아니지만, 개인적으로는 VAA보다 가정이 많아 우월한 전략이라는 생각은 들지 않습니다.

화면 캡처 2021-06-13 230054

보유 자산 개수와 유니버스 구성에 따라 여러 가지 버전을 소개하고 있지만, 가장 간단하게 구현하기 위해서 12개 자산 중 2개를 보유하고, 카나리아 중 1개라도 모멘텀이 음수가 되면 방어 자산으로 전환하는 것으로 하겠습니다.

import pandas as pd
import pandas_datareader as pdr
from datetime import datetime, timedelta
import backtrader as bt
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
import pyfolio as pf
import quantstats
import math
import seaborn
plt.rcParams["figure.figsize"] = (10, 6) # (w, h)
# UST starts: 2010-02-02
start = datetime(2010,2,2)
end = datetime(2021,5,31)

tickers = ['SPY','IWM','QQQ','VGK','EWJ','VWO','VNQ','GSG','GLD','TLT','HYG','LQD','SHY','IEF','UST','BND']

def get_price_data(tickers):
    df_asset = pd.DataFrame(columns=tickers)
    
    for ticker in tickers:
        df_asset[ticker] = pdr.get_data_yahoo(ticker, start, end)['Adj Close']  
         
    return df_asset
df_asset = get_price_data(tickers)

모멘텀 측정은 VAA와 같은 방식입니다.

def get_momentum(x):
    temp = [0 for _ in range(len(x.index))]
    momentum = pd.Series(temp, index=x.index)
    
    try:
        before_1m = df_asset[x.name-timedelta(days=35):x.name-timedelta(days=30)].iloc[-1]
        before_3m = df_asset[x.name-timedelta(days=95):x.name-timedelta(days=90)].iloc[-1]
        before_6m = df_asset[x.name-timedelta(days=185):x.name-timedelta(days=180)].iloc[-1]
        before_12m = df_asset[x.name-timedelta(days=370):x.name-timedelta(days=365)].iloc[-1]
        momentum = (x/before_1m - 1) * 12 + (x/before_3m - 1) * 4 + (x/before_6m - 1) * 2 + (x/before_12m - 1) * 1
        
    except:
        pass
    
    return momentum
momentum_col = [col + '_m' for col in df_asset.columns]
df_asset[momentum_col] = df_asset.apply(lambda x: get_momentum(x), axis=1)

12개월 모멘텀 때문에 맨 앞 1년은 사용 못 합니다. 2011년 2월부터로 합니다.

df_asset = df_asset.loc[df_asset.index >= '2011-02-01']

월말 리밸런싱이니 월말 데이터만 남깁니다.

df_asset = df_asset.resample(rule='M').last()

공격 자산 중 모멘텀이 가장 좋은 2개를 고릅니다. 카나리아 자산 중 1개라도 모멘텀이 음수면 방어 자산 중 모멘텀이 가장 좋은 자산으로 갑니다.

def select_asset(x):
    selected_asset = pd.Series([0,0,0,0], index=['ASSET1','PRICE1','ASSET2','PRICE2'])
    momentum1 = None
    momentum2 = None
    
    # 모든 카나라아 자산 > 0
    if x['VWO_m'] > 0 and x['BND_m'] > 0:
        # momentum_col = [col + '_m' for col in df_asset.columns]
        selected_momentum = x[momentum_col].sort_values(ascending=False)
        momentum1 = selected_momentum[0]
        momentum2 = selected_momentum[1]
        
        selected_asset['ASSET1'] = x[x==momentum1].index[0][:3]
        selected_asset['PRICE1'] = x[selected_asset['ASSET1']]
        selected_asset['ASSET2'] = x[x==momentum2].index[0][:3]
        selected_asset['PRICE2'] = x[selected_asset['ASSET2']]

    # 카나리아 자산 중 1개라도 < 0
    else:
        selected_momentum = max(x['SHY_m'], x['IEF_m'], x['UST_m'])
    
        # 이 경우 ASSET1, ASSET2는 같음
        selected_asset['ASSET1'] = x[x==selected_momentum].index[0][:3]
        selected_asset['PRICE1'] = x[selected_asset['ASSET1']]
        selected_asset['ASSET2'] = x[x==selected_momentum].index[0][:3]
        selected_asset['PRICE2'] = x[selected_asset['ASSET2']]
    
    return selected_asset
df_asset[['ASSET1','PRICE1','ASSET2','PRICE2']] = df_asset.apply(lambda x: select_asset(x), axis=1) 

각각의 수익률을 계산합니다.

return_col = [ticker + '_r' for ticker in tickers]
df_asset[return_col] = df_asset[tickers].pct_change()

전략의 월별 수익률을 구합니다.

df_asset['RETURN'] = 0
df_asset['RETURN_ACC'] = 0
df_asset['LOG_RETURN'] = 0
df_asset['LOG_RETURN_ACC'] = 0

for i in range(len(df_asset)):
    strat_return = 0
    log_strat_return = 0
    
    # 직전 달 모멘텀이 좋은 것으로 리밸런싱해서 앞으로 한 달 가져가는 것
    if i > 0:
        strat_return = (df_asset[df_asset.iloc[i-1]['ASSET1']+'_r'].iloc[i]+df_asset[df_asset.iloc[i-1]['ASSET2']+'_r'].iloc[i])/2
        log_strat_return = math.log(strat_return + 1)
        
    df_asset.loc[df_asset.index[i], 'RETURN'] = strat_return
    # 누적 = 직전 누적 * 현재
    df_asset.loc[df_asset.index[i], 'RETURN_ACC'] = (1+df_asset.loc[df_asset.index[i-1], 'RETURN_ACC'])*(1+strat_return)-1
    df_asset.loc[df_asset.index[i], 'LOG_RETURN'] = log_strat_return
    # 로그누적 = 직전 로그누적 + 현재 로그
    df_asset.loc[df_asset.index[i], 'LOG_RETURN_ACC'] = df_asset.loc[df_asset.index[i-1], 'LOG_RETURN_ACC'] + log_strat_return
    
# 수익률 * 100
df_asset[['RETURN','RETURN_ACC','LOG_RETURN','LOG_RETURN_ACC']] = df_asset[['RETURN','RETURN_ACC','LOG_RETURN','LOG_RETURN_ACC']]*100
df_asset[return_col] = df_asset[return_col] * 100
# MDD

df_asset['BALANCE'] = (1+df_asset['RETURN']/100).cumprod()
df_asset['DRAWDOWN'] = -(df_asset['BALANCE'].cummax() - df_asset['BALANCE']) / df_asset['BALANCE'].cummax()

df_asset[['BALANCE','DRAWDOWN']] = df_asset[['BALANCE','DRAWDOWN']] * 100

이 전략도 VAA 전략만큼이나 수익도 괜찮고 MDD 낮고 연 변동성도 낮습니다. 특히 MDD -7.5%가 인상적입니다. 다만, 앞서 말한대로 과최적화 가능성과 카나리아 동작의 원리에 대한 명확한 규명이 되지 않아 실전에서 사용하기는 가정이 적고 단순한 VAA가 부담이 덜하다고 생각합니다.

total_month = len(df_asset)
profit_month = len(df_asset[df_asset['RETURN'] >= 0])
loss_month = len(df_asset[df_asset['RETURN'] < 0])
win_rate = profit_month / total_month * 100
CAGR = ((1+df_asset['RETURN_ACC'][-1]/100)**(1/(total_month/12)))-1
STDEV = np.std(df_asset['RETURN'][1:])*math.sqrt(12)
RRR = CAGR * 100 / STDEV

print(total_month, "개월 중 수익 월 :", profit_month, "개월")
print(total_month, "개월 중 손실 월 :", loss_month, "개월")
print("승률 :", round(win_rate, 2))

print('CAGR : ', round(CAGR*100, 2))
print('MDD : ', round(df_asset['DRAWDOWN'].min(), 2))
print('STDEV :', round(STDEV, 2))
print('Return-Risk Ratio: ', round(RRR, 2))
124 개월 중 수익 월 : 82 개월
124 개월 중 손실 월 : 42 개월
승률 : 66.13
CAGR :  11.57
MDD :  -7.5
STDEV : 9.85
Return-Risk Ratio:  1.17
plt.figure(figsize=(15,5))
seaborn.lineplot(data=df_asset, x=df_asset.index, y=df_asset['LOG_RETURN_ACC'])
<matplotlib.axes._subplots.AxesSubplot at 0x21db66099c8>

output_22_1

plt.figure(figsize=(15,5))
seaborn.lineplot(data=df_asset, x=df_asset.index, y=df_asset['DRAWDOWN'])
<matplotlib.axes._subplots.AxesSubplot at 0x21db5daa348>

output_23_1

아래 그림의 샤프 비율은 월간 데이터로 구해진 것이라 왜곡되어 있습니다. 환산시킨 1.16이나 Return-Risk Ratio 1.17이 더 믿을 만 합니다.

quantstats.stats.sharpe(df_asset['RETURN'])/math.sqrt(252/12)
1.163734609238336
quantstats.reports.plots(df_asset['RETURN']/100, mode='basic')

output_26_0 output_26_1

참고

Keller, Wouter J. and Keuning, Jan Willem, Breadth Momentum and the Canary Universe: Defensive Asset Allocation (DAA) (July 12, 2018). Available at SSRN: https://ssrn.com/abstract=3212862 or http://dx.doi.org/10.2139/ssrn.3212862




© 2021.03. by JacobJinwonLee

Powered by theorydb