SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 4.5.1 Lagrange multipliersConcept · 30 min
      2. 4.5.2 Inequality constraints and the KKT conditionsConcept · 30 min
      3. 4.5.3 Convex functions and convex problemsConcept · 30 min
      4. 4.5.4 Practice set: constrained optimisationPractice set · 25 min
      5. 4.5.5 Case study: the minimum-variance portfolio and the efficient frontierCase study · 35 min

Level 4, Multivariable calculus and optimisation 4.5 Constrained optimisation Lesson 5 of 5

Case studyLesson 4.5.5 in Level 4, Multivariable calculus and optimisation. About 35 minutes.

Case study: the minimum-variance portfolio and the efficient frontier

The two-asset minimum by Lagrange

A portfolio puts a fraction wiw_i of the money in asset ii. Its variance is w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w}, with Σ\Sigma the covariance matrix (case study 3.2.5).

The weights must add up to 1, so that all the money is invested: 1⊤w=1\mathbf{1}^\top\mathbf{w} = 1, where 1\mathbf{1} is the vector of ones. A negative weight is a short sale (case study 1.2.4).

So the safest portfolio solves: minimise f(w)=w⊤Σwf(\mathbf{w}) = \mathbf{w}^\top\Sigma\mathbf{w} subject to g(w)=1⊤w=1g(\mathbf{w}) = \mathbf{1}^\top\mathbf{w} = 1. Lesson 4.4.1's quadratic 12w⊤Aw\frac{1}{2}\mathbf{w}^\top A\mathbf{w} has gradient AwA\mathbf{w}, and here A=2ΣA = 2\Sigma, so ∇f=2Σw\nabla f = 2\Sigma\mathbf{w}. The budget has ∇g=1\nabla g = \mathbf{1}.

Lagrange's condition ∇f=λ∇g\nabla f = \lambda\nabla g reads 2Σw=λ12\Sigma\mathbf{w} = \lambda\mathbf{1}: every entry of Σw\Sigma\mathbf{w} is the same number, λ2\frac{\lambda}{2}. For case study 1.2.4's two assets, with Σ=(0.040.0120.0120.09)\Sigma = \begin{pmatrix} 0.04 & 0.012 \\ 0.012 & 0.09 \end{pmatrix}, the two entries are

0.04 wA+0.012 wB=λ20.012 wA+0.09 wB=λ2\begin{gathered} 0.04\,w_A + 0.012\,w_B = \tfrac{\lambda}{2} \\[4pt] 0.012\,w_A + 0.09\,w_B = \tfrac{\lambda}{2} \end{gathered}

Subtracting gives 0.028 wA=0.078 wB0.028\,w_A = 0.078\,w_B. With wB=1−wAw_B = 1 - w_A, that is 0.106 wA=0.0780.106\,w_A = 0.078, so wA=0.0780.106≈0.7358w_A = \frac{0.078}{0.106} \approx 0.7358. That is case study 1.2.4's vertex, 0.1560.212\frac{0.156}{0.212}, the same number found a second way.

Rule: The minimum-variance portfolio

Σw=λ2 1w=Σ−111⊤Σ−11\begin{gathered} \Sigma\mathbf{w} = \tfrac{\lambda}{2}\,\mathbf{1} \\[4pt] \mathbf{w} = \frac{\Sigma^{-1}\mathbf{1}}{\mathbf{1}^\top\Sigma^{-1}\mathbf{1}} \end{gathered}

Every entry of Σw\Sigma\mathbf{w} is equal, and the common value is the variance: w⊤Σw=λ2 w⊤1=λ2\mathbf{w}^\top\Sigma\mathbf{w} = \frac{\lambda}{2}\,\mathbf{w}^\top\mathbf{1} = \frac{\lambda}{2}. The minimum variance is 11⊤Σ−11\frac{1}{\mathbf{1}^\top\Sigma^{-1}\mathbf{1}}.

Case study 3.2.5 noticed that at case study 1.2.4's weight both entries of Σw\Sigma\mathbf{w} equal 0.0326, the variance itself. The rule says why: it holds at every minimum-variance portfolio, with any number of assets.

