σμ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.3·~50 minIntermediate

Minimum-variance portfolio

Skill positionModule 5 of 11 · Topic 3 of 5
5.3 Minimum-variance portfolio
—Lab score— / 100Total XP—Streak—

The safest mix

Combining assets lowers risk, but by how much depends on the weights. The minimum-variance portfolio is the choice of weights, summing to 1, that has the smallest possible variance. It needs no forecast of returns, only the covariance matrix, which makes it popular: expected returns are very hard to estimate and covariances much less so. For two assets the answer is a formula; for many it is the solution of a linear system.

# Two assets: volatility 20% and 10%, correlation 0.3
def variance(w, vol1, vol2, rho):
    return w * w * vol1 ** 2 + (1 - w) ** 2 * vol2 ** 2 + 2 * w * (1 - w) * rho * vol1 * vol2

vol1, vol2, rho = 0.20, 0.10, 0.3
w = (vol2 ** 2 - rho * vol1 * vol2) / (vol1 ** 2 + vol2 ** 2 - 2 * rho * vol1 * vol2)
print(round(w, 4), round(variance(w, vol1, vol2, rho) ** 0.5, 4))        # 0.1053 0.0979
print(round(variance(0.0, vol1, vol2, rho) ** 0.5, 4))                   # 0.1      all in the safe asset: riskier than the mix

Many assets: one linear system

To minimise w @ cov @ w subject to sum(w) = 1, set the derivative of the Lagrangian to zero. The result is w = cov^-1 * 1 / (1' cov^-1 1): solve the system cov @ x = ones, and rescale x so the weights add up to 1. The minimum variance itself is 1 / (1' cov^-1 1). In code use np.linalg.solve(cov, ones), not np.linalg.inv(cov) @ ones: it is faster and more accurate, because it never forms the inverse.

# Minimum-variance weights of three assets
import numpy as np

cov = np.array([[0.0400, 0.0060, 0.0100],
                [0.0060, 0.0100, 0.0020],
                [0.0100, 0.0020, 0.0225]])

x = np.linalg.solve(cov, np.ones(3))
weights = x / x.sum()
print(np.round(weights, 4), round(float(weights.sum()), 4))        # [0.0192 0.7082 0.2726] 1.0
print(round(float(weights @ cov @ weights), 6), round(1 / float(x.sum()), 6))      # 0.007742 twice: the same number
# No other mix with weights summing to 1 does better: try 5000 random ones
import numpy as np

cov = np.array([[0.0400, 0.0060, 0.0100],
                [0.0060, 0.0100, 0.0020],
                [0.0100, 0.0020, 0.0225]])
x = np.linalg.solve(cov, np.ones(3))
best = x / x.sum()

rng = np.random.default_rng(0)
trial = rng.normal(size=(5000, 3))
trial = trial / trial.sum(axis=1, keepdims=True)                  # each row sums to 1
variances = np.einsum("ij,jk,ik->i", trial, cov, trial)            # w @ cov @ w for every row
print(round(float(variances.min()), 6), round(float(best @ cov @ best), 6))
print(bool(variances.min() >= best @ cov @ best))                  # True

Shorts and constraints

The formula allows negative weights: it may short one asset to hedge another. Many investors cannot or will not, and want long-only weights. Clipping the negative weights to zero and rescaling is a quick fix that keeps the weights valid, but it is not the true long-only optimum, which needs a numerical solver (quadratic programming) because the constraint w >= 0 makes the problem inequality-constrained. The clipped portfolio is at least safe to hold and easy to compare with the unconstrained one.

# The unconstrained solution shorts an asset; clip it and compare the risks
import numpy as np

cov = np.array([[0.04, 0.0342, 0.0],
                [0.0342, 0.0324, 0.0],
                [0.0, 0.0, 0.0225]])        # assets 1 and 2: volatility 20% and 18%, correlation 0.95

x = np.linalg.solve(cov, np.ones(3))
free = x / x.sum()
clipped = np.clip(free, 0, None)
clipped = clipped / clipped.sum()
print(np.round(free, 4), round(float(free @ cov @ free), 6))
print(np.round(clipped, 4), round(float(clipped @ cov @ clipped), 6))
Optimal in theory, fragile in practiceThe weights use the inverse of the covariance matrix, which amplifies errors: assets that look slightly less correlated or less volatile in the sample get large weights, often with big shorts, and the weights jump when the data window moves. Practitioners shrink the covariance estimate, bound the weights, or add a penalty. A minimum-variance portfolio that looks great on the data it was fitted to is tested on later data, not on the same sample.
  • Minimum variance needs only the covariance matrix: for two assets (vol2**2 - rho * vol1 * vol2) / (vol1**2 + vol2**2 - 2 * rho * vol1 * vol2) in asset 1.
  • In general w = solve(cov, ones) / sum(...) and the minimum variance is 1 / (1' cov^-1 1); use np.linalg.solve, not the inverse.
  • The unconstrained answer may short assets; clipping to zero and rescaling is a quick long-only fix, not the true optimum, and the weights are fragile to estimation error.

Next up: mean-variance optimisation, adding expected returns to find the best trade-off between return and risk.

Lab · what's graded

  • Mix two assets for least risk with two_asset_weight25 pts
  • Solve for the safest weights with min_variance_weights25 pts
  • Report the lowest risk with min_variance25 pts
  • Drop the shorts with long_only_weights25 pts
Explorer · lab-84-1
▾ lab-84-1
…
bash · in-browser
learner@quantcraft:~/lab-84-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-84-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