What Are Consensus Odds and How to Calculate Them
Average what the market says, take the margin out, and know what the number does and does not tell you.
Consensus odds summarise what many bookmakers say about one outcome. A no-vig price goes one step further and strips out the bookmaker's margin. Both are useful, and both are easy to get wrong.
SockOdds API v1 returns each bookmaker's own price on each outcome and does not return a consensus or fair price field. That is deliberate: you choose which bookmakers count and which method to use. This post shows one simple, transparent way to do it.
Start from implied probability
Every outcome in v1 lists one price per bookmaker, best first, and each price has decimal_odds and implied_probability, which is one divided by the decimal price. A price of 2.00 implies 50%.
Add up the implied probabilities of every side of one bookmaker's market and you get more than 100%. The amount over 100% is the bookmaker's margin, sometimes called the overround or vig. On a two-way market priced 1.72 and 2.12, the implied probabilities are 58.1% and 47.2%, a total of 105.3%, so the margin is 5.3%.
Remove the margin
The simplest way to remove it is proportional: divide each side's implied probability by that bookmaker's total. 58.1 / 105.3 and 47.2 / 105.3 give 55.2% and 44.8%, which add up to 100%. One divided by each is the no-vig price for that bookmaker: about 1.81 and 2.23.
Other methods spread the margin differently, for example putting more of it on long shots. Proportional is easy to explain and a reasonable default.
Combine bookmakers
For a consensus, average the no-vig probabilities across bookmakers for each side, then turn the average back into a price. Average probabilities, not decimal prices: averaging prices gives long shots too much weight.
Three rules keep the result honest:
- Only use a bookmaker that prices every side of the market, so its margin can be measured.
- Only combine outcomes with the same
line. A handicap at 6.5 and one at 7.5 are different bets. - Only use fresh quotes. v1 leaves quotes older than 10 minutes out of current responses, and each price carries
observed_atso you can be stricter.
A working program
This program reads the head-to-head (winner) market for one Rugby League World Cup game, regroups its prices by bookmaker key, prints each bookmaker's margin and no-vig prices, then the consensus across them:
const response = await fetch("https://api.sockodds.com/v1/events/ev_000ckk0zrx33/odds?markets=winner", {
headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
const event = (await response.json()).data;
// Regroup the market's prices by bookmaker: each bookmaker's own price on every side.
const books = new Map();
for (const outcome of event.markets[0].outcomes) {
for (const price of outcome.prices) {
if (price.decimal_odds === null) continue;
if (!books.has(price.bookmaker)) books.set(price.bookmaker, []);
books.get(price.bookmaker).push({ name: outcome.name, side: outcome.side, decimal_odds: price.decimal_odds });
}
}
const sides = new Map();
for (const [bookmaker, outcomes] of books) {
if (outcomes.length < 2) continue;
const total = outcomes.reduce((sum, o) => sum + 1 / o.decimal_odds, 0);
const fair = outcomes.map((o) => `${o.name} ${(total * o.decimal_odds).toFixed(2)}`);
console.log(`${bookmaker}: margin ${((total - 1) * 100).toFixed(2)}%, no-vig ${fair.join(", ")}`);
for (const o of outcomes) {
const side = sides.get(o.side) ?? { name: o.name, sum: 0, count: 0 };
side.sum += 1 / o.decimal_odds / total;
side.count += 1;
sides.set(o.side, side);
}
}
for (const side of sides.values()) {
const p = side.sum / side.count;
console.log(`Consensus ${side.name}: ${(p * 100).toFixed(2)}% = ${(1 / p).toFixed(2)} from ${side.count} bookmakers`);
}import json, os, urllib.request
request = urllib.request.Request(
"https://api.sockodds.com/v1/events/ev_000ckk0zrx33/odds?markets=winner",
headers={"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
event = json.load(response)["data"]
# Regroup the market's prices by bookmaker: each bookmaker's own price on every side.
books = {}
for outcome in event["markets"][0]["outcomes"]:
for price in outcome["prices"]:
if price["decimal_odds"] is None:
continue
books.setdefault(price["bookmaker"], []).append(
{"name": outcome["name"], "side": outcome["side"], "decimal_odds": price["decimal_odds"]}
)
sides = {}
for bookmaker, outcomes in books.items():
if len(outcomes) < 2:
continue
total = sum(1 / o["decimal_odds"] for o in outcomes)
fair = [f"{o['name']} {total * o['decimal_odds']:.2f}" for o in outcomes]
print(f"{bookmaker}: margin {(total - 1) * 100:.2f}%, no-vig {', '.join(fair)}")
for o in outcomes:
side = sides.setdefault(o["side"], {"name": o["name"], "sum": 0.0, "count": 0})
side["sum"] += 1 / o["decimal_odds"] / total
side["count"] += 1
for side in sides.values():
p = side["sum"] / side["count"]
print(f"Consensus {side['name']}: {p * 100:.2f}% = {1 / p:.2f} from {side['count']} bookmakers")Output from a local copy of the API with invented test prices
playup: margin 0.09%, no-vig Australia 1.81, New Zealand 2.23
tab: margin 2.37%, no-vig Australia 1.81, New Zealand 2.23
sportsbet: margin 5.31%, no-vig Australia 1.81, New Zealand 2.23
ladbrokes: margin 7.25%, no-vig Australia 1.81, New Zealand 2.23
Consensus Australia: 55.19% = 1.81 from 4 bookmakers
Consensus New Zealand: 44.81% = 2.23 from 4 bookmakersThe prices come from a local copy of the API with invented numbers. Its bookmakers shade one set of prices up and down by the same proportion, so every no-vig price comes out the same there. On real data, bookmakers disagree and the no-vig prices differ.
What the number is, and is not
A consensus is the market's average view with the margin removed. It is a useful reference for spotting a bookmaker that is out of line with the rest, or for tracking how the market moves over time with history. It is not a prediction and not a guarantee: prices move, bookmakers disagree for real reasons, and a price that looks generous may be limited or gone by the time you reach it.
To weight bookmakers differently, or to use only exchanges, filter with bookmakers= before you average. The odds comparison example builds a full comparison table from the same response. For the margin-removal methods themselves (multiplicative, additive, power and Shin) and where each breaks, see how to remove the bookmaker margin.