The second line follows from the first. Σw=λ21\Sigma\mathbf{w} = \frac{\lambda}{2}\mathbf{1} gives w=λ2Σ−11\mathbf{w} = \frac{\lambda}{2}\Sigma^{-1}\mathbf{1}, and the budget fixes λ2\frac{\lambda}{2} so that the weights add up to 1. In code, solve Σy=1\Sigma\mathbf{y} = \mathbf{1} with np.linalg.solve (lesson 3.2.3) and divide y\mathbf{y} by the sum of its entries.

Why the entries must agree

Entry ii of Σw\Sigma\mathbf{w} is the covariance of asset ii with the portfolio (case study 3.2.5). Move a little money from asset jj to asset ii, along ei−ej\mathbf{e}_i - \mathbf{e}_j (standard basis vectors, lesson 3.5.1): the budget stays the same. The variance changes at the rate ∇f⋅(ei−ej)=2((Σw)i−(Σw)j)\nabla f \cdot (\mathbf{e}_i - \mathbf{e}_j) = 2\big((\Sigma\mathbf{w})_i - (\Sigma\mathbf{w})_j\big) (lesson 4.2.1).

If two entries differ, moving money towards the smaller one lowers the variance. Only when all entries agree does no move help.

Try it

np.linalg.solve(S2, np.ones(2)) is Σ−11\Sigma^{-1}\mathbf{1}, found without forming the inverse. Dividing by its sum gives the weights, next to case study 1.2.4's 0.1560.212\frac{0.156}{0.212}.

The second line prints Σw\Sigma\mathbf{w}, the variance and the volatility: both entries are 0.0326.

⌘/Ctrl + Enter runs the cell

Three assets

The three assets of case study 3.2.5 have expected returns μ=(0.06,0.08,0.11)\boldsymbol{\mu} = (0.06, 0.08, 0.11), volatilities 12%, 18% and 25%, and correlations ρAB=0.3\rho_{AB} = 0.3, ρAC=0.1\rho_{AC} = 0.1, ρBC=0.5\rho_{BC} = 0.5. The same rule gives the minimum-variance portfolio:

ABC
weight74.65%15.11%10.23%
(Σw)i(\Sigma\mathbf{w})_i0.0120360.0120360.012036

Its variance is 0.012036, the common value, so its volatility is 10.97%. Its expected return is w⋅μ=6.81%\mathbf{w} \cdot \boldsymbol{\mu} = 6.81\%.

The 50/30/20 portfolio of case study 3.2.5 has Σw=(0.009744,0.01746,0.02075)\Sigma\mathbf{w} = (0.009744, 0.01746, 0.02075). The entries differ, so it is not the minimum. Moving money from C to A lowers the variance at the rate 2(0.009744−0.02075)=−0.0220122(0.009744 - 0.02075) = -0.022012 per unit.

Case study 3.2.5's risk contribution of asset ii is wi(Σw)iw_i(\Sigma\mathbf{w})_i. At the minimum every (Σw)i(\Sigma\mathbf{w})_i equals the variance, so each asset's share of the risk equals its share of the money. The shares are 74.65%, 15.11% and 10.23%.

Is this the lowest variance, or only a candidate? Σ\Sigma is positive definite, with eigenvalues 0.011741, 0.022502 and 0.075058 (lesson 3.7.1). So w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w} is strictly convex and the budget is linear: lesson 4.5.3 says the Lagrange point is the one global minimum.

Try it

The cell repeats case study 3.2.5's Σ\Sigma, then the rule. The third line prints the risk shares wi(Σw)i/w⊤Σww_i(\Sigma\mathbf{w})_i / \mathbf{w}^\top\Sigma\mathbf{w} in percent.

Next, Σw\Sigma\mathbf{w} for 50/30/20, the rate of moving money from C to A, and a check that ∇f=2Σw\nabla f = 2\Sigma\mathbf{w} by central differences (lesson 4.1.2). Last, SciPy's answer (lesson 4.5.2) and the eigenvalues of Σ\Sigma.

⌘/Ctrl + Enter runs the cell
Check yourself
At the minimum-variance portfolio of A, B and C, (Σw)A=0.012036(\Sigma\mathbf{w})_A = 0.012036. What is (Σw)C(\Sigma\mathbf{w})_C?

A target return

The minimum-variance portfolio expects only 6.81%. An investor who wants more return asks for the safest portfolio that expects a target mm: a second constraint, μ⊤w=m\boldsymbol{\mu}^\top\mathbf{w} = m.

Two equality constraints bring two multipliers, as in lesson 4.5.2's rule for two constraints:

2Σw=λ11+λ2μ1⊤w=1,μ⊤w=m\begin{gathered} 2\Sigma\mathbf{w} = \lambda_1\mathbf{1} + \lambda_2\boldsymbol{\mu} \\[4pt] \mathbf{1}^\top\mathbf{w} = 1, \qquad \boldsymbol{\mu}^\top\mathbf{w} = m \end{gathered}

That is five linear equations in wAw_A, wBw_B, wCw_C, λ1\lambda_1 and λ2\lambda_2, solved as one system like lesson 4.5.1's. For m=9%m = 9\% the answer is 29.87% in A, 16.88% in B and 53.25% in C, with volatility 15.99%.

The multiplier λ2=1.2393\lambda_2 = 1.2393 is a rate, as in lesson 4.5.1: the variance per unit of target return. From 9% to 9.1% the variance rises by 0.001268, close to λ2×0.001=0.001239\lambda_2 \times 0.001 = 0.001239. Extra return costs risk, and λ2\lambda_2 says how much.

Try it

solve_target builds the 5×55 \times 5 system: the first three rows are 2Σw−λ11−λ2μ=02\Sigma\mathbf{w} - \lambda_1\mathbf{1} - \lambda_2\boldsymbol{\mu} = \mathbf{0}, the last two the constraints.

The last line compares the change in variance from 9% to 9.1% with λ2×0.001\lambda_2 \times 0.001.

⌘/Ctrl + Enter runs the cell

The efficient frontier

Solving for every target at once gives a formula, with a=λ12a = \frac{\lambda_1}{2} and b=λ22b = \frac{\lambda_2}{2}. The first equation becomes Σw=a1+bμ\Sigma\mathbf{w} = a\mathbf{1} + b\boldsymbol{\mu}, so w=a Σ−11+b Σ−1μ\mathbf{w} = a\,\Sigma^{-1}\mathbf{1} + b\,\Sigma^{-1}\boldsymbol{\mu}. Every frontier portfolio mixes the same two vectors.

Put that w\mathbf{w} into the two constraints. With three numbers from the data,

k1=1⊤Σ−11,k2=1⊤Σ−1μk3=μ⊤Σ−1μ\begin{gathered} k_1 = \mathbf{1}^\top\Sigma^{-1}\mathbf{1}, \quad k_2 = \mathbf{1}^\top\Sigma^{-1}\boldsymbol{\mu} \\[4pt] k_3 = \boldsymbol{\mu}^\top\Sigma^{-1}\boldsymbol{\mu} \end{gathered}

they read ak1+bk2=1ak_1 + bk_2 = 1 and ak2+bk3=mak_2 + bk_3 = m. This 2×22 \times 2 system (lesson 3.2.3) gives a=k3−k2mda = \frac{k_3 - k_2m}{d} and b=k1m−k2db = \frac{k_1m - k_2}{d}, with d=k1k3−k22d = k_1k_3 - k_2^2.

The variance is short to find too: w⊤Σw=w⊤(a1+bμ)=a+bm\mathbf{w}^\top\Sigma\mathbf{w} = \mathbf{w}^\top(a\mathbf{1} + b\boldsymbol{\mu}) = a + bm, since w⊤1=1\mathbf{w}^\top\mathbf{1} = 1 and w⊤μ=m\mathbf{w}^\top\boldsymbol{\mu} = m.

Rule: The minimum-variance frontier

σ2(m)=k1m2−2k2m+k3d\sigma^2(m) = \frac{k_1m^2 - 2k_2m + k_3}{d}

The lowest variance for each target return mm: a parabola in mm. Its vertex, at m=k2k1m = \frac{k_2}{k_1}, is the minimum-variance portfolio, with variance 1k1\frac{1}{k_1}.

Here k1=83.0812k_1 = 83.0812, k2=5.6611k_2 = 5.6611, k3=0.421027k_3 = 0.421027 and d=2.93109d = 2.93109. The vertex is at k2k1=6.81%\frac{k_2}{k_1} = 6.81\%, with variance 1k1=0.012036\frac{1}{k_1} = 0.012036: the minimum-variance portfolio again.

target mmABCvolatility
5%111.81%13.64%−25.46%-25.46\%14.62%
6%91.33%14.45%−5.78%-5.78\%11.80%
6.81%74.65%15.11%10.23%10.97%
8%50.36%16.07%33.57%12.66%
9%29.87%16.88%53.25%15.99%
10%9.39%17.69%72.92%20.20%
11%−11.10%-11.10\%18.50%92.60%24.84%
12%−31.58%-31.58\%19.30%112.28%29.71%

Plotted with volatility σ\sigma across and expected return up, the frontier is a hyperbola, the shape lesson 3.7.1 met as a level curve. Only its upper half is worth holding: the efficient frontier. A 6% target needs a volatility of 11.80%, and so does 7.63% on the upper half: same risk, more return.

The 50/30/20 portfolio lies inside the curve. For its 7.6% the frontier needs only 11.74%, against its 11.94%.

The frontier and its weights. With short sales the weights are straight lines in the target; long-only weights follow them inside the shaded band and bend away outside it.

Try it

The cell computes k1k_1, k2k_2, k3k_3 and dd, then k2k1\frac{k_2}{k_1} and 1k1\frac{1}{k_1}. frontier(m) is a Σ−11+b Σ−1μa\,\Sigma^{-1}\mathbf{1} + b\,\Sigma^{-1}\boldsymbol{\mu}.

The loop prints the table. Its last two columns are the volatility from the weights and from the rule: they agree.

⌘/Ctrl + Enter runs the cell

Two funds are enough

aa and bb are straight-line functions of mm, so the frontier weights are too. Each extra point of target return moves the weights by the same amounts: −20.49-20.49 points in A, +0.81+0.81 in B and +19.68+19.68 in C. In the figure they are three straight lines.

Rule: The two-fund theorem

w(m)=(1−t) w(m1)+t w(m2)t=m−m1m2−m1\begin{gathered} \mathbf{w}(m) = (1 - t)\,\mathbf{w}(m_1) + t\,\mathbf{w}(m_2) \\[4pt] t = \frac{m - m_1}{m_2 - m_1} \end{gathered}

Any frontier portfolio is a mix of any two others. So two funds on the frontier are enough to build every frontier portfolio, short sales allowed.

Take the 8% and 12% frontier portfolios as the two funds. Then 75% of the first and 25% of the second gives exactly the 9% portfolio, (29.87%, 16.88%, 53.25%). The minimum-variance portfolio and the 11% portfolio, mixed with t=0.5222t = 0.5222, give it too.

Try it

The first line is the change in the weights for one extra point of target. Then the two mixes, each next to the 9% portfolio.

The last line checks the inefficient half: 6% and 7.63% need the same volatility, and the frontier at 7.6% needs 11.74%.

⌘/Ctrl + Enter runs the cell
Check yourself
Fund P is the frontier portfolio for 8% and fund Q the one for 12%. Which mix of them is the frontier portfolio for 11%?

No short sales

Many funds may not sell short, so every weight must satisfy wi≥0w_i \ge 0. Each is an inequality constraint of lesson 4.5.2, with its own multiplier νi\nu_i (nu). A ≥\ge constraint has νi≥0\nu_i \ge 0.

Rule: Long-only KKT conditions

2(Σw)i=λ1+λ2μi+νiνi≥0,νi wi=0\begin{gathered} 2(\Sigma\mathbf{w})_i = \lambda_1 + \lambda_2\mu_i + \nu_i \\[4pt] \nu_i \ge 0, \quad \nu_i\,w_i = 0 \end{gathered}

