Davis

Understanding the Kelly Criterion in Algo-Trading

Trading Strategy


Welcome to my first post! Today, we’re diving into an essential concept for traders: the Kelly Criterion. This mathematical formula helps determine the optimal size of a series of bets or investments to maximize long-term growth. Let’s explore its assumptions, application in the stock market, and provide a practical Python script to implement a trading strategy based on this criterion.


What is the Kelly Criterion?

The Kelly Criterion was developed by John L. Kelly Jr. in 1956. It provides a strategy for bet sizing in gambling and investing, aiming to maximize the expected logarithm of wealth. The formula is:


f * = (bp - q) / b


where

  • f * = fraction of the capital to wager
  • b = odds received on the wager (b to 1)
  • p = probability of winning
  • q = probability of losing (1 - p)

Assumptions of the Kelly Criterion

  • Probability Estimation: The user must accurately estimate the probability of winning and losing.
  • Independent Events: Each investment or bet must be independent of the others.
  • Infinite Capital: The model assumes unlimited capital and continuous betting opportunities.
  • Logarithmic Utility: It assumes that wealth grows exponentially, which might not hold in all market conditions.

Applying the Kelly Criterion in the Stock Market

In the stock market, the Kelly Criterion can be used to determine the optimal investment size in a particular stock or portfolio based on the expected returns and risks:

  • Estimate Probabilities: Use historical data or statistical models to estimate the probability of a price increase (p) and decrease (q).
  • Determine Odds: Calculate the expected return relative to the risk involved.
  • Calculate the Fraction: Use the Kelly formula to determine the fraction of your capital to invest.

Assume you have a stock with:

  • 70% probability of increasing in value (p = 0.7)
  • 30% probability of decreasing in value (q = 0.3)
  • Expected return of 20% (b = 0.2)

Using the Kelly Criterion: f * = (0.2*0.7 - 0.3) / 0.2 = -0.8

In this case, the result indicates a negative fraction, suggesting that this investment isn't favorable.


Quick Example

Assume our initial capital is $1. Suppose we have a trading strategy with winning rate 55%. The average win is +10% return, and the average loss is -5% return. Let's run a simulation for 1000 trades if we apply the Kelly fraction for the investment size.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import numpy as np
import matplotlib.pyplot as plt

# Parameters
np.random.seed(42)  # For reproducibility
num_trades = 1000  # Number of trades
win_rate = 0.55  # Probability of winning
avg_win = 0.1  # Average win (10% return)
avg_loss = -0.05  # Average loss (-5% return)

# Kelly Criterion Calculation
def calculate_kelly(p, b):
    q = 1 - p
    return (b * p - q) / b

# Simulate Trades
def simulate_trades(num_trades, win_rate, avg_win, avg_loss):
    kelly_fraction = calculate_kelly(win_rate, avg_win / abs(avg_loss))
    
    capital = 1.0  # Starting capital
    capital_history = [capital]

    for _ in range(num_trades):
        if np.random.rand() < win_rate:
            # Win
            capital += capital * kelly_fraction * avg_win
        else:
            # Loss
            capital += capital * kelly_fraction * avg_loss
        
        capital_history.append(capital)

    return capital_history

# Run the simulation
capital_history = simulate_trades(num_trades, win_rate, avg_win, avg_loss)

# Plot the results
plt.figure(figsize=(12, 6))
plt.plot(capital_history, label='Capital Growth', color='blue')
plt.title('Capital Growth Simulation Using Kelly Criterion')
plt.xlabel('Number of Trades')
plt.ylabel('Capital')
plt.axhline(y=1, color='red', linestyle='--', label='Initial Capital')
plt.legend()
plt.grid()
plt.show()



SPY Strategy Backtest Example

Wow! We got a very promising result from the example above. Let's further backtest this idea using real market data. We will apply on SPY.US

Backtest Setting

  • Instrument: SPYUS
  • Initial Capital: US$100,000
  • Start Date: 2024-01
  • End Date: 2024-07
  • Data Interval: 1-day
  • Short-selling: No
  • Leverage: 1

Step 1. Define a function to calculate Kelly fraction

1
2
3
def calculate_kelly(p, b):
    q = 1 - p
    return (b * p - q) / b

Step 2. Initialize variables and get the contract size of SPY

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest
from datetime import datetime, timedelta

class AlgoEvent:
    def __init__(self):
        self.timer = datetime(1970,1,1)
        self.last_close = None
        self.returns = []
        self.numObs = 30

    def start(self, mEvt):
        self.evt = AlgoAPI_Backtest.AlgoEvtHandler(self, mEvt)

        # get lot size
        self.instrument = mEvt['subscribeList'][0]
        self.contractSize = self.evt.getContractSpec(self.instrument)["contractSize"]

        self.evt.start()

