# How to size your bets - The Kelly Rule

he Kelly rule has a long history in gambling because it provides guidance on how much to stake on each of an (infinite) sequence of bets with varying (but favorable) odds to maximize terminal wealth. It was published as A New Interpretation of the Information Rate in 1956 by John Kelly who was a colleague of Claude Shannon's at Bell Labs. He was intrigued by bets placed on candidates at the new quiz show The $64,000 Question, where a viewer on the west coast used the three-hour delay to obtain insider information about the winners. 

Kelly drew a connection to Shannon's information theory to solve for the bet that is optimal for long-term capital growth when the odds are favorable, but uncertainty remains. His rule maximizes logarithmic wealth as a function of the odds of success of each game, and includes implicit bankruptcy protection since log(0) is negative infinity so that a Kelly gambler would naturally avoid losing everything.

## Imports


```python
import warnings
warnings.filterwarnings('ignore')
```


```python
%matplotlib inline
from pathlib import Path

import numpy as np
from numpy.linalg import inv
from numpy.random import dirichlet
import pandas as pd

from sympy import symbols, solve, log, diff
from scipy.optimize import minimize_scalar, newton, minimize
from scipy.integrate import quad
from scipy.stats import norm

import matplotlib.pyplot as plt
import seaborn as sns
```


```python
sns.set_style('whitegrid')
np.random.seed(42)
```


```python
DATA_STORE = Path('..', 'data', 'assets.h5')
```

## The optimal size of a bet

Kelly began by analyzing games with a binary win-lose outcome. The key variables are:
- b: The odds define the amount won for a \\$1 bet. Odds = 5/1 implies a \\$5 gain if the bet wins, plus recovery of the \\$1 capital.
- p: The probability defines the likelihood of a favorable outcome.
- f: The share of the current capital to bet.
- V: The value of the capital as a result of betting.

The Kelly rule aims to maximize the value's growth rate, G, of infinitely-repeated bets (see Chapter 5 for background).
$$G=\lim_{N\rightarrow\infty}=\frac{1}{N}\log\frac{V_N}{V_0}$$

We can maximize the rate of growth G by maximizing G with respect to f, as illustrated using sympy as follows:


```python
share, odds, probability = symbols('share odds probability')
Value = probability * log(1 + odds * share) + (1 - probability) * log(1 - share)
solve(diff(Value, share), share)
```




    [(odds*probability + probability - 1)/odds]




```python
f, p = symbols('f p')
y = p * log(1 + f) + (1 - p) * log(1 - f)
solve(diff(y, f), f)
```




    [2*p - 1]



## Get S&P 500 Data


```python
with pd.HDFStore(DATA_STORE) as store:
    sp500 = store['sp500/stooq'].close
```

### Compute Returns & Standard Deviation


```python
annual_returns = sp500.resample('A').last().pct_change().dropna().to_frame('sp500')
```


```python
return_params = annual_returns.sp500.rolling(25).agg(['mean', 'std']).dropna()
```


```python
return_ci = (return_params[['mean']]
                .assign(lower=return_params['mean'].sub(return_params['std'].mul(2)))
                .assign(upper=return_params['mean'].add(return_params['std'].mul(2))))
```


```python
return_ci.plot(lw=2, figsize=(14, 8))
plt.tight_layout()
sns.despine();
```

### Kelly Rule for a Single Asset - Index Returns

In a financial market context, both outcomes and alternatives are more complex, but the Kelly rule logic does still apply. It was made popular by Ed Thorp, who first applied it profitably to gambling (described in Beat the Dealer) and later started the successful hedge fund Princeton/Newport Partners.

With continuous outcomes, the growth rate of capital is defined by an integrate over the probability distribution of the different returns that can be optimized numerically.
We can solve this expression (see book) for the optimal f* using the `scipy.optimize` module:


```python
def norm_integral(f, mean, std):
    val, er = quad(lambda s: np.log(1 + f * s) * norm.pdf(s, mean, std), 
                               mean - 3 * std, 
                               mean + 3 * std)
    return -val
```


```python
def norm_dev_integral(f, mean, std):
    val, er = quad(lambda s: (s / (1 + f * s)) * norm.pdf(s, mean, std), m-3*std, mean+3*std)
    return val
```


```python
def get_kelly_share(data):
    solution = minimize_scalar(norm_integral, 
                        args=(data['mean'], data['std']), 
                        bounds=[0, 2], 
                        method='bounded') 
    return solution.x
```


```python
annual_returns['f'] = return_params.apply(get_kelly_share, axis=1)
```


```python
return_params.plot(subplots=True, lw=2, figsize=(14, 8));
```


```python
annual_returns.tail()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>sp500</th>
      <th>f</th>
    </tr>
    <tr>
      <th>Date</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2015-12-31</th>
      <td>-0.007266</td>
      <td>1.999996</td>
    </tr>
    <tr>
      <th>2016-12-31</th>
      <td>0.095350</td>
      <td>1.999996</td>
    </tr>
    <tr>
      <th>2017-12-31</th>
      <td>0.194200</td>
      <td>1.999996</td>
    </tr>
    <tr>
      <th>2018-12-31</th>
      <td>-0.062373</td>
      <td>1.999996</td>
    </tr>
    <tr>
      <th>2019-12-31</th>
      <td>0.288781</td>
      <td>1.999996</td>
    </tr>
  </tbody>
</table>
</div>



### Performance Evaluation


```python
(annual_returns[['sp500']]
 .assign(kelly=annual_returns.sp500.mul(annual_returns.f.shift()))
 .dropna()
 .loc['1900':]
 .add(1)
 .cumprod()
 .sub(1)
 .plot(lw=2));

