Kelly Criterion Python Calculator: Stake Sizing with Free Odds API
Kelly Criterion Staking: Stop Guessing Your Bet Size
You’ve found a +EV bet. Now how much do you stake?
Most retail bettors flat-stake — 1% of bankroll on every pick, regardless of edge or odds. Sharps use the Kelly Criterion — a formula that tells you the optimal stake to maximize long-run bankroll growth, given your edge and the decimal odds on offer. Bet too little and you leave growth on the table. Bet too much and variance wipes you out.
This post builds a Python Kelly calculator that uses Pinnacle’s no-vig lines as the “fair probability” benchmark, tested on three live fixtures pulled from the OddsPapi API. Includes the fractional Kelly variant every professional uses in practice, and a full scanner that finds +EV Kelly bets across today’s slate.
The Formula (60 Seconds)
For a two-outcome bet at decimal odds d, with your estimated true probability p of winning:
f* = (b * p - q) / b
where:
b = d - 1 (net decimal odds, i.e. profit per £1 staked)
p = true probability of winning
q = 1 - p (probability of losing)
f* = fraction of bankroll to stake
Three things to notice:
- If
b*p < q, Kelly returns negative. This means the bet has negative EV — don’t bet. Clip to zero. - Kelly scales with edge. A 1% edge at 2.0 odds recommends 1% stake. A 10% edge at the same odds recommends 10%.
- Kelly is ruthless. It assumes your probability estimate is exact. In practice, it’s not — which is why every real-world Kelly user bets a fraction of the formula’s output. More on that in Step 5.
The Hard Part: Where Does p Come From?
The formula is easy. The hard part is estimating the true probability p. You have three options:
- Build a model. This is what quants do — xG, Elo, regression — but it takes months to calibrate and years to trust.
- Use market consensus. Average implied probability across 350+ books. Fast and reasonable for most markets.
- Use Pinnacle no-vig. Pinnacle is the sharpest book globally. Strip out their 2–3% margin and the resulting implied probability is the de facto industry benchmark for “fair.”
Option 3 is the professional default. Pinnacle takes bets from sharps without limiting them, which forces their lines to reflect true probability more tightly than any soft book. Here’s the Python:
def devig(decimal_odds):
"""Power-method de-vig: normalize implied probabilities to sum to 1."""
implied = [1/p for p in decimal_odds]
total = sum(implied)
return [i/total for i in implied], total
def kelly(decimal_odds, true_prob):
"""Kelly stake as fraction of bankroll. Clips negative to zero."""
b = decimal_odds - 1
p = true_prob
q = 1 - p
f = (b * p - q) / b
return max(f, 0)
devig() uses the simplest normalization — divide each implied probability by the overround. For 3-way markets (soccer 1X2), power methods or Shin’s method give slightly more accurate results, but normalization is good enough for 95% of use cases and is what we’ll use throughout this post.
Example 1: When Kelly Says Bet Nothing
Pinnacle’s moneyline on Dallas Stars vs Minnesota Wild (NHL, market 151 — Winner incl. OT/SO):
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
fixture_id = "id1500023470558100" # Dallas vs Minnesota
r = requests.get(f"{BASE}/odds",
params={"apiKey": API_KEY, "fixtureId": fixture_id})
books = r.json()["bookmakerOdds"]
pin = books["pinnacle"]["markets"]["151"]["outcomes"]
dal_odds = pin["151"]["players"]["0"]["price"] # 1.847
min_odds = pin["152"]["players"]["0"]["price"] # 2.07
fair_probs, vig = devig([dal_odds, min_odds])
print(f"Pinnacle raw: {dal_odds} / {min_odds}")
print(f"Vig: {(vig-1)*100:.2f}%")
print(f"Fair probs: Dallas={fair_probs[0]:.4f} Minnesota={fair_probs[1]:.4f}")
print(f"Fair odds: Dallas={1/fair_probs[0]:.4f} Minnesota={1/fair_probs[1]:.4f}")
Pinnacle raw: 1.847 / 2.07
Vig: 2.45%
Fair probs: Dallas=0.5285 Minnesota=0.4715
Fair odds: Dallas=1.8923 Minnesota=2.1207
Now check every US sportsbook — is any of them offering better than Pinnacle’s fair odds?
US_BOOKS = ["draftkings", "fanduel", "betmgm", "caesars", "betrivers", "bovada.lv"]
print(f"{'Book':<12} {'Side':<10} {'Offered':<8} {'Fair':<8} {'Edge':<8} {'Kelly':<8}")
for book in US_BOOKS:
b = books.get(book, {}).get("markets", {}).get("151", {}).get("outcomes", {})
for oid, name, fair_p in [("151", "Dallas", fair_probs[0]),
("152", "Minnesota", fair_probs[1])]:
price = b.get(oid, {}).get("players", {}).get("0", {}).get("price")
if not price: continue
fair_odds = 1 / fair_p
edge = (price / fair_odds - 1) * 100
f = kelly(price, fair_p) * 100
print(f"{book:<12} {name:<10} {price:<8} {fair_odds:<8.3f} "
f"{edge:+6.2f}% {f:.2f}%")
Book Side Offered Fair Edge Kelly
draftkings Dallas 1.83 1.892 -3.29% 0.00%
draftkings Minnesota 2.00 2.121 -5.69% 0.00%
fanduel Dallas 1.83 1.892 -3.29% 0.00%
fanduel Minnesota 2.00 2.121 -5.69% 0.00%
caesars Dallas 1.833 1.892 -3.13% 0.00%
caesars Minnesota 2.00 2.121 -5.69% 0.00%
betrivers Dallas 1.82 1.892 -3.82% 0.00%
betrivers Minnesota 2.07 2.121 -2.39% 0.00%
bovada.lv Dallas 1.82 1.892 -3.82% 0.00%
bovada.lv Minnesota 2.02 2.121 -4.75% 0.00%
Every US book is pricing worse than Pinnacle’s fair line. Kelly says bet zero on every one of them. That’s the discipline — most games don’t have an edge, and Kelly’s job is to tell you that.
Contrast this with flat-staking, where a retail bettor might slap 1% on Minnesota at DraftKings because they “like the value.” Kelly says no — you’re giving up 5.7% in expected value before the puck drops.
Example 2: A Real (Tiny) Edge
Manchester City vs Arsenal, Premier League, 1X2 market. Pinnacle’s three-way line:
pin = books["pinnacle"]["markets"]["101"]["outcomes"]
raw = [pin[oid]["players"]["0"]["price"] for oid in ["101", "102", "103"]]
# [1.877, 3.65, 4.37]
fair_probs, vig = devig(raw)
# Fair probs: H=0.5145 D=0.2646 A=0.2210
# Fair odds: H=1.9438 D=3.7798 A=4.5254
Compare to Betfair Exchange’s draw price of 3.80 and Unibet.nl’s Arsenal price of 4.60:
| Bet | Offered | Fair | Edge | Full Kelly | 1/4 Kelly |
|---|---|---|---|---|---|
| Draw @ betfair-ex | 3.80 | 3.780 | +0.53% | 0.19% | 0.05% |
| Arsenal @ unibet.nl | 4.60 | 4.525 | +1.65% | 0.46% | 0.11% |
This is what a realistic +EV Kelly bet looks like. Tiny edges, tiny stakes. On a £10,000 bankroll, full Kelly on Arsenal at unibet.nl is £46. Quarter Kelly is £11. That’s not glamorous — but compound it across 2,000 bets a year and you’re ahead.
If a Kelly calculator ever tells you to bet 10%+ of your bankroll on a single event, your probability estimate is almost certainly wrong. Which brings us to Example 3.
Example 3: When Kelly Lies (Enhanced Prices)
Paddypower was offering 2.20 on Manchester City in the same match — a huge price compared to Pinnacle’s 1.877 fair odds. Plug it into Kelly:
offered = 2.20
fair_p = 0.5145 # Pinnacle no-vig
edge = (offered / (1/fair_p) - 1) * 100 # +13.18%
full_kelly = kelly(offered, fair_p) * 100 # 10.99%
print(f"Edge: {edge:+.2f}%")
print(f"Full Kelly: {full_kelly:.2f}%")
print(f"Quarter Kelly: {full_kelly/4:.2f}%")
# Edge: +13.18%
# Full Kelly: 10.99%
# Quarter Kelly: 2.75%
Full Kelly says stake 10.99% of your bankroll. Do not do this.
A 13% edge over Pinnacle on a marquee Premier League game is not real. Paddypower’s 2.20 is almost certainly a price boost — a marketing promotion with a tiny max-stake cap (often £10–25) and a TOS clause that voids arbitrage-style betting. Kelly has no way to know this. It just sees “price + probability” and crunches.
This is exactly why professional bettors use fractional Kelly — typically 1/4 or 1/2 — to protect against the two things Kelly can’t see:
- Your
pis wrong. Pinnacle isn’t a perfect oracle. Public news, lineup changes, and weather move true probability between scrapes. - The price isn’t real. Boosts, stale lines, limit caps, and palpable-error voiding all mean the price you saw isn’t the price you’ll actually get to stake at size.
Fractional Kelly caps your downside when either of those assumptions breaks.
Step 5: The Fractional Kelly Function
def fractional_kelly(decimal_odds, true_prob, fraction=0.25):
"""Bet `fraction` of what full Kelly recommends.
Standard choices:
0.5 - Half Kelly. Sharper but still aggressive.
0.25 - Quarter Kelly. Professional default.
0.1 - Tenth Kelly. Ultra-conservative, common for model uncertainty.
"""
return kelly(decimal_odds, true_prob) * fraction
# Usage on the Arsenal unibet.nl bet
stake_pct = fractional_kelly(4.60, 0.2210, fraction=0.25)
bankroll = 10_000
stake = bankroll * stake_pct
print(f"1/4 Kelly stake: £{stake:.2f} ({stake_pct*100:.2f}% of bankroll)")
# 1/4 Kelly stake: £11.49 (0.11% of bankroll)
Professional sports bettors almost universally use some variant of fractional Kelly, typically 0.25. A 1/4 Kelly bettor gives up about 3/4 of Kelly’s theoretical growth rate in exchange for dramatically lower variance — and most importantly, survives the inevitable runs where their probability estimates are systematically wrong.
Step 6: Full +EV Scanner Across Today’s Slate
Single bets are fine. The real workflow is looping the calculation across every game on the slate, every market, every book — and surfacing only the +EV bets worth taking:
import time
from datetime import date, timedelta
def scan_sport(sport_id, market_id, outcome_ids, min_edge=0.01,
kelly_fraction=0.25, min_pin_vig=0.01, max_pin_vig=0.06):
"""Find all Kelly-positive bets for a given market on today's slate.
Args:
sport_id: OddsPapi sportId (10=soccer, 15=NHL, etc.)
market_id: int, e.g. 101 for soccer 1X2 or 151 for NHL moneyline
outcome_ids: list of outcome IDs for this market
min_edge: require this much edge vs Pinnacle fair to flag
kelly_fraction: 0.25 = quarter Kelly
min/max_pin_vig: reject fixtures where Pinnacle's overround is
suspiciously low or high (data quality filter)
"""
today = date.today().isoformat()
tomorrow = (date.today() + timedelta(days=1)).isoformat()
r = requests.get(f"{BASE}/fixtures", params={
"apiKey": API_KEY, "sportId": sport_id,
"from": today, "to": tomorrow,
})
fixtures = [f for f in r.json() if f.get("hasOdds")]
results = []
for f in fixtures:
time.sleep(0.2)
r2 = requests.get(f"{BASE}/odds",
params={"apiKey": API_KEY, "fixtureId": f["fixtureId"]})
books = r2.json().get("bookmakerOdds", {})
# Need Pinnacle as our fair benchmark
pin = books.get("pinnacle", {}).get("markets", {}).get(str(market_id), {}).get("outcomes", {})
pin_prices = []
for oid in outcome_ids:
p = pin.get(str(oid), {}).get("players", {}).get("0", {}).get("price")
if not p: break
pin_prices.append(p)
if len(pin_prices) != len(outcome_ids):
continue
fair_probs, vig = devig(pin_prices)
if not (1 + min_pin_vig <= vig <= 1 + max_pin_vig):
continue # Pinnacle data looks bogus, skip
# Check every other book for +EV on every outcome
for slug, b in books.items():
if slug == "pinnacle": continue
outs = b.get("markets", {}).get(str(market_id), {}).get("outcomes", {})
for oid, fair_p in zip(outcome_ids, fair_probs):
player = outs.get(str(oid), {}).get("players", {}).get("0", {})
if not player.get("active"): continue
price = player.get("price")
if not price: continue
fair_odds = 1 / fair_p
edge = price / fair_odds - 1
if edge < min_edge: continue
stake = fractional_kelly(price, fair_p, kelly_fraction)
results.append({
"fixture": f"{f['participant1Name']} vs {f['participant2Name']}",
"book": slug,
"outcome_id": oid,
"price": price,
"fair_odds": round(fair_odds, 3),
"edge_pct": round(edge*100, 2),
"kelly_pct": round(stake*100, 3),
})
return sorted(results, key=lambda x: -x["kelly_pct"])
# Scan soccer 1X2 for today
bets = scan_sport(sport_id=10, market_id=101, outcome_ids=[101, 102, 103])
for b in bets[:10]:
print(b)
Pipe the output to a CSV, filter by your book whitelist, and size your bets. That’s a complete +EV workflow in 80 lines of Python.
Practical Guardrails
Things the formula won’t tell you but that will save your bankroll:
- Correlated bets. Kelly assumes independent outcomes. Two bets on the same game (e.g., moneyline + over) are correlated — never sum their Kelly stakes. Pick the higher-edge one.
- Limit caps. If the book caps you at £50, your “Kelly stake” is £50 even if the formula says £200. Factor this in.
- Closing line value. The ultimate sanity check on your
pestimate is whether your bets beat the closing line on average. OddsPapi’s historical odds endpoint is free — pull closing prices and track CLV on every bet. - Vig too tight or too wide. If Pinnacle’s overround is under 1% or over 8%, something’s off — a market that’s about to suspend, a long-tail sport, or bad data. Filter these out.
- Minimum edge threshold. Don’t take +0.1% edges. Model uncertainty alone is usually 1–2% — you need a real cushion. Professional thresholds are usually 2–3% minimum edge.
Fractional Kelly Cheat Sheet
| Fraction | Use Case | Variance | Growth Rate |
|---|---|---|---|
| Full (1.0) | Certainty about p — almost never |
Ruinous | Maximum |
| Half (0.5) | High-confidence model, well-calibrated | High | ~94% of max |
| Quarter (0.25) | Professional default | Moderate | ~75% of max |
| Tenth (0.1) | New model, low confidence, large drawdown budget | Low | ~36% of max |
| Flat staking | Retail default | Lowest | Lowest |
Growth rates are rough — see Thorp’s 1997 Kelly paper for the actual math. The pattern is consistent though: 1/4 Kelly captures most of the long-run growth of full Kelly with a fraction of the variance and drawdown risk.
Stop Flat Staking. Start Compounding.
You’ve got the formula, you’ve got a reliable probability source (Pinnacle no-vig via OddsPapi), and you’ve got a scanner that surfaces +EV bets automatically. Flat staking 1% per bet leaves money on the table when the edge is large and ruins your bankroll when the edge is thin.
Grab a free OddsPapi API key and you’re pulling Pinnacle no-vig probabilities into your Kelly calculator in the next ten minutes. Pair it with the line shopping tool to find the best available price, then let Kelly size the bet.
FAQ
What’s the difference between Kelly and EV betting?
EV (expected value) betting identifies +EV opportunities. Kelly tells you how much to stake on them. They’re complementary — Kelly assumes you’ve already done the edge calculation. Without an edge, Kelly outputs zero.
Why fractional Kelly instead of full Kelly?
Full Kelly assumes your probability estimate is perfectly accurate. It almost never is. Fractional Kelly (typically 1/4) trades a small amount of theoretical growth for dramatically lower variance and survival through the inevitable runs where your estimates are systematically off.
Can I use consensus odds across 350+ books instead of Pinnacle no-vig?
Yes, and for markets where Pinnacle doesn’t offer, you have to. Consensus odds (averaged across all active bookmakers) are a reasonable substitute — slightly noisier than Pinnacle alone, but more robust. Weight by book quality if you have the data. See our consensus odds tutorial.
How do I handle correlated bets?
Kelly assumes outcomes are independent. Two bets on the same game (moneyline + over, parlay legs, same-game props) are correlated — you cannot simply sum their Kelly stakes. Either pick the highest-edge single bet, or use joint-Kelly formulas that account for correlation. For simple workflows, just pick one bet per event.
Is historical data useful for Kelly?
Critical. Tracking your Closing Line Value (CLV) against historical closing prices is the only way to validate that your probability estimates are actually better than market. OddsPapi’s /historical-odds endpoint is on the free tier — use it to backtest your Kelly stakes against closing lines.