Markets: A Rigorous Course

Exercise · m14_portfolio.py

Mean-variance closed forms, risk parity / ERC, Euler risk contributions, shrinkage

Exercise stubs. See notes.md 14.1–14.7. Notation: mu (n,) expected returns, cov (n,n) positive-definite covariance, 1 = ones(n), A = 1'S^-1 1, B = 1'S^-1 mu, C = mu'S^-1 mu, D = AC - B^2. Use np.linalg.solve, not inv.

Tests (9)

  • test_min_variance_closed_form_vs_numeric
  • test_tangency_closed_form_vs_numeric_and_sharpe
  • test_frontier_and_two_fund_separation
  • test_euler_contributions_sum_to_vol
  • test_erc
  • test_combining_two_strategies
  • test_tangency_is_sensitive_to_mean_errors
  • test_ledoit_wolf_beats_sample_when_n_large
  • test_vol_target

Theory: M14 notes. Run with Ctrl/⌘+Enter.

not run yet

Not saved: sign up to keep your code
"""M14 · Mean-variance closed forms, risk parity / ERC, Euler risk contributions, shrinkage.

Exercise stubs. See notes.md §14.1–§14.7.
Notation: mu (n,) expected returns, cov (n,n) positive-definite covariance, 1 = ones(n),
A = 1'S^-1 1, B = 1'S^-1 mu, C = mu'S^-1 mu, D = AC - B^2. Use np.linalg.solve, not inv.
"""

from __future__ import annotations

import numpy as np


def portfolio_vol(w, cov) -> float:
    """sigma(w) = sqrt(w' S w) as a float."""
    raise NotImplementedError


def min_variance_weights(cov) -> np.ndarray:
    """Global minimum-variance portfolio w = S^-1 1 / (1'S^-1 1)  (Proposition 14.1)."""
    raise NotImplementedError


def tangency_weights(mu, cov, rf: float = 0.0) -> np.ndarray:
    """Tangency portfolio w = S^-1 (mu - rf 1) / 1'S^-1 (mu - rf 1)  (Proposition 14.3)."""
    raise NotImplementedError


def frontier_weights(mu, cov, target: float) -> np.ndarray:
    """Frontier portfolio with mean `target`: w = [(C - B m) S^-1 1 + (A m - B) S^-1 mu] / D  (Prop. 14.2)."""
    raise NotImplementedError


def frontier_variance(mu, cov, target: float) -> float:
    """Frontier variance sigma^2(m) = (A m^2 - 2 B m + C)/D."""
    raise NotImplementedError


def max_sharpe(mu, cov, rf: float = 0.0) -> float:
    """Tangency Sharpe ratio sqrt((mu - rf)' S^-1 (mu - rf))  (Corollary 14.4)."""
    raise NotImplementedError


def numeric_min_variance(cov) -> np.ndarray:
    """Minimise w'Sw subject to sum(w) = 1 with scipy.optimize.minimize (SLSQP, ftol ~1e-15).
    Shorting allowed (no bounds). Used to check the closed form."""
    raise NotImplementedError


def numeric_tangency(mu, cov, rf: float = 0.0) -> np.ndarray:
    """Maximise (w'mu - rf)/sqrt(w'Sw) subject to sum(w) = 1 numerically (SLSQP). No bounds."""
    raise NotImplementedError


def risk_contributions(w, cov) -> np.ndarray:
    """Euler risk contributions RC_i = w_i (S w)_i / sigma(w). They sum to sigma(w) (Prop. 14.7)."""
    raise NotImplementedError


def inverse_vol_weights(cov) -> np.ndarray:
    """w_i proportional to 1/sigma_i, normalised to sum to 1."""
    raise NotImplementedError


def erc_weights(cov, budgets=None, tol: float = 1e-12, max_iter: int = 200) -> np.ndarray:
    """Long-only risk-budget weights summing to 1 with RC_i / sigma = b_i (equal b_i = 1/n if None).

    Suggested method (Proposition 14.8): minimise the strictly convex
        f(y) = 0.5 y'S y - sum_i b_i log y_i   over y > 0
    by damped Newton (gradient S y - b/y, Hessian S + diag(b/y^2), backtrack to stay positive),
    then return y / sum(y). Results must satisfy the tests to ~1e-8.
    """
    raise NotImplementedError


def combined_sharpe_optimal(s1: float, s2: float, rho: float) -> float:
    """Max Sharpe of two strategies combined: sqrt((s1^2 + s2^2 - 2 rho s1 s2)/(1 - rho^2))  (Prop. 14.12)."""
    raise NotImplementedError


def combined_sharpe_equal_risk(s1: float, s2: float, rho: float) -> float:
    """Sharpe of an equal-volatility 50/50 mix: (s1 + s2)/sqrt(2(1 + rho))."""
    raise NotImplementedError


def ledoit_wolf_identity(returns) -> tuple[np.ndarray, float]:
    """Ledoit-Wolf (2004) shrinkage toward m I (notes eq. 14.5). Return (shrunk covariance, delta).

    X = demeaned (T, n) returns; S = X'X/T; m = tr(S)/n; F = m I;
    d2 = ||S - F||_F^2 / n;  b2_bar = (1/T^2) sum_t ||x_t x_t' - S||_F^2 / n;
    b2 = min(b2_bar, d2);  delta = b2/d2;  shrunk = delta F + (1 - delta) S.
    """
    raise NotImplementedError


def vol_target_leverage(sigma_hat: float, target: float, cap: float = 2.0) -> float:
    """Exposure multiplier min(target/sigma_hat, cap)."""
    raise NotImplementedError

No runs yet

The stub's functions raise NotImplementedError until you write them. Fill in one, press Run tests, and watch its tests turn green.

Reference solution

Available after your first run, with an account approved for code runs.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.