Available Hire Me
← All Writing Betfair

Market Making Strategies on Betting Exchanges — a Java Implementation

How market making works on a betting exchange, what makes it different from financial markets, and how to implement a practical market making strategy in Java against the Betfair API.

Market making in financial markets means quoting a bid and an offer simultaneously, profiting from the spread when someone takes the other side of each. On a betting exchange it works the same way in principle: back at a high price (lay the bet mentally), lay at a low price (take the role of bookmaker), and earn the spread when both sides fill. The mechanics are different enough that financial market making literature doesn’t translate cleanly — the exchange’s two-sided structure, the overround, and the way liquidity forms around key prices all require a different approach.

This post covers what makes exchange market making viable, where it breaks down, and how to implement the core loop in Java against the Betfair API.

The Exchange Structure

On Betfair, every market has a back side (backers who want to win if the outcome occurs) and a lay side (layers who accept liability if it does). Unlike a traditional bookmaker, Betfair doesn’t take positions itself — it matches bettors against each other and takes commission.

This creates a continuous order book. For a horse racing market at 10 minutes to post, you might see:

Back (want to back)     Lay (willing to lay)
  £420 @ 3.85           £310 @ 3.90
  £180 @ 3.80           £540 @ 3.95
  £900 @ 3.75           £220 @ 4.00

A market maker sits on both sides: laying at 3.85 (accepting liability if the horse wins) and backing at 3.90 (getting paid out if it wins). If both sides fill at £100, the maker earns roughly £4–5 on the spread depending on the commission rate and the final price. The risk: if the true probability shifts significantly between the two fills, one side is filled at a bad price.

What Makes Exchange Market Making Different

No continuous two-sided quotes. Financial exchanges quote in microseconds. Betfair markets trade for hours before an event and then move violently in-play. A pre-race market-making strategy must account for the different liquidity regime: thick and slow before the off, thin and fast once racing starts.

Overround exists but is diffuse. A bookmaker’s margin is built in. On Betfair, the overround is whatever the market collectively implies — and it changes as money moves. A market maker doesn’t have a structural edge from overround; the edge comes from correctly estimating where the fair price is relative to the current spread.

Commission is per-matched-bet, not per-spread. Betfair charges 2–5% commission on net winnings per market. This has to be modelled explicitly — a strategy that earns 2% on the spread before commission is not profitable after a 5% commission rate.

In-play risk is asymmetric and fast. If you have open positions when the event starts, market depth collapses and spreads widen. Most pre-race market-making strategies hedge or close all positions before the off. Failing to do so is a common and expensive mistake.

Core Strategy: Fade-the-Move

The simplest viable market-making approach on Betfair is to fade short-term price moves — assuming prices revert to a fair value and that any short-term drift is noise rather than signal.

The strategy:

  1. Estimate the fair price for a selection using available signals (Weight of Money ratio, recent matched volume, LTP trend).
  2. Quote a back order slightly above fair value and a lay order slightly below fair value — capturing spread if both fill.
  3. Hedge the position partially if only one side fills and the price moves against you.
  4. Close everything before the off.

The critical parameter is how aggressively to hedge on partial fills. Hedging too eagerly turns every half-fill into a loss; not hedging enough creates directional exposure that market making wasn’t designed to carry.

Implementation

Data Structures

public record MarketMakerState(
        String  selectionId,
        double  fairValue,
        double  backQuote,
        double  layQuote,
        double  backExposure,
        double  layExposure,
        double  netPosition,
        Instant lastUpdated) {

    public boolean hasOpenPosition() {
        return Math.abs(netPosition) > 0.01;
    }

    public double spreadEarned() {
        return layQuote - backQuote;
    }
}

Fair Value Estimation

@Component
public class FairValueEstimator {

    private static final double WOM_WEIGHT  = 0.5;
    private static final double LTP_WEIGHT  = 0.3;
    private static final double VWAP_WEIGHT = 0.2;

