Exercise · m03_kelly.py
The Kelly criterion
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.