[PortfolioOptimization] Maximum Diversification
in ToyCode on PortfolioOptimization
개요
Maximum Diversification 방식의 포트폴리오 최적화를 구현해 봅니다.
import pandas as pd
import pandas_datareader.data as web
import datetime
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
import math
import pyfolio as pf
import quantstats
plt.rcParams["figure.figsize"] = (10, 6) # (w, h)
import sys
from scipy.stats import rankdata
from scipy.stats import stats
from scipy.optimize import minimize
Maximum Diversification 방식의 포트폴리오 최적화를 구현해 보겠습니다. 분산효과 DR을 최대화하는 것은 -DR을 최소화하는 것과 같습니다.
def MaximumDiversification(rets, lb, ub):
covmat = pd.DataFrame.cov(rets)
def MaxDivObjective(x):
# average weighted vol
#x_vol = np.dot(np.sqrt(np.diag(covmat), x.T))
x_vol = x.T @ np.sqrt(np.diag(covmat))
# portfolio vol. @: matrix multiplication
variance = x.T @ covmat @ x
port_vol = variance ** 0.5
diversification_ratio = x_vol / port_vol
return -diversification_ratio
def minimum_weight(x):
return x
def sum_weight(x):
return (sum(x)-1)
x0 = np.repeat(1/covmat.shape[1], covmat.shape[1])
lbound = np.repeat(lb, covmat.shape[1])
ubound = np.repeat(ub, covmat.shape[1])
bnds = tuple(zip(lbound, ubound))
constraints = ({'type': 'ineq', 'fun': minimum_weight},
{'type': 'eq', 'fun': sum_weight})
options = {'ftol': 1e-20, 'maxiter': 1000}
result = minimize(fun = MaxDivObjective,
x0 = x0,
method = 'SLSQP',
constraints = constraints,
options = options,
bounds = bnds)
return(result.x)
미국 주식, 미국 장기 국채, 금, 원자재에 대해서 실험해 보겠습니다. 비중 합은 1로 하고, 모두 0 이상으로 가져가게 해서 레버리지는 안 하는 것으로 가정합니다. Rolling period는 12개월로(252거래일) 해 보겠습니다.
start = '2006-07-21'
end = '2021-05-19'
vti = web.DataReader("VTI", 'yahoo', start, end)['Adj Close'].to_frame("vti")
tlt = web.DataReader("TLT", 'yahoo', start, end)['Adj Close'].to_frame("tlt")
iau = web.DataReader("IAU", 'yahoo', start, end)['Adj Close'].to_frame("iau")
gsg = web.DataReader("GSG", 'yahoo', start, end)['Adj Close'].to_frame("gsg")
price_df = pd.concat([vti, tlt, iau, gsg], axis=1)
return_df = price_df.pct_change().dropna(axis=0)
# 252 days(12 months) 기준으로 rolling
weight_df = pd.DataFrame(columns=['vti','tlt','iau','gsg'])
for i in range(len(return_df)-252):
print(i)
# 253일 분량 데이터 잘라오기 (for문 돌 때마다 index 0~252, 1~253, 2~254, ...)
temp_return_df = return_df.iloc[i:(i+253), :]
# 253일 중 252일치를 계산에 사용하고 253번째 행은 날짜를 추출
date_index = str(temp_return_df.iloc[252, :].name)[0:10]
# 최소/최대 보유 비중은 0.01과 0.5 (최소 1%는 편입, 최대 50%까지만 편입. 0 ~ 100% 사이에서 자유 조정 가능)
temp = pd.DataFrame(MaximumDiversification(temp_return_df.iloc[0:252, :], 0.01, 0.5), index=['vti','tlt','iau','gsg'], columns=[date_index]).T
weight_df = pd.concat([weight_df, temp])
weight_df
vti | tlt | iau | gsg | |
---|---|---|---|---|
2007-07-25 | 0.278986 | 0.500000 | 0.085533 | 0.135481 |
2007-07-26 | 0.281984 | 0.499464 | 0.081660 | 0.136892 |
2007-07-27 | 0.283241 | 0.500000 | 0.078752 | 0.138007 |
2007-07-30 | 0.283082 | 0.500000 | 0.077508 | 0.139410 |
2007-07-31 | 0.283098 | 0.500000 | 0.077026 | 0.139876 |
... | ... | ... | ... | ... |
2021-05-13 | 0.206885 | 0.459383 | 0.143799 | 0.189933 |
2021-05-14 | 0.207187 | 0.458433 | 0.143107 | 0.191273 |
2021-05-17 | 0.201711 | 0.458192 | 0.143013 | 0.197085 |
2021-05-18 | 0.202391 | 0.457923 | 0.142873 | 0.196814 |
2021-05-19 | 0.202711 | 0.453325 | 0.146720 | 0.197245 |
3480 rows × 4 columns
장기 국채인 TLT는 비중 상한인 50%에 닿는 경우가 꽤 많습니다. Maximum Diversification이 꼭 최고의 성과를 준다기보다는 이런 방법도 테스트해볼 겸 한 것이라서 괜찮습니다.
weight_df.plot(title = 'Weights for each asset class')
max_div_return = (weight_df['vti'] * return_df.iloc[252:, :]['vti'] +
weight_df['tlt'] * return_df.iloc[252:, :]['tlt'] +
weight_df['iau'] * return_df.iloc[252:, :]['iau'] +
weight_df['gsg'] * return_df.iloc[252:, :]['gsg'])
max_div_return
2007-07-25 0.002205
2007-07-26 -0.004458
2007-07-27 -0.002834
2007-07-30 0.003076
2007-07-31 0.001080
...
2021-05-13 -0.001327
2021-05-14 0.010107
2021-05-17 0.003095
2021-05-18 -0.003733
2021-05-19 -0.005739
Length: 3480, dtype: float64
max_div_return.index
Index(['2007-07-25', '2007-07-26', '2007-07-27', '2007-07-30', '2007-07-31',
'2007-08-01', '2007-08-02', '2007-08-03', '2007-08-06', '2007-08-07',
...
'2021-05-06', '2021-05-07', '2021-05-10', '2021-05-11', '2021-05-12',
'2021-05-13', '2021-05-14', '2021-05-17', '2021-05-18', '2021-05-19'],
dtype='object', length=3480)
인덱스가 DatetimeIndex 형식이 아니므로 DatetimeIndex 형식으로 변환합니다.
max_div_return.index = pd.to_datetime(max_div_return.index)
max_div_return.index
DatetimeIndex(['2007-07-25', '2007-07-26', '2007-07-27', '2007-07-30',
'2007-07-31', '2007-08-01', '2007-08-02', '2007-08-03',
'2007-08-06', '2007-08-07',
...
'2021-05-06', '2021-05-07', '2021-05-10', '2021-05-11',
'2021-05-12', '2021-05-13', '2021-05-14', '2021-05-17',
'2021-05-18', '2021-05-19'],
dtype='datetime64[ns]', length=3480, freq=None)
위험 균형 전략에 목표 변동성을 더했던(risk parity with target vol) 테스트보다 샤프 비율이 잘 나옵니다. 레버리지를 사용하면 샤프 비율이 내려가는 경우가 많으니 감안해서 보아야 하지만, maximum diversification도 괜찮은 방법이라는 것 정도는 알 수 있습니다.
quantstats.reports.plots(max_div_return, mode='basic')
quantstats.reports.metrics(max_div_return, mode='full')
Strategy
------------------------- ----------
Start Period 2007-07-25
End Period 2021-05-19
Risk-Free Rate 0.0%
Time in Market 100.0%
Cumulative Return 163.93%
CAGR% 7.27%
Sharpe 0.87
Sortino 1.23
Max Drawdown -18.15%
Longest DD Days 553
Volatility (ann.) 8.54%
Calmar 0.4
Skew -0.43
Kurtosis 7.5
Expected Daily % 0.03%
Expected Monthly % 0.58%
Expected Yearly % 6.68%
Kelly Criterion 7.72%
Risk of Ruin 0.0%
Daily Value-at-Risk -0.86%
Expected Shortfall (cVaR) -0.86%
Payoff Ratio 0.98
Profit Factor 1.17
Common Sense Ratio 1.12
CPC Index 0.62
Tail Ratio 0.96
Outlier Win Ratio 3.8
Outlier Loss Ratio 3.71
MTD -0.46%
3M -1.04%
6M 1.17%
YTD -1.71%
1Y 9.88%
3Y (ann.) 8.55%
5Y (ann.) 8.02%
10Y (ann.) 7.18%
All-time (ann.) 7.27%
Best Day 3.46%
Worst Day -5.28%
Best Month 7.78%
Worst Month -10.93%
Best Year 20.4%
Worst Year -6.22%
Avg. Drawdown -1.37%
Avg. Drawdown Days 30
Recovery Factor 9.03
Ulcer Index 1.02
Avg. Up Month 1.86%
Avg. Down Month -1.74%
Win Days % 54.34%
Win Month % 65.27%
Win Quarter % 75.0%
Win Year % 73.33%
quantstats.reports.html(max_div_return, output='Report_MAXDIV.html', title='Maximum Diversification')