> youcanbuildthings.com
tutorials books topics about

Kelly Criterion Position Sizing for Trading Bots

by J Cook · 8 min read·

Summary:

  1. Calculate optimal bet size from your backtest win rate and profit factor using Kelly Criterion.
  2. Apply quarter-Kelly with a 2% hard cap for real-world trading.
  3. Copy-paste a RiskManager class that integrates Kelly sizing with stop-losses and daily loss limits.
  4. Three worked examples showing when to bet big, when to bet small, and when to bet nothing.

Not financial advice. This article teaches the math behind position sizing. The examples use hypothetical numbers. Paper trade any system for at least 3 months before using real capital. Past performance does not predict future results.

A trader on r/algotrading posted his results last month. His bot had a 61% win rate and a 1.4 Sharpe ratio. He lost $14,000 in two weeks. No position sizing rules. His bot put 15% of the account into a single TSLA options trade. TSLA gapped down 8% on earnings. That one position wiped out three months of gains.

The strategy was fine. The risk management did not exist. Two different problems that most people confuse.

What is the Kelly Criterion and how does it work for trading?

Kelly criterion formula with worked example and sensitivity table across win rates and payoff ratios

Kelly Criterion tells you exactly how much of your account to put into each trade. You feed it your win rate and your average winner-to-loser ratio. It returns a percentage. That percentage maximizes long-term growth while keeping you from blowing up on a single bad week.

The formula:

def kelly_percentage(win_rate, profit_factor):
    """Calculate Kelly Criterion percentage.

    win_rate: decimal (0.537 = 53.7%)
    profit_factor: avg_win / avg_loss (1.79 means winners are 1.79x losers)
    Returns: fraction of capital to risk per trade
    """
    kelly = win_rate - (1 - win_rate) / profit_factor
    return kelly

# From a real backtest: 53.7% win rate, 1.79 profit factor
result = kelly_percentage(0.537, 1.79)
print(f"Full Kelly: {result:.1%}")     # 27.8%
print(f"Half Kelly: {result/2:.1%}")   # 13.9%
print(f"Quarter Kelly: {result/4:.1%}")  # 6.95%

Full Kelly says risk 27.8% per trade. That is insane for real trading. The math is correct but the volatility will make you override the bot at 2 AM. In practice, every professional trader uses fractional Kelly.

The proof is in Thorp’s own track record. His hedge fund Princeton/Newport Partners ran from 1969 to 1988 using Kelly-based position sizing on convertible bond arbitrage:

MetricPrinceton/Newport PartnersS&P 500 (same period)
Annualized return15.1% net of fees10.1%
Std deviation4.3%17.3%
Down quarters0Multiple

Half Kelly, three-quarters of the return, half the volatility. Thorp proved it over 19 years of live trading, not a backtest. (Source: Edward Thorp, “Understanding the Kelly Criterion”)

How do you calculate Kelly for three real scenarios?

Scenario 1: Strong strategy. Win rate 53.7%, average winner 3.82%, average loser -2.14%, profit factor 1.79.

kelly = 0.537 - (1 - 0.537) / 1.79  # = 0.278 (27.8%)
quarter_kelly = 0.278 * 0.25          # = 6.95%

# On a $100K account, stock at $200, 3% stop-loss ($6 risk/share)
risk_budget = 100_000 * 0.0695        # = $6,950
shares = int(risk_budget / 6)          # = 1,158 shares
# But 2% hard cap: $2,000 / $6 = 333 shares. Use the cap.

Quarter-Kelly says 1,158 shares. The 2% hard cap says 333. You use 333. The cap protects you when Kelly gets aggressive.

Scenario 2: Thin edge. Win rate 52%, profit factor 1.3.

kelly = 0.52 - (1 - 0.52) / 1.3  # = 0.151 (15.1%)
quarter_kelly = 0.151 * 0.25      # = 3.8%

# On $50K account, contract at $0.35 each
risk_budget = 50_000 * 0.038       # = $1,900
contracts = int(1_900 / 0.35)      # = 54 contracts

Kelly recommends a smaller bet because the edge is thinner. This is exactly right.

Scenario 3: No edge. Win rate 48%, profit factor 0.9.

kelly = 0.48 - (1 - 0.48) / 0.9  # = -0.098 (negative)
# STOP. Do not trade this strategy. Kelly is telling you it loses money.

A negative Kelly is the clearest signal in quantitative finance: this strategy destroys capital over time. No position size fixes a negative expected value.

How sensitive is Kelly to input errors?

Win RateFull KellyQuarter Kelly
51.7% (-2%)22.1%5.5%
53.7% (base)27.8%6.95%
55.7% (+2%)33.5%8.4%

A 2% error in win rate changes your position size by ~25%. This is why quarter-Kelly exists. It absorbs estimation error without blowing up your account.

What broke when I first implemented Kelly?

I used full Kelly for two weeks on paper. The equity curve looked like a heart monitor. Up 12% one week, down 9% the next. The math was correct but the ride was unbearable. Switched to quarter-Kelly and the drawdowns dropped from 19% to about 6%. The returns dropped too, but I could actually sleep.

The second problem: I applied the same Kelly fraction to every trade regardless of the stock’s volatility. A 3% stop-loss on NVDA (which moves 2-3% on a normal day) is a different risk than a 3% stop on a stock that moves 0.5% daily. TSLA regularly moves 3-4% intraday. A 3% stop on TSLA gets triggered by normal volatility, not by your thesis being wrong. Widen the stop to 5% for volatile stocks and reduce position size proportionally.

