Portfolio & Risk·Portfolio Construction·Advanced

Black Litterman Model

Implement the Black-Litterman portfolio allocation model that elegantly combines market equilibrium implied returns with investor subjective views on specific assets, overcoming the extreme estimation error sensitivity and corner solution concentration problems of traditional unconstrained mean-variance optimization.

machine-learningportfolio-theoryrisk-management

Black-Litterman Portfolio Model — Portfolio Construction

Category: Portfolio | Subcategory: Construction


What This Notebook Does

Black-Litterman (BL) fixes a fundamental problem with classical MVO: garbage-in → garbage-out. Small errors in expected return estimates cause extreme, unstable portfolio weights.

BL blends two sources of information via Bayesian updating:

  • Prior: the market-implied equilibrium returns (from CAPM reverse optimisation)
  • Views: the investor's own forward-looking beliefs with confidence levels
E[R]_BL = [(τΣ)⁻¹ + P'Ω⁻¹P]⁻¹ × [(τΣ)⁻¹Π + P'Ω⁻¹Q]

where: Π = market-implied returns, Q = investor views,
       P = view pick matrix, Ω = view uncertainty

This notebook:

  1. Reverse-engineers market-implied equilibrium returns
  2. Encodes investor views with confidence levels
  3. Applies BL formula to blend prior and views
  4. Optimises with BL expected returns
  5. Compares BL vs unconstrained MVO
  6. Exports results

This section sets up the parameters and constants required for the Black-Litterman model, including data source toggle, tickers, date ranges, asset names, uncertainty scalar (TAU), market risk aversion coefficient (DELTA), and risk-free rate (RF).

[ ]
!pip install numpy pandas matplotlib seaborn scipy yfinance --quiet
[ ]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.optimize import minimize
import warnings

warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')
Imports ready.

Section 1 — Configuration

[ ]
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False
TICKERS       = ['BTC-USD', 'ETH-USD', 'SOL-USD', 'BNB-USD', 'AVAX-USD']
START_DATE    = '2022-01-01'
END_DATE      = '2024-12-31'
# ──────────────────────────────────────────────────
ASSETS  = ['BTC', 'ETH', 'SOL', 'BNB', 'AVAX']
TAU     = 0.05   # uncertainty scalar for prior; small value = more trust in equilibrium
DELTA   = 2.5    # market risk aversion coefficient
RF      = 0.04 / 252
print('Config ready.')
Config ready.

Section 2 — Data

This section handles data acquisition. It either downloads live cryptocurrency price data using yfinance or generates synthetic return data based on predefined volatilities, correlations, and expected returns. It then calculates the annualized covariance matrix (Sigma) and the number of assets (n_assets).

[ ]
if USE_LIVE_DATA:
    import yfinance as yf
    prices  = yf.download(TICKERS, start=START_DATE, end=END_DATE)['Close']
    prices.columns = ASSETS
    prices.dropna(inplace=True)
    returns = prices.pct_change().dropna()
    print(f'Live data: {len(returns)} days')
else:
    rng  = np.random.default_rng(42)
    n    = 730
    vols = np.array([0.65,0.75,1.20,0.70,1.10]) / np.sqrt(252)
    corr = np.array([[1,.85,.70,.75,.65],[.85,1,.75,.70,.68],
                      [.70,.75,1,.65,.72],[.75,.70,.65,1,.60],[.65,.68,.72,.60,1]])
    cov  = np.outer(vols,vols)*corr
    L    = np.linalg.cholesky(cov)
    mu   = np.array([0.50,0.45,0.80,0.35,0.70])/252
    data = rng.standard_normal((n,5)) @ L.T + mu
    idx  = pd.date_range('2022-01-01', periods=n, freq='B')
    returns = pd.DataFrame(data, columns=ASSETS, index=idx)
    print(f'Synthetic data: {len(returns)} days')

Sigma = returns.cov().values * 252  # annualised covariance
n_assets = len(ASSETS)
Synthetic data: 730 days

Section 3 — Market-Implied Equilibrium Returns

This section calculates the market-implied equilibrium returns (Π) using reverse optimization, based on the market risk aversion coefficient (DELTA), the covariance matrix (Sigma), and the market capitalization weights of the assets.

[ ]
# Market cap weights (approximated by market cap)
mkt_caps = np.array([1.0, 0.35, 0.08, 0.05, 0.04])  # relative sizes
w_mkt    = mkt_caps / mkt_caps.sum()

# Reverse-optimisation: Π = δ * Σ * w_mkt
Pi = DELTA * Sigma @ w_mkt
print('Market-implied equilibrium returns (annualised):')
for asset, pi in zip(ASSETS, Pi):
    print(f'  {asset}: {pi:.2%}')
