Modern Portfolio Theory and Mean-Variance Optimization › The Efficient Frontier

Michael BrenndoerferDecember 13, 202511 min read
Quantitative FinanceAll chapters
Part 9 of 15Portfolio Theory and Investment Analysis
Chapter 46Modern Portfolio Theory and Mean-Variance Optimization › The Efficient Frontier
Reading section 5 of 12 · The Efficient Frontier · 11 minSection 5 of 12 · 11 min

Book contents

Section 5 of 12 · 11 min read

Modern Portfolio Theory and Mean-Variance Optimization › The Efficient FrontierOne section · Sections in this chapter5 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

The efficient frontier is the undominated part of the upper-left boundary of the feasible risk-return set. Upper-boundary points on a flat-return segment can still be dominated by lower-risk portfolios with the same return.

To understand the efficient frontier, imagine plotting every possible portfolio in risk-return space. The feasible set is defined by the budget constraint and any position limits. With unrestricted short selling, the budget equality alone does not bound leverage. Some portfolios lie on the boundary of this region, while others lie in the interior. Interior portfolios are dominated by boundary portfolios that offer either higher return for the same risk or lower risk for the same return. The efficient frontier consists of all portfolios that are not dominated by any other portfolio.

Efficient Frontier

A portfolio is efficient if no other feasible portfolio offers at least as high an expected return and no more risk, with at least one strict improvement. The collection of all efficient portfolios forms the efficient frontier.

Two-Fund Separation Theorem

A property of the unrestricted-weight model with positive-definite covariance and expected returns not proportional to 1\mathbf{1} is the two-fund separation theorem: any efficient portfolio can be expressed as a linear combination of two distinct efficient portfolios. The combination must remain on the efficient upper branch; arbitrary proportions need not be efficient. Position limits can invalidate this two-fund representation.

Within those assumptions, two efficiently managed funds span the minimum-variance curve. An investor can select an efficient combination according to risk tolerance, subject to the borrowing or short-selling needed to implement those proportions.

Mathematically, if w1\mathbf{w}_1 and w2\mathbf{w}_2 are two distinct efficient portfolios in this model, then any scalar α\alpha gives another portfolio on the minimum-variance curve:

w=αw1+(1−α)w2\mathbf{w} = \alpha \mathbf{w}_1 + (1 - \alpha) \mathbf{w}_2

where:

  • w\mathbf{w}: the new minimum-variance portfolio
  • α\alpha: weighting scalar
  • w1,w2\mathbf{w}_1, \mathbf{w}_2: two distinct efficient portfolios

The parameter α\alpha determines the combined expected return. When α=1\alpha = 1, we hold only portfolio 1; when α=0\alpha = 0, we hold only portfolio 2. The combination is efficient only when its combined expected return is at least the GMV return. Extrapolating outside 0≤α≤10\le\alpha\le1 requires short selling one fund and can reach either the efficient upper branch or the inefficient lower branch.

Computing the Frontier

Let's compute the minimum-variance curve at exact target returns. Unlike the unrestricted analytical model, allow_short=True below still bounds every asset weight between -1 and 2; False uses [0,1]. The curve includes its inefficient lower branch, and binding bounds can make weight paths piecewise smooth.

In[8]:
Code
import numpy as np
from scipy.optimize import minimize


