How to calculate the overround on a Betfair Exchange market, what it tells you about market efficiency, and how to build an efficiency score that filters for trading opportunities in Java.
The overround (or book percentage) measures how much the total implied probability in a market exceeds 100%. In a perfectly efficient market with no margin, the sum of all runners’ implied win probabilities is exactly 1.0. In a traditional bookmaker market it is typically 1.10–1.20 (10–20% over-round). On Betfair Exchange, the best-back prices typically produce a book percentage of 1.01–1.04 — the market is close to efficient, but the degree of efficiency changes over time and contains information.
public class OverroundCalculator {
public double compute(Map<Long, Double> bestBackPrices) {
if (bestBackPrices.isEmpty()) return Double.NaN;
double sumImplied = bestBackPrices.values().stream()
.filter(price -> price >= 1.01)
.mapToDouble(price -> 1.0 / price)
.sum();
return sumImplied; // > 1.0 means over-round; < 1.0 means under-round
}
public double overroundPercent(Map<Long, Double> bestBackPrices) {
double raw = compute(bestBackPrices);
return Double.isNaN(raw) ? Double.NaN : (raw - 1.0) * 100.0;
}
}
For a 6-runner market with best back prices:
Runner A: 2.50 → implied 0.400
Runner B: 3.50 → implied 0.286
Runner C: 6.00 → implied 0.167
Runner D: 9.00 → implied 0.111
Runner E: 12.0 → implied 0.083
Runner F: 20.0 → implied 0.050
Sum: 1.097 → 9.7% over-round
The lay side computes identically using best lay prices:
public double computeLayOverround(Map<Long, Double> bestLayPrices) {
double sumImplied = bestLayPrices.values().stream()
.filter(price -> price >= 1.01)
.mapToDouble(price -> 1.0 / price)
.sum();
return sumImplied; // < 1.0 for lays (under-round from layer's perspective)
}
On a healthy exchange market, back overround is slightly above 1.0 and lay overround is slightly below 1.0. The spread between them represents the exchange’s margin.
A lower overround indicates a more efficient (harder to beat) market. An unusually high overround on the back side, or unusually low on the lay side, can indicate inefficiency worth investigating:
public record MarketEfficiency(
double backOverround,
double layOverround,
double spread,
double efficiency // 0.0 = perfect; positive = backers are getting bad value
) {
public static MarketEfficiency compute(
Map<Long, Double> backs, Map<Long, Double> lays) {
double bo = new OverroundCalculator().compute(backs);
double lo = new OverroundCalculator().computeLayOverround(lays);
double spread = bo - lo; // the exchange's take
double efficiency = (bo - 1.0); // excess above fair book
return new MarketEfficiency(bo, lo, spread, efficiency);
}
public boolean isEfficient() {
return efficiency < 0.03; // < 3% overround = well-priced
}
}
In the hours before a race, the market typically starts inefficient (few participants, wide spreads) and becomes progressively more efficient as volume builds. Track this trajectory:
public class EfficiencyTimeSeries {
private final List<EfficiencyPoint> points = new ArrayList<>();
public record EfficiencyPoint(Instant time, double efficiency, long matchedVolume) {}
public void record(MarketEfficiency eff, long matchedVolume) {
points.add(new EfficiencyPoint(Instant.now(), eff.efficiency(), matchedVolume));
}
public boolean isStabilising() {
if (points.size() < 10) return false;
List<EfficiencyPoint> recent = points.subList(points.size() - 10, points.size());
double variance = computeVariance(recent.stream()
.mapToDouble(EfficiencyPoint::efficiency).toArray());
return variance < 0.0001; // efficiency has stabilised
}
private double computeVariance(double[] values) {
double mean = Arrays.stream(values).average().orElse(0.0);
return Arrays.stream(values)
.map(v -> (v - mean) * (v - mean))
.average().orElse(0.0);
}
}
Beyond the market-level overround, identify individual runners where the implied probability differs significantly from their price relative to the field:
public Map<Long, Double> computeImpliedProbabilities(Map<Long, Double> bestBackPrices) {
double overround = new OverroundCalculator().compute(bestBackPrices);
return bestBackPrices.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> (1.0 / e.getValue()) / overround // normalised to sum to 1.0
));
}
Normalising by the overround gives each runner’s implied win probability assuming the market is correctly calibrated. Compare this to an independent probability estimate (ratings, form, historical SP) to find runners where the market’s probability differs from your model.
Overround is only meaningful with sufficient matched volume. An early market with £500 matched can have extreme overrounds that normalise as volume builds:
public Optional<MarketEfficiency> computeIfSufficient(
Map<Long, Double> backs, Map<Long, Double> lays, long matchedVolume) {
if (matchedVolume < 10_000) return Optional.empty(); // too thin to trust
return Optional.of(MarketEfficiency.compute(backs, lays));
}
Before applying any price-based signal, check whether the market is sufficiently liquid and efficient:
public boolean shouldActivateStrategy(String marketId) {
MarketState state = marketStateMap.get(marketId);
if (state == null) return false;
Optional<MarketEfficiency> eff = computeIfSufficient(
state.getBestBacks(), state.getBestLays(), state.getMatchedVolume());
return eff.map(MarketEfficiency::isEfficient).orElse(false);
}
Price-based strategies applied to thin, inefficient markets are unreliable — the signals are noisy and the spreads make execution costly. Overround as a filter lets your system focus on the markets where signal quality is highest.
If you’re building market analysis tools for Betfair in Java and want a review of your signal quality filtering, get in touch.