import numpy as np
import pandas as pd
import yfinance as yf
import warnings
warnings.filterwarnings("ignore")
pd.options.display.float_format = '{:.4%}'.format
# Date range
start = '2016-01-01'
end = '2019-12-30'
# Tickers of assets
assets = ['JCI', 'TGT', 'CMCSA', 'CPB', 'MO', 'APA', 'MMC', 'JPM',
'ZION', 'PSA', 'BAX', 'BMY', 'LUV', 'PCAR', 'TXT', 'TMO',
'DE', 'MSFT', 'HPQ', 'SEE', 'VZ', 'CNP', 'NI', 'T', 'BA']
assets.sort()
# Downloading data
data = yf.download(assets, start = start, end = end)
data = data.loc[:,('Adj Close', slice(None))]
data.columns = assets
[*********************100%%**********************] 25 of 25 completed
# Calculating returns
Y = data[assets].pct_change().dropna()
display(Y.head())
APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | HPQ | JCI | ... | NI | PCAR | PSA | SEE | T | TGT | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Date | |||||||||||||||||||||
2016-01-05 | -2.0257% | 0.4057% | 0.4035% | 1.9693% | 0.0180% | 0.9305% | 0.3678% | 0.5783% | 0.9483% | -1.1953% | ... | 1.5881% | 0.0212% | 2.8236% | 0.9759% | 0.6987% | 1.7539% | -0.1730% | 0.2409% | 1.3734% | -1.0857% |
2016-01-06 | -11.4864% | -1.5878% | 0.2411% | -1.7557% | -0.7727% | -1.2473% | -0.1736% | -1.1239% | -3.5867% | -0.9551% | ... | 0.5547% | 0.0212% | 0.1592% | -1.5647% | 0.3108% | -1.0155% | -0.7652% | -3.0048% | -0.9035% | -2.9145% |
2016-01-07 | -5.1388% | -4.1922% | -1.6573% | -2.7699% | -1.1047% | -1.9770% | -1.2207% | -0.8855% | -4.6058% | -2.5394% | ... | -2.2066% | -3.0309% | -1.0410% | -3.1557% | -1.6148% | -0.2700% | -2.2845% | -2.0570% | -0.5492% | -3.0019% |
2016-01-08 | 0.2736% | -2.2705% | -1.6036% | -2.5425% | 0.1099% | -0.2241% | 0.5706% | -1.6402% | -1.7642% | -0.1649% | ... | -0.1539% | -1.1366% | -0.7308% | -0.1448% | 0.0895% | -3.3839% | -0.1117% | -1.1387% | -0.9719% | -1.1254% |
2016-01-11 | -4.3384% | 0.1692% | -1.6851% | -1.0215% | 0.0915% | -1.1791% | 0.5674% | 0.5287% | 0.6616% | 0.0331% | ... | 1.6436% | 0.0000% | 0.9869% | -0.1450% | 1.2224% | 1.4570% | 0.5367% | -0.4607% | 0.5800% | -1.9918% |
5 rows × 25 columns
import riskfolio as rp
# Building the portfolio object
port = rp.Portfolio(returns=Y)
# Calculating optimal portfolio
# Select method and estimate input parameters:
method_mu='hist' # Method to estimate expected returns based on historical data.
method_cov='hist' # Method to estimate covariance matrix based on historical data.
port.assets_stats(method_mu=method_mu, method_cov=method_cov)
# Estimate optimal portfolio:
model='Classic' # Could be Classic (historical), BL (Black Litterman) or FM (Factor Model)
rm = 'MV' # Risk measure used, this time will be variance
obj = 'Sharpe' # Objective function, could be MinRisk, MaxRet, Utility or Sharpe
hist = True # Use historical scenarios for risk measures that depend on scenarios
rf = 0 # Risk free rate
l = 0 # Risk aversion factor, only useful when obj is 'Utility'
w = port.optimization(model=model, rm=rm, obj=obj, rf=rf, l=l, hist=hist)
display(w.T)
APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | HPQ | JCI | ... | NI | PCAR | PSA | SEE | T | TGT | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
weights | 0.0000% | 6.1590% | 11.5018% | 0.0000% | 0.0000% | 8.4807% | 0.0000% | 3.8194% | 0.0000% | 0.0000% | ... | 10.8263% | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 7.1805% | 0.0000% | 0.0000% | 4.2740% | 0.0000% |
1 rows × 25 columns
# Plotting the composition of the portfolio
ax = rp.plot_pie(w=w, title='Sharpe Mean Variance', others=0.05, nrow=25, cmap = "tab20",
height=6, width=10, ax=None)
points = 50 # Number of points of the frontier
frontier = port.efficient_frontier(model=model, rm=rm, points=points, rf=rf, hist=hist)
display(frontier.T.head())
APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | HPQ | JCI | ... | NI | PCAR | PSA | SEE | T | TGT | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0000% | 0.0000% | 5.2377% | 4.3880% | 2.1296% | 6.9906% | 3.2270% | 0.0742% | 0.0000% | 2.8447% | ... | 11.4528% | 0.0000% | 14.9247% | 0.1681% | 6.5902% | 4.0900% | 0.0000% | 0.0000% | 8.2768% | 0.0000% |
1 | 0.0000% | 2.0125% | 8.4903% | 0.7981% | 1.6844% | 8.5600% | 2.1529% | 1.3613% | 0.0000% | 1.0445% | ... | 13.4495% | 0.0000% | 9.2100% | 0.0000% | 5.5815% | 5.5235% | 0.0000% | 0.0000% | 8.7591% | 0.0000% |
2 | 0.0000% | 2.8165% | 9.3711% | 0.0000% | 1.1980% | 9.1477% | 1.7065% | 1.7396% | 0.0000% | 0.0872% | ... | 14.2327% | 0.0000% | 6.4848% | 0.0000% | 5.1002% | 6.0539% | 0.0000% | 0.0000% | 9.0040% | 0.0000% |
3 | 0.0000% | 3.4305% | 9.9328% | 0.0000% | 0.5693% | 9.5579% | 1.0812% | 1.9316% | 0.0000% | 0.0000% | ... | 14.7707% | 0.0000% | 3.6631% | 0.0000% | 4.2578% | 6.4249% | 0.0000% | 0.0000% | 9.2132% | 0.0000% |
4 | 0.0000% | 3.9343% | 10.3864% | 0.0000% | 0.0003% | 9.8875% | 0.3998% | 2.0811% | 0.0000% | 0.0000% | ... | 15.1318% | 0.0000% | 1.0860% | 0.0000% | 3.3463% | 6.7109% | 0.0000% | 0.0000% | 9.2765% | 0.0000% |
5 rows × 25 columns
# Plotting the efficient frontier in Std. Dev. dimension
label = 'Max Risk Adjusted Return Portfolio' # Title of point
mu = port.mu # Expected returns
cov = port.cov # Covariance matrix
returns = port.returns # Returns of the assets
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm=rm,
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
# Plotting the efficient frontier in CVaR dimension
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm='CVaR',
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
We can see that in this case, the efficient frontier made using mean-variance optimization has a similar form when we plot it using CVaR as risk measure.
# Plotting the efficient frontier in Max Drawdown dimension
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm='MDD',
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
We can see that in this case, the efficient frontier made using mean-variance optimization looks like a snake when we plot it using Max Drawdown as risk measure.
This is the first step, we are going to estimate the min and max values for each risk measures. I recommend this step because in large scale problems is not practical to build the entire efficient frontier, it is faster to find the first and last point of the frontier for each risk measure. Using this portfolios we can find the lowest and higher values for each risk measure that we can obtained with the available set of assets.
risk = ['MV', 'CVaR', 'MDD']
label = ['Std. Dev.', 'CVaR', 'Max Drawdown']
alpha = 0.05
for i in range(3):
limits = port.frontier_limits(model=model, rm=risk[i], rf=rf, hist=hist)
risk_min = rp.Sharpe_Risk(limits['w_min'], cov=cov, returns=returns, rm=risk[i], rf=rf, alpha=alpha)
risk_max = rp.Sharpe_Risk(limits['w_max'], cov=cov, returns=returns, rm=risk[i], rf=rf, alpha=alpha)
if 'Drawdown' in label[i]:
factor = 1
else:
factor = 252**0.5
print('\nMin Return ' + label[i] + ': ', (mu @ limits['w_min']).item() * 252)
print('Max Return ' + label[i] + ': ', (mu @ limits['w_max']).item() * 252)
print('Min ' + label[i] + ': ', risk_min * factor)
print('Max ' + label[i] + ': ', risk_max * factor)
Min Return Std. Dev.: 0.12970349626397973 Max Return Std. Dev.: 0.3121064279992899 Min Std. Dev.: 0.1037684149785468 Max Std. Dev.: 0.21939026367243655 Min Return CVaR: 0.11458888779891989 Max Return CVaR: 0.31210642854266435 Min CVaR: 0.23762686453041693 Max CVaR: 0.5141980373025518 Min Return Max Drawdown: 0.16948825274027346 Max Return Max Drawdown: 0.3121064286542591 Min Max Drawdown: 0.08596968087253676 Max Max Drawdown: 0.1850672455622686
We can see from the above information, that if our objective function uses Std. Dev. as risk measure, we only can obtain returns between 12.85% and 31.17%, and Std. Dev. between 10.37% and 21.92%. The same applies for the other risk measures. This is very useful because if we put a constraint on max CVaR below 23.75% the optimization problem doesn't have a solution.
rm = 'MV' # Risk measure
# Constraint on minimum Return
port.lowerret = 0.16/252 # We transform annual return to daily return
# Constraint on maximum CVaR
port.upperCVaR = 0.26/252**0.5 # We transform annual CVaR to daily CVaR
# Constraint on maximum Max Drawdown
port.uppermdd = 0.131 # We don't need to transform drawdowns risk measures
w = port.optimization(model=model, rm=rm, obj=obj, rf=rf, l=l, hist=hist)
display(w.T)
APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | HPQ | JCI | ... | NI | PCAR | PSA | SEE | T | TGT | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
weights | 0.0000% | 2.1345% | 8.1286% | 0.0000% | 0.0000% | 12.0965% | 0.0000% | 2.2793% | 0.0000% | 0.4927% | ... | 10.6012% | 0.0000% | 6.2090% | 0.0000% | 0.2056% | 8.9234% | 0.0000% | 0.0000% | 13.0767% | 0.0000% |
1 rows × 25 columns
ax = rp.plot_pie(w=w, title='Sharpe Mean CVaR', others=0.05, nrow=25, cmap = "tab20",
height=6, width=10, ax=None)
points = 50 # Number of points of the frontier
frontier = port.efficient_frontier(model=model, rm=rm, points=points, rf=rf, hist=hist)
display(frontier.T.head())
APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | HPQ | JCI | ... | NI | PCAR | PSA | SEE | T | TGT | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0000% | 1.8258% | 8.1429% | 1.1158% | 1.8639% | 8.5536% | 2.2107% | 1.3941% | 0.0000% | 1.1400% | ... | 13.2104% | 0.0000% | 9.8929% | 0.0000% | 5.4600% | 5.2956% | 0.0000% | 0.0000% | 9.0260% | 0.0000% |
1 | 0.0000% | 1.8931% | 8.2406% | 0.9954% | 1.8205% | 8.5854% | 2.1823% | 1.4102% | 0.0000% | 1.0879% | ... | 13.2818% | 0.0000% | 9.6771% | 0.0000% | 5.4616% | 5.3551% | 0.0000% | 0.0000% | 8.9984% | 0.0000% |
2 | 0.0000% | 1.9580% | 8.3350% | 0.8790% | 1.7786% | 8.6160% | 2.1548% | 1.4259% | 0.0000% | 1.0376% | ... | 13.3512% | 0.0000% | 9.4686% | 0.0000% | 5.4629% | 5.4125% | 0.0000% | 0.0000% | 8.9718% | 0.0000% |
3 | 0.0000% | 2.0209% | 8.4264% | 0.7662% | 1.7382% | 8.6455% | 2.1282% | 1.4411% | 0.0000% | 0.9889% | ... | 13.4186% | 0.0000% | 9.2666% | 0.0000% | 5.4642% | 5.4679% | 0.0000% | 0.0000% | 8.9460% | 0.0000% |
4 | 0.0000% | 2.0820% | 8.5151% | 0.6570% | 1.6989% | 8.6743% | 2.1024% | 1.4557% | 0.0000% | 0.9417% | ... | 13.4836% | 0.0000% | 9.0709% | 0.0000% | 5.4655% | 5.5219% | 0.0000% | 0.0000% | 8.9210% | 0.0000% |
5 rows × 25 columns
# Plotting the efficient frontier in Std. Dev. dimension
label = 'Max Risk Adjusted Return Portfolio' # Title of point
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm=rm,
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
We can see that the new efficient frontier has a lower bound on returns of 16%.
# Plotting the efficient frontier in CVaR dimension
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm='CVaR',
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
We can see that the new efficient frontier has a upper bound on CVaR of 26%.
# Plotting the efficient frontier in Max Drawdown dimension
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm='MDD',
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
We can see that the new efficient frontier has a upper bound on Max Drawdown of 13.1%.