[Strategy] Bond Momentum
in AssetAllocation on Strategy
개요
채권들의 모멘텀을 이용한 전략을 구상해 봅니다.
꽤 오래 전에 채권에 관한 글에서 경기 침체/회복기에서는 시장 금리 하락 가능성이 높고 위험 회피 성향으로 가산 금리는 올라가니까 만기가 긴 국채/우량 회사채로 수익을 내고, 경기 호황에서는 인플레이션이 오면서 시장 금리는 오를 가능성이 높고 위험 선호로 가산 금리가 낮아지니 그것을 이용할 수 있도록 만기가 짧은 하이일드 채권으로 대응하자는 언급을 했습니다. 그리고, 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'])
plt.figure(figsize=(15,5))
seaborn.lineplot(data=df_asset, x=df_asset.index, y=df_asset['DRAWDOWN'])
아래 그림의 샤프 비율은 월간 데이터로 구해진 것이라 왜곡되어 있습니다. 환산시킨 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')
벤치마크 느낌으로 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')
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%