Markets: A Rigorous Course

Exercise · m03_kelly.py

The Kelly criterion

Implement every function, then press Run tests.

numpy only. Notation: p = win probability, q = 1 - p, b = net odds (a winning unit stake returns +b, a losing one -1), f = fraction of wealth staked.

Run locally

From the repository root, with the exercises environment installed:

cd exercises && .venv/bin/python -m pytest modules/M03-betting-kelly-ruin/tests/test_m03_kelly.py

Tests (8)

  • test_kelly_binary_values
  • test_monte_carlo_growth_matches_analytic_and_peaks_at_kelly
  • test_overbetting_even_money
  • test_overbetting_long_shot_counterexample
  • test_continuous_kelly_and_growth
  • test_multi_asset_kelly
  • test_fractional_kelly_hit_probability
  • test_estimation_risk_shrinkage

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

not run yet

Not saved: sign up to keep your code
"""Exercise: the Kelly criterion (notes §3.2-3.5).

Implement every function, then run from the repo root:

    cd exercises && .venv/bin/python -m pytest modules/M03-betting-kelly-ruin/tests/test_m03_kelly.py

numpy only. Notation: p = win probability, q = 1 - p, b = net odds (a winning
unit stake returns +b, a losing one -1), f = fraction of wealth staked.
"""

from __future__ import annotations

import numpy as np


def kelly_binary(p: float, b: float) -> float:
    """Kelly fraction for a binary bet: f* = p - q / b  (notes Prop. 3.2)."""
    raise NotImplementedError


def growth_binary(f, p: float, b: float):
    """Expected log growth per bet g(f) = p ln(1 + b f) + q ln(1 - f) (notes eq. 3.4).

    Vectorised over f (scalar in -> float out; array in -> array out).
    Return -inf where f >= 1 (a loss wipes you out). Use np.log1p.
    """
    raise NotImplementedError


def simulate_growth_binary(f: float, p: float, b: float, n_bets: int, n_paths: int, seed: int) -> np.ndarray:
    """Monte Carlo of realised per-bet log growth (1/n) ln(W_n / W_0) on n_paths
    independent paths of n_bets bets each, staking fraction f every time.

    rng = np.random.default_rng(seed); the number of wins on each path is
    rng.binomial(n_bets, p, size=n_paths) (order does not matter for W_n).
    Return the array of length n_paths. Its mean should approach g(f) (Prop. 3.3).
    """
    raise NotImplementedError


def zero_growth_fraction(p: float, b: float, tol: float = 1e-12) -> float:
    """The over-bet fraction f0 in (f*, 1) at which g(f0) = 0. Use bisection on
    [f*, 1): g > 0 between 0 and f0 and g < 0 beyond. Raise ValueError if f* <= 0.
    """
    raise NotImplementedError


def kelly_continuous(mu: float, r: float, sigma: float) -> float:
    """Continuous-time Kelly fraction in a GBM asset: f* = (mu - r) / sigma^2 (Prop. 3.6)."""
    raise NotImplementedError


def growth_continuous(f, mu: float, r: float, sigma: float):
    """Growth rate of log wealth at constant fraction f:
    g(f) = r + f (mu - r) - f^2 sigma^2 / 2  (notes eq. 3.9). Vectorised over f.
    """
    raise NotImplementedError


def simulate_growth_gbm(f: float, mu: float, r: float, sigma: float, T: float, n_steps: int, n_paths: int, seed: int) -> np.ndarray:
    """Discrete-rebalancing check of growth_continuous.

    dt = T / n_steps. Each step draw z ~ N(0,1) (rng.standard_normal(n_paths)),
    risky gross return G = exp((mu - sigma^2/2) dt + sigma sqrt(dt) z), and update
        ln W += ln(1 + r dt + f (G - 1 - r dt)).
    Return the length-n_paths array (1/T) ln(W_T / W_0).
    """
    raise NotImplementedError


def kelly_multi(mu, r: float, cov) -> np.ndarray:
    """Multi-asset Kelly weights f* = Sigma^{-1} (mu - r 1) (Prop. 3.10). Use np.linalg.solve."""
    raise NotImplementedError


def growth_multi(f, mu, r: float, cov) -> float:
    """g(f) = r + f'(mu - r 1) - f' Sigma f / 2."""
    raise NotImplementedError


def fractional_kelly_hit_prob(c: float, x: float) -> float:
    """Probability that wealth ever falls to fraction x in (0,1) of its starting
    value when betting c times Kelly in the GBM model: x^(2/c - 1) for 0 < c < 2,
    and 1 for c >= 2 (Prop. 3.8).
    """
    raise NotImplementedError


def simulate_hit_prob(c: float, x: float, sharpe: float, T: float, n_steps: int, n_paths: int, seed: int) -> float:
    """Monte Carlo of fractional_kelly_hit_prob, with r = 0 and asset Sharpe ratio `sharpe`.

    Log-wealth Y is Brownian motion with drift m = sharpe^2 (c - c^2/2) and
    volatility s = c * sharpe, started at 0; the barrier is L = ln x.
    Simulate on a grid dt = T / n_steps. A path is absorbed at the first step where
    either Y_new <= L, or (Brownian-bridge correction for a crossing between grid
    points) a uniform draw is below exp(-2 (Y_old - L)(Y_new - L) / (s^2 dt)).
    Return the fraction of paths absorbed by time T.
    """
    raise NotImplementedError


def expected_growth_estimated_kelly(k: float, mu_ex: float, sigma: float, se: float) -> float:
    """Estimation risk (Prop. 3.9). You bet f = k * mu_hat / sigma^2 where the
    estimate mu_hat ~ N(mu_ex, se^2) and mu_ex is the true excess return.
    Return the expected excess growth E[g(f)] - r:
        k mu_ex^2 / sigma^2 - k^2 (mu_ex^2 + se^2) / (2 sigma^2).
    """
    raise NotImplementedError


def simulate_growth_estimated_kelly(k: float, mu_ex: float, sigma: float, se: float, n_sims: int, seed: int) -> float:
    """Monte Carlo of the previous function: draw n_sims values of mu_hat with
    rng.normal(mu_ex, se, n_sims), set f = k mu_hat / sigma^2, and average
    f mu_ex - f^2 sigma^2 / 2.
    """
    raise NotImplementedError


def optimal_shrinkage(t_stat: float) -> float:
    """The k maximising expected_growth_estimated_kelly: k* = t^2 / (1 + t^2), t = mu_ex / se."""
    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.