[Backtest] Bond Momentum


개요

채권들의 모멘텀을 이용한 전략을 구상해 봅니다.

꽤 오래 전에 채권에 관한 글에서 경기 침체/회복기에서는 시장 금리 하락 가능성이 높고 위험 회피 성향으로 가산 금리는 올라가니까 만기가 긴 국채/우량 회사채로 수익을 내고, 경기 호황에서는 인플레이션이 오면서 시장 금리는 오를 가능성이 높고 위험 선호로 가산 금리가 낮아지니 그것을 이용할 수 있도록 만기가 짧은 하이일드 채권으로 대응하자는 언급을 했습니다. 그리고, VAA와 DAA 등 전략에서 모멘텀 전략이 시장과 비슷하거나 약간 나은 수익을 내면서 하락폭은 훨씬 낮은 것을 볼 수 있었습니다. 두 가지를 섞어서 채권에 대한 모멘텀 전략을 구상해 보겠습니다.

하이일드(정크본드)에 속하는 것은 JNK와 ANGL ETF가 있습니다. JNK는 티커 그대로 정크본트 ETF입니다. 전환사채 사용도 고려해 볼 수 있는데, 전환사채도 일반적으로 채권을 갚기에 부담스러운 상황인 경우에 발행을 하니(주식으로 전환해주면 돈 안 갚아도 됩니다) 전환사채도 신용도가 낮은 기업들이 주로 발행할 것이라 생각할 수 있습니다. 전환사채 쪽 ETF는 CWB가 있습니다. 위험 자산은 JNK, CWB 두 가지로 구성해 보겠습니다.

안전 자산은 중기 국채인 SPTI로 하겠습니다. 만기를 늘릴 수도 있고, 레버리지를 할 수도 있는데 ‘안전’ 자산이니 부담이 덜한 중기 국채로 합니다.

모멘텀을 판단하는 목적으로는 산업금속(DBB), 달러(UUP), 필수소비재(XLP), 임의소비재(XLY)를 사용해 보려 합니다. 경기가 회복세를 보이면 구리 등 산업금속 수요가 증가할 것이고, 자연히 달러보다 금속이 수익률이 좋을 것이라는 예상이 됩니다. 필수소비재와 임의소비재의 경우도 경기가 좋으면 임의소비재 수요가 크게 늘어나 임의소비재 수익이 좋을 것으로 예상해 볼 수 있습니다. 산업금속 모멘텀이 달러 모멘텀보다 좋고, 임의소비재 모멘텀이 필수소비재 모멘텀보다 좋다는 조건 두 개를 동시에 걸어서 판단하겠습니다. 위험 자산과 안전 자산 모두 1가지만 가져가는 것으로 가정합니다. 매우 단순한 듀얼 모멘텀이라 할 수 있습니다.

아예 코드도 같이 놓고 가겠습니다. 지난주부터인 것으로 기억하는데, Pandas Datareader에서 Yahoo Finance를 가져오는 기능이 정상적이지 않은 것 같습니다. FinanceDataReader라는 대안을 사용하도록 하겠습니다.

import pandas as pd
import pandas_datareader as pdr
import FinanceDataReader as fdr
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)
# CWB starts: 2009-04-14
start = datetime(2009,5,1)
end = datetime(2021,6,30)

tickers = ['JNK','CWB','SPTI','DBB','UUP','XLY','XLP']

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

모멘텀 기간은 각자 기호에 따라 하면 됩니다. 최적화를 원한다면 잘 맞추면 되고, 저는 작동 가능성이 있는지 체크해 보는 목적이라 3개월로 하겠습니다. 3개월 누적 수익률(3개월 모멘텀) 상위인 것을 가져가는데, 산업금속(DBB) 모멘텀이 달러(UUP) 모멘텀보다 좋고, 임의소비재(XLY) 모멘텀이 필수소비재(XLP) 모멘텀보다 좋을 때 위험자산, 나머지 경우에서 안전자산을 가져갑니다.

def get_momentum(x):
    temp = [0 for _ in range(len(x.index))]
    momentum = pd.Series(temp, index=x.index)
    
    try:
        before_3m = df_asset[x.name-timedelta(days=95):x.name-timedelta(days=90)].iloc[-1]
        momentum = x / before_3m - 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)

3개월 모멘텀이라 맨 앞 3개월은 사용 못합니다. 2009년 8월부터로 합니다.

df_asset = df_asset.loc[df_asset.index >= '2009-08-01']

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

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

위험 자산 JNK, CWB 중 모멘텀이 가장 좋은 1개를 고릅니다. 산업금속-달러, 임의소비재-필수소비재 모멘텀 페어 중 하나라도 위험 자산 신호가 아니라면 안전 자산 SPTI를 선택합니다.

