Compare two ways to build a portfolio of ten S&P 500 sector ETFs, with the same data and the same constraint.
A Python implementation comparing Markowitz mean-variance optimisation against the Black-Litterman model on ten S&P 500 sector ETFs, with a walk-forward backtest from October 2018 through December 2025. The Black-Litterman portfolio earns a higher Sharpe ratio (0.86 vs 0.64) with a 41% smaller maximum drawdown and 4× lower portfolio concentration than naive Markowitz.
Live dashboard: stable-sharpe.streamlit.app
The top chart is what mean-variance optimisation does in practice: it concentrates on whichever sector had the highest in-sample mean return, then jumps elsewhere as soon as the leader changes. The bottom chart is Black-Litterman with the same covariance and the same long-only constraint, anchored to market-cap implied returns and tilted with a 6-month momentum view.
Markowitz (1952) gives a tidy quadratic program for the optimal mean-variance portfolio, but the inputs (expected returns and the covariance matrix) are estimated. Sample means in particular are dominated by noise. Tiny perturbations to the expected return vector flip the optimal portfolio across the simplex, a phenomenon known as Markowitz's "error maximiser" property.
Black & Litterman (1990) sidestep this by replacing historical means with market-implied equilibrium returns and updating them through a Bayesian posterior whenever the manager has a view. The prior is grounded in observed market-cap weights; the posterior is the Gaussian product of prior and view. Out of sample, the resulting portfolios are diversified, smooth, and far less sensitive to input noise.
This project codes both approaches end to end, runs a 7-year walk-forward backtest on real data, and compares them on Sharpe, Sortino, drawdown, turnover, and Herfindahl concentration.
Ledoit-Wolf shrinkage covariance. Sample covariance is unbiased but with N similar to T the off-diagonals are sampling noise. We shrink toward a structured target,
with
Implied equilibrium returns. Reverse-optimise the consensus portfolio:
Here
View. A single relative momentum view says the top three sectors by trailing 6-month return outperform the bottom three by 2% annualised:
The pick matrix
Posterior. The Black-Litterman master formula is the Gaussian product of prior and view:
Both
- Universe: 10 SPDR sector ETFs (XLK, XLF, XLV, XLY, XLI, XLP, XLE, XLU, XLRE, XLB). Benchmark: SPY.
- Window: October 2015 through December 2025 (daily adjusted close from Yahoo Finance).
- Walk-forward: 3-year rolling training window, monthly rebalance, no overlap. 86 out-of-sample months.
- Strategies: equal weight, SPY, Markowitz with sample-mean expected returns, Black-Litterman with momentum view.
| Strategy | Ann. Return | Ann. Vol | Sharpe | Sortino | Max DD | Turnover | Concentration (HHI) |
|---|---|---|---|---|---|---|---|
| Equal Weight | 13.3% | 16.5% | 0.68 | 0.62 | -23.2% | 0.00 | 0.10 |
| S&P 500 (SPY) | 16.1% | 16.8% | 0.84 | 0.74 | -23.9% | n/a | n/a |
| Mean-Variance | 14.6% | 19.8% | 0.64 | 0.63 | -33.1% | 0.10 | 0.83 |
| Black-Litterman | 15.8% | 16.1% | 0.86 | 0.81 | -19.5% | 0.18 | 0.21 |
A few things to call out:
- Sharpe. Black-Litterman beats Markowitz by 22 Sharpe points and edges out SPY by 2. Markowitz is the worst risk-adjusted strategy of the four, despite having the highest in-sample mean returns plugged into it.
- Drawdown. Markowitz drew down 33%, holding technology as its largest position throughout the 2022 fall (XLK at 0.59 to 0.76 from June to August 2022,
results/weights_mvo.csv). It only rotated into energy from the January 2023 decision, after the drawdown. Black-Litterman drew down 19%, a 41% smaller max drawdown for a comparable return. - Concentration. The Herfindahl index (sum of squared weights) is 0.83 for Markowitz and 0.21 for Black-Litterman. Markowitz holds a median of one sector per month; Black-Litterman holds a median of nine.
- Turnover. Markowitz turnover looks low (10%) only because it parks in the same single sector for stretches at a time, then jumps when the in-sample mean leader changes. Black-Litterman's 18% reflects gradual rebalancing across a diversified set.
black-litterman-optimizer/
config.py ticker list, market weights, hyperparameters
data/fetch.py yfinance pull + parquet cache
models/covariance.py Ledoit-Wolf shrinkage
models/mean_variance.py SLSQP long-only Markowitz
models/black_litterman.py Pi = lambda * Sigma * w_mkt + posterior
views/momentum.py 6-month momentum -> P, Q, Omega
backtest/engine.py walk-forward loop
backtest/metrics.py Sharpe, Sortino, drawdown, turnover, HHI
notebooks/ one notebook stepping through the math
run_backtest.py entry point
results/ CSVs and summary JSON
figures/ PNGs used here
pip install -r requirements.txt
python run_backtest.pyThe first run pulls roughly 2,500 daily prices per ticker from Yahoo and caches them under data/cache/. Subsequent runs are local and finish in a few seconds. Results land in results/ (CSVs + JSON) and figures/ (the PNGs above).
Live at stable-sharpe.streamlit.app (headline KPIs, weight paths, equity and drawdown curves, and a monthly snapshot picker). To run locally instead:
streamlit run app.pyThe dashboard reads the pre-computed artefacts in results/ only. No Yahoo pulls happen on launch, so it starts in under a second.
- Markowitz, H. (1952). Portfolio Selection. Journal of Finance.
- Black, F. & Litterman, R. (1990). Asset Allocation: Combining Investor Views with Market Equilibrium. Goldman Sachs Fixed Income Research.
- He, G. & Litterman, R. (1999). The Intuition Behind Black-Litterman Model Portfolios. Goldman Sachs.
- Ledoit, O. & Wolf, M. (2004). Honey, I Shrunk the Sample Covariance Matrix. Journal of Portfolio Management.
- Idzorek, T. (2005). A Step-by-Step Guide to the Black-Litterman Model.





