Lesson 4.1.3 · 70 min
Black-Litterman: from market equilibrium to views
Black-Litterman replaces the sample mean, the most fragile input of Markowitz, with the returns that make the market portfolio optimal, and only departs from them where the manager has an opinion and to the extent of their confidence. This lesson builds it from scratch, checks it against PyPortfolioOpt and tests it with six views whose truth is published: three correct and three wrong, one of them from a machine learning model.
- Module
- 4.1 · Portfolio theory and optimisation
- Duration
- 70 min
- Prerequisites
- 4.1.1 · Markowitz: efficient frontier and real constraints, 4.1.2 · Covariance estimation: shrinkage, factor models and denoising
- Author
- Jesús Cuesta
- Updated
Level 4 lessons
4.1 · Portfolio theory and optimisation
- 4.1.1 Markowitz: efficient frontier and real constraints
- 4.1.2 Covariance estimation: shrinkage, factor models and denoising
- 4.1.3 Black-Litterman: from market equilibrium to views
- 4.1.4 Risk parity, minimum variance, HRP and HERC
- 4.1.5 Optimization with CVaR, drawdown, turnover and costs
- 4.1.6 Factor models: CAPM, Fama-French, PCA and attribution
- 4.1.7 Rebalancing and strategy portfolios
- 4.1.8 Portfolio tools: Riskfolio-Lib, PyPortfolioOpt, skfolio and bt
4.2 · Risk management and position sizing
- 4.2.1 Risk per trade, stops and risk of ruin
- 4.2.2 Kelly criterion and volatility targeting: how much to risk
- 4.2.3 VaR, Expected Shortfall and VaR backtesting
- 4.2.4 Stress testing and correlation, liquidity, gap and counterparty risk
- 4.2.5 Drawdown control and kill switch
- 4.2.6 Derivatives risk: portfolio Greeks, margin and liquidations
4.3 · Microstructure and execution
4.4 · Brokers and APIs
- 4.4.1 Choosing a broker from Spain or the EU: regulation, assets, costs and API
- 4.4.2 Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders
- 4.4.3 Alpaca, Tradier, MetaTrader 5 with Python and OANDA v20
- 4.4.4 Crypto exchange APIs: ccxt, Binance, Bybit, Kraken and Coinbase
- 4.4.5 Secure credentials: environment variables, secret managers and least privilege
4.5 · Live trading system
- 4.5.1 Live system architecture: from data to reconciliation
- 4.5.2 Order management: states, partial fills, cancellations and reconnections
- 4.5.3 Paper trading, shadow trading and slippage monitoring
- 4.5.4 Task scheduling and alerts: cron, APScheduler, Prefect, Telegram, Slack and email
- 4.5.5 Fault tolerance: idempotency, persistent state and reconciliation at startup
- 4.5.6 NautilusTrader or LEAN live: from backtest to production with the same code
The problem it solves
Lesson 4.1.1 left two results. The first, that the mean is the most fragile input of Markowitz: with 20 years, its standard error is around 5 points for an equity index. The second, that the optimizer amplifies that error: it looks for the estimated return differences and bets on them. It is enough to see what it does with the 20-year sample mean of the 38 assets of the universe, without constraints:
import numpy as np
import pandas as pd
precios = pd.read_parquet("universo4_precios.parquet")
precios = precios.set_index("fecha")
fac = pd.read_parquet("universo4_factores_diarios.parquet")
r = precios.pct_change()
r.iloc[0] = precios.iloc[0] / 100 - 1
exceso = r.sub(fac.set_index("fecha")["rf"], axis=0)
activos = list(exceso.columns)
info = pd.read_csv("universo4_activos.csv", index_col="activo")
verdad = pd.read_csv("universo4_verdad.csv", index_col="activo")
cov_v = pd.read_csv("universo4_cov_verdad.csv")
S = cov_v[cov_v["regimen"] == "total"].set_index("activo")
S = S.loc[activos, activos].to_numpy()
w_mkt = info.loc[activos, "peso_mercado"].to_numpy()
mu_v = verdad.loc[activos, "mu_verdadero"].to_numpy()
mu_m = exceso.mean().to_numpy() * 252
delta = 2.5
w_m = np.linalg.solve(delta * S, mu_m)
print(f"gross leverage: {np.abs(w_m).sum():.0f} times")
print((100 * pd.Series(w_m, index=activos)).round(0)
.sort_values().iloc[[0, 1, -2, -1]])gross leverage: 138 times
MONET -2077.0
RF_T710 -1991.0
RF_CIG 1214.0
RF_T37 2395.0
dtype: float64To isolate the problem of the mean, the lesson uses the overall true covariance of the universe (lesson 4.1.2 dealt with the covariance; exercise 1 repeats the calculation with the sample one). Even with the exact covariance, the optimal portfolio for a risk aversion of 2.5 has gross leverage of 138 times: it sells 2,077% of the money market and 1,991% of the 7-10 year bond to buy 2,395% of the 3-7 year bond, almost identical assets whose sample means differ by tenths. No manager would do it. Black and Litterman (1992), at Goldman Sachs, proposed another starting point.
Equilibrium: reverse optimization
The first-order condition of a mean-variance investor with risk aversion is . If the weights are known and the mean is not, you solve it the other way round:
are the expected excess returns that would make the market portfolio optimal. If every investor were mean-variance and had the same information, the market portfolio would be the tangency portfolio (the CAPM) and would be the truth. The risk aversion can be calibrated with the market Sharpe: .
Mermaid source
flowchart TB W[Market weights] --> PI["π = δΣw"] S[Covariance Σ] --> PI V["Views P, Q"] --> BL[Black-Litterman] O["Confidence Ω"] --> BL PI --> BL BL --> M[Posterior mean] M --> OP[Optimizer]
The universe publishes the weights of the market portfolio (peso_mercado in universo4_activos.csv: the 18
non-overlapping blocks, by capitalization as of 2025) and its own with :
pi = delta * S @ w_mkt
pub = verdad.loc[activos, "pi_equilibrio"].to_numpy()
print("same as the published one:", np.allclose(pi, pub, atol=1e-4))
mu_mkt, vol_mkt = w_mkt @ mu_v, np.sqrt(w_mkt @ S @ w_mkt)
print(f"market: excess {mu_mkt:.2%}, vol {vol_mkt:.2%}, "
f"Sharpe {mu_mkt / vol_mkt:.2f}")
print(f"implied δ with the truth: {mu_mkt / vol_mkt**2:.2f}")
r_mkt = exceso.to_numpy() @ w_mkt
print(f"implied δ with the sample: "
f"{r_mkt.mean() / r_mkt.var():.2f}")
w_tan = np.linalg.solve(S, mu_v)
print("Sharpe of the true tangency portfolio:",
(w_tan @ mu_v / np.sqrt(w_tan @ S @ w_tan)).round(2))same as the published one: True
market: excess 5.77%, vol 12.95%, Sharpe 0.45
implied δ with the truth: 3.44
implied δ with the sample: 2.94
Sharpe of the true tangency portfolio: 1.35The implied by the data (2.94 with the sample, 3.44 with the truth) is somewhat higher than the 2.5 with which is published in the data. Changing scales the whole of equally; what matters is its shape. And the shape has two features of this universe that are worth seeing before going on:
ventana = exceso.loc["2021":"2025"].mean().to_numpy() * 252
def ecm(x):
return np.sqrt(np.mean((x - mu_v) ** 2))
print(f"root mean squared error against the truth: "
f"equilibrium {ecm(pi):.2%}, 20-year sample {ecm(mu_m):.2%}, "
f"2021-2025 sample {ecm(ventana):.2%}")
sel = ["RV_EEUU", "RV_EMG", "RF_T20", "RF_CIG", "MP_ORO", "MP_ENE"]
tabla = pd.DataFrame({"equilibrio": pi, "verdad": mu_v,
"muestra_20": mu_m, "muestra_5": ventana},
index=activos).loc[sel]
print((100 * tabla).round(2))root mean squared error against the truth: equilibrium 3.45%, 20-year sample 1.71%, 2021-2025 sample 6.32%
equilibrio verdad muestra_20 muestra_5
RV_EEUU 6.46 9.62 8.79 10.51
RV_EMG 7.96 3.80 5.46 7.22
RF_T20 -0.52 3.68 1.54 -11.60
RF_CIG 0.41 2.76 2.13 -2.06
MP_ORO 0.77 9.98 10.03 13.72
MP_ENE 3.32 -4.62 -5.46 10.38The equilibrium gives almost zero or negative premiums to Treasury bonds (−0.52% to the 20+ year bond, against a true 3.68%) because in the calm, the dominant regime, bonds are negatively correlated with stocks: in a market portfolio dominated by stocks, they hedge, and what hedges “deserves” a low premium. And it gives positive premiums to commodities (3.32% to energy), which lose in the script of these 20 years (−4.62%). Gold is the extreme case: equilibrium 0.77%, truth 9.98%. In this universe the market portfolio is not efficient: its true Sharpe is 0.45, and that of the true unconstrained tangency portfolio, 1.35.
In root mean squared error, the equilibrium (3.45 points) does worse than the 20-year sample mean (1.71 points) and better than the five-year one (6.32). The 20-year sample is unusually good: the generator seeds were chosen so that the realized means would end up close to the true ones. The normal case, and what a manager looking at five years has, looks more like the last column. The advantage of the equilibrium is not being more accurate than a good sample, but not producing absurd portfolios: a consistent with the market portfolio leads to the market portfolio.
Views: P, Q and Ω
A view is a statement about the expected return of a portfolio. Absolute views are about one asset (“emerging markets return 5% a year in excess”); relative ones, about a difference (“momentum beats value by 3%”). With views they are written as
with a matrix (one row per view, with the weights of its portfolio), the returns they claim and
their uncertainty, usually diagonal. The file universo4_visiones.csv contains six, with the answer known:
verdad is with the true mean and correcta flags the ones that are within one point of it.
vis = pd.read_csv("universo4_visiones.csv")
P = np.zeros((len(vis), len(activos)))
for i, fila in enumerate(vis["activos"]):
for parte in fila.split(";"):
activo, peso = parte.split(":")
P[i, activos.index(activo)] = float(peso)
Q = vis["q"].to_numpy()
conf = vis["confianza"].to_numpy()
print(vis[["vision", "q", "confianza", "pi_equilibrio", "verdad",
"correcta"]].round(4).to_string(index=False))
print("P·π and P·μ match the file:",
np.allclose(P @ pi, vis["pi_equilibrio"], atol=1e-4),
np.allclose(P @ mu_v, vis["verdad"], atol=1e-4))vision q confianza pi_equilibrio verdad correcta
V1 0.05 0.50 0.0796 0.0380 False
V2 0.02 0.40 -0.0055 -0.0330 False
V3 0.03 0.60 0.0027 0.0328 True
V4 0.02 0.30 0.0206 0.0220 True
V5 -0.01 0.50 -0.0200 -0.0173 True
V6 0.08 0.25 0.0077 0.0998 False
P·π and P·μ match the file: True TrueViews V3, V4 and V5 are correct; V1, V2 and V6 are not. V6 comes “from an ML model” and claims that gold returns 8% a year in excess: it is incorrect by the one-point criterion (the truth is 9.98%), but it is much closer to the truth than the equilibrium. That difference is going to matter.
The Black-Litterman posterior
Black-Litterman treats as a prior on the true mean, , with a small scalar (the uncertainty of the mean is much smaller than that of returns), and combines it with the views as in a Bayesian regression. The posterior mean is
The last factor is the “surprise” of each view against the equilibrium, . It is spread across all assets according to their covariances with the view portfolios, so a view on emerging markets also moves the mean of the other stock markets and of emerging debt.
is still missing. Idzorek proposed setting it with a more intuitive confidence between 0 and 1: with 100% confidence the view holds exactly; with 0%, it is ignored; with , the portfolio weights travel a fraction of the way between the market portfolio and the full-confidence one. His original method finds each numerically; Walters, in his review of the model, showed that without constraints the solution is closed-form:
tau = 0.05
def omega_idzorek(P, conf):
return np.diag([tau * (1 - c) / c * p @ S @ p
for p, c in zip(P, conf)])
def black_litterman(P, Q, Om):
tS = tau * S
sorpresa = np.linalg.solve(P @ tS @ P.T + Om, Q - P @ pi)
return pi + tS @ P.T @ sorpresa
Om = omega_idzorek(P, conf)
mu_bl = black_litterman(P, Q, Om)
from pypfopt.black_litterman import BlackLittermanModel
blm = BlackLittermanModel(
pd.DataFrame(S, index=activos, columns=activos),
pi=pd.Series(pi, index=activos), P=P, Q=Q, tau=tau,
omega="idzorek", view_confidences=conf)
print("same as PyPortfolioOpt:",
np.allclose(blm.bl_returns().to_numpy(), mu_bl, atol=1e-12))
post = pd.DataFrame({"q": Q, "equilibrio": P @ pi,
"posterior": P @ mu_bl, "verdad": P @ mu_v},
index=vis["vision"])
print((100 * post).round(2))same as PyPortfolioOpt: True
q equilibrio posterior verdad
vision
V1 5.0 7.96 6.21 3.80
V2 2.0 -0.55 0.43 -3.30
V3 3.0 0.27 1.76 3.28
V4 2.0 2.06 1.46 2.20
V5 -1.0 -2.00 -1.36 -1.72
V6 8.0 0.77 2.46 9.98The figure at the top draws this table. The posterior of each view lies between the equilibrium and what the view claims, closer to the latter the higher the confidence and the lower the variance of its portfolio. V6, with 25% confidence, moves gold from 0.77% to 2.46%: less than the model says and very far from the truth. V4 moves from 2.06% to 1.46%, in the opposite direction to its own : views interact, because high yield credit is correlated with stocks and other views (V1, V2) move stocks.
That the closed form reproduces Idzorek's original method is checked by finding each the way he did: the one that leaves the unconstrained weights a fraction of the way.
from scipy.optimize import minimize_scalar
def pesos(m):
return np.linalg.solve(delta * S, m)
for k in range(len(Q)):
p, q = P[[k]], Q[[k]]
w_100 = pesos(black_litterman(p, q, np.array([[1e-12]])))
objetivo = w_mkt + conf[k] * (w_100 - w_mkt)
def distancia(log_om):
m = black_litterman(p, q, np.array([[np.exp(log_om)]]))
return np.sum((pesos(m) - objetivo) ** 2)
res = minimize_scalar(distancia, bounds=(-20, 5),
method="bounded",
options={"xatol": 1e-10})
print(vis["vision"][k], f"{np.exp(res.x) / Om[k, k]:.6f}")V1 1.000000
V2 1.000000
V3 1.000000
V4 1.000000
V5 1.000000
V6 1.000000The ratio between the numerical and the closed-form one is 1 for all six views. With constraints (long only, for example) there is no longer a closed form and the numerical method is the only correct one.
Does each view help?
The practical question is not whether a view is correct, but whether the portfolio improves with it. For each view on its own, the posterior mean is computed, then the long-only maximum Sharpe portfolio (the same function as in lesson 4.1.1) and its Sharpe with the true mean and covariance:
import cvxpy as cp
def max_sharpe(m):
y = cp.Variable(len(m))
pr = cp.Problem(cp.Minimize(cp.quad_form(y, cp.psd_wrap(S))),
[m @ y == 1, y >= 0])
pr.solve(solver="CLARABEL")
return y.value / y.value.sum()
def sharpe(w):
return w @ mu_v / np.sqrt(w @ S @ w)
filas = []
for k in range(len(Q)):
m = black_litterman(P[[k]], Q[[k]], Om[[k]][:, [k]])
filas.append({
"vision": vis["vision"][k],
"correcta": vis["correcta"][k],
"error_equilibrio": abs(P[k] @ pi - P[k] @ mu_v),
"error_q": abs(Q[k] - P[k] @ mu_v),
"error_posterior": abs(P[k] @ m - P[k] @ mu_v),
"sharpe": sharpe(max_sharpe(m))})
cada = pd.DataFrame(filas).set_index("vision")
print(cada.round(3))
print("Sharpe of the market portfolio:", sharpe(w_mkt).round(3)) correcta error_equilibrio error_q error_posterior sharpe
vision
V1 False 0.042 0.012 0.027 0.492
V2 False 0.028 0.053 0.038 0.357
V3 True 0.030 0.003 0.014 0.583
V4 True 0.001 0.002 0.002 0.446
V5 True 0.003 0.007 0.002 0.473
V6 False 0.092 0.020 0.074 0.627
Sharpe of the market portfolio: 0.446The table separates two ideas that are often confused:
- V1 and V6, “wrong”, improve the portfolio. Their miss by more than a point, but the equilibrium was much further away: 4.2 points for emerging markets and 9.2 for gold. The gold view, with only 25% confidence, raises the Sharpe from 0.45 to 0.63.
- V4 and V5, “correct”, change almost nothing. The equilibrium was already 0.1 and 0.3 points from the truth; the view adds no information and the Sharpe stays at 0.45 and 0.47.
- V2 is the only harmful one. It claims that hedged Europe beats the US by 2%, when the truth is −3.30% and the equilibrium said −0.55%: it pulls the mean away from the truth and the Sharpe falls to 0.36, below the market.
- V3, correct and far from the equilibrium, is the textbook case: 3.0 points of error in the equilibrium, 0.3 in the view, and the Sharpe rises to 0.58.
A view is worth as much as it is closer to the truth than the equilibrium. That is why it is not enough to count the hits of an analyst or a model: you have to measure whether they are right more often than the market.
The Black-Litterman portfolio
He and Litterman (1999) showed a property that makes the model easy to explain to a committee: without constraints, the optimal portfolio with the posterior mean is the market portfolio plus a position in the portfolio of each view. Assets that do not appear in any view keep their market weight, even though their mean has changed:
w_bl = pesos(mu_bl)
dif = pd.Series(w_bl - w_mkt, index=activos)
en_vistas = np.abs(P).sum(axis=0) > 0
print("assets with change > 0.01%:", list(dif[dif.abs() > 1e-4].index))
cambian = set(dif[dif.abs() > 1e-4].index)
print("all in some view:",
cambian <= set(np.array(activos)[en_vistas]))
print((100 * dif[dif.abs() > 1e-4]).round(1))assets with change > 0.01%: ['RV_EEUU', 'RV_EUR_C', 'RV_EMG', 'ES_VAL', 'ES_MOM', 'ES_MIN', 'RF_CIG', 'RF_CHY', 'MP_ORO']
all in some view: True
RV_EEUU -96.4
RV_EUR_C 71.8
RV_EMG -6.9
ES_VAL -55.7
ES_MOM 55.7
ES_MIN 24.7
RF_CIG -10.3
RF_CHY 10.3
MP_ORO 34.7
dtype: float64The unconstrained portfolio holds short positions in the US (its weight drops 96 points, because of V2 and V5), in value (V3) and in investment grade (V4): these are the portfolios of the relative views, which are long-short by definition. A long-only fund applies the optimizer with the posterior mean and its constraints. The final comparison, with the Sharpe evaluated with the truth:
grupos = {"market": None, "correct only": [2, 3, 4],
"incorrect only": [0, 1, 5], "all six": list(range(6))}
res = {}
for nombre, idx in grupos.items():
if idx is None:
res[nombre] = sharpe(w_mkt)
continue
m = black_litterman(P[idx], Q[idx], Om[np.ix_(idx, idx)])
res[nombre] = sharpe(max_sharpe(m))
res["2021-2025 mean"] = sharpe(max_sharpe(ventana))
res["20-year mean"] = sharpe(max_sharpe(mu_m))
print(pd.Series(res).round(3))market 0.446
correct only 0.595
incorrect only 0.597
all six 0.700
2021-2025 mean 0.668
20-year mean 0.901
dtype: float64With the six views, the true Sharpe goes from 0.45 (market) to 0.70. The three correct ones alone give 0.60; the three incorrect ones, another 0.60, because of V1 and V6. The mean of the last five years gives 0.67 with a portfolio chosen by the chance of the window (lesson 4.1.1), and the 20-year one, 0.90: with data as good as this generator's, the sample mean with long-only constraints wins. Black-Litterman does not promise the best portfolio; it promises to start from a reasonable one and to depart from it only where something is known.
Views from machine learning models
An ML model (those of level 5 of the course) produces predictions, and a prediction can be turned into a view: is the prediction and , the uncertainty of that prediction. The typical mistake is to take the confidence from the fit inside the training sample. The right thing is to measure it out of sample and against the prior: if the model does not predict better than the equilibrium, its view should not move anything.
Mermaid source
flowchart TB D[Data up to t] --> M[Model] M --> Q["Prediction Q"] M --> E[Out-of-sample errors] E --> O["Ω"] Q --> BL[Black-Litterman] O --> BL
A simple example with gold: a “model” that predicts next year's excess return with the mean of the previous three, evaluated year by year from 2009 to 2025 (each prediction uses only past data). Its squared error is compared with that of the equilibrium, and what the model gains over the noise of the annual return itself is what justifies :
oro = exceso["MP_ORO"]
anual = (1 + oro).groupby(oro.index.year).prod() - 1
pred = {a: oro.loc[str(a - 3):str(a - 1)].mean() * 252
for a in range(2009, 2026)}
pred = pd.Series(pred)
real = anual.loc[pred.index]
i_oro = activos.index("MP_ORO")
ecm_mod = np.mean((pred - real) ** 2)
ecm_eq = np.mean((pi[i_oro] - real) ** 2)
print(f"squared error: model {np.sqrt(ecm_mod):.1%}, "
f"equilibrium {np.sqrt(ecm_eq):.1%}, "
f"out-of-sample R² {1 - ecm_mod / ecm_eq:.3f}")
ruido = S[i_oro, i_oro] # variance of one year of gold
omega_ml = max(ecm_mod - ruido, 1e-6) # model error on the mean
p6 = P[5]
c_ml = 1 / (1 + omega_ml / (tau * p6 @ S @ p6))
print(f"model ω {omega_ml:.4f} against the published {Om[5, 5]:.4f}")
print(f"implied confidence: {c_ml:.1%} (published {conf[5]:.0%})")squared error: model 20.8%, equilibrium 21.5%, out-of-sample R² 0.068
model ω 0.0218 against the published 0.0032
implied confidence: 4.7% (published 25%)The model beats the equilibrium by very little (an out-of-sample R² of 0.07, with 17 years, indistinguishable from zero). Its squared error is double the variance of one year of gold: half of the error is noise of the annual return itself, which no model can remove, and the other half, , is model error on the mean, almost seven times the published . The confidence that justifies is about 5%, not 25%. In this universe, view V6 is close to the truth, but a manager could not know that with this track record: its confidence has to come from the data and not from the expected result.
Four precautions with ML views:
- Truly out of sample: predictions made with data prior to each date, with the purging of lesson 2.5.3 if the labels overlap.
- Against the prior, not against zero: a model that predicts “stocks go up” is right more often than zero, but not more often than the equilibrium.
- Do not count twice: if the model uses the same data that estimates or the mean, the view repeats information the prior already has.
- Consistent horizon: and in annual excess terms, like ; a one-month prediction is annualized, and so is its error.
Typical mistakes
- Mixing total and excess returns between (in excess) and .
- Badly written relative views: the row of for “A beats B” is in A and in B; with several assets per side, the weights of each side sum to 1 and −1.
- Confusing τ with confidence: with the closed-form Idzorek, τ does not change the posterior mean (exercise 3); it does change the posterior covariance.
- Market weights that are not market weights: inherits whatever is put in . With a universe that includes sectors and styles that overlap with the index, only the non-overlapping blocks should be used, as the file does.
- Believing the posterior is the truth: it is a compromise between two sources with error, and in this universe the equilibrium has large errors in bonds and commodities.
Exercises
- Repeat the equilibrium and the posterior with the 20-year sample covariance instead of the true one. How much do and the posterior of each view change?
Solution
S_m = exceso.cov().to_numpy() * 252
pi_m = delta * S_m @ w_mkt
print(f"maximum difference in π: {np.abs(pi_m - pi).max():.2%}")
tS_m = tau * S_m
Om_m = np.diag([tau * (1 - c) / c * p @ S_m @ p
for p, c in zip(P, conf)])
mu_bl_m = pi_m + tS_m @ P.T @ np.linalg.solve(
P @ tS_m @ P.T + Om_m, Q - P @ pi_m)
print((100 * pd.DataFrame({"verdadera": P @ mu_bl,
"muestral": P @ mu_bl_m},
index=vis["vision"])).round(2))maximum difference in π: 0.33%
verdadera muestral
vision
V1 6.21 6.44
V2 0.43 0.43
V3 1.76 1.83
V4 1.46 1.43
V5 -1.36 -1.37
V6 2.46 2.45With 20 years, the sample covariance is very close to the true one (lesson 4.1.2), and changes by at most a third of a point. The posteriors move by a few tenths. Black-Litterman is not very sensitive to covariance error and very sensitive to the views: the model's risk lies in and in the confidence, not in .
- Remove view V2 from the six. How much does the true Sharpe of the long-only portfolio improve? Could a manager have known in advance that it was the view to remove?
Solution
idx = [0, 2, 3, 4, 5]
m = black_litterman(P[idx], Q[idx], Om[np.ix_(idx, idx)])
print("without V2:", sharpe(max_sharpe(m)).round(3),
"| all six:", res["all six"].round(3))without V2: 0.798 | all six: 0.7Without V2, the Sharpe rises. But that is only known because the truth is published. With real data, V2 was as defensible as the others. What can be done is to keep a record of the views and of their out-of-sample result against the equilibrium, and to adjust the confidence of each source (analyst or model) with that track record.
- Check that, with the closed-form Idzorek Ω, the posterior mean does not change if τ goes from 0.05 to 0.5. Explain why.
Solution
def bl_tau(t):
Om_t = np.diag([t * (1 - c) / c * p @ S @ p
for p, c in zip(P, conf)])
tS = t * S
return pi + tS @ P.T @ np.linalg.solve(P @ tS @ P.T + Om_t,
Q - P @ pi)
print("same mean:", np.allclose(bl_tau(0.05), bl_tau(0.5)))same mean: TrueIn the formula, the posterior mean depends on (the weight of the prior) against (that of the views). If is proportional to , as in the closed-form Idzorek, both grow together and cancels out. It matters when is set from outside, as in the ML section, where it comes from the model's error: there a larger gives more weight to the view.
Each answer is folded under its question
Self-assessment
What expected returns does the Black-Litterman reverse optimization return?
- AThe historical means of each asset
- BThose that would make the market portfolio optimal for a mean-variance investor with risk aversion δ: π = δΣw_market
- CThose of the CAPM with each asset's beta against the S&P 500
- DZero returns for every asset
Show answer
Answer B. Those that would make the market portfolio optimal for a mean-variance investor with risk aversion δ: π = δΣw_market
You start from Markowitz's first-order condition (the optimal weights are the inverse of δΣ times μ) and solve for μ with the market weights. If the market were mean-variance efficient, π would be the true expected returns; it is an assumption, not a fact.
View V4 (high yield beats investment grade by 2%) is correct according to the data's criterion and V1 (emerging markets return 5%) is not. Yet, on its own, V1 improves the portfolio and V4 does not. Why?
- ABy chance
- BBecause V1 has more confidence
- CBecause absolute views weigh more than relative ones
- DBecause what matters is whether the view is closer to the truth than the equilibrium: for V4 the equilibrium already said 2.06% (truth 2.21%), and for V1 it said 7.96% against a truth of 3.81%, so 5% gets closer
Show answer
Answer D. Because what matters is whether the view is closer to the truth than the equilibrium: for V4 the equilibrium already said 2.06% (truth 2.21%), and for V1 it said 7.96% against a truth of 3.81%, so 5% gets closer
Black-Litterman moves the posterior from π toward Q. If π was already right, a correct view adds nothing; if π was far off, an imperfect view in the right direction helps. V2 is the only one of the six that makes things worse: it claims +2% where the truth is −3.30% and the equilibrium, −0.55%.
With the Idzorek method in its closed form, the Ω of a view is τ·(1 − c)/c·pΣp′. What happens to the posterior mean if τ changes from 0.05 to 0.5?
- ANothing: τ appears in the weight of the prior and in Ω, and cancels out
- BIt moves closer to the views
- CIt moves closer to the equilibrium
- DIt becomes unstable
Show answer
Answer A. Nothing: τ appears in the weight of the prior and in Ω, and cancels out
The posterior depends on the ratio between the uncertainty of the prior (τΣ) and that of the views (Ω). If Ω is defined proportional to τ, the ratio does not change. τ does matter if Ω is set another way, for example from a model's error.
A view from an ML model predicts the return of gold. Where should its Ω come from?
- AFrom the model's R² on the training sample
- BFrom the confidence of the team that developed it
- CFrom the error of its out-of-sample predictions, compared with that of the equilibrium: if it does not beat it, the view should not move the portfolio
- DFrom the volatility of gold
Show answer
Answer C. From the error of its out-of-sample predictions, compared with that of the equilibrium: if it does not beat it, the view should not move the portfolio
The fit on the training sample is always optimistic. In the lesson, a simple model that beats the equilibrium by very little out of sample justifies a confidence of about 5%, far below the published 25%.
The unconstrained Black-Litterman portfolio differs from the market portfolio...
- AIn every asset, because the mean of every asset changes
- BOnly in the assets that appear in some view: it is the market portfolio plus a position in each view portfolio
- COnly in the assets with the largest market weight
- DNot at all, if the views are correct
Show answer
Answer B. Only in the assets that appear in some view: it is the market portfolio plus a position in each view portfolio
With the unconstrained optimal weights (the inverse of δΣ times μ_BL), the posterior mean changes in every correlated asset, but the inverse of Σ undoes that propagation and the weights only move in the rows of P (He and Litterman, 1999). It is one of the properties that make Black-Litterman intuitive.
Key points
- The equilibrium π = δΣw_market gives expected returns that are consistent with the market portfolio and stable. It is an assumption: in the course universe, the market portfolio is not efficient and π gets bonds and commodities wrong.
- Black-Litterman combines the prior N(π, τΣ) with views P·μ = Q + noise of variance Ω. The custom implementation matches PyPortfolioOpt, and the closed-form Idzorek confidence matches his original method.
- What decides whether a view helps is not whether it is correct, but whether it is closer to the truth than the equilibrium: V1 and V6, “wrong”, improve the portfolio; V4 and V5, “correct”, change almost nothing; only V2 makes it worse.
- With the six views, the long-only portfolio goes from a true Sharpe of 0.45 (market) to 0.70, with reasonable weights. With the unconstrained sample mean, the portfolio would need gross leverage of 138 times.
- An ML view is treated like any other: Q from its prediction and Ω from its out-of-sample error against the equilibrium. A model that barely beats it deserves a confidence close to zero.