Position sizing is where most bettors — and most betting bots — fail. You can find genuine edges, build a beautiful +EV scanner, and still go broke by betting too much or too little. Bet too aggressively and a cold streak wipes you out. Bet too conservatively and your bankroll barely moves.
The Kelly criterion solves this mathematically. It’s the formula that tells you exactly how much of your bankroll to risk on each bet to maximize your long-run growth rate. It’s been the secret weapon of professional gamblers and quantitative investors since the 1950s, and it’s the backbone of any serious automated betting system.
In this tutorial, you’ll build a KellyBankrollManager in Python that takes your edge estimates and odds, calculates optimal stake sizes, and manages your bankroll dynamically as bets resolve.
The Kelly Formula
The Kelly criterion gives you the optimal fraction of your bankroll to wager:
f* = (bp - q) / b
Where:
- f* = fraction of bankroll to bet
- b = net decimal odds (decimal odds - 1). For American odds of -110, decimal odds are 1.909, so b = 0.909
- p = your estimated probability of winning
- q = probability of losing (1 - p)
The intuition behind Kelly is that it maximizes the expected logarithm of your wealth. Why logarithm? Because bankroll growth is multiplicative, not additive. If you double your money then lose half, you’re back to even — not up by 50%. The log function captures this compounding reality, and Kelly optimizes for it.
A Concrete Example
You’ve identified a -110 line at an offshore book where your model estimates a 55% true win probability. The book’s implied probability is about 52.4%, giving you a 2.6% edge.
- Decimal odds: 1.909
- b = 0.909
- p = 0.55, q = 0.45
- f* = (0.909 × 0.55 - 0.45) / 0.909
- f* = (0.500 - 0.45) / 0.909
- f = 0.055 (5.5% of bankroll)*
On a $10,000 bankroll, Kelly says to bet $550. The formula scales naturally: larger edges and shorter odds produce bigger recommended stakes, while thin edges or long shots produce smaller ones. If your edge is zero or negative, Kelly returns zero or a negative number — telling you not to bet at all.
What the Formula Tells You
Two key relationships drive Kelly sizing:
- Bet more when your edge is larger. A 5% edge on a coin flip produces a bigger Kelly stake than a 2% edge.
- Bet less on longshots. Even with a large percentage edge, long-odds bets get smaller Kelly stakes because one loss wipes out many wins.
This is why Kelly works so well for sports betting at standard odds ranges (-110 to +200) — the formula naturally moderates stake sizes in exactly the range where you’re most likely to find real edges.
Why Full Kelly Is Too Aggressive
Here’s the problem: full Kelly assumes your edge estimate is exactly correct. In practice, you’re estimating probabilities from models, sharp benchmarks, or historical data — and every estimate carries noise.
If your true edge is 3% but you think it’s 6%, full Kelly sizes as if you have twice the edge you actually do. Over hundreds of bets, that systematic overestimation leads to dramatically larger drawdowns and a meaningful risk of practical ruin (hitting the book’s minimum bet before your bankroll recovers).
Full Kelly’s variance profile is brutal:
- Expected drawdowns of 50%+ are common even with a genuine edge
- The standard deviation of outcomes is enormous relative to the mean
- Recovery from deep drawdowns takes hundreds of bets
Fractional Kelly: The Practical Solution
The fix is simple — multiply the Kelly fraction by a safety factor:
f_adjusted = kelly_fraction × f*
Where kelly_fraction is typically between 0.25 (quarter Kelly) and 0.5 (half Kelly).
The math on the tradeoff is remarkable:
| Kelly Fraction | Growth Rate (% of Full) | Variance (% of Full) |
|---|---|---|
| 1.0 (Full) | 100% | 100% |
| 0.5 (Half) | 75% | 25% |
| 0.25 (Quarter) | 44% | 6.25% |
Half Kelly gives you 75% of the maximum growth rate with only 25% of the variance. That’s an extraordinary deal — you give up a quarter of your growth to eliminate three-quarters of the volatility. For any automated system running on estimated edges, half Kelly or less is the rational default.
Building the Kelly Bot
Here’s a complete KellyBankrollManager class that handles stake sizing, bet tracking, and bankroll management:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class BetRecord:
timestamp: datetime
event: str
odds: float
edge: float
kelly_full: float
kelly_adjusted: float
stake: float
result: float | None = None
bankroll_after: float | None = None
class KellyBankrollManager:
def __init__(
self,
bankroll: float,
kelly_fraction: float = 0.5,
min_bet: float = 5.0,
max_bet_pct: float = 0.05,
max_exposure_pct: float = 0.20,
):
self.bankroll = bankroll
self.kelly_fraction = kelly_fraction
self.min_bet = min_bet
self.max_bet_pct = max_bet_pct
self.max_exposure_pct = max_exposure_pct
self.peak_bankroll = bankroll
self.history: list[BetRecord] = []
self.pending: list[BetRecord] = []
def kelly_stake(self, odds: float, win_prob: float) -> dict:
"""Calculate Kelly stake for a bet opportunity.
Args:
odds: Decimal odds (e.g. 1.909 for -110)
win_prob: Estimated probability of winning (0 to 1)
"""
b = odds - 1
q = 1 - win_prob
edge = win_prob - (1 / odds)
full_kelly = (b * win_prob - q) / b
if full_kelly <= 0:
return {"stake": 0, "full_kelly": full_kelly, "edge": edge, "skip": True}
adjusted = full_kelly * self.kelly_fraction
stake = self.bankroll * adjusted
max_bet = self.bankroll * self.max_bet_pct
stake = min(stake, max_bet)
pending_exposure = sum(p.stake for p in self.pending)
remaining_exposure = (self.bankroll * self.max_exposure_pct) - pending_exposure
stake = min(stake, max(0, remaining_exposure))
if stake < self.min_bet:
return {"stake": 0, "full_kelly": full_kelly, "edge": edge, "skip": True}
return {
"stake": round(stake, 2),
"full_kelly": round(full_kelly, 4),
"adjusted_kelly": round(adjusted, 4),
"edge": round(edge, 4),
"skip": False,
}
def place_bet(self, event: str, odds: float, win_prob: float) -> BetRecord | None:
sizing = self.kelly_stake(odds, win_prob)
if sizing["skip"]:
return None
record = BetRecord(
timestamp=datetime.now(),
event=event,
odds=odds,
edge=sizing["edge"],
kelly_full=sizing["full_kelly"],
kelly_adjusted=sizing["adjusted_kelly"],
stake=sizing["stake"],
)
self.pending.append(record)
return record
def resolve_bet(self, record: BetRecord, won: bool):
profit = record.stake * (record.odds - 1) if won else -record.stake
self.bankroll += profit
record.result = profit
record.bankroll_after = self.bankroll
self.peak_bankroll = max(self.peak_bankroll, self.bankroll)
if record in self.pending:
self.pending.remove(record)
self.history.append(record)
@property
def current_drawdown(self) -> float:
if self.peak_bankroll == 0:
return 0
return (self.peak_bankroll - self.bankroll) / self.peak_bankroll
@property
def max_drawdown(self) -> float:
if not self.history:
return 0
peak = self.history[0].bankroll_after or self.bankroll
max_dd = 0
for record in self.history:
if record.bankroll_after is None:
continue
peak = max(peak, record.bankroll_after)
dd = (peak - record.bankroll_after) / peak
max_dd = max(max_dd, dd)
return max_dd
def summary(self) -> dict:
resolved = [r for r in self.history if r.result is not None]
wins = sum(1 for r in resolved if r.result > 0)
total_profit = sum(r.result for r in resolved)
return {
"bankroll": round(self.bankroll, 2),
"total_bets": len(resolved),
"wins": wins,
"losses": len(resolved) - wins,
"total_profit": round(total_profit, 2),
"roi_pct": round((total_profit / sum(r.stake for r in resolved)) * 100, 2)
if resolved
else 0,
"max_drawdown_pct": round(self.max_drawdown * 100, 2),
"current_drawdown_pct": round(self.current_drawdown * 100, 2),
}
Usage Example
mgr = KellyBankrollManager(bankroll=10_000, kelly_fraction=0.5)
bet1 = mgr.place_bet("Chiefs -3 vs Ravens", odds=1.909, win_prob=0.55)
bet2 = mgr.place_bet("Lakers ML vs Celtics", odds=2.10, win_prob=0.52)
print(f"Bet 1: ${bet1.stake} on {bet1.event} (edge: {bet1.edge:.1%})")
print(f"Bet 2: ${bet2.stake} on {bet2.event} (edge: {bet2.edge:.1%})")
mgr.resolve_bet(bet1, won=True)
mgr.resolve_bet(bet2, won=False)
print(mgr.summary())
# {'bankroll': 10199.55, 'total_bets': 2, 'wins': 1, 'losses': 1,
# 'total_profit': 199.55, 'roi_pct': 34.21, ...}
The manager dynamically adjusts every stake based on the current bankroll. After a win, your bankroll grows and Kelly sizes up. After a loss, your bankroll shrinks and Kelly sizes down. This is the core advantage of Kelly over flat staking — it’s self-correcting.
Multi-Bet Kelly
When your scanner finds multiple simultaneous opportunities, you need to manage total exposure. The KellyBankrollManager above already handles this with max_exposure_pct, but here’s the logic made explicit:
def size_multiple_bets(
mgr: KellyBankrollManager,
opportunities: list[dict],
) -> list[dict]:
"""Size a batch of simultaneous bets with exposure management."""
sized = []
for opp in sorted(opportunities, key=lambda x: x["edge"], reverse=True):
result = mgr.kelly_stake(opp["odds"], opp["win_prob"])
if not result["skip"]:
record = mgr.place_bet(opp["event"], opp["odds"], opp["win_prob"])
if record:
sized.append({"event": opp["event"], "stake": record.stake})
return sized
The key details:
- Sort by edge descending so the highest-edge bets get sized first and consume available exposure before lower-edge bets
- The exposure cap ensures that even if Kelly recommends 5% on each of six concurrent bets, total exposure stays within your 20% cap
- Treat bets as independent unless you have correlated outcomes (e.g., two bets on the same game), which should share a single Kelly allocation
For most sports betting scenarios — different games, different sports, different markets — independence is a safe assumption. If you’re making multiple bets on the same event (spread + total + prop), you need to account for correlation manually or simply cap per-event exposure.
Practical Considerations
Edge Estimation Is Everything
Kelly is only as good as your edge estimate. If you consistently overestimate your edge by 2%, Kelly will systematically oversize your bets and your bankroll will erode instead of grow.
The best way to calibrate: track your closing line value. If your CLV consistently shows 3% and your Kelly model assumes 3%, you’re calibrated. If CLV shows 1.5% but you’re sizing as if you have 3%, cut your Kelly fraction or fix your edge model.
Book Constraints
Offshore sportsbooks impose constraints that interact with Kelly:
- Minimum bets ($5-$25 depending on the book) create a floor. If Kelly says bet $3, you either skip the bet or accept the over-sizing risk.
- Maximum bets cap your upside. If you have a huge edge and Kelly says bet $2,000 but the book limits you to $500, you’re under-betting relative to optimal — which is fine for bankroll safety but limits growth.
- Account limits can reduce your max bet over time if the book identifies you as sharp. Factor declining limits into your long-term bankroll projections.
Ruin Probability
Under full Kelly with accurate edge estimates, the theoretical probability of your bankroll ever dropping to a fraction f of its peak is exactly f. So there’s a 10% chance of a 90% drawdown under full Kelly — which is why nobody runs full Kelly in production.
Under half Kelly, these probabilities drop dramatically. The probability of a 50% drawdown under half Kelly with a 3% average edge is roughly 6%, compared to ~50% under full Kelly. This alone justifies the fractional approach.
When to Re-evaluate
Your Kelly system needs periodic auditing:
- Monthly: Compare actual results vs Kelly projections. Are drawdowns within expected ranges?
- After 500+ bets: Run a formal CLV analysis to verify your edge estimates are calibrated
- After any 20%+ drawdown: Pause automated betting and evaluate whether your edge model is still valid or the market has shifted
What’s Next
Kelly staking is one piece of the sharp betting stack. Combine it with edge detection and market analysis:
- +EV Betting Bots — Find the edges that feed into your Kelly sizing
- Closing Line Value API — Calibrate your edge estimates with CLV tracking
- Juice Comparison — Find the softest lines to maximize your edge
- Sharp Betting Concepts — The full sharp betting toolkit
- Betting Bots for Offshore Sportsbooks — Architecture patterns for automated betting agents