How market status transitions are signalled in the Betfair Streaming API, what each status means for your order book and positions, and how to implement correct state machine handling in Java.
A Betfair market moves through a series of statuses during its lifetime — from inactive before trading opens, through active during pre-race betting, to suspended at the off and after in-play events, and finally closed after settlement. Each transition has implications for your order book state, open positions, and what actions are valid. Handling them correctly prevents trading on a suspended market, misinterpreting stale prices, and missing the transition to in-play.
INACTIVE → OPEN → SUSPENDED → (OPEN ↔ SUSPENDED)* → CLOSED
INACTIVE: The market exists but betting is not yet available. The order book is empty.
OPEN (value "o" in the stream): Betting is live. Back and lay orders accumulate. The order book reflects current matched and unmatched money.
SUSPENDED (value "s"): Temporarily halted — at the off, during in-play events (goals, non-runners), or by Betfair for administrative reasons. In-play markets oscillate between SUSPENDED and OPEN as events occur.
CLOSED (value "c"): Market is settled or voided. No further bets are accepted. Price data may no longer be accurate.
Market status arrives in the MarketChange object as the status field:
private void handleMarketChange(MarketChange mc) {
if (mc.status() != null) {
MarketStatus newStatus = MarketStatus.from(mc.status());
MarketStatus prev = currentStatus.get(mc.id());
currentStatus.put(mc.id(), newStatus);
if (prev != null && prev != newStatus) {
onStatusTransition(mc.id(), prev, newStatus);
}
}
// Only process price updates when OPEN
if (MarketStatus.OPEN.equals(currentStatus.get(mc.id()))) {
applyPriceUpdates(mc);
}
}
The status field is only present in the message when it changes — absence means no change, not OPEN.
Model the status transitions explicitly rather than a simple field:
public enum MarketStatus {
INACTIVE("i"),
OPEN("o"),
SUSPENDED("s"),
CLOSED("c");
private final String streamCode;
MarketStatus(String streamCode) {
this.streamCode = streamCode;
}
public static MarketStatus from(String code) {
for (MarketStatus s : values()) {
if (s.streamCode.equals(code)) return s;
}
throw new IllegalArgumentException("Unknown market status: " + code);
}
public boolean isTrading() {
return this == OPEN;
}
public boolean isFinal() {
return this == CLOSED;
}
}
Each transition requires different action:
private void onStatusTransition(String marketId, MarketStatus from, MarketStatus to) {
log.info("Market {} status: {} → {}", marketId, from, to);
switch (to) {
case SUSPENDED -> {
orderManager.pauseNewOrders(marketId);
signalEngine.invalidateSignals(marketId);
}
case OPEN -> {
if (from == MarketStatus.SUSPENDED) {
// Resuming from suspension — prices may have moved significantly
signalEngine.recalibrate(marketId);
orderManager.resumeOrders(marketId);
}
}
case CLOSED -> {
positionManager.settle(marketId);
marketStateMap.remove(marketId);
}
}
}
The SUSPENDED → OPEN transition (resuming after an in-play event) is distinct from the initial INACTIVE → OPEN transition. Signals built on pre-suspension price history may no longer be valid after a non-runner or goal.
Separate from status, the inplay boolean signals that the market has gone in-play. A suspended market at the off goes in-play when the race starts — the sequence is:
private void handleMarketChange(MarketChange mc) {
if (mc.inplay() != null && mc.inplay()) {
onMarketGoesInPlay(mc.id());
}
// ...
}
private void onMarketGoesInPlay(String marketId) {
log.info("Market {} is now in-play", marketId);
strategyEngine.deactivatePreRaceStrategies(marketId);
// Pre-race positions should have been closed by now
positionManager.verifyAllPreRaceClosed(marketId);
}
The streaming API does not provide a reason code for suspension — you must infer it from context:
For pre-race strategies, only the first matters — detect it and confirm your positions are closed before going in-play.
When a market resumes from suspension, the first price update may reflect significant movement. Do not trade on the pre-suspension order book state as if prices are current. Mark the order book as stale on SUSPENDED and only mark it fresh after receiving at least one price update following the resume:
public class MarketOrderBook {
private boolean stale = false;
public void onSuspended() {
stale = true;
}
public void onPriceUpdate(RunnerChange rc) {
stale = false; // first update after suspend clears stale flag
applyDelta(rc);
}
public boolean isReliable() {
return !stale;
}
}
Any signal computation should check isReliable() before using prices.
The streaming API signals CLOSED before settlement data is available via the REST API. If you need BSP or settlement results immediately after the race, poll listMarketBook with PriceData.SP_TRADED after receiving CLOSED — the BSP is usually available within 1–2 minutes.
With status transitions handled as first-class events — not afterthoughts — your trading system responds correctly to every phase of a market’s lifetime.
If you’re building Betfair trading infrastructure in Java and want help with market lifecycle handling, get in touch.