Black-Litterman & Implied Returns

Table of contents

Intro to the Black-Litterman model

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 = \delta\Sigma w$$

$\pi$ : implied returns (N x 1)

$\delta$ : risk aversion parameter (1 x 1)

  • Original paper uses a value of 2.5 for this parameter
  • Another approach is the set it to the market price of risk:
    • $\delta = \mu_M / \sigma^2_M$

$\Sigma$ : Covariance of the assets (N x N)

$w$ : Equilibrium market weights (N x 1)

Additional variables

$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

  • Original paper suggests to use a small number, like $\tau = 1/T$
    • $T$ = number of periods of data used
    • Example: 5 years of data - > $T = 1/(5 \cdot 12) = 0.02$

How to represent views in $Q$ and $P$ :

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)$.

Equations for retrieving the posterior distribution on the returns as well as the covariance of the returns.

\begin{equation} \label{eq:blMu} \mu^{BL} = \pi + \tau \Sigma P^T[(P \tau \Sigma P^T) + \Omega]^{-1}[Q - P \pi] \end{equation}\begin{equation} \label{eq:blSigma} \Sigma^{BL} = \Sigma + \tau \Sigma - \tau\Sigma P^T(P \tau \Sigma P^T + \Omega)^{-1} P \tau \Sigma \end{equation}

Calculating implied returns from a Black-Litterman model

For the examples in this notebook, use data from 1980 to 2015 for the following 5 industries:

  • Beer
  • Hlth
  • Fin
  • Softw
  • Clths

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
In [1]:
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
In [2]:
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
Out[2]:
Beer     0.013814
Hlth     0.020526
Fin      0.032345
Softw    0.029059
Clths    0.023959
Name: Implied Returns, dtype: float64

Inserting active views to a Black-Litterman model

Use $\Omega = diag(P (\tau \Sigma) P^T)$ in the Black-Litterman equations

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)

Black-Litterman equations

\begin{equation} \label{eq:blMu} \mu^{BL} = \pi + \tau \Sigma P^T[(P \tau \Sigma P^T) + \Omega]^{-1}[Q - P \pi] \end{equation}\begin{equation} \label{eq:blSigma} \Sigma^{BL} = \Sigma + \tau \Sigma - \tau\Sigma P^T(P \tau \Sigma P^T + \Omega)^{-1} P \tau \Sigma \end{equation}
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)

Insert the active view that Softw will outperform Fin and Clths by 1%

What will be the entry in the P matrix for Fin? For Softw?

In [3]:
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)
Entries for the P matrix:
    Beer  Hlth       Fin  Softw     Clths
0   0.0   0.0 -0.890879    1.0 -0.109121

What will the new expected returns be?

In [4]:
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
Out[4]:
Beer     0.013861
Hlth     0.020609
Fin      0.031688
Softw    0.034657
Clths    0.023933
dtype: float64

Calculate the weights in the Max Sharpe Ratio (MSR) portfolio given these new expected returns

In [5]:
pd.DataFrame({
    'Weight':erk.msr(riskfree_rate=0.005, er=bl_mu, cov=bl_sigma)
    },
    index=industries
)
Out[5]:
Weight
Beer 7.757919e-19
Hlth 2.033586e-03
Fin 7.122574e-01
Softw 2.531547e-01
Clths 3.255433e-02

Insert a different view to a Black-Litterman model

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

In [6]:
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)
P:
    Beer  Hlth       Fin  Softw     Clths
0   0.0   0.0 -0.890879    1.0 -0.109121
In [7]:
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
New expected returns:
Out[7]:
Beer     0.013973
Hlth     0.020810
Fin      0.030095
Softw    0.048231
Clths    0.023871
dtype: float64
In [8]:
print('New MSR weights')
pd.DataFrame({
    'Weight':erk.msr(riskfree_rate=0.005, er=bl_mu, cov=bl_sigma)
    },
    index=industries
)
New MSR weights
Out[8]:
Weight
Beer 4.034580e-18
Hlth 0.000000e+00
Fin 1.573940e-01
Softw 8.426060e-01
Clths 0.000000e+00