For each asset held, νi=0\nu_i = 0: the frontier condition holds for it. For each asset left out, 2(Σw)i2(\Sigma\mathbf{w})_i is at least λ1+λ2μi\lambda_1 + \lambda_2\mu_i. Without a target return, drop λ2\lambda_2: the assets held share one value of (Σw)i(\Sigma\mathbf{w})_i, and any asset left out has at least that.

Two assets. Case study 1.2.4 met ρ=0.8\rho = 0.8, where the rule gives 123.53% in A and −23.53%-23.53\% in B. Long-only, try all in A, where Σw=(0.04,0.048)\Sigma\mathbf{w} = (0.04, 0.048).

B is left out, and its entry 0.048 is above A's 0.04, so the conditions hold. The problem is convex (lesson 4.5.3), so all in A is the best long-only portfolio: case study 1.2.4's "end nearest the vertex".

Three assets, no target. The minimum-variance weights are all positive, so the ban changes nothing.

Three assets, with a target. The frontier weights are all positive from 6.29% to 10.46%, the band in the figure. There the long-only frontier is the same.

Above 10.46%, the formula would sell A short. Hold A at 0 instead: B and C alone must meet both constraints, which fixes their weights.

At 10.8% that gives 6.67% in B and 93.33% in C, with volatility 23.96%, against 23.89% with short sales. Its νA=0.009042≥0\nu_A = 0.009042 \ge 0, so the conditions hold.

The sign test also catches a wrong guess. At 10%, holding A at 0 gives νA=−0.012124\nu_A = -0.012124, the wrong sign: A should be held. There the short-sales portfolio, (9.39%, 17.69%, 72.92%), is already long-only.

Below 6.29% it is C that would be sold short; at 6.2% the long-only portfolio is 90% A and 10% B. Long-only targets run only from 6% (all A) to 11% (all C), the lowest and highest expected returns. With these assets the ban costs at most 0.20 points of volatility, at the 6% end: 12.00% against 11.80%.

Try it

The first line is the two-asset case: the rule's short sale of B, then Σw\Sigma\mathbf{w} for all in A.

without_A holds A at 0 and finds B and C from the two constraints. It then solves for λ1\lambda_1 and λ2\lambda_2 from the rows of B and C, and returns νA\nu_A from A's row.

⌘/Ctrl + Enter runs the cell

With more assets, guessing which weights are 0 gets slow, and a solver does the search. SLSQP (lesson 4.5.2) takes the two equations as "eq" constraints and wi≥0w_i \ge 0 as bounds. Check that the target lies between the lowest and highest expected return first, and check r.success after.

Try it

long_only(m) minimises w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w} with bounds=[(0, 1)] * 3, which keeps each weight between 0 and 1. With the budget, wi≥0w_i \ge 0 already forces wi≤1w_i \le 1.

The answers match the cases above. No long-only portfolio expects 11.5%: every asset expects at most 11%.

⌘/Ctrl + Enter runs the cell

The explorer draws the frontier for the three assets. Set the target to 10.8% and read the weights with short sales allowed; then turn short sales off and watch A drop to 0. Move the target below 6.29% to see C do the same.

Explore

Every mix of the three assets is a point: its volatility across, its expected return up. Move the target return and read the cheapest mix in risk that earns it.

0%5%10%15%20%25%30%5%10%ABC

Grey dots are the assets, the open circle the minimum-variance portfolio.

Frontier portfolio
A 29.9%, B 16.9%, C 53.2%
Return and risk
expected return 9%, volatility 15.99%
Minimum variance
A 74.7%, B 15.1%, C 10.2%: return 6.81%, volatility 10.97%

On the desk

Try it

frontier is this case study's formula as a function of μ\boldsymbol{\mu} and Σ\Sigma. The first line compares the 9% weights from Σ\Sigma in decimals and in percent.

The loop adds the near-copy D and prints the smallest eigenvalue of Σ\Sigma and the 9% weights. The last line is the impossible Σ\Sigma: its eigenvalues, the rule's "variance", and the "variance" after moving along (1,−0.5,−0.5)(1, -0.5, -0.5) by 1 and by 10.

⌘/Ctrl + Enter runs the cell
Exercise

Minimum-variance and frontier portfolios