def select_asset(x):
    selected_asset = pd.Series([0,0], index=['ASSET','PRICE'])
    
    # DBB_m > UUP_m & XLY_m > XLP_m
    if x['DBB_m'] > x['UUP_m'] and x['XLY_m'] > x['XLP_m']:
        selected_momentum = max(x['JNK_m'], x['CWB_m'])
        selected_asset['ASSET'] = x[x==selected_momentum].index[0][:3]
        selected_asset['PRICE'] = x[selected_asset['ASSET']]
        
    # DBB_m < UUP_m | XLY_m < XLP_m --> SPTI 4글자 주의
    else:
        selected_momentum = x['SPTI_m']
        selected_asset['ASSET'] = x[x==selected_momentum].index[0][:4]
        selected_asset['PRICE'] = x[selected_asset['ASSET']]
    
    return selected_asset
df_asset[['ASSET','PRICE']] = 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]['ASSET']+'_r'].iloc[i]
        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

2009년 8월부터 2021년 6월까지 143개월 동안 낸 성과입니다. 연 복리 수익률 7%에 변동성 7.29%, MDD -9.53%, RRR 0.96입니다. 전략에 활용된 채권들이 SPTI는 듀레이션 5.47년, JNK는 3.66년, CWB는 1.74년(전환사채라서 채권처럼 생각하면 안되기는 합니다) 입니다. 다소 특수한 재료들을 사용했다지만 성과만 보면 괜찮다고 생각합니다. 다만, 작년 7-8월, 11-12월에 전환사채가 홈런을 친 것으로 보이는데, 이 부분은 감안해서 보아야 할 것입니다. 그 4개월을 없는 것으로 친다면 연 복리 4.1% 수익입니다.

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))
143 개월 중 수익 월 : 89 개월
143 개월 중 손실 월 : 54 개월
승률 : 62.24
CAGR :  7.0
MDD :  -9.53
STDEV : 7.29
Return-Risk Ratio:  0.96
plt.figure(figsize=(15,5))
seaborn.lineplot(data=df_asset, x=df_asset.index, y=df_asset['LOG_RETURN_ACC'])

output_23_1

plt.figure(figsize=(15,5))
seaborn.lineplot(data=df_asset, x=df_asset.index, y=df_asset['DRAWDOWN'])

output_24_1

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

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

output_27_0 output_27_1

벤치마크 느낌으로 SPTI를 한번 보겠습니다. 일별 데이터로 가져오니 quantstats 라이브러리 그대로 써도 됩니다. FinanceDataReader로 가져온 데이터에 따르면 연 복리 1% 수익이었습니다. 변동성 3.06%, MDD -6.73%, 샤프 비율 0.35로 정크본드와 전환사채를 넣은 오늘의 전략보다 많이 못하긴 합니다. 심지어 오늘 만든 전략에서 가장 좋은 4개월을 빼더라도 SPTI는 가볍게 이기는 것으로 보입니다.

spti_bm = fdr.DataReader('SPTI', '2009-07-31', '2021-06-30')['Close']
spti_bm = spti_bm.pct_change(periods=1)
spti_bm = spti_bm.dropna()
quantstats.reports.plots(spti_bm, mode='basic')

output_30_0 output_30_1

quantstats.reports.metrics(spti_bm, mode='full')
                           Strategy
-------------------------  ----------
Start Period               2009-08-03
End Period                 2021-06-30
Risk-Free Rate             0.0%
Time in Market             91.0%

Cumulative Return          12.94%
CAGR%                      1.03%
Sharpe                     0.35
Sortino                    0.5
Max Drawdown               -6.73%
Longest DD Days            1432
Volatility (ann.)          3.06%
Calmar                     0.15
Skew                       0.03
Kurtosis                   4.47

Expected Daily %           0.0%
Expected Monthly %         0.09%
Expected Yearly %          0.94%
Kelly Criterion            3.03%
Risk of Ruin               0.0%
Daily Value-at-Risk        -0.31%
Expected Shortfall (cVaR)  -0.31%

Payoff Ratio               0.99
Profit Factor              1.06
Common Sense Ratio         1.06
CPC Index                  0.54
Tail Ratio                 1.0
Outlier Win Ratio          4.09
Outlier Loss Ratio         3.37

MTD                        0.03%
3M                         0.75%
6M                         -2.0%
YTD                        -2.06%
1Y                         -2.56%
3Y (ann.)                  3.34%
5Y (ann.)                  1.01%
10Y (ann.)                 0.84%
All-time (ann.)            1.03%

Best Day                   1.46%
Worst Day                  -1.43%
Best Month                 2.76%
Worst Month                -2.46%
Best Year                  6.89%
Worst Year                 -2.64%

Avg. Drawdown              -0.86%
Avg. Drawdown Days         71
Recovery Factor            1.92
Ulcer Index                inf

Avg. Up Month              0.65%
Avg. Down Month            -0.55%
Win Days %                 51.74%
Win Month %                53.15%
Win Quarter %              56.25%
Win Year %                 53.85%





© 2021.03. by JacobJinwonLee

Powered by theorydb