Investing On ETFs Using The Kelly Formula Part 2

How To Calculate The Kelly Formula Using Python

Photo by the author

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 g of a portfolio composed of a single security

\displaystyle \max_{f} g(f),

where the growth rate g is

\displaystyle g(f) = r_{Risk-free} + f m - \frac{1}{2} f^2 s^2,

and f is the fraction of capital we allocate to the security.

Assuming that the security follows a Normal distribution with mean m and variance s^2, the optimal allocation f^* is

\displaystyle f^* = \frac{m}{s^2},

and the optimum growth rate is

\displaystyle g(f^*) = r_{Risk-free} + \frac{1}{2} f^{*2} s^2.

Case 2: Multiple Securities

We extend the previous case to maximize the growth rate g of a portfolio composed of multiple securities

\displaystyle \max_{F} g(F),

where the growth rate g is

\displaystyle g(F) = r_{Risk-free} + F^T M - \frac{1}{2} F^T C F,

and F is a vector containing fractions of capital we allocate to each security

\displaystyle F = \left( f_1, f_2, ..., f_n \right).

Assuming that each security i follows a Normal distribution, $M$ is the vector containing average of excess returns of each security

\displaystyle M = \left( m_1, m_2, ..., m_n \right),

and C is the covariance matrix of the returns between securities i and j

\displaystyle \begin{pmatrix}  c_{11} & c_{12} & ... & c_{1n} \\  c_{21} & c_{22} & ... & c_{2n} \\  \vdots & \vdots & \ddots & \vdots \\  c_{n1} & c_{n2} & ... & c_{nn} \\  \end{pmatrix}

the optimal allocation F^* is

\displaystyle F^* = C^{-1} M,

and the optimum growth rate is

\displaystyle g(F^*) = r_{Risk-free} + \frac{1}{2} F^T C F.

Step By Step

  1. Calculate the Kelly leverages and performance metrics.
  2. 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:

  1. “Unconstrained” Kelly leverages using the formula above.
  2. “Full” Kelly fractions with the constraint of maximum leverage set to 4.
  3. “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:

  1. Risk-free rate is assumed to be 2.5%.
  2. 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'])
UnconstrainedFull KellyHalf Kelly
EDV12.1562320.0334350.016717
BIV116.3245250.3199410.159971
VGIT-94.226520-0.259162-0.129581
BLV46.8311680.1288050.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
VCIT212.4253570.5842590.292129
VCLT-71.501762-0.196660-0.098330
VCSH387.6077711.0660840.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'])
UnconstrainedFull KellyHalf Kelly
Growth Rate17.1210540.1189130.071989
Sharpe Ratio5.8474020.0160830.008041

Applying the Kelly formula to US bond ETFs yield following remarks:

  1. Unconstrained Kelly proposes significant leverages e.g. 388 for VCSH and -205 for BSV.
  2. 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'])
UnconstrainedFull KellyHalf Kelly
VIG11.5834350.0411410.020571
VUG282.3216061.0027290.501365
VYM-20.176372-0.071661-0.035830
VV-89.070723-0.316355-0.158177
MGC45.1221430.1602620.080131
MGK-215.370435-0.764937-0.382468
MGV39.4376990.1400720.070036
VOO-147.455077-0.523720-0.261860
VTI104.5916840.3714810.185741
VTV-22.634360-0.080391-0.040196
VXF-0.051763-0.000184-0.000092
VO64.8701500.2304010.115201
VOT10.1769840.0361460.018073
VOE-0.257829-0.000916-0.000458
VB7.0608040.0250780.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'])
UnconstrainedFull KellyHalf Kelly
Growth Rate7.1256070.0753490.050197
Sharpe Ratio3.7684500.0133850.006692

Applying the Kelly formula to US stock ETFs yield following remarks:

  1. Unconstrained Kelly propose significant leverages e.g. 282 for VUG and -215 for MGK.
  2. 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'])
UnconstrainedFull KellyHalf Kelly
BNDX210.4939732.7054691.352734
VWOB100.7185991.2945310.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'])
UnconstrainedFull KellyHalf Kelly
Growth Rate16.4622210.4448180.235588
Sharpe Ratio5.7336240.0736940.036847

Applying the Kelly formula to international bond ETFs yields the following remarks:

  1. Unconstrained Kelly propose significant leverages e.g. 210 for BNDX and 101 for VWOB.
  2. 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'])
UnconstrainedFull KellyHalf Kelly
VT11.8735990.1739850.086992
VEU56.4467950.8271190.413560
VSS-6.710062-0.098323-0.049162
VEA47.1702260.6911890.345595
VGK-10.031345-0.146990-0.073495
VPL-52.156336-0.764251-0.382125
VNQI26.6739470.3908550.195428
VXUS-59.953430-0.878502-0.439251
VWO1.9644450.0287850.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'])
UnconstrainedFull KellyHalf Kelly
Growth Rate3.5021370.1261550.075764
Sharpe Ratio2.6370960.0386420.019321

Applying the Kelly formula to international stock ETFs yields the following remarks:

  1. Unconstrained Kelly propose relatively smaller leverages (compared to US bond and US stock ETFs) and Sharpe ratio of 2.64.
  2. 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'])
UnconstrainedFull KellyHalf Kelly
VOX1.2191170.0506290.025314
VCR-8.332042-0.346022-0.173011
VDC-2.505247-0.104041-0.052020
VDE-11.760600-0.488407-0.244203
VFH17.2090400.7146750.357338
VHT-19.652473-0.816149-0.408074
VIS3.8426070.1595800.079790
VGT14.8552840.6169260.308463
VAW0.3967230.0164760.008238
VNQ12.3864440.5143970.257199
VPU4.1585220.1727000.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'])
UnconstrainedFull KellyHalf Kelly
Growth Rate6.2715590.5330540.281720
Sharpe Ratio3.5345600.1467870.073393

Applying the Kelly formula to sector ETFs yields the following remarks:

  1. Unconstrained Kelly propose relatively small leverages (compared to US bond and US stock ETFs) and Sharpe ratio of 3.53.
  2. 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.

Advertisements