Market-implied equilibrium returns (annualised):
  BTC: 108.03%
  ETH: 121.79%
  SOL: 155.07%
  BNB: 97.39%
  AVAX: 138.99%

Section 4 — Investor Views & BL Formula

This section defines the investor's views (absolute and relative) and their associated uncertainties. It then applies the Black-Litterman formula to combine these views with the market-implied equilibrium returns to derive the Black-Litterman expected returns (mu_bl).

[ ]
# Views:
# View 1: BTC will return 60% annually (absolute view)
# View 2: SOL will outperform BNB by 20% (relative view)
P = np.array([
    [1, 0, 0, 0, 0],   # View 1: BTC absolute
    [0, 0, 1, -1, 0],  # View 2: SOL vs BNB
])
Q = np.array([0.60, 0.20])  # view returns
# Omega: diagonal uncertainty proportional to view variance
omega_diag = TAU * np.diag(P @ Sigma @ P.T)
Omega = np.diag(omega_diag)

# Black-Litterman combined return
inv_tau_sigma = np.linalg.inv(TAU * Sigma)
inv_omega     = np.linalg.inv(Omega)
M_inv = inv_tau_sigma + P.T @ inv_omega @ P
M     = np.linalg.inv(M_inv)
mu_bl = M @ (inv_tau_sigma @ Pi + P.T @ inv_omega @ Q)

print('Black-Litterman expected returns vs equilibrium (annualised):')
for a, pi, bl in zip(ASSETS, Pi, mu_bl):
    print(f'  {a}: Equilibrium={pi:.2%}  BL={bl:.2%}  Δ={bl-pi:+.2%}')
Black-Litterman expected returns vs equilibrium (annualised):
  BTC: Equilibrium=108.03%  BL=82.46%  Δ=-25.57%
  ETH: Equilibrium=121.79%  BL=93.77%  Δ=-28.02%
  SOL: Equilibrium=155.07%  BL=112.29%  Δ=-42.78%
  BNB: Equilibrium=97.39%  BL=77.87%  Δ=-19.52%
  AVAX: Equilibrium=138.99%  BL=105.46%  Δ=-33.53%

Section 5 — Optimise BL Portfolio

This section optimizes the portfolio weights using the Black-Litterman expected returns and compares them to portfolio weights optimized solely based on equilibrium returns (without investor views). It uses the negative Sharpe ratio as the objective function for optimization.

[ ]
def neg_sharpe(w, mu_r, cov, rf):
    ret = w @ mu_r
    vol = np.sqrt(w @ cov @ w)
    return -(ret - rf * 252) / (vol + 1e-9)

w0   = np.ones(n_assets) / n_assets
cons = [{'type': 'eq', 'fun': lambda w: w.sum() - 1}]
bnds = [(0, 1)] * n_assets

# BL-based portfolio
res_bl  = minimize(neg_sharpe, w0, args=(mu_bl, Sigma, RF),
                    method='SLSQP', bounds=bnds, constraints=cons)
w_bl    = res_bl.x

# Equilibrium-based portfolio (no views)
res_eq  = minimize(neg_sharpe, w0, args=(Pi, Sigma, RF),
                    method='SLSQP', bounds=bnds, constraints=cons)
w_eq    = res_eq.x

fig, ax = plt.subplots(figsize=(10, 5))
x = np.arange(n_assets)
ax.bar(x - 0.2, w_eq * 100, 0.4, label='Equilibrium (no views)', color='#9e9e9e', alpha=0.8)
ax.bar(x + 0.2, w_bl * 100, 0.4, label='Black-Litterman', color='#1976d2', alpha=0.8)
ax.set_xticks(x); ax.set_xticklabels(ASSETS)
ax.set_ylabel('Weight (%)')
ax.legend(fontsize=9)
ax.set_title('Black-Litterman vs Equilibrium Portfolio Weights')
plt.tight_layout(); plt.show()
cell output

Section 6 — Export

This section exports the results of the analysis, including equilibrium returns, Black-Litterman returns, and the corresponding portfolio weights, into a CSV file named black_litterman_model.csv.

[ ]
out = pd.DataFrame({'Asset': ASSETS,
                     'Equilibrium_Ret': Pi.round(4),
                     'BL_Ret': mu_bl.round(4),
                     'Equil_Weight': w_eq.round(4),
                     'BL_Weight': w_bl.round(4)})
out.to_csv('black_litterman_model.csv', index=False)
print('Saved: black_litterman_model.csv')
Saved: black_litterman_model.csv

Conclusion

This notebook demonstrates the application of the Black-Litterman model to construct a portfolio by combining market-implied equilibrium returns with an investor's specific views. The resulting portfolio weights reflect a more informed approach, balancing market expectations with individual insights, which can lead to more robust and tailored investment strategies compared to traditional MVO methods.