Skip to content

How Black–Litterman Updates Returns With Investor Views

Original question

Can you explain the Black-Litterman model and show how investor views are incorporated into expected return estimates?

Black-Litterman starts from implied equilibrium excess returns and updates them with investor views weighted by confidence. A view can specify an absolute return or a spread between assets. The result is a revised vector of expected excess returns that can feed a portfolio optimiser.

Historical average returns can vary sharply with the observation window. Black-Litterman begins with a benchmark allocation and asks which expected excess returns would make that allocation optimal under the chosen risk model. You then express views in the dimensions where you have information. For instance, saying that equities will outperform bonds by six percentage points is a view on the equity-bond spread. You can be confident about the spread even when the level of each return is uncertain.
The update compares your stated spread with the spread already implied by the benchmark. A small difference produces a small update. A large difference with high confidence produces a larger tilt. Assets outside the view can also move when their expected returns are linked to that view by the prior covariance.

Write the prior and the view in compatible units

Let wmw_m be the benchmark weights, Σ\Sigma the asset return covariance, and δ\delta the risk aversion parameter. The unconstrained quadratic objective w⊤π−δ2w⊤Σww^\top\pi-\tfrac{\delta}{2}w^\top\Sigma w has first-order condition

π=δΣwm.\pi=\delta\Sigma w_m.

Use π\pi as the prior mean for unknown expected excess returns μ\mu. Model μ∼N(π,C)\mu\sim N(\pi,C) with C=τΣC=\tau\Sigma. Express the views as Q=Pμ+εQ=P\mu+\varepsilon, where ε∼N(0,Ω)\varepsilon\sim N(0,\Omega) is independent of the prior. Each row of PP defines the portfolio whose return the corresponding view describes. Conditioning the joint Gaussian gives

μBL=π+CP⊤(PCP⊤+Ω)−1(Q−Pπ).\mu_{\mathrm{BL}}=\pi+CP^\top(PCP^\top+\Omega)^{-1}(Q-P\pi).

The posterior covariance of the uncertain mean is C−CP⊤(PCP⊤+Ω)−1PCC-CP^\top(PCP^\top+\Omega)^{-1}PC. Under a model where next-period returns have conditional covariance Σ\Sigma given μ\mu, the predictive covariance is Σ\Sigma plus that posterior covariance. State which matrix your allocation process uses.

A relative view is a statement about a linear combination. A row [1,−1][1,-1] says “equity return minus bond return,” so the view input qq is a spread and its uncertainty is uncertainty about that spread. One can read the update as a prior estimate plus a scaled surprise q−Pπq-P\pi. If the prior already predicts the view, the surprise is zero. Increasing the view-error covariance makes the update smaller, with other inputs fixed. The covariance can still transmit the adjustment to assets that are not named in the view.
Take a 60% equity and 40% bond benchmark, δ=2.5\delta=2.5, equity variance 0.04, bond variance 0.01 and covariance 0.006. The implied excess returns are 6.6% and 1.9%, a spread of 4.7%. Express a 6.0% spread view with P=[1,−1]P=[1,-1], τ=0.05\tau=0.05 and Ω=[0.002]\Omega=[0.002] in squared-return units. The prior variance of the spread is 0.00190.0019, so the posterior spread moves about 0.0019/(0.0019+0.002)=48.7%0.0019/(0.0019+0.002)=48.7\% of the way from 4.7% to 6.0%. The posterior equity and bond returns are about 7.17% and 1.83%.

In the example the prior spread is 4.7% and the view is 6.0%, so the input to the update is a positive surprise of 1.3 percentage points. The updated spread is only part of the way to the view because the view is uncertain. If you increase its uncertainty while holding the prior fixed, the same 1.3-point surprise has less effect on the revised returns. Keep the view scale and prior covariance in compatible units. Treating 6.0 as a decimal return would overwhelm the calculation for a reason unrelated to confidence.

Compute the view update without explicit matrix inversion

import numpy as np cov = np.array([[0.04, 0.006], [0.006, 0.01]]) prior = 2.5 * (cov @ np.array([0.6, 0.4])) prior_mean_cov = 0.05 * cov view_exposure = np.array([[1.0, -1.0]]) view = np.array([0.06]) view_error_cov = np.array([[0.002]]) surprise = view - view_exposure @ prior view_cov = view_exposure @ prior_mean_cov @ view_exposure.T update = np.linalg.solve(view_cov + view_error_cov, surprise) posterior = prior + prior_mean_cov @ view_exposure.T @ update print(100 * posterior)

References and next steps