```


```python
annual_returns.f.describe()
```




    count    45.000000
    mean      1.979025
    std       0.062282
    min       1.708206
    25%       1.999996
    50%       1.999996
    75%       1.999996
    max       1.999996
    Name: f, dtype: float64




```python
return_ci.head()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>mean</th>
      <th>lower</th>
      <th>upper</th>
    </tr>
    <tr>
      <th>Date</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>1975-12-31</th>
      <td>0.076574</td>
      <td>-0.289442</td>
      <td>0.442591</td>
    </tr>
    <tr>
      <th>1976-12-31</th>
      <td>0.077649</td>
      <td>-0.289600</td>
      <td>0.444897</td>
    </tr>
    <tr>
      <th>1977-12-31</th>
      <td>0.068336</td>
      <td>-0.306402</td>
      <td>0.443074</td>
    </tr>
    <tr>
      <th>1978-12-31</th>
      <td>0.071410</td>
      <td>-0.299973</td>
      <td>0.442794</td>
    </tr>
    <tr>
      <th>1979-12-31</th>
      <td>0.058325</td>
      <td>-0.278930</td>
      <td>0.395581</td>
    </tr>
  </tbody>
</table>
</div>



### Compute Kelly Fraction


```python
m = .058
s = .216
```


```python
# Option 1: minimize the expectation integral
sol = minimize_scalar(norm_integral, args=(m, s), bounds=[0., 2.], method='bounded')
print('Optimal Kelly fraction: {:.4f}'.format(sol.x))
```

    Optimal Kelly fraction: 1.1974



```python
# Option 2: take the derivative of the expectation and make it null
x0 = newton(norm_dev_integral, .1, args=(m, s))
print('Optimal Kelly fraction: {:.4f}'.format(x0))
```

    Optimal Kelly fraction: 1.1974


## Kelly Rule for Multiple Assets

We will use an example with various equities. [E. Chan (2008)](https://www.amazon.com/Quantitative-Trading-Build-Algorithmic-Business/dp/0470284889) illustrates how to arrive at a multi-asset application of the Kelly Rule, and that the result is equivalent to the (potentially levered) maximum Sharpe ratio portfolio from the mean-variance optimization. 

The computation involves the dot product of the precision matrix, which is the inverse of the covariance matrix, and the return matrix:


```python
with pd.HDFStore(DATA_STORE) as store:
    sp500_stocks = store['sp500/stocks'].index 
    prices = store['quandl/wiki/prices'].adj_close.unstack('ticker').filter(sp500_stocks)
```


```python
prices.info()
```

    <class 'pandas.core.frame.DataFrame'>
    DatetimeIndex: 14277 entries, 1962-01-02 to 2018-03-27
    Columns: 482 entries, MMM to ZTS
    dtypes: float64(482)
    memory usage: 52.6 MB



```python
monthly_returns = prices.loc['1988':'2017'].resample('M').last().pct_change().dropna(how='all').dropna(axis=1)
stocks = monthly_returns.columns
monthly_returns.info()
```

    <class 'pandas.core.frame.DataFrame'>
    DatetimeIndex: 359 entries, 1988-02-29 to 2017-12-31
    Freq: M
    Columns: 207 entries, MMM to XRX
    dtypes: float64(207)
    memory usage: 583.4 KB


### Compute Precision Matrix


```python
cov = monthly_returns.cov()
precision_matrix = pd.DataFrame(inv(cov), index=stocks, columns=stocks)
```


```python
kelly_allocation = monthly_returns.mean().dot(precision_matrix)
```


```python
kelly_allocation.describe()
```




    count    207.000000
    mean       0.237375
    std        3.470213
    min      -11.586454
    25%       -1.750238
    50%        0.093455
    75%        2.505289
    max        9.255888
    dtype: float64




```python
kelly_allocation.sum()
```




    49.136694419591635



### Largest Portfolio Allocation

The plot shows the tickers that receive an allocation weight > 5x their value:


```python
kelly_allocation[kelly_allocation.abs()>5].sort_values(ascending=False).plot.barh(figsize=(8, 10))
plt.yticks(fontsize=12)
sns.despine()
plt.tight_layout();
```

### Performance vs SP500

The Kelly rule does really well. But it has also been computed from historical data..


```python
ax = monthly_returns.loc['2010':].mul(kelly_allocation.div(kelly_allocation.sum())).sum(1).to_frame('Kelly').add(1).cumprod().sub(1).plot(figsize=(14,4));
sp500.filter(monthly_returns.loc['2010':].index).pct_change().add(1).cumprod().sub(1).to_frame('SP500').plot(ax=ax, legend=True)
plt.tight_layout()
sns.despine();
```

fac2f52c5fa02cbc7402bf9f987dc337595ebfa4
ipynb
Jupyter Notebook
ml4trading-2ed/05_strategy_evaluation/05_kelly_rule.ipynb
JennEYoon/SEC-data
a532364f0d1314a29e701acbcb904ba01e19acb3
BSD-3-Clause
ml4trading-2ed/05_strategy_evaluation/05_kelly_rule.ipynb
JennEYoon/SEC-data
a532364f0d1314a29e701acbcb904ba01e19acb3
BSD-3-Clause
2020-10-27T19:44:15.000Z
2020-11-03T23:55:36.000Z
ml4trading-2ed/05_strategy_evaluation/05_kelly_rule.ipynb
JennEYoon/SEC-data
a532364f0d1314a29e701acbcb904ba01e19acb3
BSD-3-Clause
Qwen/Qwen-72B
1. YES
2. YES