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.0256% | 0.4057% | 0.4035% | 1.9692% | 0.0180% | 0.9305% | 0.3678% | 0.5783% | 0.9483% | -1.1953% | ... | 1.5881% | 0.0212% | 2.8236% | 0.9758% | 0.6987% | 1.7539% | -0.1730% | 0.2409% | 1.3734% | -1.0857% |
2016-01-06 | -11.4863% | -1.5879% | 0.2412% | -1.7557% | -0.7727% | -1.2473% | -0.1736% | -1.1239% | -3.5867% | -0.9551% | ... | 0.5547% | 0.0212% | 0.1592% | -1.5646% | 0.3108% | -1.0155% | -0.7653% | -3.0048% | -0.9035% | -2.9144% |
2016-01-07 | -5.1388% | -4.1922% | -1.6573% | -2.7699% | -1.1047% | -1.9769% | -1.2207% | -0.8856% | -4.6059% | -2.5394% | ... | -2.2066% | -3.0310% | -1.0411% | -3.1557% | -1.6148% | -0.2700% | -2.2844% | -2.0570% | -0.5492% | -3.0020% |
2016-01-08 | 0.2736% | -2.2705% | -1.6037% | -2.5425% | 0.1099% | -0.2241% | 0.5707% | -1.6402% | -1.7641% | -0.1649% | ... | -0.1538% | -1.1366% | -0.7308% | -0.1449% | 0.0896% | -3.3839% | -0.1117% | -1.1387% | -0.9720% | -1.1254% |
2016-01-11 | -4.3384% | 0.1692% | -1.6851% | -1.0215% | 0.0915% | -1.1791% | 0.5674% | 0.5287% | 0.6616% | 0.0330% | ... | 1.6435% | 0.0000% | 0.9870% | -0.1450% | 1.2224% | 1.4570% | 0.5367% | -0.4607% | 0.5800% | -1.9919% |
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:
port.alpha = 0.05
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.5017% | 0.0000% | 0.0000% | 8.4806% | 0.0000% | 3.8194% | 0.0000% | 0.0000% | ... | 10.8264% | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 7.1804% | 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)
asset_classes = {'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'],
'Industry': ['Consumer Discretionary','Consumer Discretionary',
'Consumer Discretionary', 'Consumer Staples',
'Consumer Staples','Energy','Financials',
'Financials','Financials','Financials',
'Health Care','Health Care','Industrials','Industrials',
'Industrials','Health care','Industrials',
'Information Technology','Information Technology',
'Materials','Telecommunications Services','Utilities',
'Utilities','Telecommunications Services','Financials']}
asset_classes = pd.DataFrame(asset_classes)
asset_classes = asset_classes.sort_values(by=['Assets'])
views = {'Disabled': [False, False, False],
'Type': ['Classes', 'Classes', 'Classes'],
'Set': ['Industry', 'Industry', 'Industry'],
'Position': ['Energy', 'Consumer Staples', 'Materials'],
'Sign': ['>=', '>=', '>='],
'Weight': [0.08, 0.1, 0.09], # Annual terms
'Type Relative': ['Classes', 'Classes', 'Classes'],
'Relative Set': ['Industry', 'Industry', 'Industry'],
'Relative': ['Financials', 'Utilities', 'Industrials']}
views = pd.DataFrame(views)
display(views)
Disabled | Type | Set | Position | Sign | Weight | Type Relative | Relative Set | Relative | |
---|---|---|---|---|---|---|---|---|---|
0 | False | Classes | Industry | Energy | >= | 8.0000% | Classes | Industry | Financials |
1 | False | Classes | Industry | Consumer Staples | >= | 10.0000% | Classes | Industry | Utilities |
2 | False | Classes | Industry | Materials | >= | 9.0000% | Classes | Industry | Industrials |
P, Q = rp.assets_views(views, asset_classes)
display(pd.DataFrame(P.T))
display(pd.DataFrame(Q))
0 | 1 | 2 | |
---|---|---|---|
0 | 100.0000% | 0.0000% | 0.0000% |
1 | -20.0000% | 0.0000% | 0.0000% |
2 | 0.0000% | 0.0000% | 0.0000% |
3 | 0.0000% | 0.0000% | 0.0000% |
4 | 0.0000% | 0.0000% | 0.0000% |
5 | 0.0000% | -50.0000% | 0.0000% |
6 | 0.0000% | 50.0000% | 0.0000% |
7 | 0.0000% | 0.0000% | -25.0000% |
8 | 0.0000% | 0.0000% | 0.0000% |
9 | 0.0000% | 0.0000% | 0.0000% |
10 | -20.0000% | 0.0000% | 0.0000% |
11 | 0.0000% | 0.0000% | -25.0000% |
12 | -20.0000% | 0.0000% | 0.0000% |
13 | 0.0000% | 50.0000% | 0.0000% |
14 | 0.0000% | 0.0000% | 0.0000% |
15 | 0.0000% | -50.0000% | 0.0000% |
16 | 0.0000% | 0.0000% | -25.0000% |
17 | -20.0000% | 0.0000% | 0.0000% |
18 | 0.0000% | 0.0000% | 100.0000% |
19 | 0.0000% | 0.0000% | 0.0000% |
20 | 0.0000% | 0.0000% | 0.0000% |
21 | 0.0000% | 0.0000% | 0.0000% |
22 | 0.0000% | 0.0000% | -25.0000% |
23 | 0.0000% | 0.0000% | 0.0000% |
24 | -20.0000% | 0.0000% | 0.0000% |
0 | |
---|---|
0 | 8.0000% |
1 | 10.0000% |
2 | 9.0000% |
# Estimate Black Litterman inputs:
port.blacklitterman_stats(P, Q/252, rf=rf, w=w, delta=None, eq=True)
# Estimate optimal portfolio:
model='BL'# Black Litterman
rm = 'MV' # Risk measure used, this time will be variance
obj = 'Sharpe' # Objective function, could be MinRisk, MaxRet, Utility or Sharpe
hist = False # Use historical scenarios for risk measures that depend on scenarios
w_bl = port.optimization(model=model, rm=rm, obj=obj, rf=rf, l=l, hist=hist)
display(w_bl.T)
APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | HPQ | JCI | ... | NI | PCAR | PSA | SEE | T | TGT | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
weights | 0.5319% | 5.0300% | 11.1756% | 0.0039% | 0.0000% | 2.3431% | 5.8447% | 0.6343% | 0.0000% | 0.0000% | ... | 4.8615% | 0.0000% | 0.0000% | 8.1605% | 0.0000% | 6.8848% | 0.0000% | 0.0000% | 4.5133% | 0.0000% |
1 rows × 25 columns
# Plotting the composition of the portfolio
ax = rp.plot_pie(w=w_bl, title='Sharpe Black Litterman', 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.2376% | 4.3880% | 2.1297% | 6.9906% | 3.2269% | 0.0743% | 0.0000% | 2.8447% | ... | 11.4530% | 0.0000% | 14.9246% | 0.1680% | 6.5902% | 4.0900% | 0.0000% | 0.0000% | 8.2767% | 0.0000% |
1 | 0.0000% | 1.8830% | 7.9577% | 2.9165% | 1.6406% | 5.5445% | 4.1149% | 0.5323% | 0.0000% | 1.7322% | ... | 9.3257% | 0.0000% | 9.7755% | 3.0748% | 4.1231% | 5.1901% | 0.0000% | 0.0000% | 6.9797% | 0.0000% |
2 | 0.0000% | 2.6285% | 8.7003% | 2.2994% | 1.2735% | 4.9388% | 4.5122% | 0.6084% | 0.0000% | 1.2641% | ... | 8.4071% | 0.0000% | 7.6806% | 4.2311% | 3.2354% | 5.5890% | 0.0000% | 0.0000% | 6.4599% | 0.0000% |
3 | 0.0856% | 3.1842% | 9.2639% | 1.8158% | 0.9420% | 4.4196% | 4.8234% | 0.6471% | 0.0000% | 0.8588% | ... | 7.6804% | 0.0000% | 5.9971% | 5.1202% | 2.4839% | 5.8910% | 0.0000% | 0.0000% | 6.0815% | 0.0000% |
4 | 0.2000% | 3.6362% | 9.7323% | 1.4016% | 0.6188% | 3.9499% | 5.0862% | 0.6643% | 0.0000% | 0.4799% | ... | 7.0463% | 0.0000% | 4.5151% | 5.8624% | 1.7948% | 6.1410% | 0.0000% | 0.0000% | 5.7825% | 0.0000% |
5 rows × 25 columns
# Plotting the efficient frontier
label = 'Max Risk Adjusted Return Portfolio' # Title of point
mu = port.mu_bl # Expected returns of Black Litterman model
cov = port.cov_bl # Covariance matrix of Black Litterman model
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_bl, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
# Plotting efficient frontier composition
ax = rp.plot_frontier_area(w_frontier=frontier, cmap="tab20", height=6, width=10, ax=None)
# Risk Measures available:
#
# 'MV': Standard Deviation.
# 'MAD': Mean Absolute Deviation.
# 'MSV': Semi Standard Deviation.
# 'FLPM': First Lower Partial Moment (Omega Ratio).
# 'SLPM': Second Lower Partial Moment (Sortino Ratio).
# 'CVaR': Conditional Value at Risk.
# 'EVaR': Entropic Value at Risk.
# 'WR': Worst Realization (Minimax)
# 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio).
# 'ADD': Average Drawdown of uncompounded cumulative returns.
# 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
# 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
# 'UCI': Ulcer Index of uncompounded cumulative returns.
rms = ['MV', 'MAD', 'MSV', 'FLPM', 'SLPM', 'CVaR',
'EVaR', 'WR', 'MDD', 'ADD', 'CDaR', 'UCI', 'EDaR']
w_s = pd.DataFrame([])
port.alpha = 0.05
for i in rms:
if i == 'MV':
hist = False
else:
hist = True
w = port.optimization(model=model, rm=i, obj=obj, rf=rf, l=l, hist=hist)
w_s = pd.concat([w_s, w], axis=1)
w_s.columns = rms
w_s.style.format("{:.2%}").background_gradient(cmap='YlGn')
MV | MAD | MSV | FLPM | SLPM | CVaR | EVaR | WR | MDD | ADD | CDaR | UCI | EDaR | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
APA | 0.53% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
BA | 5.03% | 6.06% | 3.10% | 4.27% | 2.82% | 0.00% | 0.00% | 0.00% | 0.00% | 8.05% | 2.64% | 6.75% | 0.00% |
BAX | 11.18% | 8.54% | 10.66% | 8.54% | 10.66% | 11.94% | 7.32% | 0.00% | 0.00% | 3.50% | 0.00% | 4.20% | 0.00% |
BMY | 0.00% | 2.11% | 0.00% | 3.01% | 0.00% | 0.00% | 4.13% | 8.94% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
CMCSA | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
CNP | 2.34% | 3.26% | 1.92% | 2.98% | 1.58% | 2.27% | 2.20% | 0.00% | 52.20% | 11.94% | 31.88% | 16.08% | 39.44% |
CPB | 5.84% | 2.39% | 6.71% | 4.21% | 7.18% | 10.57% | 17.55% | 15.82% | 0.00% | 2.11% | 0.17% | 0.00% | 0.00% |
DE | 0.63% | 1.56% | 0.00% | 0.22% | 0.00% | 0.00% | 0.00% | 0.00% | 5.92% | 0.09% | 0.05% | 0.00% | 0.00% |
HPQ | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
JCI | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 1.54% | 0.00% | 0.00% | 0.00% |
JPM | 6.41% | 12.14% | 7.80% | 11.52% | 7.43% | 0.01% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
LUV | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
MMC | 20.51% | 14.47% | 20.37% | 15.33% | 20.52% | 24.15% | 15.99% | 35.57% | 0.00% | 15.91% | 8.81% | 15.99% | 0.00% |
MO | 6.20% | 4.34% | 3.95% | 3.90% | 3.96% | 3.01% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
MSFT | 16.89% | 20.52% | 22.07% | 22.39% | 22.10% | 20.20% | 26.02% | 19.67% | 37.83% | 40.33% | 45.06% | 41.64% | 53.96% |
NI | 4.86% | 7.82% | 6.04% | 6.28% | 6.13% | 1.25% | 0.00% | 0.00% | 0.00% | 3.96% | 3.45% | 3.12% | 0.00% |
PCAR | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 2.69% |
PSA | 0.00% | 0.00% | 1.10% | 0.00% | 1.26% | 8.30% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.51% | 0.00% |
SEE | 8.16% | 2.95% | 4.23% | 3.06% | 4.36% | 4.93% | 8.72% | 0.00% | 4.04% | 0.00% | 0.00% | 0.00% | 0.00% |
T | 0.00% | 3.12% | 0.00% | 2.55% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.51% | 0.00% | 1.75% | 0.00% |
TGT | 6.88% | 4.90% | 8.78% | 5.46% | 9.17% | 10.06% | 18.08% | 20.00% | 0.00% | 1.14% | 0.00% | 0.64% | 0.00% |
TMO | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.47% | 0.00% | 0.00% | 0.00% |
TXT | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
VZ | 4.51% | 4.67% | 3.28% | 5.11% | 2.84% | 3.30% | 0.00% | 0.00% | 0.00% | 8.99% | 7.94% | 9.30% | 3.91% |
ZION | 0.00% | 1.15% | 0.00% | 1.16% | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% | 1.47% | 0.00% | 0.00% | 0.00% |
import matplotlib.pyplot as plt
# Plotting a comparison of assets weights for each portfolio
fig = plt.gcf()
fig.set_figwidth(14)
fig.set_figheight(6)
ax = fig.subplots(nrows=1, ncols=1)
w_s.plot.bar(ax=ax)
<Axes: >