04 — Efficient Frontier and Tangency Portfolio¶
Learning objectives¶
Compute the return and volatility of a random portfolio; derive the two-asset minimum-variance weight by hand; trace an efficient frontier and find its minimum-variance and tangency (max-Sharpe) points; explain why "efficient" only describes the upper half of the frontier curve.
Free learning pack¶
- MIT OCW Portfolio Theory https://ocw.mit.edu/courses/15-401-finance-theory-i-fall-2008/pages/video-lectures-and-slides/portfolio-theory/
- Modern portfolio theory — Wikipedia (efficient frontier, capital allocation line) https://en.wikipedia.org/wiki/Modern_portfolio_theory
reference/concepts/mean_variance_optimization.mdreference/concepts/efficient_frontier.md
Do not search for more material until these are insufficient.
PREDICT¶
Two portfolios have the same expected return. Portfolio A has lower volatility than Portfolio B. Without any calculation: is there ever a good reason to hold Portfolio B? What's the word for a portfolio like B, relative to A?
HAND CALCULATION¶
Two uncorrelated assets, variances sigma1^2 = 0.04 and
sigma2^2 = 0.01. The minimum-variance weight in asset 1, from setting
d/dw1 [w1^2*sigma1^2 + (1-w1)^2*sigma2^2] = 0 and solving:
w1 = sigma2^2 / (sigma1^2 + sigma2^2)
Compute w1 and w2 by hand before running anything.
import numpy as np
sigma1_sq, sigma2_sq = 0.04, 0.01
# MANUAL FIRST:
# apply the closed-form formula above.
w1 = None
w2 = None
print(w1, w2)
# CHECK (uncomment after your attempt):
# assert np.isclose(w1, 0.2) and np.isclose(w2, 0.8)
MANUAL FIRST — random portfolio cloud¶
Generate 10,000 random long-only portfolios (3 assets) and compute each one's expected return and volatility.
import matplotlib.pyplot as plt
import pandas as pd
mu = np.array([0.06, 0.08, 0.04])
cov = np.array([
[0.04, 0.01, 0.002],
[0.01, 0.09, 0.004],
[0.002, 0.004, 0.01],
])
rng = np.random.default_rng(42)
# MANUAL FIRST:
# generate 10,000 Dirichlet(alpha=1,1,1) weight vectors (rng.dirichlet),
# then compute each portfolio's return (w @ mu) and volatility
# (sqrt(w @ cov @ w)).
weights = None
portfolio_returns = None
portfolio_vols = None
# CHECK (uncomment after your attempt - needs pm installed, see
# docs/getting-started.md if this import fails):
# from pm.optimization import minimum_variance
# gmv_vol = np.sqrt(minimum_variance(cov) @ cov @ minimum_variance(cov))
# assert portfolio_vols.min() >= gmv_vol - 1e-6, (
# "no random long-only portfolio can beat the true minimum-variance point"
# )
PREDICT¶
As you raise the target expected return from the lowest achievable value to the highest, does the minimum achievable volatility change monotonically, or does it fall and then rise again? Where's the turning point?
Formula (efficient frontier)¶
For a target return mu_target:
minimize w^T Sigma w s.t. sum(w) = 1, w^T mu = mu_target, w >= 0
traced across a range of targets by efficient_frontier in
src/pm/optimization.py.
from pm.optimization import efficient_frontier, max_sharpe, minimum_variance
# MANUAL FIRST:
# call efficient_frontier(mu, cov, n_points=30) to get
# (targets, frontier_vols, frontier_weights).
# Then call minimum_variance(cov) for the GMV portfolio, and
# max_sharpe(mu, cov, risk_free_rate=0.02) for the tangency portfolio.
targets, frontier_vols, frontier_weights = None, None, None
w_gmv = None
w_tangency = None
# CHECK (uncomment after your attempt):
# assert np.isclose(targets.min(), mu.min()) and np.isclose(targets.max(), mu.max())
# gmv_vol = np.sqrt(w_gmv @ cov @ w_gmv)
# assert np.isclose(frontier_vols.min(), gmv_vol, atol=1e-3), (
# "the frontier's lowest-volatility point should match minimum_variance directly"
# )
Plot¶
Overlay the random portfolio cloud from above with the traced frontier, the GMV point, and the tangency portfolio.
rf = 0.02
tangency_vol = np.sqrt(w_tangency @ cov @ w_tangency)
tangency_ret = w_tangency @ mu
gmv_vol = np.sqrt(w_gmv @ cov @ w_gmv)
gmv_ret = w_gmv @ mu
fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(portfolio_vols, portfolio_returns, s=4, alpha=0.15, color="#9AA2AE", label="random portfolios")
ax.plot(frontier_vols, targets, color="#1f6feb", linewidth=2, label="efficient frontier")
ax.scatter([gmv_vol], [gmv_ret], color="#2E6F5E", s=90, zorder=5, label="GMV (minimum variance)")
ax.scatter([tangency_vol], [tangency_ret], color="#B9793A", s=90, zorder=5, label="tangency (max Sharpe)")
# capital market line: from (0, rf) through the tangency portfolio
cml_x = np.array([0, portfolio_vols.max()])
cml_y = rf + (tangency_ret - rf) / tangency_vol * cml_x
ax.plot(cml_x, cml_y, color="#B9793A", linestyle="--", linewidth=1, label="capital market line")
ax.set_xlabel("volatility")
ax.set_ylabel("expected return")
ax.set_title("Random portfolios, efficient frontier, and tangency portfolio")
ax.legend()
plt.show()
Experiment¶
Recompute max_sharpe at risk_free_rate=0.04 instead of 0.02. Does
the tangency point move up or down the frontier, and which asset drops
to zero weight? Does the capital market line get steeper or flatter —
and does that match your intuition about what a higher risk-free rate
should do to the reward for taking risk?
Reference¶
reference/concepts/mean_variance_optimization.md
reference/concepts/efficient_frontier.md
Promote¶
Use src/pm/optimization.py (minimum_variance, efficient_frontier,
max_sharpe) only after your own implementation.
Test¶
pytest tests/test_optimization.py
ORAL CHECK¶
Explain to a PM why the lower half of the traced frontier (below the GMV point) is a real, computable set of portfolios but not an "efficient" one — and why every portfolio in the random cloud sits on or to the right of the frontier curve, never to the left of it.
Try /tutor efficient frontier for an adaptive walkthrough.