Hire Me
← All Writing Betfair

Handling Non-Runners and Reduction Factors in the Streaming API

How the Betfair Streaming API signals non-runner removals, how reduction factors work, and how to update open positions and historical signals correctly when a runner is withdrawn.

A non-runner mid-race changes the entire market. Prices compress, the book recalculates, and any position you hold on a remaining runner is worth more or less than it was a second ago. Handling this correctly in a trading system requires understanding exactly how Betfair signals the removal and what the reduction factor means for your open bets.

What happens when a runner is removed

When a horse is withdrawn, Betfair applies a reduction factor to all remaining runners. The reduction factor represents the probability mass that was assigned to the withdrawn runner — it is redistributed proportionally across the remaining field, compressing prices inward.

For a bettor who backed a remaining runner before the removal, their potential winnings are reduced by the reduction factor. If you backed at 5.0 and the reduction factor is 0.20 (20%), your effective odds are now 4.0. Betfair applies this automatically to settled bets, but in-play or pre-race open bets must be tracked manually in your system.

How the Streaming API signals a removal

In the market change message, a non-runner appears in the rc (runner change) array for the affected runner:

{
  "op": "mcm",
  "id": 1,
  "mc": [{
    "id": "1.234567",
    "rc": [{
      "id": 98765432,
      "status": "REMOVED",
      "reductionFactor": 12.5
    }]
  }]
}

Key fields:

A second form of the signal appears when Betfair needs to send a late removal:

{
  "rc": [{
    "id": 98765432,
    "status": "REMOVED_VACANT"
  }]
}

REMOVED_VACANT indicates a late withdrawal where a reduction factor has not yet been calculated. Handle it defensively — treat it as a removal event even before the factor arrives.

Java model for runner state

Track runner status alongside the order book:

public enum RunnerStatus {
    ACTIVE, REMOVED, REMOVED_VACANT, WINNER, LOSER, PLACED
}

public class RunnerState {

    private final long selectionId;
    private RunnerStatus status;
    private double reductionFactor;   // 0.0 if not yet received
    private final CondensedOrderBook orderBook;

    public RunnerState(long selectionId) {
        this.selectionId = selectionId;
        this.status = RunnerStatus.ACTIVE;
        this.reductionFactor = 0.0;
        this.orderBook = new CondensedOrderBook();
    }

    public void applyRemoval(double reductionFactor) {
        this.status = RunnerStatus.REMOVED;
        this.reductionFactor = reductionFactor;
        this.orderBook.clear();   // removed runners have no prices
    }

    public boolean isActive() {
        return status == RunnerStatus.ACTIVE;
    }
}

Processing the removal event

In your market change handler:

private void handleRunnerChange(RunnerChange rc, Map<Long, RunnerState> runners) {
    RunnerState runner = runners.computeIfAbsent(
        rc.getId(), RunnerState::new);

    if ("REMOVED".equals(rc.getStatus()) || "REMOVED_VACANT".equals(rc.getStatus())) {
        double rf = rc.getReductionFactor() != null ? rc.getReductionFactor() : 0.0;
        runner.applyRemoval(rf);
        log.info("Runner {} removed, reduction factor: {}%", rc.getId(), rf);
        notifyPositionManager(rc.getId(), rf);
        return;
    }

    if (runner.isActive()) {
        if (rc.getBatb() != null) runner.getOrderBook().applyBackDelta(rc.getBatb());
        if (rc.getBatl() != null) runner.getOrderBook().applyLayDelta(rc.getBatl());
    }
}

Always check isActive() before applying order book updates — Betfair occasionally sends stale price data for removed runners.

Adjusting open positions

When a non-runner is signalled, adjust any open positions on remaining runners:

public void applyReductionFactor(long removedRunnerId, double reductionFactor) {
    double multiplier = 1.0 - (reductionFactor / 100.0);

    for (OpenPosition position : openPositions.values()) {
        if (position.getRunnerId() == removedRunnerId) {
            position.markAsVoided();
            continue;
        }
        // Reduce potential profit on remaining runners
        position.applyReductionFactor(multiplier);
    }
}

The position on the removed runner is voided — Betfair returns stakes on removed runners automatically. Positions on remaining runners have their expected profit reduced by the multiplier.

Adjusting historical signal data

If you store a price history or WoM signal history for remaining runners, those pre-removal snapshots now describe a different market. Whether you invalidate them depends on your strategy:

Store the removal timestamp against each market so downstream components can filter signal history cleanly:

public record MarketEvent(
    Instant occurredAt,
    MarketEventType type,
    long selectionId,
    double reductionFactor
) {}

Multiple removals

Multiple runners can be withdrawn in the same race. Each removal generates a separate REMOVED status in the stream. Process them in order — the reduction factors are independent, but the combined effect is multiplicative on your position’s expected profit:

double combinedMultiplier = 1.0;
for (double rf : reductionFactors) {
    combinedMultiplier *= (1.0 - rf / 100.0);
}
double adjustedProfit = originalExpectedProfit * combinedMultiplier;

The rule 4 deduction table

Betfair’s reduction factor is derived from the runner’s starting price at the time of withdrawal. Short-priced favourites removed close to the off will carry large reduction factors (sometimes 75%+), compressing the market significantly. Long-shots carry single-digit reductions.

The official Rule 4 deduction table maps SP ranges to fixed deductions. The streaming API applies these — you do not need to compute them yourself. Trust the reductionFactor field in the removal message.

Testing removal handling

A non-runner scenario worth testing explicitly: a runner is REMOVED before the streaming connection is established. In this case the initial snapshot will already include the runner with status: "REMOVED" and reductionFactor populated. Your handler must not treat this as a new event requiring position adjustment — the adjustment already happened in a previous session. Use a flag or event log to prevent double-application.

With correct removal handling, a mid-race non-runner is a routine event rather than an edge case that corrupts your position tracking or triggers spurious signals.

If you’re building Betfair trading systems in Java and want help with market event handling, 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.