The BL model uses "views" from the active investor and incorporates them with the implied returns calculated from the value-weighted market portfolio. The model uses the implied returns as a prior distribution and treats the "views" as observed data used to calculate a posterior distribution on the returns as well as a posterior for the covariance of the returns.
$\pi$ : implied returns (N x 1)
$\delta$ : risk aversion parameter (1 x 1)
$\Sigma$ : Covariance of the assets (N x N)
$w$ : Equilibrium market weights (N x 1)
$Q$ : (K x 1) Matrix containing views
$P$ : (K x N) Pick matrix. Links each view to the corresponding asset
$\Omega$ : Covariance matrix of excess returns. (In the demo, I'm assuming a risk-free rate of 0%)
$\tau$ : A scalar indicating the uncertainty of the prior
For your $k$-th view, set $Q_k$ to be the difference of returns between assets i and j. Set $P_ki$ to -1 for the asset with the lower expected return (asset i). Set $P_kj$ to 1 for the asset with the higher expected return (asset j). All other elements in row $k$ should be set to 0. $\Omega$ represents the uncertaintly of the views. The original paper recommends setting $\Omega = diag(P (\tau \Sigma) P^T)$.
For the examples in this notebook, use data from 1980 to 2015 for the following 5 industries:
Calculate the implied annual returns using a delta of 2.5 and the same sigma prior method as the He-Litterman paper. Neutral prior distribution on expected returns is obtained by reverse engineering, assuming that the cap-weighted market portfolio is the optimal portfolio. In short, covariance matrix, cap weights, & delta -> implied returns.
Use equation $\pi = \delta\Sigma w$
def implied_returns(delta, sigma, w):
"""
Obtain the implied expected returns by reverse engineering the weights
Inputs:
delta: Risk Aversion Coefficient (scalar)
sigma: Variance-Covariance Matrix (N x N) as DataFrame
w: Portfolio weights (N x 1) as Series
Returns an N x 1 vector of Returns as Series
"""
ir = delta * sigma.dot(w).squeeze() # to get a series from a 1-column dataframe
ir.name = 'Implied Returns'
return ir
import numpy as np
import pandas as pd
import edhec_risk_kit as erk
industries = ['Beer', 'Hlth', 'Fin', 'Softw', 'Clths']
ind_rets = erk.get_ind_returns(n_inds=49).loc['1980':'2015', industries]
ind_mcap = erk.get_ind_market_caps(49).loc['1980':'2015', industries]
weights = ind_mcap.loc['1980-01'] / ind_mcap.loc['1980-01'].sum()
delta = 2.5
s = pd.DataFrame(ind_rets.cov()*np.sqrt(12), index=industries, columns=industries)
pi = erk.implied_returns(delta=2.5, sigma=s, w=weights)
pi
def proportional_prior(sigma, tau, p):
"""
Returns the He-Litterman simplified Omega
Inputs:
sigma: N x N Covariance Matrix as DataFrame
tau: a scalar
p: a K x N DataFrame linking Q and Assets
returns a P x P DataFrame, a Matrix representing Prior Uncertainties
"""
helit_omega = p.dot(tau * sigma).dot(p.T)
# Make a diag matrix from the diag elements of Omega
return pd.DataFrame(np.diag(np.diag(helit_omega.values)),index=p.index, columns=p.index)
def bl(w_prior, sigma_prior, p, q,
omega=None,
delta=2.5, tau=.02):
"""
# Computes the posterior expected returns based on
# the original black litterman reference model
#
# W.prior must be an N x 1 vector of weights, a Series
# Sigma.prior is an N x N covariance matrix, a DataFrame
# P must be a K x N matrix linking Q and the Assets, a DataFrame
# Q must be an K x 1 vector of views, a Series
# Omega must be a K x K matrix a DataFrame, or None
# if Omega is None, we assume it is
# proportional to variance of the prior
# delta and tau are scalars
"""
if omega is None:
omega = proportional_prior(sigma_prior, tau, p)
# Force w.prior and Q to be column vectors
# How many assets do we have?
N = w_prior.shape[0]
# And how many views?
K = q.shape[0]
# First, reverse-engineer the weights to get pi
pi = implied_returns(delta, sigma_prior, w_prior)
# Adjust (scale) Sigma by the uncertainty scaling factor
sigma_prior_scaled = tau * sigma_prior
# posterior estimate of the mean, use the "Master Formula"
# we use the versions that do not require
# Omega to be inverted (see previous section)
# this is easier to read if we use '@' for matrixmult instead of .dot()
# mu_bl = pi + sigma_prior_scaled @ p.T @ inv(p @ sigma_prior_scaled @ p.T + omega) @ (q - p @ pi)
mu_bl = pi + sigma_prior_scaled.dot(p.T).dot(inv(p.dot(sigma_prior_scaled).dot(p.T) + omega).dot(q - p.dot(pi).values))
# posterior estimate of uncertainty of mu.bl
# sigma_bl = sigma_prior + sigma_prior_scaled - sigma_prior_scaled @ p.T @ inv(p @ sigma_prior_scaled @ p.T + omega) @ p @ sigma_prior_scaled
sigma_bl = sigma_prior + sigma_prior_scaled - sigma_prior_scaled.dot(p.T).dot(inv(p.dot(sigma_prior_scaled).dot(p.T) + omega)).dot(p).dot(sigma_prior_scaled)
return (mu_bl, sigma_bl)
What will be the entry in the P matrix for Fin? For Softw?
q = pd.Series([.01]) # one view
p = pd.DataFrame([0.]*len(industries), index=industries).T
w_Fin = weights.loc["Fin"]/(weights.loc["Fin"]+weights.loc["Clths"])
w_Clths = weights.loc["Clths"]/(weights.loc["Fin"]+weights.loc["Clths"])
p.iloc[0]['Softw'] = 1.
p.iloc[0]['Fin'] = -w_Fin
p.iloc[0]['Clths'] = -w_Clths
print('Entries for the P matrix:\n', p)
delta = 2.5
tau = 0.05
sigma_prior = ind_rets.cov()*np.sqrt(12)
# Find the Black Litterman Expected Returns
bl_mu, bl_sigma = erk.bl(weights, sigma_prior, p, q, tau = tau)
bl_mu
pd.DataFrame({
'Weight':erk.msr(riskfree_rate=0.005, er=bl_mu, cov=bl_sigma)
},
index=industries
)
Change the view to be that you believe Software will outperform Finance and Clothes by 4%. Then calculate the new weights of the Max Sharpe Ratio portfolio
q = pd.Series([.04]) # one view
p = pd.DataFrame([0.]*len(industries), index=industries).T
w_Fin = weights.loc["Fin"]/(weights.loc["Fin"]+weights.loc["Clths"])
w_Clths = weights.loc["Clths"]/(weights.loc["Fin"]+weights.loc["Clths"])
p.iloc[0]['Softw'] = 1.
p.iloc[0]['Fin'] = -w_Fin
p.iloc[0]['Clths'] = -w_Clths
# P matrix doesn't change
print('P:\n', p)
delta = 2.5
tau = 0.05
sigma_prior = ind_rets.cov()*np.sqrt(12)
# Find the Black Litterman Expected Returns
bl_mu, bl_sigma = erk.bl(weights, sigma_prior, p, q, tau = tau)
print('New expected returns:')
bl_mu
print('New MSR weights')
pd.DataFrame({
'Weight':erk.msr(riskfree_rate=0.005, er=bl_mu, cov=bl_sigma)
},
index=industries
)