Implied Probability From Odds Data: Formulas, Overround and Code
Compute implied probability from odds data at scale. Formulas in both directions, the format=probability option and its rounding, overround across a whole response, pushes, probability curves from alternate ladders, consensus across books and probability time series.
Implied probability is the common currency of odds data. American odds are awkward to compute with, decimal odds are better, but probabilities are what you actually want for comparing books, measuring margins, feeding models and drawing charts that make sense. Almost every analytical feature in an odds product starts by converting prices to implied probabilities.
The formulas fit on one line. The engineering around them is where the work is: knowing when to let the API do the conversion and when to do it yourself, computing margins across thousands of markets in one pass, handling markets that can push, turning alternate ladders into probability curves, and building time series that don't mislead. This post covers those pieces with the PreciseAPI feed. It assumes you know the basic odds formats; if not, start with the odds formats guide.
The formulas, both directions
The implied probability of a price is the break-even rate for it: the probability at which the price would be exactly fair if the book took no margin.
| From | To probability | Example |
|---|---|---|
| American, negative | -A / (-A + 100) | -110 → 110 / 210 = 0.5238 |
| American, positive | 100 / (A + 100) | +250 → 100 / 350 = 0.2857 |
| Decimal | 1 / D | 1.80 → 0.5556 |
And back again, which you need when you display a normalized probability as a price:
From probability p | Formula | Example |
|---|---|---|
| To decimal | 1 / p | 0.55 → 1.818 |
To American, p ≥ 0.5 | -100 × p / (1 - p) | 0.55 → -122.2 |
To American, p < 0.5 | 100 × (1 - p) / p | 0.45 → +122.2 |
export const toDecimal = (a) => (a > 0 ? 1 + a / 100 : 1 + 100 / -a);
export const implied = (a) => 1 / toDecimal(a);
export const probToAmerican = (p) => (p >= 0.5 ? (-100 * p) / (1 - p) : (100 * (1 - p)) / p);
Keep probToAmerican output unrounded internally and round only for display. Books quote integers, but a normalized price like -122.2 is a real number and rounding it early compounds error.
Letting the API convert: format=probability
Every odds endpoint accepts format=probability, which returns each price as its implied probability instead of American odds. It applies to /v1/odds, /v1/events/{id}/odds, /v1/props, /v1/futures, the best_price field of /v1/best-lines and the prices array of /v1/history.
curl "https://api.preciseapi.com/v1/odds?league=nfl&markets=main&periods=full_game&books=pinnacle,draftkings&format=probability" \
-H "Authorization: Bearer $PRECISEAPI_KEY"
{
"key": "spread",
"period": "full_game",
"line": -3.5,
"outcomes": [
{ "id": "sel_0w3fk8x1npa", "name": "Kansas City Chiefs", "side": "home", "point": -3.5, "price": 0.5238 },
{ "id": "sel_1m6dv2c9hyt", "name": "Buffalo Bills", "side": "away", "point": 3.5, "price": 0.5238 }
]
}
Two properties to keep in mind:
- Margin is included. These are raw implied probabilities, so a market's outcomes sum to more than 1. That is a feature: the sum is the overround.
- Rounded to four places. -110 is exactly 0.523809…, returned as 0.5238. The error is at most 0.00005 per outcome, which is invisible on a chart and harmless for display, but it accumulates if you chain calculations.
The rule of thumb: use format=probability for display and quick analysis, and request the default American format for anything you store and compute on later. American prices are integers as quoted by the book, so they are the lossless representation. Only prices change with the format; line and point are unaffected.
Overround across a whole response
The sum of implied probabilities across a market's outcomes is 1 + overround. For a single market it is one line of code. For a full response, the work is deciding which outcomes form one complete market:
- Main markets and team totals. Each market's
outcomesare the complete set: two outcomes, or three for soccer moneylines. - Alternate ladders. An
alternate_spreadoralternate_totalmarket contains many pairs. Group by number first; each rung is its own two-way market. - One-sided lines. DFS pick'em lines tagged
goblinordemoncan come with a single outcome. They have no complement, so skip them.
// Overround for every complete market in a /v1/odds response (American prices).
export function overrounds(data) {
const rows = [];
for (const event of data) {
for (const book of event.bookmakers) {
for (const m of book.markets) {
const sets = m.key.startsWith('alternate_')
? Object.values(Object.groupBy(m.outcomes, (o) => (o.side === 'away' ? -o.point : o.point)))
: [m.outcomes];
for (const outs of sets) {
if (outs.length < 2) continue;
const sum = outs.reduce((s, o) => s + implied(o.price), 0);
rows.push({ event: event.id, book: book.id, key: m.key, period: m.period, point: outs[0].point ?? null, overround: sum - 1 });
}
}
}
}
return rows;
}
Grouping the spread ladder by the home handicap (the away point negated) pairs each home rung with its matching away rung; totals pair naturally because over and under share a point. Aggregating these rows by book and market key is the core of a hold dashboard, which the sportsbook hold post builds out in full.
What the numbers look like
| Market | Prices | Sum | Overround |
|---|---|---|---|
| Two-way, standard | -110 / -110 | 1.0476 | 4.76% |
| Two-way, lopsided | -300 / +250 | 1.0357 | 3.57% |
| Three-way soccer | +150 / +240 / +180 | 1.0513 | 5.13% |
All three are illustrative. Futures boards are the extreme case: dozens of outcomes whose implied probabilities sum well above 1. Treat each /v1/futures board as one market and sum across every entrant.
Normalizing, briefly
Dividing each implied probability by the market's sum removes the margin proportionally. For the three-way market above, that turns 0.4000, 0.2941 and 0.3571 into 0.3805, 0.2798 and 0.3397. It is the simplest of several methods, and the no-vig guide compares it with the additive, power and Shin approaches and explains which book to de-vig. Everything below works with whichever method you choose; the code uses proportional normalization for brevity.
export const normalize = (probs) => {
const sum = probs.reduce((a, b) => a + b, 0);
return probs.map((p) => p / sum);
};
What the probability is a probability of
It is easy to label a number "win probability" when it isn't one. Be precise in your schema and your UI:
- Moneyline. The probability of that side winning the period.
- Spread. The probability of covering that
point, not of winning. Home -3.5 at 0.5238 says nothing directly about the moneyline. - Total and team total. The probability of the combined (or one team's) score landing over or under that
point. - Props. The probability of the player's stat landing over or under the line, as the book defines it.
Whole numbers and pushes
On a whole-number spread or total, the result can land exactly on the number and push. The normalized two-way probabilities are then conditional on no push: they split the non-push outcomes between the two sides. If a total of 44 is priced -110 / -110, normalization gives 50% / 50%, but if you believe there is a 5% chance of exactly 44, the unconditional probability of the over is 0.5 × 0.95 = 0.475. For display this rarely matters. For models and backtests that compare probabilities across half-point and whole-number lines, it does.
Probability curves from alternate ladders
An alternate ladder gives one book's probability for many numbers on the same game. Converted and normalized rung by rung, it becomes a curve you can query:
// Rungs from an alternate_total plus the main total: [{ point, over, under }]
export function overCurve(rungs) {
return rungs
.map((r) => ({ point: r.point, pOver: normalize([implied(r.over), implied(r.under)])[0] }))
.sort((a, b) => a.point - b.point);
}
// Linear interpolation between rungs, for comparing books on different numbers.
export function pOverAt(curve, x) {
if (!curve.length) return null;
const i = curve.findIndex((r) => r.point >= x);
if (i === -1) return curve.at(-1).pOver; // beyond the ladder: clamp
if (i === 0) return curve[0].pOver;
const [a, b] = [curve[i - 1], curve[i]];
return a.pOver + ((x - a.point) / (b.point - a.point)) * (b.pOver - a.pOver);
}
The curve should be monotonic: the probability of the over falls as the number rises. A rung that breaks that is almost always a stale price, which makes this a useful automated QA check. Interpolation is a simplification (real margin distributions are lumpy, especially around football's key numbers), but it is good enough to put two books quoting 47.5 and 48 on the same footing. alternate_spread covers the football and basketball leagues and alternate_total adds MLB and NHL. The point spread guide and the totals guide cover the ladder shapes.
Consensus across books
Once every book's market is normalized, you can combine them into one consensus probability per outcome. A median is a robust default, because one slow book cannot drag it far:
export function consensus(values) {
const v = [...values].sort((a, b) => a - b);
const mid = Math.floor(v.length / 2);
return v.length % 2 ? v[mid] : (v[mid - 1] + v[mid]) / 2;
}
Only combine outcomes that share a selection id, which guarantees the same event, market, period, side and number. Weighting books by how quickly they react to information is the more refined version, discussed in the no-vig guide and the steam move post. The line shopping post uses the same grouping to find the best price per outcome.
Probability time series
/v1/history returns the opening value, the current value and every timestamped change for full-game moneylines, spreads and totals, with prices in outcome order. Chart those as normalized probabilities, not American odds. American odds have a gap at even money (-101 and +101 are nearly the same price but 202 points apart), and the margin can change between points, so raw prices exaggerate some moves and hide others.
// h = data from /v1/history?…&format=probability
const series = h.points.map(({ t, line, prices }) => ({ t, line, p: normalize(prices)[0] }));
For spreads and totals, keep line in the series. A jump in probability at a constant line is a price move; a change of line resets the probability toward even and should be drawn as a separate event. History costs 3 credits per request and is included on the Sandbox, Starter, Pro and Enterprise plans; see pricing. The CLV guide and the backtesting post build on these series.
Exchange and prediction market prices
Exchanges and prediction markets such as Kalshi and Polymarket are in the same schema, so the same conversion applies. Their outcomes also carry a liquidity field, and a probability backed by little size deserves less weight in a consensus. Contract prices map to probabilities almost directly, with fees as the main adjustment; the Kalshi and Polymarket post covers that conversion, and the prediction markets comparison explains how exchange margins compare with sportsbook vig.
Storage and numeric hygiene
- Store the source of truth. Keep the American integer and the
updated_attimestamp; derive probabilities on read or in a materialized column. - Use doubles. Probabilities need more than four decimal places once you normalize and chain calculations.
- Compare with a tolerance. Never test two computed probabilities for exact equality.
- Never average American odds. Average probabilities, then convert back if you need a price.
- Label the method. A chart of "probability" should say whether it is raw implied or normalized, and which normalization.
The historical odds API page and the betting models use case show where these numbers end up, and the quickstart gets a key working in a few minutes.
Frequently asked questions
What does format=probability return?
The implied probability of each price, rounded to four decimal places, with the book's margin included. It works on every odds endpoint, including the best_price field of /v1/best-lines and the prices array of /v1/history. Lines and points are unchanged.
Why do the implied probabilities of a market add up to more than 1?
Each price includes the book's margin. The excess over 1 is the overround: -110 / -110 sums to 1.0476, an overround of 4.76%.
Should I use format=probability or convert myself?
Use it for display and quick analysis. For stored data and chained calculations, request American odds, which are lossless integers, and convert at full precision in your code.
Is the implied probability of a spread a win probability?
No. It is the probability of covering that specific point. On whole-number lines it is also conditional on the result not landing exactly on the number.
How do I compute overround for alternate ladders?
Group the ladder's outcomes by number first. Each rung is its own two-way market: pair home and away by the home handicap for spreads, and over and under by point for totals, then sum each pair.
How do I combine probabilities from several books?
Normalize each book's market, group outcomes by selection id, and take a median or a weighted average. Never average American odds directly.