Write two functions, with NumPy, short sales allowed.

  1. min_variance(cov) returns the weights Σ−111⊤Σ−11\frac{\Sigma^{-1}\mathbf{1}}{\mathbf{1}^\top\Sigma^{-1}\mathbf{1}}. Use np.linalg.solve, not the inverse.
  2. frontier_portfolio(mu, cov, m) returns the frontier weights a Σ−11+b Σ−1μa\,\Sigma^{-1}\mathbf{1} + b\,\Sigma^{-1}\boldsymbol{\mu} for the target return m.

Both raise ValueError unless cov is square, symmetric and positive definite. frontier_portfolio also raises it if mu has the wrong length, or if every expected return is the same.

The tests use case study 1.2.4's two assets and these three, percent units, and the bad inputs from the desk notes. They check that each entry of Σw\Sigma\mathbf{w} equals the variance, and the two-fund theorem. They also check that no other portfolio with the same budget and return has less variance.

See the tests
import numpy as np

MU = np.array([0.06, 0.08, 0.11])
SIGMA = np.array([0.12, 0.18, 0.25])
R = np.array([[1.0, 0.3, 0.1], [0.3, 1.0, 0.5], [0.1, 0.5, 1.0]])
COV = np.diag(SIGMA) @ R @ np.diag(SIGMA)
TWO = np.array([[0.04, 0.012], [0.012, 0.09]])


def _weights(value, what):
    assert value is not None, f"{what} returned None: return the array of weights"
    return np.asarray(value, dtype=float)


def _raises_value_error(fn, *args):
    try:
        fn(*args)
    except ValueError:
        return True
    return False


def test_two_assets():
    """Case study 1.2.4's two assets: 73.58% in A and 26.42% in B"""
    w = _weights(min_variance(TWO), "min_variance")
    expect_close(w, [0.156 / 0.212, 1 - 0.156 / 0.212], what="min_variance for case study 1.2.4", rel=1e-9)


def test_three_assets():
    """The three assets: about 74.65% in A, 15.11% in B and 10.23% in C"""
    w = _weights(min_variance(COV), "min_variance")
    expect_close(w, [0.7465402, 0.1511109, 0.1023489], what="min_variance for A, B and C", rel=1e-5)
    expect_close(w.sum(), 1.0, what="sum of the weights")


def test_equal_entries():
    """At the minimum, every entry of Σw equals the variance, for a random 4-asset Σ too"""
    rng = np.random.default_rng(455)
    M = rng.normal(size=(6, 4))
    cov = M.T @ M / 20
    w = _weights(min_variance(cov), "min_variance")
    expect_close(cov @ w, np.full(4, w @ cov @ w), what="Σw against wᵀΣw in every entry", rel=1e-9)


def test_target_return():
    """The 9% frontier portfolio is about (29.87%, 16.88%, 53.25%), with volatility 15.99%"""
    w = _weights(frontier_portfolio(MU, COV, 0.09), "frontier_portfolio")
    expect_close(w, [0.2987268, 0.1687887, 0.5324845], what="frontier_portfolio(mu, cov, 0.09)", rel=1e-5)
    expect_close(np.sqrt(w @ COV @ w), 0.1599427, what="volatility at 9%", rel=1e-5)


def test_constraints_hold():
    """A frontier portfolio sums to 1 and earns its target, here 12%"""
    w = _weights(frontier_portfolio(MU, COV, 0.12), "frontier_portfolio")
    expect_close(w.sum(), 1.0, what="sum of the weights")
    expect_close(w @ MU, 0.12, what="expected return")


def test_no_lower_variance():
    """No other portfolio with the same budget and return has a lower variance"""
    w = _weights(frontier_portfolio(MU, COV, 0.09), "frontier_portfolio")
    # (3, −5, 2) keeps both the budget and the return: 3 − 5 + 2 = 0 and 0.18 − 0.40 + 0.22 = 0
    d = np.array([3.0, -5.0, 2.0])
    for step in [-0.01, 0.01, 0.1]:
        other = w + step * d
        assert other @ COV @ other > w @ COV @ w, f"moving by {step} × (3, −5, 2) lowered the variance: not the minimum"


