Markets: A Rigorous Course

Exercise · m05_almgren_chriss.py

Almgren-Chriss optimal execution, VWAP and TWAP

Sell X shares over [0, T] in N equal intervals of length tau = T / N. Holdings x_0 = X, x_1, ..., x_N = 0; the k-th child order sells n_k = x_{k-1} - x_k.

Price dynamics (linear impact): S_k = S_{k-1} + sigma sqrt(tau) xi_k - gamma n_k (permanent impact) execution price of child k = S_{k-1} - epsilon sgn(n_k) - (eta / tau) n_k (temporary impact)

Implementation shortfall IS = X S_0 - sum n_k * (execution price) has E[IS] = gamma X^2 / 2 + epsilon sum |n_k| + (eta_tilde / tau) sum n_k^2, Var[IS] = sigma^2 tau sum_{k=1}^{N} x_k^2, eta_tilde = eta - gamma tau / 2 (notes eq. (5.20)).

Proposition 5.5: minimising E + lam Var gives x_j = X sinh(kappa (T - t_j)) / sinh(kappa T), (2 / tau^2)(cosh(kappa tau) - 1) = lam sigma^2 / eta_tilde.

Check: cd exercises && .venv/bin/python -m pytest modules/M05-intraday-execution/tests

Tests (8)

  • test_closed_form_matches_numerical
  • test_lambda_zero_is_twap
  • test_more_risk_aversion_trades_faster
  • test_closed_form_is_minimum_of_objective
  • test_efficient_frontier_monotone
  • test_cost_formula_twap
  • test_discrete_converges_to_continuous
  • test_vwap_and_shortfall

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

not run yet

Not saved: sign up to keep your code
"""Exercise 5.3: Almgren-Chriss optimal execution, VWAP and TWAP (notes.md §5.6-5.7).

Sell X shares over [0, T] in N equal intervals of length tau = T / N.
Holdings x_0 = X, x_1, ..., x_N = 0; the k-th child order sells n_k = x_{k-1} - x_k.

Price dynamics (linear impact):
    S_k = S_{k-1} + sigma sqrt(tau) xi_k - gamma n_k          (permanent impact)
    execution price of child k = S_{k-1} - epsilon sgn(n_k) - (eta / tau) n_k   (temporary impact)

Implementation shortfall IS = X S_0 - sum n_k * (execution price) has
    E[IS] = gamma X^2 / 2 + epsilon sum |n_k| + (eta_tilde / tau) sum n_k^2,
    Var[IS] = sigma^2 tau sum_{k=1}^{N} x_k^2,
    eta_tilde = eta - gamma tau / 2          (notes eq. (5.20)).

Proposition 5.5: minimising E + lam Var gives
    x_j = X sinh(kappa (T - t_j)) / sinh(kappa T),
    (2 / tau^2)(cosh(kappa tau) - 1) = lam sigma^2 / eta_tilde.

Check:
    cd exercises && .venv/bin/python -m pytest modules/M05-intraday-execution/tests
"""

from __future__ import annotations

import numpy as np  # noqa: F401
from scipy.optimize import minimize  # noqa: F401


def eta_tilde(eta: float, gamma: float, tau: float) -> float:
    """eta - gamma * tau / 2."""
    raise NotImplementedError


def ac_cost_mean_var(x: np.ndarray, T: float, sigma: float, eta: float, gamma: float, epsilon: float = 0.0) -> tuple[float, float]:
    """(E[IS], Var[IS]) for holdings x (length N + 1, x[0] = X, x[-1] = 0) using the formulas above."""
    raise NotImplementedError


def ac_kappa(T: float, N: int, sigma: float, eta: float, gamma: float, lam: float) -> float:
    """Solve (2 / tau^2)(cosh(kappa tau) - 1) = lam sigma^2 / eta_tilde for kappa > 0 (use arccosh)."""
    raise NotImplementedError


def ac_trajectory(X: float, T: float, N: int, sigma: float, eta: float, gamma: float, lam: float) -> np.ndarray:
    """Closed-form optimal holdings x_0..x_N (length N + 1).

    x_j = X sinh(kappa (T - t_j)) / sinh(kappa T), t_j = j T / N.
    For lam == 0 return the straight line X (1 - t_j / T).
    """
    raise NotImplementedError


def ac_numerical(X: float, T: float, N: int, sigma: float, eta: float, gamma: float, lam: float) -> np.ndarray:
    """Minimise E + lam Var numerically over x_1..x_{N-1} with scipy.optimize.minimize.

    Return the full holdings vector x_0..x_N. Hints: the gamma X^2/2 and
    epsilon X terms do not depend on the schedule for a pure sell program;
    rescale to w = x / X and divide the objective by eta_tilde X^2 / tau so the
    problem is well conditioned; supply the analytic gradient; use BFGS with a
    tight gtol (1e-12).
    """
    raise NotImplementedError


def ac_continuous(X: float, T: float, t: np.ndarray, sigma: float, eta: float, lam: float) -> np.ndarray:
    """Continuous-time limit x(t) = X sinh(k (T - t)) / sinh(k T), k = sqrt(lam sigma^2 / eta)."""
    raise NotImplementedError


def twap_schedule(X: float, N: int) -> np.ndarray:
    """N child orders of X / N each."""
    raise NotImplementedError


def vwap(prices: np.ndarray, volumes: np.ndarray) -> float:
    """sum p_i v_i / sum v_i."""
    raise NotImplementedError


def vwap_schedule(X: float, volume_profile: np.ndarray) -> np.ndarray:
    """Child orders proportional to an expected volume profile: X v_k / sum v."""
    raise NotImplementedError


def implementation_shortfall_bps(side: str, decision_price: float, fill_prices: np.ndarray, fill_qty: np.ndarray) -> float:
    """Shortfall of the executed quantity versus the decision price, in bps; positive = cost.

    buy:  (avg_fill - decision) / decision * 1e4
    sell: (decision - avg_fill) / decision * 1e4
    with avg_fill the VWAP of the fills.
    """
    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.