Harry Markowitz's 1952 insight launched modern portfolio theory: don't pick assets in isolation, pick the combination that gives the most return per unit of risk. We build the efficient frontier from real data across six asset classes and find the two portfolios everyone quotes — minimum variance and maximum Sharpe.

1.Summary

Mean-variance optimization treats a portfolio's expected return as the weighted average of its assets, and its risk as the portfoliovariance — which depends not just on each asset's volatility but on how they co-move. Minimizing variance for each level of target return traces out the efficient frontier: the set of portfolios you can't beat. Two points on it get special names — the minimum-variance portfolio (leftmost) and the maximum-Sharpe (tangency) portfolio, where the risk-adjusted return peaks.

2.Intuition

2.1The only free lunch in finance

Combine two assets that don't move in lockstep and the portfolio's risk is lessthan the weighted average of their risks — because their wiggles partly cancel. That cancellation is diversification, and it's why a stock-bond-gold mix can have lower volatility than any single sleeve.

2.2Correlation does the heavy lifting

Volatility tells you how much one asset moves; correlation tells you whether they move together. Two 15%-vol assets with −0.3 correlation build a far smoother portfolio than two with +0.9. The whole power of MVO lives in the covariance matrix.

2.3What the frontier shows

Plot every possible portfolio in risk-return space and they fill a bullet-shaped cloud. Only the upper-left edgematters — for any risk level, that's the highest return available. Anything below it is a portfolio you'd never rationally hold.

3.Theory & Mechanics

With weights , expected returns and covariance matrix :

The efficient frontier solves, for each target return :

Maximum Sharpe maximizes ; minimum variance just minimizes . We'll build the frontier two ways — a transparent NumPy Monte-Carlo cloud, then the exact optimum with PyPortfolioOpt.

4.Applied Example — Six ETFs

4.1A diversified multi-asset universe

Six asset classes chosen to not move together — the raw material of a good frontier:

TickerAsset class
SPYUS equities (S&P 500)
TLTLong US Treasuries
GLDGold
VNQUS real estate (REITs)
VEADeveloped ex-US equities
VWOEmerging-market equities
python
TICKERS = ["SPY", "TLT", "GLD", "VNQ", "VEA", "VWO"]
START, END = "2015-01-01", "2024-12-31"

def load_prices(tickers, start, end):
    """Adjusted-close prices: yfinance first, Stooq as fallback."""
    try:
        import yfinance as yf
        df = yf.download(tickers, start=start, end=end, auto_adjust=True, progress=False)["Close"]
        if not df.empty:
            return df[tickers].dropna()
    except Exception as exc:
        print(f"yfinance failed ({exc}); trying Stooq…")
    cols = {}
    for t in tickers:
        url = f"https://stooq.com/q/d/l/?s={t.lower()}.us&i=d"
        s = pd.read_csv(url, parse_dates=["Date"], index_col="Date")["Close"].rename(t)
        cols[t] = s
    return pd.concat(cols, axis=1).loc[start:end].dropna()

px = load_prices(TICKERS, START, END)
rets = px.pct_change().dropna()

mu    = rets.mean() * 252          # annualised expected returns
Sigma = rets.cov() * 252           # annualised covariance
vol   = np.sqrt(np.diag(Sigma))

summary = pd.DataFrame({"ann. return": mu, "ann. vol": vol,
                        "Sharpe": mu / vol}).round(3)
print(summary)
print(f"\n{len(px)} trading days, {px.index[0].date()} → {px.index[-1].date()}")

Ten years of daily data (2,515 trading days, 2015–2024). Note how different the standalone Sharpe ratios are — and that TLT earned essentially nothing over the decade:

TickerAnn. returnAnn. volSharpe
SPY13.9%17.6%0.79
TLT−0.0%15.3%−0.00
GLD8.5%14.1%0.60
VNQ6.9%20.8%0.33
VEA6.9%17.3%0.40
VWO6.0%19.8%0.30

4.2The correlation matrix — the source of the free lunch

Note where correlations are low: Treasuries (TLT, −0.21 to SPY) and gold (GLD, +0.05 to SPY) barely track equities, which is exactly why they smooth the portfolio.

python
corr = rets.corr()

fig, ax = plt.subplots(figsize=(6.5, 5.5))
im = ax.imshow(corr, cmap="coolwarm", vmin=-1, vmax=1)
ax.set_xticks(range(len(TICKERS))); ax.set_xticklabels(TICKERS)
ax.set_yticks(range(len(TICKERS))); ax.set_yticklabels(TICKERS)
for i in range(len(TICKERS)):
    for j in range(len(TICKERS)):
        ax.text(j, i, f"{corr.iloc[i,j]:.2f}", ha="center", va="center",
                color="white" if abs(corr.iloc[i,j]) > 0.5 else "black", fontsize=9)
fig.colorbar(im, fraction=0.046, pad=0.04)
ax.set_title("Correlation of daily returns")
plt.tight_layout(); plt.show()
Six-by-six correlation matrix of daily ETF returns; the equity block (SPY, VNQ, VEA, VWO) is highly correlated at 0.55 to 0.86, while TLT is slightly negative against equities and GLD is near zero
Figure 4.2 · Correlation of daily returns, 2015–2024