Step 3. Compute daily return and Kelly fraction

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    def on_marketdatafeed(self, md, ab):
        if md.timestamp >= self.timer+timedelta(hours=24):
            self.timer = md.timestamp

            # update return series
            if self.last_close!=None:
                self.returns.append(md.lastPrice/self.last_close-1)
            self.last_close = md.lastPrice

            # keep only the recent observations
            if len(self.returns)>=self.numObs:
                self.returns = self.returns[-self.numObs:]

                # calculate Kelly Criterion
                win_rate = sum([1 for r in self.returns if r>0])/len(self.returns)
                avg_win = sum([r for r in self.returns if r>0])/len(self.returns)
                avg_loss = sum([r for r in self.returns if r<0])/len(self.returns)
                b = abs(avg_win/avg_loss)
                kelly_fraction = calculate_kelly(win_rate, b)

                if kelly_fraction<=0 or kelly_fraction>1:
                    return

Step 4. Round to the closest number of lot and open position

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
quantity = round(ab['availableBalance']*kelly_fraction/(md.lastPrice*self.contractSize), 0)
if quantity>0:
    order = AlgoAPIUtil.OrderObject(
        openclose = "open",
        instrument = md.instrument,
        buysell = 1,
        volume = quantity,
        ordertype = 0,       # market order
        holdtime = 24*60*60  # hold for 1 day only
    )
    self.evt.sendOrder(order)

Full Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest
from datetime import datetime, timedelta


def calculate_kelly(p, b):
    q = 1 - p
    return (b * p - q) / b

class AlgoEvent:
    def __init__(self):
        self.timer = datetime(1970,1,1)
        self.last_close = None
        self.returns = []
        self.numObs = 30

    def start(self, mEvt):
        self.evt = AlgoAPI_Backtest.AlgoEvtHandler(self, mEvt)

        # get lot size
        self.instrument = mEvt['subscribeList'][0]
        self.contractSize = self.evt.getContractSpec(self.instrument)["contractSize"]

        self.evt.start()

    def on_marketdatafeed(self, md, ab):
        if md.timestamp >= self.timer+timedelta(hours=24):
            self.timer = md.timestamp

            # update return series
            if self.last_close!=None:
                self.returns.append(md.lastPrice/self.last_close-1)
            self.last_close = md.lastPrice

            # keep only the recent observations
            if len(self.returns)>=self.numObs:
                self.returns = self.returns[-self.numObs:]

                # calculate Kelly Criterion
                win_rate = sum([1 for r in self.returns if r>0])/len(self.returns)
                avg_win = sum([r for r in self.returns if r>0])/len(self.returns)
                avg_loss = sum([r for r in self.returns if r<0])/len(self.returns)
                b = abs(avg_win/avg_loss)
                kelly_fraction = calculate_kelly(win_rate, b)

                if kelly_fraction<=0 or kelly_fraction>1:
                    return

                # round to the closest number of lot
                quantity = round(ab['availableBalance']*kelly_fraction/(md.lastPrice*self.contractSize), 0)
                if quantity>0:
                    order = AlgoAPIUtil.OrderObject(
                        openclose = "open",
                        instrument = md.instrument,
                        buysell = 1,
                        volume = quantity,
                        ordertype = 0,       # market order
                        holdtime = 24*60*60  # hold for 1 day only
                    )
                    self.evt.sendOrder(order)

Backtest Result



Final Thoughts

Why the performance is not as good as in the previous example???

The Kelly fraction is originally designed to apply to gambling in casino, not financial market. There are fundamental differences:


Gambling Investment
The risk and reward are determined. (eg. 50% win rate for flipping a coin) The chance of winning a trade is a variable
Each bet has only binary outcomes which are determined. (eg. Every time when if you win, you get $10, if you loss, you loss $5) The outcome is the investment return which is a continuous variable
Each bet is independent There could be serial correlation

Yet Kelly fraction is still a useful technique for capital management. Some modifications we can consider when applying Kelly crierion in financial market:

  • For risk averse traders, we only trade at a fraction of 𝑓
  • Define a take-profit and stop-loss level (eg. 20 points from entry price) so that it fixes the profit/loss amount of each trade
  • Instead of applying Kelly formula directly to the original price, we can apply it to the trading signals generated from other models. It is to ensure the trades are independent
  • Other research paper proposes a continous version
  • 𝑓 = (𝜇 − r) / 𝜎2

Like and follow me

If you find my articles inspiring, like this post and follow me here to receive my latest updates.

Enter my promote code "UYxWJS8cH1uG" for any purchase on ALGOGENE, you will automatically get 5% discount.