Python Risk Management: Kelly Criterion | by Lester Leong | Towards Data Science

From the recent events in the financial market correction, I thought it would be a fun time to talk about risk management. Specifically, we’ll go over the Kelly Criterion with a concrete example in Python. First, we’ll discuss a brief overview of the Kelly Criterion. Next, we’ll go over a simple coin flip example. Lastly, we’ll take that simple example and apply it to a financial index.
Data for this article was gathered from: https://www.investing.com/etfs/spdr-s-p-500-historical-data
John Kelly was a scientist in AT&T Bell Labs. At that time, Kelly heard about professional gamblers and became curious on how gamblers managed their capital in the face of uncertainty. Interestingly, he didn’t care about how much money they made, but how they set up their bet sizing to get the most money. Like how most successful gambling stories go, Kelly applied mathematics from information theory to create the Kelly Criterion (KC) [1]. From KC, a gambler could make the best bet size to get the most money in the long run, if she knew her odds of winning and losing.
There are two main parts to the Kelly Criterion (KC): win probability and win/loss ratio. In math formula form, that would be [2]:
But the KC has more applications than just gambling. It can apply to any decision where we know the values of the variables.
From a quick Google search, you’ll find a ton of people citing quotes from value investors applying KC such as [3]:
I can’t be involved in 50 or 75 things. That’s a Noah’s Ark way of investing — you end up with a zoo that way. I like to put meaningful amounts of money in a few things. — Warren Buffett
The wise ones bet heavily when the world offers them that opportunity. They bet big when they have the odds. And the rest of the time, they don’t. It’s just that simple. — Charlie Munger
Most of these big name value investors don’t really apply KC directly anymore. Sure, they apply concentrated bets with their portfolios, but the KC requires repeatable bets for optimal return [4]. This is why if you dig further, most of these investors drop KC. Not because KC is bad, but it is a philosophical investment difference. Value investing involves selecting a few companies and holding them for awhile to fair value. In the calculation for fair value, most do not have enough repeatable odds. This is why most investors that do apply KC tend to employ more quantitative strategies that mirror a casino’s edge.
Enough background talk, let’s get to some coding examples! Now, we’ll go over how to apply KC, when you bet with a coin flip that is favored to you by 55% (a fair coin would have a 50% win chance). For our example, let’s assume you win $1 or lose $1 for the heads or tails outcome that is related to how much risked. In other words, we are assuming 1 to 1 payoff relative to our KC or risked amount per bet. Also the code going forward was modified from Python for Finance [5].
#Import librariesimport mathimport timeimport numpy as npimport pandas as pdimport datetime as dtimport cufflinks as cffrom pylab import plt﻿﻿﻿np.random.seed(1) # For reproducibilityplt.style.use('seaborn') # I think this looks pretty%matplotlib inline # To get out plots#Coin flip variable set up﻿﻿p = 0.55 #Fixes the probability for heads.f = p - (1-p) #Calculates the optimal fraction according to the Kelly criterion.f
f = 0.10
The above is optimal Kelly Criterion bet size (f). This means that for a 1 to 1 payoff and a 55% favored chance to win, we should risk 10% of our total capital for maximizing our profit.
# Preparing our simulation of coin flips with variablesI = 50 #The number of series to be simulated.n = 100 #The number of trials per series.﻿﻿﻿def run_simulation(f):    c = np.zeros((n, I)) #Instantiates an ndarray object to store the simulation results.    c[0] = 100 #Initializes the starting capital with 100.    for i in range(I): #Outer loop for the series simulations.        for t in range(1,n): #Inner loop for the series itself.            o = np.random.binomial(1, p) #Simulates the tossing of a coin.            if o > 0: #If 1, i.e., heads ...                c[t, i] = (1+f) * c[t-1,i] #... then add the win to the capital.            else: #If 0, i.e., tails ...                c[t, i] = (1-f) * c[t-1,i] #... then subtract the loss from the capital.    return c﻿﻿﻿c_1 = run_simulation(f) #Runs the simulation.c_1.round(2) #Looking at a simulation
When running simulations, I sometimes like to check how the data looks just to see nothing crazy is going on. We’ll visualize this data to make it easier to understand.
plt.figure(figsize=(10,6))plt.plot(c_1, 'b', lw=0.5) #Plots all 50 series.plt.plot(c_1.mean(axis=1), 'r', lw=2.5); #Plots the average over all 50 series.plt.title('50 Simulations of Rigged Coin Flips');plt.xlabel('Number of trials');plt.ylabel('$ Amount Won/Lost');
For the plot above, the blue lines represent the 50 simulation coin flips we ran, and the red line represents the average of all the simulations. It’s interesting to see how even with a slight edge (55% favored for us to win) that there are few situations where we are still losing. This is probably why a lot of people have problems assessing the profitability of situations that have to do with chance.
Besides betting with 10% of our capital at all times, what would happen with other bet sizes/different KC values?
c_2 = run_simulation(0.05) #Simulation with f = 0.05.c_3 = run_simulation(0.25) #Simulation with f = 0.25.c_4 = run_simulation(0.5) #Simulation with f = 0.5.plt.figure(figsize=(10, 6))plt.plot(c_1.mean(axis=1), 'r', label='$f^*=0.1$')plt.plot(c_2.mean(axis=1), 'b', label='$f=0.05$')plt.plot(c_3.mean(axis=1), 'y', label='$f=0.25$')plt.plot(c_4.mean(axis=1), 'm', label='$f=0.5$')plt.legend(loc=0);﻿﻿plt.title('Varied KC Simulations of Rigged Coin Flips');plt.xlabel('Number of trials');plt.ylabel('$ Amount Won/Lost');
From our varied KC, we can see that returns ($ Amount won) vary greatly. Specifically, the higher we increase KC, we increase the chance of winning more money, but also run higher risk. So far our optimal value of 0.10 does appear best (a consistent profitable return), while higher values have huge changes in returns (f=0.25) or even losing (f=0.5).
Now let’s get out of the simple example and push it into the real world. We’re going to apply KC to the S&P 500 stock index. To apply these changes, our KC formula will look like this [5]:
Mu ( μ) = average returns of SPY r = risk free rate sigma2 (σ2) = variance of SPY Instead of scrolling up for the data link, you can find it here again: https://www.investing.com/etfs/spdr-s-p-500-historical-data [6].
#Loading SPY datadata = pd.read_csv('SPY Historical Data.csv', index_col=0, parse_dates=True)#Light Feature Engineering on Returnsdata['Change %'] = data['Change %'].map(lambda x: x.rstrip('%')).astype(float) / 100data.dropna(inplace=True)data.tail()
Just checking the data to see if our feature engineering worked. Data from Investing.com normally has a % symbol that Python tends to pick up as an object and not a number.
mu = data['Change %'].mean() * 252 #Calculates the annualized return.sigma = (data['Change %'].std() * 252 ** 0.5)  #Calculates the annualized volatility.r = 0.0179 #1 year treasury ratef = (mu - r) / sigma ** 2 #Calculates the optimal Kelly fraction to be invested in the strategy.f
f = 4.2
From our calculations, the KC value is 4.2, meaning we should bet 4.2x for every $1 invested in the SPY. For those of you more comfortable with finance, it means we should leverage our trade by 4.2x to maximize our expected return. Intuitively, sounds kind of high to me, but let’s run a few simulations like before.
equs = [] # preallocating space for our simulationsdef kelly_strategy(f):    global equs    equ = 'equity_{:.2f}'.format(f)    equs.append(equ)    cap = 'capital_{:.2f}'.format(f)    data[equ] = 1  #Generates a new column for equity and sets the initial value to 1.    data[cap] = data[equ] * f  #Generates a new column for capital and sets the initial value to 1·f∗.    for i, t in enumerate(data.index[1:]):        t_1 = data.index[i]  #Picks the right DatetimeIndex value for the previous values.        data.loc[t, cap] = data[cap].loc[t_1] * math.exp(data['Change %'].loc[t])        data.loc[t, equ] = data[cap].loc[t] - data[cap].loc[t_1] + data[equ].loc[t_1]        data.loc[t, cap] = data[equ].loc[t] * f ﻿﻿﻿kelly_strategy(f * 0.5) # Values for 1/2 KCkelly_strategy(f * 0.66) # Values for 2/3 KCkelly_strategy(f) # Values for optimal KCax = data['Change %'].cumsum().apply(np.exp).plot(legend=True,figsize=(10, 6))         data[equs].plot(ax=ax, legend=True);﻿﻿﻿﻿plt.title('Varied KC Values on SPY, Starting from $1');plt.xlabel('Years');plt.ylabel('$ Return')
Just like in our coin flip example, higher values for KC lead to greater volatility. The optimal amount of 4.2 lost over 50% of its value around the beginning of 2019. That’s pretty scary for most people, which is why practitioners that apply KC normally apply half of KC (equity_2.10 from out plot).
We covered quite a bit of ground today. We went over a brief history of the Kelly Criterion (KC) with how investors apply the formula. Then we maximized our return with a simple rigged coin flip example. Lastly, we applied what we learned from the coin flip example into the S&P 500 stock index. From our little simulation, we learned that, even though KC might recommend a high value, sometimes we will take a reduced KC value to avoid unwanted volatility. There are not a lot of people who can tolerate 50% draw downs (largest loss amount) and don’t get me started on the mental stress of going through one! So take the strategic solution, have more peace of mind by going lower than full KC.
Disclaimer: All things stated in this article are of my own opinion and not of any employer. Investing carries serious risk and consult your investment advisor before taking any investment action. Also, this post contains affiliate links.
References
[1] Finbox, Investing With Kelly (2018), https://www.valuewalk.com/2018/04/investing-with-the-kelly-criterion-model/
[2] J. Kuepper, Using the Kelly Criterion for Asset Allocation and Money Management (2019), https://www.investopedia.com/articles/trading/04/091504.asp
[3] OSV, Apply the Kelly Criterion to Investing and Your Portfolio Sizing (2014), https://www.oldschoolvalue.com/blog/investing-strategy/kelly-criterion-investing-portfolio-sizing/
[4] P. Lindmark, Investing with the Kelly Criterion (2007), https://www.gurufocus.com/news/4883/investing-with-the-kelly-criterion
[5] Y. Hiplisch, Python for Finance: Mastering Data-Driven Finance (2018), O’Reilly Media https://amzn.to/2KuHPRG
[6] Investing.com, SPY 500 Historical Data, https://www.investing.com/etfs/spdr-s-p-500-historical-data