Hire Me
← All Writing Betfair

Parsing Best Available to Back and Lay Prices from the Streaming API

How the Betfair Streaming API represents best available back and lay prices, how delta updates work, and how to reconstruct a clean order book view in Java without the full ladder overhead.

When you consume the Betfair Exchange Streaming API, your first instinct is often to use the full ladder — bdatb (Best Display Available to Back) and bdatl (Best Display Available to Lay) — because it gives you every price level. But for most trading applications, you only need the best few prices on each side. That is what batb (Best Available to Back) and batl (Best Available to Lay) are for.

Understanding exactly what these fields represent, how their delta updates work, and how to reconstruct the condensed order book cleanly in Java saves you from subtle bugs and unnecessary processing overhead.

What batb and batl contain

Each field is a list of [index, price, size] arrays. The index is the position in the sorted order book — 0 is the best price, 1 is second-best, and so on. Betfair returns up to three levels for batb and batl in the streaming format.

For backs, prices are sorted descending (highest price first — best value for a backer). For lays, prices are sorted ascending (lowest price first — cheapest to lay).

batb: [[0, 2.5, 120.0], [1, 2.48, 85.0], [2, 2.46, 200.0]]
batl: [[0, 2.52, 95.0],  [1, 2.54, 180.0], [2, 2.56, 310.0]]

The spread here is 2.50 (best back) to 2.52 (best lay).

The delta update model

The streaming API sends full snapshots on connection and deltas on every subsequent update. A delta for batb looks like:

{ "batb": [[0, 2.5, 135.0]] }

This means: update index 0 to price 2.50 with size 135.0. Indices not mentioned are unchanged. A size of 0 means remove that level — the price has been entirely consumed or cancelled.

Your Java model must maintain the full state by merging deltas into the current book. This is where most implementations go wrong — they either overwrite the whole array on every update (safe but wasteful) or apply the delta incorrectly.

Java model

Model each level as a value object:

public record PriceLevel(int index, double price, double size) {

    public static PriceLevel from(List<Object> raw) {
        int idx    = ((Number) raw.get(0)).intValue();
        double prc = ((Number) raw.get(1)).doubleValue();
        double sz  = ((Number) raw.get(2)).doubleValue();
        return new PriceLevel(idx, prc, sz);
    }
}

Hold the condensed book per runner in a TreeMap keyed by index so position lookups are O(log n) and sorted iteration is free:

public class CondensedOrderBook {

    private final TreeMap<Integer, PriceLevel> backs = new TreeMap<>();
    private final TreeMap<Integer, PriceLevel> lays  = new TreeMap<>();

    public void applyBackDelta(List<List<Object>> delta) {
        applyDelta(backs, delta);
    }

    public void applyLayDelta(List<List<Object>> delta) {
        applyDelta(lays, delta);
    }

    private void applyDelta(TreeMap<Integer, PriceLevel> book, List<List<Object>> delta) {
        for (List<Object> raw : delta) {
            PriceLevel level = PriceLevel.from(raw);
            if (level.size() == 0.0) {
                book.remove(level.index());
            } else {
                book.put(level.index(), level);
            }
        }
    }

    public Optional<PriceLevel> bestBack() {
        return backs.isEmpty()
            ? Optional.empty()
            : Optional.of(backs.firstEntry().getValue());
    }

    public Optional<PriceLevel> bestLay() {
        return lays.isEmpty()
            ? Optional.empty()
            : Optional.of(lays.firstEntry().getValue());
    }

    public OptionalDouble spread() {
        return bestBack().flatMap(b ->
            bestLay().map(l -> OptionalDouble.of(l.price() - b.price()))
        ).orElse(OptionalDouble.empty());
    }
}

Wiring into the streaming pipeline

In your market change message handler, call applyBackDelta and applyLayDelta after deserialising the runner change:

private void handleRunnerChange(RunnerChange rc, CondensedOrderBook book) {
    if (rc.getBatb() != null) {
        book.applyBackDelta(rc.getBatb());
    }
    if (rc.getBatl() != null) {
        book.applyLayDelta(rc.getBatl());
    }
}

The key discipline here is to always apply deltas to the existing state, not replace it. On the initial subscription response (image flag set), Betfair sends the full state — but subsequent updates are always deltas even if multiple levels change simultaneously.

batb vs bdatb — which to use?

Field Levels Use case
batb / batl 3 Spread, WoM, best-price signals
bdatb / bdatl Full ladder (≤ 10 levels) Ladder UI, depth analysis, volume profile

For a signal-based strategy that only needs the spread and top-of-book size, batb/batl is sufficient and noticeably cheaper to process — smaller messages, less allocation, simpler merge logic.

Computing Weight of Money from batb/batl

With only three levels available, WoM from batb/batl is less accurate than from the full ladder, but still informative:

public double womFromCondensed(CondensedOrderBook book) {
    double backTotal = book.getBackLevels().stream()
        .mapToDouble(PriceLevel::size)
        .sum();

    double layTotal = book.getLayLevels().stream()
        .mapToDouble(PriceLevel::size)
        .sum();

    double total = backTotal + layTotal;
    return total == 0.0 ? 0.5 : backTotal / total;
}

This gives a fast approximation that often correlates well with the full-ladder WoM in liquid markets. For thin markets, prefer the full ladder.

Handling the edge cases

Empty book at start of subscription: batb and batl are absent until a market has active orders. Handle null gracefully — an empty CondensedOrderBook should be a valid state, not a NullPointerException waiting to happen.

Price removal at index 0: When the best price level is consumed, index 0 is removed and index 1 becomes the new best. Betfair does not re-index — it sends a new entry at index 0 with the updated price/size. Your TreeMap lookup by index handles this correctly as long as you remove size-zero entries.

Crossed book: During rapid price movement, you may briefly see bestLay < bestBack. This is a race condition in the snapshot — the next update will resolve it. Don’t trade on a crossed spread.

What this enables

With a clean CondensedOrderBook model you have the foundation for the most common pre-race signals: current spread, best-price availability, and a lightweight WoM — all updated on every streaming tick without needing to process the full 35-level ladder.

If you’re building Betfair trading systems in Java and want to go further — full ladder analysis, order book depth signals, or multi-market monitoring — 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. 20+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.