A silent bug sits in a lot of value-detection systems: the EV you display depends on a choice nobody wrote down. That choice is the model used to remove the bookmaker's margin. Change it, and the same bet goes from +1.2% to +2.6% EV β without a single price moving.
This is not an implementation detail. It is the central assumption of your whole pricing stack: how does the book spread its commission across outcomes?
The starting point: the sum exceeds 100%
On a moneyline at 1.55 / 2.55, the inverse of each price gives 64.5% and 39.2%. Total: 103.7%. Those 3.7 points are the margin. Every model aims to get back to 100%; they differ only in how they take the surplus out.
odds = [1.55, 2.55]
raw = [1 / o for o in odds]
print(f"overround: {sum(raw) - 1:.2%}") # 3.70 %Additive: take the same amount from everyone
The most naive model: subtract an equal share of the surplus from each probability. Easy to explain, but it treats a 90% favourite and a 5% longshot as if they carried the same margin load β which is true at no bookmaker.
def additive(odds):
raw = [1 / o for o in odds]
excess = (sum(raw) - 1) / len(raw)
return [p - excess for p in raw]On a heavily lopsided market, additive can return a negative probability for the longest price. If your code has no guard for that, it will emit absurd fair odds rather than a visible error.
Multiplicative: proportional
Divide each probability by the sum. Fast, parameter-free, the default in most tools. On a tight two-way market it is hard to beat. Its assumption β margin spread as an equal percentage β is nonetheless wrong as soon as the market is asymmetric.
def multiplicative(odds):
raw = [1 / o for o in odds]
total = sum(raw)
return [p / total for p in raw]
# 1.55 / 2.55 β probabilities 62.22 % / 37.78 %Power: bend the curve
Find an exponent k such that the sum of probabilities raised to k equals exactly 1. Instead of shifting probabilities, you warp the curve: small effect on favourites, large effect on the tail. That behaviour is the best fit for many-outcome markets.
from scipy.optimize import brentq
def power(odds):
raw = [1 / o for o in odds]
k = brentq(lambda k: sum(p ** k for p in raw) - 1, 0.5, 1.5)
return [p ** k for p in raw]Shin: margin as an insurance premium
Shin adds an economic assumption: the book sets its margin to cover itself against a proportion z of bettors better informed than it is. That proportion is inferred from the prices, then removed β which loads more margin onto longshots, exactly as books do in practice.
def shin(odds, iterations=100):
raw = [1 / o for o in odds]
total = sum(raw)
z = 0.0
for _ in range(iterations):
probs = [
(((z ** 2 + 4 * (1 - z) * p ** 2 / total) ** 0.5) - z) / (2 * (1 - z))
for p in raw
]
s = sum(probs)
z += (s - 1) / 2
return [p / s for p in probs]Odds ratio: one distortion parameter
The odds-ratio model looks for a single factor applied to each outcome's odds ratio (the "odds" rather than the probability). It often lands close to Shin, with a simpler intuition: the book does not distort probabilities, it distorts ratios.
Where the models actually diverge
On a balanced two-way market they all sit within a whisker: the spread across the five models stays under 0.3 probability points β inside the noise of your own estimate. It is away from parity that decisions change:
- Heavy favourite (1.10 against 9.00): multiplicative understates the favourite and overstates the longshot; the gap to Shin exceeds a point on the longshot.
- 1X2 with a mispriced draw: power and Shin agree, multiplicative manufactures fake value on the draw.
- Many-outcome markets (outrights, correct score, scorers): multiplicative gets genuinely bad on long prices. Power or Shin, never it alone.
- Asian handicaps and totals: very liquid two-way, thin margin β model choice barely matters, take the fastest.
A devig model is not a cosmetic setting: it is your assumption about how the bookmaker behaves. Better to choose it on purpose.
The rule that matters more than the model
Whatever you pick, keep it. The costliest trap is not picking the "wrong" model, it is switching between backtest and production: your historical EV becomes incomparable to your live EV, and you will conclude your edge collapsed when only the measuring stick moved.
- Freeze one model per market family, and write it down somewhere.
- Store the model used alongside every EV you log β otherwise your history stops being interpretable.
- Before you adjust your EV threshold, check that it was not your devig that changed.
Doing it server-side
On apinn the five models are implemented server-side and applied at tick time: every line arrives with its market price and its fair price, under the model you ask for. You pick your assumption per request β or once and for all in your account preferences β and your backtests use exactly the same maths as your bot.
curl -H "X-API-Key: $APINN_KEY" \
"https://api.apinn.io/api/odds?event_id=1610000123&model=shin"
# odds1/odds2 = market prices
# todds1/todds2 = fair prices
# model = power (default) | multiplicative | additive | shin | odds_ratioYour EV then reduces to a division β and the devig debate gets settled once, calmly, instead of on every tick.
Run this maths on real data
Real-time Pinnacle odds with fair odds included, plus opening and closing history β self-serve access, API key in minutes.