American Odds, the Vig, and Where Your Edge Actually Lives
A rating system gives you a probability. A sportsbook gives you a price. Comparing them requires putting both in the same units, and the units the book publishes are designed for quoting rather than for arithmetic.
keeks-elote does the conversion so a staking strategy can see an edge. It is worth walking through it, because the step where most of the edge disappears is the one that looks like bookkeeping.
American odds
A moneyline is quoted as a signed integer, and the sign changes what the number means.
Positive is what you win on a 100 stake. +150 returns 150 profit on 100 risked.
Negative is what you must risk to win 100. -150 returns 100 profit on 150 risked.
As decimal odds, which is total return per unit staked:
| American | decimal | implied probability |
|---|---|---|
| -200 | 1.5000 | 0.6667 |
| -150 | 1.6667 | 0.6000 |
| -110 | 1.9091 | 0.5238 |
| +100 | 2.0000 | 0.5000 |
| +110 | 2.1000 | 0.4762 |
| +150 | 2.5000 | 0.4000 |
| +300 | 4.0000 | 0.2500 |
The conversion is two branches:
def american_to_decimal(american_odds):
if american_odds > 0:
return american_odds / 100.0 + 1.0
return 100.0 / abs(american_odds) + 1.0
+100 and -100 both give 2.0, which is the hinge the convention is built around, and there is no such thing as a moneyline between -100 and +100. That gap is the first thing to check when parsing: a -50 in your data is not a price, it is a bad record.
Implied probability is then just 1 / decimal.
The two prices do not agree
Take both sides of a game and convert each to an implied probability. If the book were quoting an honest forecast, they would sum to 1. They do not. Across 664 priced college football games:
median overround 1.0551
mean 1.0534
in a plausible band 661 of 664 games
The implied probabilities sum to 1.055, not 1.000. That 5.5% is the vig, and it is the house’s margin expressed as a distortion of the probabilities rather than as a fee. Nothing is ever deducted from your account. The prices are just slightly worse than the book’s actual opinion, on both sides at once.
Which means the number you have to beat is not the implied probability. It is the de-vigged one:
implied_winner = 1 / decimal_winner
implied_loser = 1 / decimal_loser
overround = implied_winner + implied_loser
fair_winner = implied_winner / overround # normalize back to 1.0
That normalization is where a model’s apparent edge usually goes. A model reading a game at 55% against a price implying 52% looks like a three-point edge. De-vig the price and it might imply 49.3%, and the edge is 5.7 points, or it might imply 55.1% and the edge is gone. You cannot tell which without dividing by the overround, and dividing by the overround requires having both sides of the price.
If your data only has the side you want to bet, you cannot compute your edge. That is worth saying plainly, because a one-sided odds feed looks perfectly usable right up until you try to measure anything with it.
The margin is flatter than expected
I expected the vig to widen on lopsided games, where a book has more room to shade a price nobody is scrutinizing. It mostly does not:
| favourite’s implied probability | games | median overround |
|---|---|---|
| 50-60% | 104 | 5.54% |
| 60-70% | 102 | 5.57% |
| 70-80% | 183 | 5.63% |
| 80-90% | 149 | 5.51% |
| 90-100% | 126 | 4.31% |
Flat at about 5.5% across four of the five buckets, and narrower on the heaviest favourites. The margin is a policy, applied fairly uniformly, rather than something opportunistic.
The 90-100% row makes sense once you think about the mechanics rather than the intent: at those prices the favourite side is something like -1200, where a percentage point of margin is a very large move in the American number, so the granularity of the quote itself limits how much can be added.
Rejecting prices instead of repairing them
Real odds feeds contain records that are not prices. In this data:
- 2 games of 664 have an overround below 1.00, which implies a guaranteed profit from betting both sides. Sometimes that is a genuine cross-book arbitrage, and in a single-book feed it is almost always a stale or mistyped quote.
- 1 game has an overround above 1.15, a 15% margin no book charges on a major sport.
Both are the same category of problem: a value that parses fine and is not a price. keeks-elote rejects malformed American odds before a strategy ever sees them, which is the right boundary for it, because the alternative is a staking formula computing a confident number from a bad input and a backtest reporting profits that came from a typo.
The general rule is the one from validating at the lowest public boundary: check the value where it enters, reject rather than repair, and never let a plausible-looking wrong number reach the part of the program that does arithmetic on it.
Where the edge actually lives
Putting it together, the chain from a rating to a stake has four steps and only one of them is modelling:
- The rating system produces a probability
- The moneyline converts to a decimal, then to an implied probability
- The two implied probabilities are normalized by the overround into a fair price
- The difference between your probability and the fair price is the edge, and Kelly turns it into a stake
Step 3 is the one people skip, and skipping it means comparing your forecast against a number that has a systematic 5.5% distortion baked in. You will find edges. They will be the vig.
Running it
from keeks_elote.backtest import american_to_decimal
dw, dl = american_to_decimal(-150), american_to_decimal(+130)
overround = 1/dw + 1/dl
print(f"overround {overround:.4f}, fair favourite {1/dw/overround:.4f}")
pip install keeks-elote.
Stay in the loop
Get notified when I publish new posts. No spam, unsubscribe anytime.