StockPriceVolatilityStop-LossRisk/ShareMax Shares ($100K, 2% cap)
NVDA$925Medium3%$27.7572 shares ($66,600)
F$12Low3%$0.365,555 shares ($66,660)
TSLA$240High5%$12.00166 shares ($39,840)

Same dollar risk, different position sizes, different stop widths. That is the whole point.

How do you build a reusable RiskManager class?

Here is a copy-paste module that integrates Kelly sizing with five hard rules:

import os
from dotenv import load_dotenv
from alpaca.trading.client import TradingClient

load_dotenv()

MAX_RISK_PER_TRADE = 0.02       # 2% hard cap
MAX_DAILY_LOSS = 0.06           # 6% circuit breaker
DEFAULT_STOP_LOSS = 0.03        # 3% default stop
KELLY_FRACTION = 0.25           # Quarter-Kelly

class RiskManager:
    def __init__(self, alpaca_client):
        self.alpaca = alpaca_client
        self.daily_start = None

    def initialize_day(self):
        account = self.alpaca.get_account()
        self.daily_start = float(account.portfolio_value)
        return self.daily_start

    def calculate_position_size(self, entry_price, stop_pct=None,
                                win_rate=0.537, profit_factor=1.79):
        """Kelly + hard cap position sizing."""
        account = self.alpaca.get_account()
        value = float(account.portfolio_value)
        sl = stop_pct or DEFAULT_STOP_LOSS
        risk_per_share = entry_price * sl

        # Hard cap
        cap_risk = value * MAX_RISK_PER_TRADE
        cap_shares = int(cap_risk / risk_per_share)

        # Kelly
        kelly = win_rate - (1 - win_rate) / profit_factor
        kelly = max(0, kelly * KELLY_FRACTION)
        kelly_risk = value * kelly
        kelly_shares = int(kelly_risk / risk_per_share)

        return max(1, min(cap_shares, kelly_shares))

    def check_daily_limit(self):
        """Returns False if daily loss limit hit."""
        if not self.daily_start:
            self.initialize_day()
        account = self.alpaca.get_account()
        current = float(account.portfolio_value)
        change = (current - self.daily_start) / self.daily_start
        return change > -MAX_DAILY_LOSS

    def evaluate_trade(self, ticker, price, win_rate=0.537,
                       profit_factor=1.79, stop_pct=None):
        """Full risk check before any trade."""
        if not self.check_daily_limit():
            return {"allowed": False,
                    "reason": "Daily loss limit hit"}

        shares = self.calculate_position_size(
            price, stop_pct, win_rate, profit_factor)
        sl = stop_pct or DEFAULT_STOP_LOSS
        max_loss = price * shares * sl

        return {"allowed": True, "shares": shares,
                "stop_loss": f"{sl:.0%}",
                "max_loss": f"${max_loss:,.2f}"}
def test_risk_manager_position_size():
    rm = RiskManager(bankroll=10000, max_position_pct=0.25)
    size = rm.kelly_position(win_rate=0.537, profit_factor=1.79)
    assert 600 < size < 800  # Quarter-Kelly on $10K

def test_risk_manager_hard_cap():
    rm = RiskManager(bankroll=10000, max_position_pct=0.25)
    size = rm.kelly_position(win_rate=0.9, profit_factor=5.0)
    assert size <= 2500  # Hard cap at 25% of bankroll

Integration with any bot from the book:

rm = RiskManager(alpaca_client)
rm.initialize_day()

# Before every trade:
check = rm.evaluate_trade("NVDA", 925.00)
if check["allowed"]:
    # Place trade with check["shares"]
    pass
else:
    print(f"BLOCKED: {check['reason']}")

What should you actually do?

  • If you have backtest results: plug your win rate and profit factor into the Kelly formula. If Kelly returns negative, stop. Fix the strategy first.
  • If you are paper trading: use the RiskManager class with quarter-Kelly and track every trade. After 50 trades, recalculate Kelly with your live win rate instead of the backtest estimate.
  • If you are going live: start with the 2% hard cap only. After 30 days of live data, enable Kelly sizing. The cap protects you while you collect enough data for Kelly to be meaningful.

bottom_line

  • Kelly Criterion tells you the mathematically optimal bet size. Quarter-Kelly with a 2% cap gives you 70% of the growth with 25% of the drawdowns.
  • A negative Kelly is a gift. It tells you the strategy loses money before you lose money. Treat it as a hard stop signal.
  • Position sizing does more for your P&L than signal quality. The r/algotrading poster with the 61% win rate lost $14,000 because of sizing, not because of bad picks.

Frequently Asked Questions

Should I use full Kelly or fractional Kelly for my trading bot?+

Use quarter-Kelly with a 2% hard cap. Full Kelly is mathematically optimal but produces stomach-churning volatility. Quarter-Kelly sacrifices some theoretical growth for dramatically smoother equity curves.

What happens when Kelly Criterion gives a negative number?+

A negative Kelly means the strategy loses money over time and you should bet zero. Do not trade that strategy. Go back to the backtest and fix the win rate or profit factor before risking any capital.

How do I calculate Kelly Criterion from my backtest results?+

Plug your win rate and profit factor into the formula: Kelly % = W - (1-W)/R, where W is win rate as a decimal and R is average win divided by average loss. Then multiply by 0.25 for quarter-Kelly.