    public double estimate(SelectionSnapshot snapshot) {
        double womImplied  = womanImpliedPrice(snapshot);
        double ltpPrice    = snapshot.lastTradedPrice();
        double vwap        = snapshot.volumeWeightedAvgPrice();

        return (WOM_WEIGHT * womImplied)
             + (LTP_WEIGHT  * ltpPrice)
             + (VWAP_WEIGHT * vwap);
    }

    private double womanImpliedPrice(SelectionSnapshot s) {
        double totalBack = s.availableToBack().stream()
            .mapToDouble(PriceSize::size).sum();
        double totalLay  = s.availableToLay().stream()
            .mapToDouble(PriceSize::size).sum();

        if (totalBack + totalLay == 0) return s.lastTradedPrice();

        double womRatio = totalBack / (totalBack + totalLay);
        // More back money → price should drift in, more lay money → price should drift out
        return s.lastTradedPrice() * (1.0 + (0.5 - womRatio) * 0.05);
    }
}

Weight of Money is a noisy signal on its own — large orders can be placed and cancelled to manipulate it (a practice known as steaming and spoofing). Blend it with LTP and VWAP rather than relying on it in isolation.

Quote Placement

@Component
public class QuoteCalculator {

    private static final double MIN_SPREAD       = 0.02; // minimum tick-adjusted spread
    private static final double TARGET_SPREAD    = 0.05;
    private static final double MAX_EXPOSURE     = 50.0;
    private static final double COMMISSION_RATE  = 0.05;

    public Quote calculate(double fairValue, MarketMakerState state) {
        double halfSpread = Math.max(MIN_SPREAD, TARGET_SPREAD / 2.0);

        double backQuote = roundToTick(fairValue + halfSpread);
        double layQuote  = roundToTick(fairValue - halfSpread);

        // Commission must be earned by the spread
        double netSpread = (layQuote - backQuote) * (1.0 - COMMISSION_RATE);
        if (netSpread <= 0) {
            return Quote.none();
        }

        double safeStake = Math.min(10.0, MAX_EXPOSURE - Math.abs(state.netPosition()));

        return new Quote(backQuote, layQuote, safeStake);
    }

    private double roundToTick(double price) {
        // Betfair tick increments vary by price range
        if (price < 2.0)  return Math.round(price / 0.01) * 0.01;
        if (price < 3.0)  return Math.round(price / 0.02) * 0.02;
        if (price < 4.0)  return Math.round(price / 0.05) * 0.05;
        if (price < 6.0)  return Math.round(price / 0.1)  * 0.1;
        if (price < 10.0) return Math.round(price / 0.2)  * 0.2;
        if (price < 20.0) return Math.round(price / 0.5)  * 0.5;
        return Math.round(price);
    }
}

Tick rounding is non-negotiable — Betfair rejects orders at prices not on the valid tick ladder. The increment varies by price range: below 2.0 the increment is 0.01; above 10.0 it’s 0.5; above 20.0 it’s 1.0.

The Market Making Loop

@Component
public class MarketMaker {

    private static final Duration PRE_OFF_CLOSE_WINDOW = Duration.ofMinutes(2);

    private final BetfairOrderClient   orderClient;
    private final FairValueEstimator   fairValueEstimator;
    private final QuoteCalculator      quoteCalculator;
    private final PositionHedger       hedger;

    public void onMarketUpdate(MarketSnapshot market) {
        if (isWithinCloseWindow(market)) {
            closeAllPositions(market);
            return;
        }

        for (SelectionSnapshot selection : market.runners()) {
            MarketMakerState state = getCurrentState(selection.id());
            double fairValue = fairValueEstimator.estimate(selection);
            Quote  quote     = quoteCalculator.calculate(fairValue, state);

            if (quote.isNone()) continue;

            cancelStaleQuotes(selection.id(), quote);
            placeQuotes(selection.id(), quote, market.marketId());
            hedgeIfNeeded(state, fairValue);
        }
    }