def test_minimum_on_frontier():
    """The frontier portfolio at the minimum-variance return is the minimum-variance portfolio"""
    w0 = _weights(min_variance(COV), "min_variance")
    w = _weights(frontier_portfolio(MU, COV, float(w0 @ MU)), "frontier_portfolio")
    expect_close(w, w0, what="frontier_portfolio at 6.81%", rel=1e-8)


def test_two_funds():
    """Two-fund theorem: 75% of the 8% portfolio plus 25% of the 12% portfolio is the 9% portfolio"""
    w8 = _weights(frontier_portfolio(MU, COV, 0.08), "frontier_portfolio")
    w12 = _weights(frontier_portfolio(MU, COV, 0.12), "frontier_portfolio")
    w9 = _weights(frontier_portfolio(MU, COV, 0.09), "frontier_portfolio")
    expect_close(0.75 * w8 + 0.25 * w12, w9, what="the mix of two frontier portfolios", rel=1e-9)


def test_percent_units():
    """Volatilities typed in percent give the same weights"""
    pct = np.diag(100 * SIGMA) @ R @ np.diag(100 * SIGMA)
    expect_close(_weights(min_variance(pct), "min_variance"), _weights(min_variance(COV), "min_variance"), what="weights from Σ in percent", rel=1e-9)


def test_not_symmetric():
    """A covariance matrix with a typo in one triangle raises ValueError"""
    typo = COV.copy()
    typo[0, 1] = 0.0648
    assert _raises_value_error(min_variance, typo), "a matrix that is not symmetric should raise ValueError"


def test_not_positive_definite():
    """Impossible correlations, with a negative eigenvalue, raise ValueError"""
    bad = np.array([[1.0, 0.9, 0.9], [0.9, 1.0, -0.9], [0.9, -0.9, 1.0]])
    assert _raises_value_error(min_variance, np.diag(SIGMA) @ bad @ np.diag(SIGMA)), "Σ with a negative eigenvalue has no minimum: raise ValueError"


def test_equal_returns():
    """Equal expected returns leave no frontier: ValueError"""
    assert _raises_value_error(frontier_portfolio, [0.08, 0.08, 0.08], COV, 0.09), "with every μᵢ = 8%, no portfolio earns 9%: raise ValueError"


def test_wrong_length():
    """Two expected returns for three assets raise ValueError"""
    assert _raises_value_error(frontier_portfolio, [0.06, 0.08], COV, 0.07), "three assets need three expected returns"

Summary card

  • Minimising w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w} subject to 1⊤w=1\mathbf{1}^\top\mathbf{w} = 1: Lagrange gives Σw=λ21\Sigma\mathbf{w} = \frac{\lambda}{2}\mathbf{1}, so every entry of Σw\Sigma\mathbf{w} is equal, and equal to the variance. w=Σ−111⊤Σ−11\mathbf{w} = \frac{\Sigma^{-1}\mathbf{1}}{\mathbf{1}^\top\Sigma^{-1}\mathbf{1}}; for case study 1.2.4, wA=0.7358w_A = 0.7358.
  • For A, B and C: 74.65%, 15.11% and 10.23%, expected return 6.81%, volatility 10.97%. Σ\Sigma is positive definite, so it is the one global minimum.
  • A target return adds μ⊤w=m\boldsymbol{\mu}^\top\mathbf{w} = m: 2Σw=λ11+λ2μ2\Sigma\mathbf{w} = \lambda_1\mathbf{1} + \lambda_2\boldsymbol{\mu}. The frontier variance σ2(m)=k1m2−2k2m+k3d\sigma^2(m) = \frac{k_1m^2 - 2k_2m + k_3}{d} is a parabola in mm, a hyperbola in volatility; its upper half is efficient.
  • The frontier weights are straight lines in mm: any two frontier portfolios build all the others (the two-fund theorem).
  • Long-only, by KKT: each asset held has 2(Σw)i=λ1+λ2μi2(\Sigma\mathbf{w})_i = \lambda_1 + \lambda_2\mu_i, each asset left out at least that. Check Σ\Sigma's eigenvalues, units, near-copies and the solver's success before trusting weights.

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.