Expected Value Betting in Python: +EV, CLV & Pinnacle Benchmark

Expected Value Betting in Python - OddsPapi API Blog
How To Guides May 19, 2026

Expected Value Betting: The Only Math That Matters

Every losing bettor sees expected value. Few calculate it.

If your decimal odds, multiplied by the true probability of winning, sum to less than 1 over thousands of bets, you lose. That’s it. That’s the entire game. Volume, hot streaks, “feel” — none of it changes the arithmetic.

This post is the EV playbook: a Python tutorial that pulls live Pinnacle prices from 350+ bookmakers, de-vigs them into fair probabilities, and scans every soft book on the market for positive expected value (+EV) bets. Then we go further — we calculate Closing Line Value (CLV) from free historical odds, which is how sharp bettors prove they have edge after the fact.

What Is Expected Value in Betting?

Expected value is the long-run average return per unit staked. The formula is simple:

EV = (decimal_odds × true_probability) − 1

If EV is positive, the bet is profitable in the long run. If negative, it isn’t. Three numbers, no ambiguity.

The problem is the middle term. Nobody knows the true probability of a Manchester United win. The closest thing the market has is Pinnacle’s de-vigged price — the sharpest sportsbook in the world, with the lowest margins and the highest betting limits, used as a benchmark by professional bettors and academic researchers alike.

Pinnacle’s API is closed to the public. OddsPapi aggregates Pinnacle (and 350+ other bookmakers) into a single REST endpoint with a free tier — making the EV calculation trivial.

Approach Fair-probability source Cost
Direct Pinnacle API Pinnacle no-vig Closed — enterprise contract only
Build your own model Custom rating system Months of work, hard to validate
Generic odds APIs (40 books) No Pinnacle, weaker benchmark $50–$500/mo
OddsPapi Pinnacle + 350+ books Free tier — historical included

The +EV Workflow (Step by Step)

  1. Pull odds for a fixture across every bookmaker on the market.
  2. Take Pinnacle’s three-way 1X2 line and remove the vig — this gives you fair probabilities.
  3. For every other bookmaker × outcome, plug their price into EV = price × fair_prob − 1.
  4. Anything > 0 is a positive expected value bet.

That’s the entire playbook. The rest is execution.

Step 1: Authentication and Setup

Grab a free OddsPapi key. Auth is a query parameter — never a header.

import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

def api_get(path, **params):
    params["apiKey"] = API_KEY
    return requests.get(f"{BASE_URL}/{path}", params=params).json()

Step 2: Pull Live Odds for a Fixture

We’ll use today’s headline fixture: Manchester United vs Liverpool (fixture ID id1000001761301215). The /odds endpoint returns every active bookmaker pricing the match.

fixture_id = "id1000001761301215"
data = api_get("odds", fixtureId=fixture_id)
books = data["bookmakerOdds"]

print(f"Bookmakers pricing this fixture: {len(books)}")
# Bookmakers pricing this fixture: 122

122 bookmakers. Pinnacle is one of them. Every soft book — DraftKings, FanDuel, Bet365, BetMGM, the EU softs, the crypto books — is in the same payload, all on the same market IDs.

Step 3: De-vig Pinnacle Into Fair Probabilities

The 1X2 (Full Time Result) market is marketId=101. Outcome IDs are 101=Home, 102=Draw, 103=Away. Implied probability is 1 / decimal_odds. The three implied probabilities sum to more than 1 — that excess is the vig. To get fair probabilities, divide each by the overround.

def fair_probabilities(book_market):
    """Devig a 1X2 market using the proportional method."""
    outs = book_market["outcomes"]
    raw = {oid: outs[oid]["players"]["0"]["price"] for oid in ("101", "102", "103")}
    implied = {oid: 1 / price for oid, price in raw.items()}
    overround = sum(implied.values())
    fair = {oid: imp / overround for oid, imp in implied.items()}
    return raw, fair, overround

pin_market = books["pinnacle"]["markets"]["101"]
raw, fair, overround = fair_probabilities(pin_market)

print(f"Pinnacle raw odds:  H={raw['101']}  D={raw['102']}  A={raw['103']}")
print(f"Vig:                {(overround - 1) * 100:.2f}%")
print(f"Fair probabilities: H={fair['101']:.4f}  D={fair['102']:.4f}  A={fair['103']:.4f}")
print(f"Fair odds:          H={1/fair['101']:.3f}  D={1/fair['102']:.3f}  A={1/fair['103']:.3f}")

Live output (May 3, 2026):

Pinnacle raw odds:  H=2.41  D=3.82  A=2.84
Vig:                2.88%
Fair probabilities: H=0.4033  D=0.2544  A=0.3422
Fair odds:          H=2.479  D=3.930  A=2.922

Pinnacle’s vig on this match is 2.88% — about a quarter of what a US soft book charges on the same line. The fair probabilities are our benchmark for the rest of the scan.

Step 4: Calculate +EV for Every Book × Outcome

Iterate every bookmaker, look up the price they’re offering on each outcome, and compute EV against Pinnacle’s fair probability.

OUTCOME_NAMES = {"101": "Home", "102": "Draw", "103": "Away"}

def expected_value(price, fair_prob):
    return (price * fair_prob - 1) * 100  # percent

results = []
for slug, bdata in books.items():
    if "101" not in bdata.get("markets", {}):
        continue
    outs = bdata["markets"]["101"]["outcomes"]
    for oid, oname in OUTCOME_NAMES.items():
        try:
            book = outs[oid]["players"]["0"]
            if not book.get("active"):
                continue
            price = book["price"]
        except (KeyError, TypeError):
            continue
        ev_pct = expected_value(price, fair[oid])
        results.append((slug, oname, price, ev_pct))

# Top +EV first
results.sort(key=lambda x: -x[3])

print(f"{'Book':<25} {'Outcome':<10} {'Price':>7} {'EV%':>8}")
for r in results[:10]:
    print(f"{r[0]:<25} {r[1]:<10} {r[2]:>7.3f} {r[3]:>7.2f}%")

Live output (Man Utd vs Liverpool, May 3, 2026 — 30 minutes before kickoff):

Book                      Outcome      Price      EV%
hardrockbet               Away         3.000     2.67%
kalshi                    Draw         4.000     1.78%
betfair-ex                Home         2.500     0.83%
kalshi                    Away         2.941     0.65%
polymarket                Away         2.941     0.65%
betfair-ex                Away         2.900    -0.75%
apuestatotal              Away         2.900    -0.75%
duel                      Draw         3.900    -0.77%
atg.se                    Home         2.450    -1.19%
unibet.nl                 Home         2.450    -1.19%

Three +EV bets are live right now: Hard Rock Bet pricing Liverpool at 3.00 (+2.67% EV), Kalshi pricing the Draw at 4.00 (+1.78%), and Betfair Exchange pricing Manchester United at 2.50 (+0.83%). The bottom of the list is what most retail bettors actually take — Winamax and No Vig pricing the same outcomes at -15% to -27% EV.

Same fixture. Same outcomes. The market spread alone — driven by which book you walk into — is the difference between long-run profit and long-run ruin.

From +EV to CLV: The Real Test of Edge

Here’s where most retail “value betting” content stops. EV at the moment of bet placement is necessary but not sufficient. The bet only has to be +EV against the final closing line — the price the sharpest book offers right before kickoff — for the edge to be real.

That’s Closing Line Value (CLV): the difference between the price you got and the price the market settled on. CLV is the gold standard sharps use to validate edge, because it strips out variance. You can lose ten bets in a row and still know you’re a winning bettor if your CLV is positive.

CLV% = (your_price / closing_pinnacle_fair_price − 1) × 100

Positive CLV: you got a better price than where the market closed. The line moved toward your number. That’s edge.

Negative CLV: the market disagreed with your bet and moved against you. Even if the bet wins, you didn’t have edge — you got lucky.

How to Calculate CLV With Free Historical Odds

The /historical-odds endpoint returns the full price-history snapshot list for any fixture. Free tier. No paywall. Maximum 3 bookmakers per call — pass them as a comma-separated list.

history = api_get(
    "historical-odds",
    fixtureId="id1000001761301215",
    bookmakers="pinnacle,bet365,draftkings",
)

pin_history = history["bookmakers"]["pinnacle"]["markets"]["101"]["outcomes"]

# players["0"] is a LIST of snapshots, not a single price
home_snapshots = pin_history["101"]["players"]["0"]
away_snapshots = pin_history["103"]["players"]["0"]

print(f"Pinnacle Manchester United price snapshots: {len(home_snapshots)}")
print(f"Opening (Apr 19): {home_snapshots[0]['price']}")
print(f"Latest  (May 3):  {home_snapshots[-1]['price']}")

Output:

Pinnacle Manchester United price snapshots: 179
Opening (Apr 19): 2.32
Latest  (May 3):  2.41

179 snapshots over two weeks. We can replay every line move Pinnacle made on this fixture. Note the structural difference between /odds and /historical-odds: in the live endpoint, players["0"] is a single dict containing the current price; in historical, it’s a list of dated snapshots. Don’t mix them up.

CLV Calculator

To compute CLV for a hypothetical bet, take the closing snapshot, de-vig it, and compare against your entry price.

