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.

In [ ]:
import numpy as np
import matplotlib.pyplot as plt 
import math
import random
from mpl_toolkits.mplot3d import axes3d
In [42]:
# 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
No description has been provided for this image

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:

  1. Initialize with guesses $(f_1^{(0)}, f_2^{(0)})$
  2. 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.
  3. 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.

In [43]:
# 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
No description has been provided for this image
In [ ]: