σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 5 · Portfolio optimisation›Topic 5.4·~55 minIntermediate

Mean-variance optimisation

Skill positionModule 5 of 11 · Topic 4 of 5
5.4 Mean-variance optimisation
—Lab score— / 100Total XP—Streak—

Return and risk in one number

Minimum variance ignores returns. Mean-variance optimisation (Markowitz) uses both: an investor wants a high expected return w @ mu and a low variance w @ cov @ w. A simple way to trade them off is the utility w @ mu - 0.5 * lambda * w @ cov @ w, where lambda is the risk aversion: the higher it is, the more a unit of variance costs. Two investors with the same beliefs but different lambda choose different portfolios, and the best portfolio for each is the maximum of a smooth, concave function.

import numpy as np

mu = np.array([0.08, 0.05, 0.10])                         # expected annual returns
cov = np.array([[0.0400, 0.0060, 0.0100],
                [0.0060, 0.0100, 0.0020],
                [0.0100, 0.0020, 0.0225]])

def utility(weights, mu, cov, risk_aversion):
    return weights @ mu - 0.5 * risk_aversion * weights @ cov @ weights

equal = np.full(3, 1 / 3)
print(round(float(equal @ mu), 4), round(float(equal @ cov @ equal), 4))      # 0.0767 0.0121
print(round(float(utility(equal, mu, cov, 4.0)), 4))                          # 0.0526

The best position for a given risk aversion

With no budget constraint (the rest of the money earns the risk-free rate, taken here as zero in excess terms) the best position is where the gradient mu - lambda * cov @ w is zero: w = cov^-1 * mu / lambda. In code that is np.linalg.solve(cov, mu) / lambda. It scales like common sense says: twice the risk aversion, half the position. The sum of the weights is whatever it needs to be; the remainder, 1 - w.sum(), is cash.

# The best position for several risk aversions: more aversion, smaller bets
import numpy as np

mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
                [0.0060, 0.0100, 0.0020],
                [0.0100, 0.0020, 0.0225]])

for aversion in (5.0, 10.0, 20.0):
    w = np.linalg.solve(cov, mu) / aversion
    print(aversion, np.round(w, 3), "invested", round(float(w.sum()), 3))

The tangency portfolio

With a risk-free asset, every investor holds the same risky mix, only in different amounts. That mix is the one with the highest Sharpe ratio (w @ mu - rf) / sqrt(w @ cov @ w), the tangency portfolio: w proportional to cov^-1 * (mu - rf), rescaled to sum to 1. The cautious investor puts only part of the money in it and the rest in cash; the bold one borrows to hold more. Deciding how risky a portfolio to hold and deciding which risky assets to hold are two separate problems.

# The tangency portfolio: no other mix of the three assets has a higher Sharpe ratio
import numpy as np

mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
                [0.0060, 0.0100, 0.0020],
                [0.0100, 0.0020, 0.0225]])
rf = 0.02

x = np.linalg.solve(cov, mu - rf)
best = x / x.sum()

def sharpe(w):
    return (w @ mu - rf) / np.sqrt(w @ cov @ w)

rng = np.random.default_rng(1)
trials = rng.normal(size=(2000, 3))
trials = trials / trials.sum(axis=1, keepdims=True)
print(np.round(best, 4), round(float(sharpe(best)), 4))
print(round(max(float(sharpe(w)) for w in trials), 4))      # the best of 2000 random mixes comes close, never above

Hitting a target return

Another way to ask the question: among all portfolios that sum to 1 and have a given expected return, which has the least variance? With two constraints, the solution is a combination of the two solved vectors s1 = cov^-1 * 1 and sm = cov^-1 * mu, with coefficients from four numbers a, b, c, d that summarise the market. Each target return gives one portfolio; plotting the risk of all of them is the efficient frontier of the next topic.

# Minimum-variance portfolios for three target returns
import numpy as np

mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
                [0.0060, 0.0100, 0.0020],
                [0.0100, 0.0020, 0.0225]])
ones = np.ones(3)
s1, sm = np.linalg.solve(cov, ones), np.linalg.solve(cov, mu)
a, b, c = ones @ s1, ones @ sm, mu @ sm
d = a * c - b ** 2

for target in (0.06, 0.08, 0.09):
    w = ((c - b * target) * s1 + (a * target - b) * sm) / d
    print(target, np.round(w, 3), round(float(w @ mu), 4), round(float(np.sqrt(w @ cov @ w)), 4))
The optimiser maximises your errorsMean-variance weights are very sensitive to mu: a small change in one expected return can swing the portfolio from one asset to another. The optimiser trusts the inputs completely, so any overestimated return becomes a big position. In practice returns are shrunk, weights are bounded, and the inputs come from models or views rather than raw past averages. Mean-variance is a good tool for thinking and a dangerous one for blind use.
  • Utility is w @ mu - 0.5 * lambda * w @ cov @ w; the best position without a budget is solve(cov, mu) / lambda, and doubling lambda halves it.
  • With a risk-free asset the best risky mix is the tangency portfolio solve(cov, mu - rf), rescaled to sum to 1: the highest Sharpe ratio.
  • For a target return, the minimum-variance weights are a combination of solve(cov, ones) and solve(cov, mu); the result is only as good as the estimated mu.

Next up: the efficient frontier, the curve of the best risk for every level of return.

Lab · what's graded

  • Score a portfolio with utility25 pts
  • Find the best position with utility_weights25 pts
  • Maximise the Sharpe ratio with tangency_weights25 pts
  • Hit a return target with target_return_weights25 pts
Explorer · lab-85-1
▾ lab-85-1
…
bash · in-browser
learner@quantcraft:~/lab-85-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-85-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub