Kelly Criterion for Discrete Multiple Investment Games¶
The Kelly criterion gives an optimal strategy for the long-term growth in risk-taking games where the player has an advantage. In its basic form, the Kelly strategy states that one should invest a fraction equal to the ratio of the expected return to the winning return. However, there is little literature online for a procedure to apply the Kelly Strategy for a multiple investment case. This notebook aims to demonstrate an application of this.
In this short notebook, we shall
- Generalise the single investment game to a case with multiple parallel investments
- Simulate the Kelly Criterion for a 2-coin game.
- Calculate the optimal fractional investment vector using simple Machine Learning (Gradient Descent)
Single Investment Case¶
Suppose we have initial assets $V_0$ and we play a game with $M$ outcomes with probabilities $p(i), \; i=1,...,M$. For outcome $i$, the return on investment is $k(i)$. The problem then becomes calculating what fraction of his assets he should bet in the game, assuming the reamining assets are unchanged. After $N$ plays, the total value of his assets will be
$$V_N = V_0(1+fk(1))^{n_1}(1+fk(2))^{n_2}...(1+fk(M))^{n_M} = V_0\prod_{i=1}^M (1+fk(i))^{n_i}$$
where $n_i$ is the number of outcomes for the $i$th event, and $f$ is the investment fraction with $0 \leq f \leq 1$, assumed constant throughout. The Kelly criterion for $f$ involves optimising the growth
$$G = \lim_{N \rightarrow \infty} \frac{1}{N}\log{\frac{V_N}{V_0}}$$
Taking derivatives one obtains the condition
$\frac{dG}{df} = \lim_{N \to \infty} \frac{1}{N V_N} \frac{dV_N}{df} = 0$
Substituting (1) into (3) one obtains
$\frac{1}{N} \sum_{i=1}^M \frac{n_i k(i)}{1+fk(i)} = 0$
For large $N$, we expect that $n_i/N \approx p(i)$, hence we obtain the criterion [13, 16, 17]
$\sum_{i=1}^M \frac{k(i) p(i)}{1+fk(i)} = 0$
B. Multiple investment case¶
We can generalize the above to the case of multiple parallel investments. Consider $L$ investments made in parallel, with a fraction $f_l$ of the investor’s assets allocated to each. In this case the total fraction invested is then
$0 \leq \sum_{l=1}^{L} f_l \leq 1$
and the remaining amount is left as cash. The return of the $i$th outcome for the $l$th investment is written $k_l(i)$. The various outcomes of all the $L$ investments is then labeled by the outcome $i = (i_1, i_2, \ldots, i_L)$, which occur with a probability $p(i) \equiv p(i_1, i_2, \ldots, i_L)$. In the event of the outcome $i$ his assets would be
$V_0 \Bigl(1 - \sum_{l=1}^L f_l\Bigr) + V_0 \sum_{l=1}^L f_l (1+k_l(i_l)) = V_0 \Bigl(1 + \sum_{l=1}^L f_l k_l(i_l)\Bigr)$
where the first term is the uninvested fraction and the second gives the return on each investment. In a similar way to (1) one can then write down the assets of the investor after $N$ plays of the game
$V_N = V_0 \prod_{i_1,i_2,\ldots,i_L} \left( 1 + \sum_{l=1}^L f_l k_l(i_l) \right)^{n_i}$
where $n_i \equiv n_{i_1, i_2, \ldots, i_L}$ is the number of times the $i$th outcome has occurred. Since there are $L$ fractions $f_l$, we have $L$ different equations (3) where the derivative is taken with respect to $f_l$. For the $l$th investment fraction we obtain the constraint
$\sum_{i_1,i_2,\ldots,i_L} \frac{k_l(i_l) p(i_1, i_2, \ldots, i_L)}{1+\sum_{r=1}^L f_r k_r(i_r)} = 0$
where we have assumed that $N$ is sufficiently large enough that $n_i/N \approx p(i)$. This is a set of $L$ equations that must be solved for the $L$ unknown fractions $f_l$.
Applying the Kelly Criterion for Multiple Investments: 2-Coin Game Example¶
We illustrate the multiple investment case of the Kelly Criterion using a simple game with two biased coins.
Game Setup¶
Coin 1 (C1):
- $P(H) = p_1$, $P(T) = 1 - p_1$
- Heads ($H$) is defined as a win with return $k_{1,H}$
- Tails ($T$) is a loss with return $k_{1,T}$
Coin 2 (C2):
- $P(H) = p_2$, $P(T) = 1 - p_2$
- Heads ($H$) is defined as a loss with return $k_{2,H}$
- Tails ($T$) is a win with return $k_{2,T}$
Each round, we flip both coins independently and update wealth accordingly.
Wealth Dynamics¶
We start with initial wealth $V_0$.
On each play, we invest fractions $f_1$ and $f_2$ of our wealth into Coin 1 and Coin 2 respectively.
- Amount wagered on Coin 1: $w_1 = f_1 V$
- Amount wagered on Coin 2: $w_2 = f_2 V$
The outcome of the flips modifies these wagers:
For Coin 1:
$w_1 \to w_1(1+k_{1,H})$ if $H$, else $w_1(1+k_{1,T})$For Coin 2:
$w_2 \to w_2(1+k_{2,T})$ if $T$, else $w_2(1+k_{2,H})$
The new wealth after one round is:
$$ V' = V(1 - f_1 - f_2) + w_1 + w_2 $$
where $(1 - f_1 - f_2)V$ is the portion of wealth left uninvested.
Growth Rate Estimation¶
To evaluate the long-term performance, we run $N$ plays of the game and track wealth evolution.
The average exponential growth rate of wealth is:
$$ g(f_1, f_2) = \frac{1}{N} \log\left(\frac{V_N}{V_0}\right) $$
We then average this growth rate over multiple runs to account for randomness in coin flips.
Simulation and Optimization¶
- We simulate many repetitions of the 2-coin game.
- For each pair $(f_1, f_2)$, we compute the average growth $g(f_1, f_2)$.
- By sweeping over a grid of $(f_1, f_2)$ values, we can plot the 3D growth surface.
- The optimal $(f_1^*, f_2^*)$ is the pair of investment fractions that maximizes expected growth.
Results¶
The 3D surface plot shows how growth depends on the choice of $f_1$ and $f_2$.
- The peak of the surface corresponds to the optimal Kelly fractions.
- These fractions tell us how much wealth to invest in each coin to maximize long-term exponential growth.
Key Insight¶
This example demonstrates the multiple investment case of the Kelly Criterion:
Instead of betting everything on a single outcome, the investor spreads wealth across multiple assets (here, two biased coins). The Kelly fractions balance risk and reward to maximize long-term growth.
import numpy as np
import matplotlib.pyplot as plt
import math
import random
from mpl_toolkits.mplot3d import axes3d
# Coin 1 flip : It will return a number between 1 and 100
# We let H for Coin 1 be a win and T a loss, and give a value for P(H) and P(T).
def flip1(p1):
return 'H' if random.random() < p1 else 'T'
# Coin 2 flip : It will return a number between 1 and 100
# We let H for Coin 2 be a loss and T a win, and give a value for P(H) and P(T).
def flip2(p2):
return 'H' if random.random() < p2 else 'T'
# This function simulates a play of the game
# we flip the 2 coins, getting an outcome for each, and return both outcomes
def playGame(p1, p2):
result1 = flip1(p1)
result2 = flip2(p2)
outcome = [result1, result2]
return outcome
# We calculate the new value of wealth generated from playing the game
# We wager a proportion of our wealth based on f1 and f2
# We then get a return on the wagers based on the outcomes of each game
# We then return the sum of our remaining assets and add the new value of the assets we put in the game
def NewValueOfWealth(f1, f2, wealth):
wager1 = wealth * f1
wager2 = wealth * f2
outcome = playGame(p1,p2)
if outcome[0] == 'H':
wager1 += k1_H*wager1
else:
wager1 += k1_T*wager1
if outcome[1] == 'T':
wager2 += k2_H*wager2
else:
wager2 += k2_T*wager2
return wealth*(1 - f1 - f2) + wager1 + wager2
# We define the variables for our program
V0 = 1000 # initial wealth
plays_of_game = 250 # we will play the game 100 times (100 coin flips)
num_runs = 100 # We will repeat the simulation of playing the game 100 times 1000 times to get our average growth rate
p1 = 0.5 # P(H) for coin 1
q1 = 1 - p1 # P(T) for coin 1
p2 = 0.5 # P(H) for coin 2
q2 = 1 - p2 # P(T) for coin 2
k1_H = 1 # Return on coin 1 given H
k1_T = -0.4 # Return on coin 1 given T
k2_H = -0.5 # Return on coin 2 given H
k2_T = 1 # Return on coin 2 given T
# This function gets the average growth of our model by running multiple iterations of the game (where in each iteration we play the game N times)
def getAverageGrowth(f1,f2):
growth_rate = 0 # This will determine the average growth rate for a given investment fraction f = (f1,f2)
for i in range(num_runs):
wealth = [V0]
# We do an iteration where we play the game a number of times and extract our wealth value at the end of the game
for i in range(1,plays_of_game):
wealth.append(NewValueOfWealth(f1,f2, wealth[-1]))
# We use our growth formula as defined in the notes
growth_rate += 1/plays_of_game * np.log(wealth[-1]/wealth[0])
# After we have taken up a number of samples, we take an average of all the growth rates obtained
average_growth = growth_rate/num_runs
return average_growth
# We build the 2D mesh of different values we can give to f1 and f2
fraction_list = np.arange(0.01, 1.01, 0.01)
F1, F2 = np.meshgrid(fraction_list, fraction_list)
# This function returns the average growth from the function above
def growth(x,y):
return getAverageGrowth(x,y)
# We plot our results below, with (f1, f2) as the x-y plane, and the growth on the z-axis
growth_values = growth(F1, F2)
print("Optimised Growth: ", np.max(growth_values))
max_index = np.unravel_index(np.argmax(growth_values), growth_values.shape)
print("Optimised f1: ", max_index[0])
print("Optimised f2: ", max_index[1])
fig = plt.figure(figsize=(10,7))
ax = fig.add_subplot(111,projection='3d')
surface = ax.plot_surface(F1,F2, growth_values, cmap="viridis")
ax.set_xlabel('f1')
ax.set_ylabel('f2')
ax.set_zlabel('Growth')
fig.colorbar(surface, shrink=0.5, aspect=10)
plt.title("3D Surface Plot of Growth Values")
plt.show()
Optimised Growth: 0.1531681178071838 Optimised f1: 42 Optimised f2: 70
Optimizing Kelly Fractions with Gradient Descent¶
In the 2-coin game, we derived an analytical formula for the growth function $G(f_1, f_2)$ by considering all four possible joint outcomes of the coin flips:
$$ G(f_1, f_2) = p_1p_2 \ln(1 + f_1k_{1,H} + f_2k_{2,H}) + p_1q_2 \ln(1 + f_1k_{1,H} + f_2k_{2,T}) + p_2q_1 \ln(1 + f_1k_{1,T} + f_2k_{2,H}) + p_2q_2 \ln(1 + f_1k_{1,T} + f_2k_{2,T}) $$
where:
- $p_1, q_1 = 1-p_1$ are probabilities of heads and tails for Coin 1,
- $p_2, q_2 = 1-p_2$ are probabilities of heads and tails for Coin 2,
- $k_{1,H}, k_{1,T}$ are the returns for Coin 1 (heads and tails),
- $k_{2,H}, k_{2,T}$ are the returns for Coin 2 (heads and tails).
Gradient Calculation¶
To find the optimal Kelly fractions $(f_1^*, f_2^*)$, we maximize $G(f_1, f_2)$.
This requires solving the first-order conditions:
$$ \frac{\partial G}{\partial f_1} = 0, \quad \frac{\partial G}{\partial f_2} = 0 $$
The partial derivatives are:
$$ \frac{\partial G}{\partial f_1} = \frac{k_{1,H}p_1p_2}{1 + f_1k_{1,H} + f_2k_{2,H}} + \frac{k_{1,H}p_1q_2}{1 + f_1k_{1,H} + f_2k_{2,T}} + \frac{k_{1,T}p_2q_1}{1 + f_1k_{1,T} + f_2k_{2,H}} + \frac{k_{1,T}p_2q_2}{1 + f_1k_{1,T} + f_2k_{2,T}} $$
$$ \frac{\partial G}{\partial f_2} = \frac{k_{2,H}p_1p_2}{1 + f_1k_{1,H} + f_2k_{2,H}} + \frac{k_{2,T}p_1q_2}{1 + f_1k_{1,H} + f_2k_{2,T}} + \frac{k_{2,H}p_2q_1}{1 + f_1k_{1,T} + f_2k_{2,H}} + \frac{k_{2,T}p_2q_2}{1 + f_1k_{1,T} + f_2k_{2,T}} $$
Gradient Descent Algorithm¶
Since solving these equations analytically is not always practical, we use gradient descent:
- Initialize with guesses $(f_1^{(0)}, f_2^{(0)})$
- Iteratively update: $$ f_1^{(t+1)} = f_1^{(t)} + \eta \frac{\partial G}{\partial f_1}(f_1^{(t)}, f_2^{(t)}) $$ $$ f_2^{(t+1)} = f_2^{(t)} + \eta \frac{\partial G}{\partial f_2}(f_1^{(t)}, f_2^{(t)}) $$ where $\eta$ is the learning rate.
- Convergence: when the gradients approach zero, $(f_1, f_2)$ stabilize at the optimal Kelly fractions.
Visualization¶
We plot:
- The growth surface $G(f_1, f_2)$ over a grid of values,
- The path of gradient descent iterations, showing how the algorithm climbs the surface towards the maximum.
The final result is:
- $f_1^*$ = optimal investment fraction in Coin 1
- $f_2^*$ = optimal investment fraction in Coin 2
- $G^*$ = optimal long-term growth rate
Key Insight¶
Gradient descent provides a numerical method to find the Kelly-optimal fractions in the multiple investment case.
Even when the analytical solution is complex or unavailable, the optimization procedure reliably converges to the fractions that maximize expected exponential growth.
# Assuming that N is sufficiently large such that we can remove the limit from the formula, this is the analytical formula for the Growth Function in the 2 coin game
def growth_function(f1, f2):
return ( p1*p2*np.log(1 + f1*k1_H + f2*k2_H) + p1*q2*np.log(1 + f1*k1_H + f2*k2_T) + p2*q1*np.log(1 + f1*k1_T + f2*k2_H) + p2*q2*np.log(1 + f1*k1_T + f2*k2_T) )
# The following two functions are the partial derivatives of the Growth function
def dG_df1(f1, f2):
return ( k1_H*p1*p2*1/(1 + f1*k1_H + f2*k2_H) + k1_H*p1*q2*1/(1 + f1*k1_H + f2*k2_T) + k1_T*p2*q1*1/(1 + f1*k1_T + f2*k2_H) + k1_T*p2*q2*1/(1 + f1*k1_T + f2*k2_T) )
def dG_df2(f1, f2):
return ( k2_H*p1*p2*1/(1 + f1*k1_H + f2*k2_H) + k2_T*p1*q2*1/(1 + f1*k1_H + f2*k2_T) + k2_H*p2*q1*1/(1 + f1*k1_T + f2*k2_H) + k2_T*p2*q2*1/(1 + f1*k1_T + f2*k2_T) )
# We define our gradient descent algorithm, which aims to find the max. of the growth function
def gradient_descent(start_f1, start_f2, learning_rate, num_iterations):
# We start with initial guesses for f1 and f2
f1 = start_f1
f2 = start_f2
history = []
# For each iteration, we get the value of the partial derivatives given our guess.
# We use these values to subtract from our initial guess times some learning rate for stability
# Note: ONce we hit the max, the partial derivatives will be 0, meaning the values for f1 and f2 won't change!
for i in range(num_iterations):
grad_f1 = dG_df1(f1,f2)
grad_f2 = dG_df2(f1,f2)
f1 = f1 + learning_rate * grad_f1
f2 = f2 + learning_rate * grad_f2
history.append((f1, f2, growth_function(f1, f2)))
return f1, f2, growth_function(f1, f2), history
fraction_list = np.arange(0.01, 1.01, 0.01)
F1, F2 = np.meshgrid(fraction_list, fraction_list)
G = growth_function(F1, F2)
# We define our initial guesses and the learning rate
start_f1 = 0.1
start_f2 = 0.1
learning_rate = 0.1
num_iterations = 500
f1_optimised, f2_optimised, G_optimised, history = gradient_descent(start_f1, start_f2, learning_rate, num_iterations)
print("Optimised Growth", G_optimised)
print("Optimised f1", f1_optimised)
print("Optimised f2", f2_optimised)
# We plot our results below, alongside the process of optimising the values for f1 and f2 through Gradient Descent
fig = plt.figure(figsize=(10,7))
ax = fig.add_subplot(111,projection='3d')
surface = ax.plot_surface(F1,F2, G, cmap="viridis", alpha=0.6)
ax.scatter(*zip(*history), c='r', marker='o', label="Grad. Descent Iterations")
ax.set_xlabel('f1')
ax.set_ylabel('f2')
ax.set_zlabel('Growth')
ax.legend()
plt.title("Optimised f1 and f2 using Gradient Descent")
plt.show()
Optimised Growth 0.15203451850981017 Optimised f1 0.7001044796321388 Optimised f2 0.4358881835824517