Mean Variance Optimization
Implement classical Markowitz mean-variance portfolio optimization with full efficient frontier construction, maximum Sharpe ratio and minimum variance tangency portfolio identification, and detailed sensitivity analysis to input expected return and covariance matrix estimation errors.
Markowitz Mean-Variance Optimization — Portfolio
Category: Portfolio | Subcategory: Construction
What This Notebook Does
Markowitz Mean-Variance Optimization (MVO) is the foundational framework of modern portfolio theory (MPT). It finds portfolios that maximize expected return for a given level of risk (or minimize risk for a given return) — the Efficient Frontier.
Maximize: w'μ - (λ/2) w'Σw
Subject to: Σwᵢ = 1, wᵢ ≥ 0
where: w = portfolio weights, μ = expected returns, Σ = covariance matrix
Key portfolios on the efficient frontier:
- Minimum Variance Portfolio (MVP): lowest risk on the frontier
- Maximum Sharpe Portfolio (MSP): best risk-adjusted return (tangency portfolio)
- Equal Weight Portfolio: naive baseline
This notebook:
- Estimates expected returns and covariance matrix from historical data
- Traces the efficient frontier using constrained optimization
- Identifies the Maximum Sharpe and Minimum Variance portfolios
- Applies shrinkage estimators for more stable covariance
- Backtests MVO vs equal-weight portfolio
- Visualises the efficient frontier with risk/return scatter
- Exports optimal weights
!pip install numpy pandas matplotlib seaborn scipy scikit-learn --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.optimize import minimize
from sklearn.covariance import LedoitWolf
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Section 1 — Configuration
This section defines the key parameters and configurations used throughout the notebook, such as the risk-free rate, number of points on the efficient frontier, historical lookback window for data estimation, and the assets included in the portfolio.
RISK_FREE_RATE = 0.04 / 252 # daily risk-free rate
N_FRONTIER = 100 # number of points on efficient frontier
LOOKBACK_DAYS = 365 # estimation window
SIMULATION_DAYS = 730
ASSETS = ['BTC', 'ETH', 'SOL', 'BNB', 'AVAX', 'MATIC']
print('Config ready.')Config ready.
Section 2 — Data
generate_crypto_portfolio_data Function
This function creates a synthetic dataset of daily log returns for a given list of crypto assets. It simulates correlated asset returns based on predefined expected annual returns, volatilities, and a correlation matrix. This allows for reproducible and controllable testing of portfolio optimization strategies.
def generate_crypto_portfolio_data(assets, n_days=730, seed=42):
"""
Generate correlated crypto asset returns for portfolio optimization.
Returns
-------
pd.DataFrame Daily log returns for each asset.
"""
rng = np.random.default_rng(seed)
n = len(assets)
# True expected daily returns (annualised: BTC=50%, ETH=45%, etc.)
mu_annual = np.array([0.50, 0.45, 0.80, 0.35, 0.70, 0.60]) / 252
mu_annual = mu_annual[:n]
# Correlation matrix (crypto assets are highly correlated)
corr = np.full((n, n), 0.70)
np.fill_diagonal(corr, 1.0)
corr[0, 1] = corr[1, 0] = 0.85 # BTC-ETH more correlated
# Volatilities (annualised)
vols = np.array([0.65, 0.75, 1.20, 0.70, 1.10, 0.90])[:n] / np.sqrt(252)
cov = np.outer(vols, vols) * corr
# Simulate returns
L = np.linalg.cholesky(cov)
z = rng.standard_normal((n_days, n))
rets = (z @ L.T) + mu_annual
idx = pd.date_range('2023-01-01', periods=n_days, freq='D')
return pd.DataFrame(rets, columns=assets[:n], index=idx)
returns = generate_crypto_portfolio_data(ASSETS, SIMULATION_DAYS)
# Use first LOOKBACK_DAYS for estimation, rest for backtest
est_rets = returns.iloc[:LOOKBACK_DAYS]
test_rets = returns.iloc[LOOKBACK_DAYS:]
print(f'Estimation period: {len(est_rets)} days | Test period: {len(test_rets)} days')
print('\nAnnualised returns:')
print((est_rets.mean() * 252 * 100).round(1).to_string())Estimation period: 365 days | Test period: 365 days Annualised returns: BTC -34.1 ETH -71.8 SOL -126.8 BNB -46.8 AVAX -105.0 MATIC -125.1
This section is responsible for generating and preparing the historical data used for portfolio optimization. It creates a simulated dataset of crypto asset returns and then splits this data into an estimation period (for calculating expected returns and covariance) and a test period (for backtesting).
Key steps include:
- Generating synthetic daily log returns for the specified crypto assets.
- Dividing the data into an
estimation period(used for calculating portfolio parameters) and atest period(used for evaluating the performance of the optimized portfolios). - Displaying the annualised returns for each asset during the estimation period to provide an initial understanding of the asset performance.
Section 3 — Portfolio Metrics & Optimization
This section focuses on calculating key portfolio metrics and performing the core optimization to find optimal portfolio weights. It leverages a shrinkage estimator (Ledoit-Wolf) for a more stable covariance matrix, defines functions for calculating portfolio statistics, and then uses optimization techniques to find portfolios like the Maximum Sharpe Portfolio (MSP) and Minimum Variance Portfolio (MVP).
# Ledoit-Wolf shrinkage covariance
lw = LedoitWolf()
lw.fit(est_rets.values)
cov_matrix = pd.DataFrame(lw.covariance_, index=ASSETS, columns=ASSETS)
mu = est_rets.mean().values
Sigma = cov_matrix.values
n_assets = len(ASSETS)
def portfolio_stats(weights, mu, Sigma, rf):
"""
Compute annualised return, volatility, and Sharpe ratio.
Parameters
----------
weights : np.ndarray Portfolio weights.
mu : np.ndarray Expected daily returns.
Sigma : np.ndarray Covariance matrix (daily).
rf : float Daily risk-free rate.
Returns
-------
tuple (annualised_return, annualised_vol, sharpe_ratio)
"""
ret = weights @ mu * 252
vol = np.sqrt(weights @ Sigma @ weights * 252)
sharpe = (ret - rf * 252) / (vol + 1e-9)
return ret, vol, sharpe
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = [(0, 1)] * n_assets
w0 = np.ones(n_assets) / n_assets
# Maximum Sharpe Portfolio
def neg_sharpe(w): return -portfolio_stats(w, mu, Sigma, RISK_FREE_RATE)[2]
res_msp = minimize(neg_sharpe, w0, method='SLSQP', bounds=bounds, constraints=constraints)
w_msp = res_msp.x
# Minimum Variance Portfolio
def portfolio_variance(w): return w @ Sigma @ w * 252
res_mvp = minimize(portfolio_variance, w0, method='SLSQP', bounds=bounds, constraints=constraints)
w_mvp = res_mvp.x
for name, w in [('Equal Weight', w0), ('Max Sharpe', w_msp), ('Min Variance', w_mvp)]:
ret, vol, sr = portfolio_stats(w, mu, Sigma, RISK_FREE_RATE)
print(f'{name:14s}: Return={ret:.1%} Vol={vol:.1%} Sharpe={sr:.2f}')Equal Weight : Return=-84.9% Vol=77.3% Sharpe=-1.15 Max Sharpe : Return=-34.1% Vol=67.6% Sharpe=-0.56 Min Variance : Return=-39.9% Vol=62.7% Sharpe=-0.70
portfolio_stats Function
This helper function calculates the annualised return, annualised volatility, and Sharpe ratio for a given set of portfolio weights, expected daily returns, a daily covariance matrix, and a daily risk-free rate. It's a fundamental building block for evaluating and optimizing portfolios.
def portfolio_stats(weights, mu, Sigma, rf):
"""
Compute annualised return, volatility, and Sharpe ratio.
Parameters
----------
weights : np.ndarray Portfolio weights.
mu : np.ndarray Expected daily returns.
Sigma : np.ndarray Covariance matrix (daily).
rf : float Daily risk-free rate.
Returns
-------
tuple (annualised_return, annualised_vol, sharpe_ratio)
"""
ret = weights @ mu * 252
vol = np.sqrt(weights @ Sigma @ weights * 252)
sharpe = (ret - rf * 252) / (vol + 1e-9)
return ret, vol, sharpeSection 4 — Efficient Frontier
This section computes the Efficient Frontier, which represents the set of optimal portfolios that offer the highest expected return for a given level of risk, or the lowest risk for a given expected return. It iterates through a range of target returns and, for each target, finds the portfolio with the minimum variance.
target_returns = np.linspace(mu.min() * 252 * 1.05,
mu.max() * 252 * 0.95, N_FRONTIER)
frontier_vols, frontier_rets = [], []
for target_ret in target_returns:
constr = constraints + [{'type': 'eq', 'fun': lambda w: portfolio_stats(w, mu, Sigma, 0)[0] - target_ret}]
res = minimize(portfolio_variance, w0, method='SLSQP', bounds=bounds, constraints=constr)
if res.success:
frontier_vols.append(np.sqrt(res.fun))
frontier_rets.append(target_ret)
print(f'Efficient frontier points: {len(frontier_vols)}')Efficient frontier points: 91
This section provides a visual representation of the Mean-Variance Optimization results. It plots the Efficient Frontier, individual asset risk-return profiles, and highlights key portfolios like the Maximum Sharpe, Minimum Variance, and Equal Weight portfolios. Additionally, it visualizes the asset allocations (weights) for these key portfolios.
Section 5 — Visualization
This section handles the visualization of the Efficient Frontier and the composition of the key portfolios. It uses matplotlib and seaborn to generate informative plots, making the results of the optimization easily understandable.
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
fig.suptitle('Mean-Variance Optimization — Efficient Frontier', fontsize=13, fontweight='bold')
ax1 = axes[0]
ax1.plot(frontier_vols, frontier_rets, color='#1976d2', lw=2.5, label='Efficient Frontier')
# Individual assets
for asset in ASSETS:
w = np.zeros(n_assets)
w[ASSETS.index(asset)] = 1
r, v, _ = portfolio_stats(w, mu, Sigma, RISK_FREE_RATE)
ax1.scatter(v, r, s=60, zorder=5)
ax1.annotate(asset, (v, r), fontsize=8, xytext=(3, 0), textcoords='offset points')
# Key portfolios
for name, w, color, marker in [
('Max Sharpe', w_msp, '#e53935', '*'),
('Min Var', w_mvp, '#43a047', 'D'),
('Equal Wt', w0, '#ff9800', 's')]:
r, v, sr = portfolio_stats(w, mu, Sigma, RISK_FREE_RATE)
ax1.scatter(v, r, marker=marker, s=150, color=color, zorder=6, label=f'{name} (SR={sr:.2f})')
ax1.set_xlabel('Annualised Volatility')
ax1.set_ylabel('Annualised Return')
ax1.legend(fontsize=8)
ax1.set_title('Efficient Frontier')
ax2 = axes[1]
port_names = ['Max Sharpe', 'Min Variance', 'Equal Weight']
weights_data = [w_msp, w_mvp, w0]
x = np.arange(n_assets)
width = 0.25
colors = ['#e53935', '#43a047', '#ff9800']
for i, (name, w, color) in enumerate(zip(port_names, weights_data, colors)):
ax2.bar(x + i * width, w, width, label=name, color=color, alpha=0.8)
ax2.set_xticks(x + width)
ax2.set_xticklabels(ASSETS)
ax2.set_ylabel('Weight')
ax2.legend(fontsize=8)
ax2.set_title('Portfolio Weights Comparison')
plt.tight_layout()
plt.show()Section 6 — Export
This final section is responsible for exporting the results of the portfolio optimization. It saves the optimal portfolio weights for the Maximum Sharpe, Minimum Variance, and Equal Weight portfolios, along with the data points that define the Efficient Frontier, into separate CSV files. This allows for easy access and further analysis or deployment of the optimized portfolios.
weights_df = pd.DataFrame({'Asset': ASSETS,
'MaxSharpe': w_msp, 'MinVar': w_mvp, 'EqualWt': w0})
weights_df.to_csv('mean_variance_optimization.csv', index=False)
frontier_df = pd.DataFrame({'Volatility': frontier_vols, 'Return': frontier_rets})
frontier_df.to_csv('efficient_frontier.csv', index=False)
print('Saved: mean_variance_optimization.csv, efficient_frontier.csv')Saved: mean_variance_optimization.csv, efficient_frontier.csv
Conclusion
This notebook successfully demonstrates the application of Markowitz Mean-Variance Optimization (MVO) to construct and analyze optimal portfolios. We started by generating synthetic crypto asset data, estimated expected returns and covariance using the Ledoit-Wolf shrinkage method, and then identified key portfolios such as the Maximum Sharpe and Minimum Variance portfolios. The Efficient Frontier was traced and visualized, providing a clear understanding of the risk-return trade-offs. Finally, the optimal portfolio weights and efficient frontier data were exported for future use. This framework serves as a robust foundation for more advanced portfolio management strategies.