def efficient_frontier(mu, cov_matrix, n_points=50, allow_short=True):
    """Compute a bounded minimum-variance curve, including its lower branch.

    Requires finite 1-D expected returns and a compatible symmetric PSD
    covariance matrix. Each weight is bounded to [-1, 2] when allow_short is
    True, or [0, 1] otherwise; shorting is not unrestricted here.

    n_points is an integer >= 2 specifying the requested exact-return targets.
    If every expected return is exactly equal, the return constraint is
    redundant: return one budget-constrained bounded GMV point instead.

    Returns arrays with shapes (m,), (m,), and (m, n_assets), where m is
    n_points in the nondegenerate case and 1 in the equal-mean case.
    A failed endpoint or target solve raises RuntimeError instead of silently
    omitting portfolios. Feasibility is checked to absolute tolerance 1e-7;
    The variance objective is divided by a positive covariance scale for the
    solver; reported risk still uses the original unscaled covariance. This
    preserves the mathematical minimizer across covariance magnitudes.
    Numerical solver success is not an optimality certificate.
    """
    mu = np.asarray(mu, dtype=float)
    cov_matrix = np.asarray(cov_matrix, dtype=float)
    if mu.ndim != 1 or mu.size == 0 or not np.all(np.isfinite(mu)):
        raise ValueError("mu must be a nonempty finite one-dimensional array")
    n_assets = mu.size
    if cov_matrix.shape != (n_assets, n_assets) or not np.all(np.isfinite(cov_matrix)):
        raise ValueError("cov_matrix must be finite with shape (n_assets, n_assets)")
    if not np.allclose(cov_matrix, cov_matrix.T, rtol=1e-10, atol=1e-12):
        raise ValueError("cov_matrix must be symmetric")
    if np.linalg.eigvalsh(cov_matrix).min() < -1e-12:
        raise ValueError("cov_matrix must be positive semidefinite")
    if (
        isinstance(n_points, (bool, np.bool_))
        or not isinstance(n_points, (int, np.integer))
        or n_points < 2
    ):
        raise ValueError("n_points must be an integer >= 2")

    lower, upper = (-1.0, 2.0) if allow_short else (0.0, 1.0)
    bounds = [(lower, upper)] * n_assets
    x0 = np.full(n_assets, 1.0 / n_assets)
    constraints = [{"type": "eq", "fun": lambda w: np.sum(w) - 1.0}]
    feasibility_tolerance = 1e-7
    options = {"maxiter": 500, "ftol": 1e-10}

    # Scale the entire variance objective by a positive constant.
    # This changes numerical conditioning, not the chosen covariance model.
    objective_scale = float(np.max(np.abs(cov_matrix)))
    if objective_scale == 0:
        objective_scale = 1.0
    optimization_covariance = cov_matrix / objective_scale

    def scaled_portfolio_variance(w):
        return float(w @ optimization_covariance @ w)

    def portfolio_variance(w):
        # Returned variances/risk retain the original units and covariance.
        return float(w @ cov_matrix @ w)

    def checked_weights(result, label, target=None):
        if not result.success:
            raise RuntimeError(f"{label} optimization failed: {result.message}")
        weights = np.asarray(result.x, dtype=float)
        if (
            weights.shape != (n_assets,)
            or not np.all(np.isfinite(weights))
            or abs(weights.sum() - 1.0) > feasibility_tolerance
            or np.any(weights < lower - feasibility_tolerance)
            or np.any(weights > upper + feasibility_tolerance)
            or (
                target is not None
                and abs(float(weights @ mu) - target) > feasibility_tolerance
            )
        ):
            raise RuntimeError(
                f"{label} optimization returned invalid or infeasible weights"
            )
        return weights

    def result_arrays(weights):
        weights = np.stack(weights)
        returns = weights @ mu
        variances = np.array([portfolio_variance(w) for w in weights])
        if not np.all(np.isfinite(variances)) or np.any(variances < -1e-12):
            raise RuntimeError("Frontier optimization returned invalid variances")
        return returns, np.sqrt(np.maximum(variances, 0.0)), weights

    if np.all(mu == mu[0]):
        result = minimize(
            scaled_portfolio_variance,
            x0,
            method="SLSQP",
            bounds=bounds,
            constraints=constraints,
            options=options,
        )
        return result_arrays([checked_weights(result, "Equal-mean GMV")])

    min_result = minimize(
        lambda w: float(w @ mu),
        x0,
        method="SLSQP",
        bounds=bounds,
        constraints=constraints,
        options=options,
    )
    max_result = minimize(
        lambda w: -float(w @ mu),
        x0,
        method="SLSQP",
        bounds=bounds,
        constraints=constraints,
        options=options,
    )
    min_weights = checked_weights(min_result, "Minimum-return endpoint")
    max_weights = checked_weights(max_result, "Maximum-return endpoint")
    min_ret, max_ret = float(min_weights @ mu), float(max_weights @ mu)
    if min_ret > max_ret:
        raise RuntimeError("Frontier endpoint returns are out of order")

    target_returns = np.linspace(min_ret, max_ret, n_points)
    frontier_weights = []
    for index, target in enumerate(target_returns):
        # A convex combination of endpoints is a feasible starting allocation.
        fraction = index / (n_points - 1)
        start = (1 - fraction) * min_weights + fraction * max_weights
        target_constraints = constraints + [
            {"type": "eq", "fun": lambda w, t=target: float(w @ mu) - t}
        ]
        result = minimize(
            scaled_portfolio_variance,
            start,
            method="SLSQP",
            bounds=bounds,
            constraints=target_constraints,
            options=options,
        )
        frontier_weights.append(checked_weights(result, f"Target {index + 1}", target))
    return result_arrays(frontier_weights)

Worked Example: Three-Asset Portfolio

Let's construct an efficient frontier for a portfolio of stocks, bonds, and commodities. This example illustrates how different asset classes with distinct risk-return profiles and correlations combine to form a diversified investment opportunity set:

In[9]:
Code
import numpy as np

# Asset parameters (annualized)
# Illustrative annual parameters for stocks, bonds, and commodities
asset_names = ["Stocks", "Bonds", "Commodities"]

# Expected returns
mu = np.array([0.08, 0.03, 0.05])  # 8%, 3%, 5%

# Volatilities
sigma = np.array([0.18, 0.05, 0.20])  # 18%, 5%, 20%

# Correlation matrix
corr = np.array(
    [
        [1.0, 0.1, 0.3],  # Stocks
        [0.1, 1.0, 0.0],  # Bonds
        [0.3, 0.0, 1.0],  # Commodities
    ]
)

# Convert to covariance matrix: Σ = diag(σ) × Corr × diag(σ)
cov_matrix = np.diag(sigma) @ corr @ np.diag(sigma)
Out[10]:
Console
Covariance Matrix:
[[0.0324 0.0009 0.0108]
 [0.0009 0.0025 0.    ]
 [0.0108 0.     0.04  ]]

Asset Statistics:
  Stocks: μ = 8.0%, σ = 18.0%
  Bonds: μ = 3.0%, σ = 5.0%
  Commodities: μ = 5.0%, σ = 20.0%

The covariance matrix confirms the relationships between our assets: stocks and commodities have higher volatility and a moderate positive correlation (0.3), while bonds give stability with low volatility and low correlation to the other assets. The near-zero correlations between bonds and the other asset classes suggest that bonds will play a central role in diversifying portfolio risk.

Out[11]:
Visualization
Heatmap showing correlation values between stocks, bonds, and commodities.
Correlation matrix heatmap for the three-asset portfolio. Bonds show low or zero correlation with both stocks and commodities, making them useful for diversification. The numerical values inside the cells highlight the low or zero correlation of bonds compared to the moderate 0.3 correlation between stocks and commodities.

Now let's compute and visualize the efficient frontier:

In[12]:
Code
# Compute the minimum-variance frontier, including its inefficient lower branch
frontier_returns, frontier_risks, frontier_weights = efficient_frontier(
    mu, cov_matrix, n_points=100, allow_short=True
)

# Find global minimum variance portfolio
gmv_weights = global_minimum_variance_portfolio(cov_matrix)
gmv_return = gmv_weights @ mu
gmv_risk = np.sqrt(gmv_weights @ cov_matrix @ gmv_weights)
Out[13]:
Visualization
Minimum-variance risk-return curve with a global minimum-variance star near 4.82% risk and 3.25% return, and labeled Stocks, Bonds, and Commodities points.
Minimum-variance curve for the bounded three-asset model. The branch above the GMV return is efficient; the lower branch is inefficient. The star marks the GMV portfolio near 4.82% risk and 3.25% expected return.

The minimum-variance curve gives the lowest risk at each exact target return. Only the branch at or above the GMV return is efficient. The individual assets (red dots) lie inside the frontier, confirming that portfolios combining imperfectly correlated assets offer superior efficiency compared to holding single securities. Notice how the frontier bows outward to the left, showing that diversification creates portfolios with better risk-return characteristics than any individual asset.

Out[14]:
Console
Global Minimum Variance Portfolio:
  Stocks: 2.95%
  Bonds: 92.03%
  Commodities: 5.02%

  Expected Return: 3.25%
  Standard Deviation: 4.82%

The GMV portfolio is heavily weighted toward bonds because of their low volatility. Notice that the GMV portfolio achieves lower risk than any individual asset, showing the power of diversification. Even the lowest-risk asset (bonds at 5% volatility) has higher risk than the optimally diversified GMV portfolio.

Portfolio Composition Along the Frontier

Understanding how portfolio weights change along the efficient frontier provides insight into the risk-return tradeoff. As you move up the frontier seeking higher returns, you must accept more risk, and the portfolio composition shifts accordingly:

Out[15]:
Visualization
Three-line chart of Stocks, Bonds, and Commodities weights versus target return, including negative weights from short selling and a vertical marker at the global minimum-variance return.
Asset weights along the minimum-variance frontier for a three-asset portfolio when short selling is allowed. As target return increases, the allocation shifts from a short position in stocks toward a large stock allocation, while bond exposure declines. The vertical dashed line marks the bond-heavy global minimum-variance portfolio.

The plotted curve includes targets below the GMV return, on the inefficient lower branch. Near the GMV portfolio, bonds dominate the allocation. Above the GMV return, seeking higher returns along the efficient branch increases risk and shifts weight toward stocks. The bounded model produces piecewise smooth weight paths; kinks occur when a position limit binds.