4.3The efficient frontier — Monte-Carlo cloud

Generate 20,000 random portfolios to see the bullet, then the frontier as its upper-left edge. Colour = Sharpe ratio.

python
N = 20_000
n = len(TICKERS)
rf = 0.02

w = np.random.dirichlet(np.ones(n), N)          # random long-only weights summing to 1
port_ret = w @ mu.values
port_vol = np.sqrt(np.einsum("ij,jk,ik->i", w, Sigma.values, w))
port_sharpe = (port_ret - rf) / port_vol

fig, ax = plt.subplots(figsize=(10, 6))
sc = ax.scatter(port_vol, port_ret, c=port_sharpe, cmap="viridis", s=6, alpha=0.5)
# individual assets
ax.scatter(vol, mu.values, c="red", marker="D", s=60, zorder=5)
for i, t in enumerate(TICKERS):
    ax.annotate(t, (vol[i], mu.values[i]), textcoords="offset points", xytext=(7, 3))
fig.colorbar(sc, label="Sharpe ratio")
ax.set_xlabel("Annualised volatility"); ax.set_ylabel("Annualised return")
ax.set_title(f"{N:,} random portfolios — the efficient frontier bullet")
plt.tight_layout(); plt.show()

4.4The exact optima with PyPortfolioOpt

The cloud shows the shape; PyPortfolioOpt solves for the exact minimum-variance and maximum-Sharpe portfolios and overlays the true frontier.

python
from pypfopt import EfficientFrontier, expected_returns, risk_models, plotting

mu_pp = expected_returns.mean_historical_return(px)
S_pp  = risk_models.sample_cov(px)

# max Sharpe
ef = EfficientFrontier(mu_pp, S_pp)
ef.max_sharpe(risk_free_rate=rf)
w_sharpe = ef.clean_weights()
perf_sharpe = ef.portfolio_performance(risk_free_rate=rf)

# min variance
ef2 = EfficientFrontier(mu_pp, S_pp)
ef2.min_volatility()
w_minvar = ef2.clean_weights()
perf_minvar = ef2.portfolio_performance(risk_free_rate=rf)

weights = pd.DataFrame({"Max Sharpe": w_sharpe, "Min Variance": w_minvar}).round(3)
print(weights)
print(f"\nMax Sharpe : return {perf_sharpe[0]:.1%}, vol {perf_sharpe[1]:.1%}, Sharpe {perf_sharpe[2]:.2f}")
print(f"Min Variance: return {perf_minvar[0]:.1%}, vol {perf_minvar[1]:.1%}, Sharpe {perf_minvar[2]:.2f}")
TickerMax SharpeMin Variance
SPY0.5630.268
TLT0.0000.371
GLD0.4370.280
VNQ0.0000.000
VEA0.0000.081
VWO0.0000.000

Max Sharpe: 10.8% return at 11.9% vol (Sharpe 0.73) — a two-asset SPY + GLD barbell. Min variance: 5.7% return at 9.3% vol (Sharpe 0.40) — note it holds 37% TLT despiteTLT's zero return, purely for its negative correlation. That is the free lunch in action: the optimizer pays for co-movement, not for standalone performance.

Scatter of 20,000 random portfolios forming a bullet shape in risk-return space, shaded by Sharpe ratio, with the exact efficient frontier drawn along the upper-left edge; a star marks the maximum-Sharpe portfolio and an open circle the minimum-variance portfolio, with the six individual ETFs plotted as diamonds well inside the cloud
Figure 4.4 · 20,000 random portfolios, the exact frontier, and the two special portfolios

Every single ETF plots insidethe cloud, well below the frontier — even SPY, the decade's best performer, sits under the line. No individual asset is efficient; only combinations are.

5.Conclusion

5aStrengths

  • Quantifies diversification — turns “don't put all your eggs in one basket” into an exact weight vector
  • One framework, any universe — stocks, bonds, gold, real estate all go in the same optimizer
  • Closed-form and fast — the frontier is a quadratic program that solves instantly
  • The foundation — every allocation method (risk parity, Black-Litterman, CVaR) is a response to MVO

5bWeaknesses & Limitations

  • Garbage in, garbage out — expected returns are notoriously hard to estimate, and MVO is hypersensitive to them
  • Concentrated, unstable weights — tiny input changes can swing allocations wildly (the “error-maximization” critique)
  • Backward-looking — historical covariance assumes the past regime persists
  • Ignores tail risk — variance treats upside and downside symmetrically; crashes aren't Gaussian

5cApplications in Practice

  • Strategic asset allocation for pension funds and endowments
  • Setting the neutral portfolio a discretionary manager tilts away from
  • The benchmark every alternative allocation method is measured against

5dAlternatives & Extensions

  • Black-Litterman — fixes the input-sensitivity problem by blending market equilibrium with views (the next tutorial)
  • Risk parity — sidesteps return estimation entirely by allocating on risk contribution (our futures-based piece)
  • Hierarchical Risk Parity — uses clustering instead of matrix inversion for stabler weights
  • CVaR optimization — replaces variance with a genuine tail-risk measure