def closing_fair_odds(history, market_id="101"):
    """Pull the final Pinnacle snapshot for each outcome and devig."""
    outcomes = history["bookmakers"]["pinnacle"]["markets"][market_id]["outcomes"]
    closing_prices = {
        oid: outcomes[oid]["players"]["0"][-1]["price"]
        for oid in outcomes
    }
    overround = sum(1 / p for p in closing_prices.values())
    fair_prob = {oid: (1 / p) / overround for oid, p in closing_prices.items()}
    fair_odds = {oid: 1 / p for oid, p in fair_prob.items()}
    return fair_odds

def clv(your_price, closing_fair_price):
    return (your_price / closing_fair_price - 1) * 100  # percent

closing = closing_fair_odds(history)
print(f"Closing fair odds: H={closing['101']:.3f}  D={closing['102']:.3f}  A={closing['103']:.3f}")

# Hypothetical: you bet Liverpool at Hard Rock @ 3.00 yesterday
your_clv = clv(3.00, closing["103"])
print(f"Hard Rock Liverpool @ 3.00 vs close {closing['103']:.3f}: CLV = {your_clv:+.2f}%")

# Hypothetical: you took Bet365 Liverpool @ 2.70 at the open
your_clv = clv(2.70, closing["103"])
print(f"Bet365 Liverpool @ 2.70 (open) vs close {closing['103']:.3f}: CLV = {your_clv:+.2f}%")

Output:

Closing fair odds: H=2.479  D=3.930  A=2.922
Hard Rock Liverpool @ 3.00 vs close 2.922: CLV = +2.67%
Bet365 Liverpool @ 2.70 (open) vs close 2.922: CLV = -7.60%

The Hard Rock bet has +2.67% CLV — exactly equal to its live EV%, because we’re measuring against the same Pinnacle no-vig benchmark in both cases. The Bet365 opening bet has -7.60% CLV: it looked like value at the time, but the sharp market disagreed and the line moved against it.

This is why volume bettors track CLV religiously. Over 1,000 bets, anyone with average CLV > +1% is mathematically a winner; anyone with CLV < 0 is mathematically a loser, regardless of the W/L record on the books.

Putting It Together: The Full +EV Scanner

The version below scans every fixture in a sport for the day, devigs Pinnacle on the 1X2 market, and surfaces every cross-book bet above a configurable EV threshold.

import time
from datetime import datetime, timedelta, timezone

EV_THRESHOLD = 1.0  # percent
SHARP_BOOK = "pinnacle"

def fair_probs_from_book(book_market):
    outs = book_market["outcomes"]
    implied = {oid: 1 / outs[oid]["players"]["0"]["price"] for oid in outs}
    total = sum(implied.values())
    return {oid: p / total for oid, p in implied.items()}

def scan_fixture(fixture_id, market_id="101"):
    data = api_get("odds", fixtureId=fixture_id)
    books = data.get("bookmakerOdds", {})
    sharp = books.get(SHARP_BOOK, {}).get("markets", {}).get(market_id)
    if not sharp:
        return []
    fair = fair_probs_from_book(sharp)
    edges = []
    for slug, bdata in books.items():
        if slug == SHARP_BOOK:
            continue
        m = bdata.get("markets", {}).get(market_id)
        if not m:
            continue
        for oid, fp in fair.items():
            try:
                player = m["outcomes"][oid]["players"]["0"]
                if not player.get("active"):
                    continue
                price = player["price"]
            except (KeyError, TypeError):
                continue
            ev = (price * fp - 1) * 100
            if ev >= EV_THRESHOLD:
                edges.append({"book": slug, "outcome": oid, "price": price, "ev_pct": ev})
    return edges