    private boolean isWithinCloseWindow(MarketSnapshot market) {
        return market.timeToOff().compareTo(PRE_OFF_CLOSE_WINDOW) < 0;
    }

    private void hedgeIfNeeded(MarketMakerState state, double fairValue) {
        if (!state.hasOpenPosition()) return;

        double priceMove = Math.abs(fairValue - state.fairValue());
        if (priceMove > 0.10) {
            hedger.hedge(state);
        }
    }
}

Order Management

Cancel-and-replace is the most expensive part of the loop. Every cancelled order and every new order generates API calls, and Betfair’s API has rate limits. The practical approach is to cancel only quotes that are more than one tick away from the target price — avoid churning quotes on every update if the price hasn’t moved meaningfully.

private void cancelStaleQuotes(String selectionId, Quote newQuote) {
    List<BetOrder> existing = activeOrders.getOrDefault(selectionId, List.of());

    List<String> toCancel = existing.stream()
        .filter(order -> isMeaningfullyStale(order, newQuote))
        .map(BetOrder::betId)
        .toList();

    if (!toCancel.isEmpty()) {
        orderClient.cancelOrders(toCancel);
    }
}

private boolean isMeaningfullyStale(BetOrder order, Quote newQuote) {
    double targetPrice = order.side() == Side.BACK
        ? newQuote.backPrice()
        : newQuote.layPrice();

    return Math.abs(order.price() - targetPrice) > 0.051; // more than one tick at typical prices
}

Risk Controls

Market making without hard limits is not a strategy — it’s a path to a large loss. The minimum controls:

@Component
public class MarketMakerRiskController {

    private static final double MAX_SELECTION_EXPOSURE = 100.0;
    private static final double MAX_MARKET_EXPOSURE    = 500.0;
    private static final double MAX_DAILY_LOSS         = 200.0;

    public boolean allowNewQuote(String selectionId, String marketId, double stakeSize) {
        if (selectionExposure(selectionId) + stakeSize > MAX_SELECTION_EXPOSURE) {
            log.warn("Selection exposure limit reached for {}", selectionId);
            return false;
        }
        if (marketExposure(marketId) + stakeSize > MAX_MARKET_EXPOSURE) {
            log.warn("Market exposure limit reached for {}", marketId);
            return false;
        }
        if (dailyLoss() > MAX_DAILY_LOSS) {
            log.error("Daily loss limit reached — halting market making");
            haltAll();
            return false;
        }
        return true;
    }
}

Hard-stop the strategy when the daily loss limit is breached. Don’t make this a soft warning — if you’re losing at a rate that triggers the daily limit, something is wrong with the fair value estimation or the market regime has changed, and continuing without human review will make it worse.

Where the Strategy Breaks Down

Informed flow. When a significant backer or layer with genuine information (an owner, a stable connection) enters a market, price moves persistently in one direction. Quoting against persistent moves is the market maker’s core risk. WOM-based fair value estimation doesn’t distinguish informed from noise — you need a flow filter that detects sustained directional pressure and pauses quoting.

Illiquid markets. Small-field or niche markets have wide spreads and infrequent matching. The commission wipes out the spread at lower volumes and the hedging options are poor. Market making works best in deep, liquid markets — big fields at major meetings.

Steam moves. A sudden, large, coordinated back or lay — a “steam” — moves a market fast. The strategy’s cancel-and-replace loop is unlikely to be fast enough to avoid being filled on the wrong side of a steam. Detection (sustained volume spike at a single price) and automatic pause are essential.

If you’re building a serious market-making or algorithmic trading framework for Betfair and want to get the architecture right, get in touch.

Samuel Jackson

Samuel Jackson

Senior Java Back End Developer & Contractor

Senior Java Back End Developer — Betfair Exchange API specialist, Spring Boot, AWS, and event-driven architecture. 25+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.