How To Calculate The Kelly Formula Using Python

Introduction
The main problem of an investor is identifying profitable trading strategies. To solve this, he uses fundamental and quantitative techniques to pick winning trading systems. The next problem concerning the investor is how to optimally allocate his capital to different winning strategies. A solution is to use the Kelly formula to calculate the amount of capital he will deploy to securities or trading systems before him.
In this article, I will present the Kelly formula which is a popular system used in both sports betting and investing. I will use the ETFs dataset sourced in the previous article to calculate the Kelly leverages or fractions for each ETF on the list. I will analyze the performance metrics of each ETF group under the different leverages considered: “unconstrained”, “full” and “half-Kelly”.
The Kelly Formula
The Kelly formula (or Kelly criterion) was introduced by Bell labs researcher John Kelly in his 1956 paper “A New Interpretation Of Information Rate”. He proposed a betting system maximizing the expected logarithm of wealth, which is equivalent to maximizing expected compounded growth rate. John Kelly demonstrated the formula’s application in horse racing where the bet size per horse is adjusted by the bettor’s informed estimate of the horse’s chance of winning.
The Kelly formula was popularized by mathematician, blackjack researcher and quantitative hedge fund pioneer Edward Thorp in his book “Beat the Dealer”. In 1960s, he combined a card-counting system and the Kelly criterion to beat dealers in the blackjack tables of Las Vegas. Later, Edward Thorp used statistical analysis and the Kelly formula to beat other investment firms in Wall Street.
The Kelly Formula In Investing
In this section, we follow Thorp’s paper presenting the Kelly formula’s application in securities investing.
Case 1: Single Security
The objective is to maximize the growth rate of a portfolio composed of a single security
where the growth rate is
and is the fraction of capital we allocate to the security.
Assuming that the security follows a Normal distribution with mean and variance
, the optimal allocation
is
and the optimum growth rate is
Case 2: Multiple Securities
We extend the previous case to maximize the growth rate of a portfolio composed of multiple securities
where the growth rate is
and is a vector containing fractions of capital we allocate to each security
Assuming that each security follows a Normal distribution, $M$ is the vector containing average of excess returns of each security
and is the covariance matrix of the returns between securities
and
the optimal allocation is
and the optimum growth rate is
Step By Step
- Calculate the Kelly leverages and performance metrics.
- Analysis of the results.
You can find the code on https://github.com/DinodC/investing-etf-kelly.
Calculate the Kelly Leverages And Performance Metrics
In this section, we calculate compounded growth rate and Sharpe ratio under the following:
- “Unconstrained” Kelly leverages using the formula above.
- “Full” Kelly fractions with the constraint of maximum leverage set to 4.
- “Half” Kelly leverages with the same contraint as the full Kelly.
Note that practitioners recommend using half Kelly over full Kelly to counter possible input errors.
Import packages
import numpy as np
import pandas as pd
from pandas import Series, DataFrame
import pickle
import matplotlib.pyplot as plt
%matplotlib inline
Set the group keys
groups = ['us bonds',
'us stocks',
'intl bonds',
'intl stocks',
'sectors']
Set the input files
input = {'us bonds': 'etf_us_bonds.pickle',
'us stocks': 'etf_us_stocks.pickle',
'intl bonds': 'etf_intl_bonds.pickle',
'intl stocks': 'etf_intl_stocks.pickle',
'sectors': 'etf_sectors.pickle'}
Create the output dictionary
output = {'us bonds': {},
'us stocks': {},
'intl bonds': {},
'intl stocks': {},
'sectors': {}}
Set the following parameters:
- Risk-free rate is assumed to be 2.5%.
- Maximum leverage is set to 4 which is consistent with US-approved brokers.
risk_free = 0.025
max_leverage = 4.00
Calculate the Kelly leverages for each portfolio
for i in groups:
# Load file
with open(input[i], 'rb') as f:
close = pickle.load(f)
f.close()
# Daily returns for the past 6 months
returns = close[-120:].pct_change()
# Excess daily returns
excess_returns = returns - risk_free / 250
# Mean excess daily returns annualized
M = excess_returns.mean() * 250
# Covariance of daily returns
C = returns.cov() * 250
# Kelly leverage
F = np.matmul(np.linalg.inv(C), M)
# Adjust leverage
adj_leverage = max_leverage / np.sum(np.abs(F))
# Leverages for unconstrained, full and half Kelly
Kelly = [F,
adj_leverage * F,
0.5 * adj_leverage * F]
Kelly_id = ['unconstrained',
'full',
'half']
for k in range(len(Kelly)):
# Growth rate
g = risk_free + np.matmul(np.transpose(Kelly[k]), M) - 0.5 * np.matmul(np.matmul(np.transpose(Kelly[k]), C), Kelly[k])
# Sharpe ratio
sharpe = np.sqrt(np.matmul(np.matmul(np.transpose(Kelly[k]), C), Kelly[k]))
# Updated outputs: F, g and sharpe
output[i][Kelly_id[k]] = Kelly[k]
output[i][Kelly_id[k] + ' growth'] = g
output[i][Kelly_id[k] + ' sharpe'] = sharpe
# Update output
output[i]['tickers'] = list(close.columns)
Analysis Of The Results
Vanguard US Bond ETFs
The Kelly leverage for US bond ETFs are
group = 'us bonds'
pd.DataFrame({'Unconstrained': output[group]['unconstrained'],
'Full Kelly': output[group]['full'],
'Half Kelly': output[group]['half']},
index=output[group]['tickers'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
EDV | 12.156232 | 0.033435 | 0.016717 |
BIV | 116.324525 | 0.319941 | 0.159971 |
VGIT | -94.226520 | -0.259162 | -0.129581 |
BLV | 46.831168 | 0.128805 | 0.064403 |
VGLT | -21.563687 | -0.059309 | -0.029655 |
VMBS | -63.459954 | -0.174542 | -0.087271 |
BSV | -204.894055 | -0.563545 | -0.281772 |
VTIP | -90.945714 | -0.250139 | -0.125069 |
VGSH | -85.628605 | -0.235515 | -0.117757 |
BND | -46.758134 | -0.128604 | -0.064302 |
VCIT | 212.425357 | 0.584259 | 0.292129 |
VCLT | -71.501762 | -0.196660 | -0.098330 |
VCSH | 387.607771 | 1.066084 | 0.533042 |
The growth rate and Sharpe ratio are
pd.DataFrame({'Unconstrained': [output[group]['unconstrained growth'], output[group]['unconstrained sharpe']],
'Full Kelly': [output[group]['full growth'], output[group]['full sharpe']],
'Half Kelly': [output[group]['half growth'], output[group]['half sharpe']]},
index=['Growth Rate', 'Sharpe Ratio'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
Growth Rate | 17.121054 | 0.118913 | 0.071989 |
Sharpe Ratio | 5.847402 | 0.016083 | 0.008041 |
Applying the Kelly formula to US bond ETFs yield following remarks:
- Unconstrained Kelly proposes significant leverages e.g. 388 for VCSH and -205 for BSV.
- Considering the max leverage of 4, the full and half Kelly produces unsatisfactory Sharpe ratios (<1).
Vanguard US Stock ETFs
The Kelly leverage for US stock ETFs are
group = 'us stocks'
pd.DataFrame({'Unconstrained': output[group]['unconstrained'],
'Full Kelly': output[group]['full'],
'Half Kelly': output[group]['half']},
index=output[group]['tickers'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
VIG | 11.583435 | 0.041141 | 0.020571 |
VUG | 282.321606 | 1.002729 | 0.501365 |
VYM | -20.176372 | -0.071661 | -0.035830 |
VV | -89.070723 | -0.316355 | -0.158177 |
MGC | 45.122143 | 0.160262 | 0.080131 |
MGK | -215.370435 | -0.764937 | -0.382468 |
MGV | 39.437699 | 0.140072 | 0.070036 |
VOO | -147.455077 | -0.523720 | -0.261860 |
VTI | 104.591684 | 0.371481 | 0.185741 |
VTV | -22.634360 | -0.080391 | -0.040196 |
VXF | -0.051763 | -0.000184 | -0.000092 |
VO | 64.870150 | 0.230401 | 0.115201 |
VOT | 10.176984 | 0.036146 | 0.018073 |
VOE | -0.257829 | -0.000916 | -0.000458 |
VB | 7.060804 | 0.025078 | 0.012539 |
VBK | -34.794622 | -0.123581 | -0.061790 |
VBR | -31.236998 | -0.110945 | -0.055473 |
The growth rate and Sharpe ratio are
pd.DataFrame({'Unconstrained': [output[group]['unconstrained growth'], output[group]['unconstrained sharpe']],
'Full Kelly': [output[group]['full growth'], output[group]['full sharpe']],
'Half Kelly': [output[group]['half growth'], output[group]['half sharpe']]},
index=['Growth Rate', 'Sharpe Ratio'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
Growth Rate | 7.125607 | 0.075349 | 0.050197 |
Sharpe Ratio | 3.768450 | 0.013385 | 0.006692 |
Applying the Kelly formula to US stock ETFs yield following remarks:
- Unconstrained Kelly propose significant leverages e.g. 282 for VUG and -215 for MGK.
- Considering the max leverage of 4, the full and half Kelly produces unsatisfactory Sharpe ratios (<1).
Vanguard International Bond ETFs
The Kelly leverage for international bond ETFs are
group = 'intl bonds'
pd.DataFrame({'Unconstrained': output[group]['unconstrained'],
'Full Kelly': output[group]['full'],
'Half Kelly': output[group]['half']},
index=output[group]['tickers'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
BNDX | 210.493973 | 2.705469 | 1.352734 |
VWOB | 100.718599 | 1.294531 | 0.647266 |
The growth rate and Sharpe ratio are
pd.DataFrame({'Unconstrained': [output[group]['unconstrained growth'], output[group]['unconstrained sharpe']],
'Full Kelly': [output[group]['full growth'], output[group]['full sharpe']],
'Half Kelly': [output[group]['half growth'], output[group]['half sharpe']]},
index=['Growth Rate', 'Sharpe Ratio'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
Growth Rate | 16.462221 | 0.444818 | 0.235588 |
Sharpe Ratio | 5.733624 | 0.073694 | 0.036847 |
Applying the Kelly formula to international bond ETFs yields the following remarks:
- Unconstrained Kelly propose significant leverages e.g. 210 for BNDX and 101 for VWOB.
- Considering the max leverage of 4, the full and half Kelly produces unsatisfactory Sharpe ratios (<1).
Vanguard International Stock ETFs
The Kelly leverage for international stock ETFs are
group = 'intl stocks'
pd.DataFrame({'Unconstrained': output[group]['unconstrained'],
'Full Kelly': output[group]['full'],
'Half Kelly': output[group]['half']},
index=output[group]['tickers'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
VT | 11.873599 | 0.173985 | 0.086992 |
VEU | 56.446795 | 0.827119 | 0.413560 |
VSS | -6.710062 | -0.098323 | -0.049162 |
VEA | 47.170226 | 0.691189 | 0.345595 |
VGK | -10.031345 | -0.146990 | -0.073495 |
VPL | -52.156336 | -0.764251 | -0.382125 |
VNQI | 26.673947 | 0.390855 | 0.195428 |
VXUS | -59.953430 | -0.878502 | -0.439251 |
VWO | 1.964445 | 0.028785 | 0.014393 |
The growth rate and Sharpe ratio are
pd.DataFrame({'Unconstrained': [output[group]['unconstrained growth'], output[group]['unconstrained sharpe']],
'Full Kelly': [output[group]['full growth'], output[group]['full sharpe']],
'Half Kelly': [output[group]['half growth'], output[group]['half sharpe']]},
index=['Growth Rate', 'Sharpe Ratio'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
Growth Rate | 3.502137 | 0.126155 | 0.075764 |
Sharpe Ratio | 2.637096 | 0.038642 | 0.019321 |
Applying the Kelly formula to international stock ETFs yields the following remarks:
- Unconstrained Kelly propose relatively smaller leverages (compared to US bond and US stock ETFs) and Sharpe ratio of 2.64.
- Considering the max leverage of 4, the full and half Kelly produces unsatisfactory Sharpe ratios (<1).
Vanguard Sector ETFs
The Kelly leverage for sector ETFs are
group = 'sectors'
pd.DataFrame({'Unconstrained': output[group]['unconstrained'],
'Full Kelly': output[group]['full'],
'Half Kelly': output[group]['half']},
index=output[group]['tickers'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
VOX | 1.219117 | 0.050629 | 0.025314 |
VCR | -8.332042 | -0.346022 | -0.173011 |
VDC | -2.505247 | -0.104041 | -0.052020 |
VDE | -11.760600 | -0.488407 | -0.244203 |
VFH | 17.209040 | 0.714675 | 0.357338 |
VHT | -19.652473 | -0.816149 | -0.408074 |
VIS | 3.842607 | 0.159580 | 0.079790 |
VGT | 14.855284 | 0.616926 | 0.308463 |
VAW | 0.396723 | 0.016476 | 0.008238 |
VNQ | 12.386444 | 0.514397 | 0.257199 |
VPU | 4.158522 | 0.172700 | 0.086350 |
The growth rate and Sharpe ratio are
pd.DataFrame({'Unconstrained': [output[group]['unconstrained growth'], output[group]['unconstrained sharpe']],
'Full Kelly': [output[group]['full growth'], output[group]['full sharpe']],
'Half Kelly': [output[group]['half growth'], output[group]['half sharpe']]},
index=['Growth Rate', 'Sharpe Ratio'])
Unconstrained | Full Kelly | Half Kelly | |
---|---|---|---|
Growth Rate | 6.271559 | 0.533054 | 0.281720 |
Sharpe Ratio | 3.534560 | 0.146787 | 0.073393 |
Applying the Kelly formula to sector ETFs yields the following remarks:
- Unconstrained Kelly propose relatively small leverages (compared to US bond and US stock ETFs) and Sharpe ratio of 3.53.
- Considering the max leverage of 4, the full and half Kelly produces unsatisfactory Sharpe ratios (<1).
Conclusion
In this article, I reviewed the Kelly formula which is a solution to the investor’s second problem. I demonstrated how we can use the Kelly criterion to calculate ETF fractions per Vanguard fund group. The unconstrained Kelly generates impressive performance but requires significant amounts of leverage. The full and half Kelly suggest reasonable levels of leverage but produces unsatisfactory performance.