# Today's slate
now = datetime.now(timezone.utc)
fixtures = api_get("fixtures", sportId=10,
    **{"from": now.strftime("%Y-%m-%dT%H:%M:%S"),
       "to":   (now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%S")})

for fx in fixtures[:50]:
    if not fx.get("hasOdds"):
        continue
    edges = scan_fixture(fx["fixtureId"])
    if edges:
        print(f"\n{fx['participant1Name']} vs {fx['participant2Name']} ({fx['tournamentName']})")
        for e in sorted(edges, key=lambda x: -x["ev_pct"]):
            print(f"  {e['book']:<20} {e['outcome']} @ {e['price']:>6.3f}  EV={e['ev_pct']:+.2f}%")
    time.sleep(0.2)  # respect free-tier rate limit

Run it on a Saturday slate and you’ll see consistent +EV opportunities at the soft-book end of the market — particularly on draws (which retail bettors avoid) and on whichever team is unfashionable in the public narrative.

The Catch: Why +EV Doesn’t Mean Free Money

This is the part most “value betting” content omits. Three caveats kill more +EV strategies than any other.

1. Soft Books Limit and Ban Sharp Bettors

FanDuel, DraftKings, BetMGM, and most US sportsbooks profile their bettors. If you consistently take +EV bets, you’ll be limited (max stake reduced to $5–$50) within a few weeks. This is why pros build large outs portfolios and bet small, frequent stakes — and why exchange-style books (Betfair, Polymarket, Kalshi) are increasingly the meat of professional bankrolls.

2. Stale Odds and Boost Outliers

If a soft book is showing a price 5%+ better than the market consensus, the question is why. Often it’s a stale price about to be corrected, a feed glitch, or a deliberate boost that bans winners faster. Always sanity-check outliers against multiple books — a price that only one book offers and that no other book is within 3% of is suspect.

3. Pinnacle Isn’t Infallible

Pinnacle is the sharpest book on the market, but they’re not omniscient. On thinly traded markets — lower-tier soccer, college sports during off-peak hours, niche tournaments — Pinnacle’s line can be just as soft as any other book. The right benchmark is whichever book sets the market for that specific sport. For NBA, Pinnacle and Circa share the lead. For Asian Handicap soccer, Singbet and SBOBet sharpen Pinnacle. For prediction markets, Polymarket and Kalshi are the market.

The OddsPapi API exposes all of these books on the same endpoint, so swapping the benchmark in the scanner is a one-line change: SHARP_BOOK = "singbet" for Asian Handicap, SHARP_BOOK = "polymarket" for elections.

What to Build Next

The +EV scanner above is the foundation. From here, the standard next steps are:

  • Stake sizing. Once you have an EV%, you need to convert it to a bet size. The Kelly Criterion calculator uses Pinnacle no-vig as the same fair-probability benchmark.
  • Sharper benchmarks. Pinnacle is good. A consensus average across multiple sharps (Pinnacle + Circa + Singbet) is better.
  • Live scanning. The value betting scanner wraps the math above into a long-running loop with alert hooks.
  • Steam detection. Pinnacle line movements often precede soft-book moves by 30–90 seconds. The steam move detector uses the same /historical-odds endpoint to flag stale soft-book prices in real time.
  • Backtesting. Before deploying anything live, replay against the free historical odds dataset to validate your CLV is consistently positive.

FAQ: Expected Value Betting

What is expected value in sports betting?

Expected value (EV) is the long-run average return per unit staked on a bet. The formula is EV = (decimal_odds × true_probability) − 1. Positive EV means the bet is profitable in the long run; negative EV means it isn’t. It’s the only metric that matters over thousands of bets — variance dominates short samples but EV converges to the mean given enough volume.

How do I calculate the “true probability” of a bet?

The professional benchmark is Pinnacle Sportsbook’s de-vigged price. Pinnacle has the lowest vig on the market (typically 2–4%), the highest betting limits, and explicitly takes sharp action — which means their line is the closest thing to a true probability the market produces. De-vigging is straightforward: take 1/odds for each outcome to get implied probabilities, then divide each by the sum (the overround) to get fair probabilities.

What’s the difference between +EV and CLV?

+EV is calculated at the moment of bet placement against the current sharp line. CLV (Closing Line Value) is calculated retrospectively against the closing sharp line — the price right before the event starts. CLV is the more rigorous test because it incorporates the market’s final assessment. A bet can be +EV when placed but -CLV by close if the line moves against you. Pros track both.

Can I make a living from +EV betting?

Mathematically yes; practically it requires solving three problems: (1) bet limits — soft books limit winners within weeks, so you need a portfolio of accounts including exchanges; (2) volume — at 2–3% average EV, you need hundreds of bets per week to overcome variance; (3) discipline — losing streaks of 30+ bets are normal even with positive expected value. Most people who try fail not because the math is wrong but because they can’t sit through the variance.

Why use Pinnacle instead of an average across all books?

Most books shade their lines based on public action — the popular team gets shorter odds because the book is balancing risk, not pricing probability. Pinnacle prices probability directly and adjusts to sharp money. An average that includes 50 soft books will be biased toward whatever the public is betting, which is exactly what you’re trying to fade. That said, a consensus average across multiple sharp books (Pinnacle, Circa, Singbet) is more robust than Pinnacle alone, especially in markets where Pinnacle’s volume is thin.

What’s the OddsPapi free tier?

The free tier includes 350+ bookmakers (including Pinnacle and the other sharps), 59 sports, 1X2/handicap/totals/player props, and full historical odds with price snapshots. Authentication is a query parameter (?apiKey=KEY). Rate limit is roughly 0.88 seconds between calls to the same endpoint — use time.sleep(0.2) in scanner loops for safety.

Get Started

The math doesn’t care how you feel about the bet. It only cares whether decimal odds × true probability is greater than 1. Stop guessing and start measuring.

Get your free OddsPapi API key and run the scanner above on tonight’s slate. Every fixture you scan, every cross-book comparison you make, is a step closer to betting like the math says you should.