How to implement greening up on Betfair in Java — the maths, a GreenUpCalculator class, when to trigger it, handling partial greens, rounding, and the edge cases that catch out new traders.
Greening up is the core profit-locking mechanism for pre-race Betfair trading. You’ve backed a horse at 5.0, the price has shortened to 3.5, and you want to secure the profit regardless of whether the horse wins or loses. Instead of hoping it wins at its original price, you lay it off at the current price — distributing the profit across both outcomes so that the net position is positive whatever happens.
It sounds simple. The maths is simple. But getting the implementation right — handling rounding, minimum bet increments, partial greening, and the edge case where the price drifts against you — requires more care than people expect. I’ve seen trading bots get the calculation wrong and eat into every winning trade through systematic rounding errors.
When you back a selection for backStake at backOdds, your position is:
backStake × (backOdds - 1)backStakeTo green up at the current lay odds layOdds, you need a lay stake that balances both outcomes to the same net value. The required lay stake is:
layStake = (backStake × backOdds) / layOdds
After this lay, the profit from each outcome is:
profit = backStake × backOdds - layStake × layOdds
= backStake × backOdds - (backStake × backOdds)
= backStake × (backOdds - layOdds) / layOdds [win scenario]
Simplified: the locked-in profit is the same regardless of outcome, and equals:
greenProfit = backStake × (backOdds - layOdds) / layOdds
This is positive when backOdds > layOdds (price has shortened — you got back at a bigger price than the current lay). It’s negative when layOdds > backOdds (the price has drifted past your back price — greening up locks in a loss).
import java.math.BigDecimal;
import java.math.RoundingMode;
public class GreenUpCalculator {
// Betfair minimum bet is £2.00; minimum increment is £0.01
private static final BigDecimal MIN_BET = new BigDecimal("2.00");
private static final int STAKE_SCALE = 2;
/**
* Calculate the lay stake required to green up a back bet.
*
* @param backStake the original back stake in GBP
* @param backOdds the odds at which you backed
* @param layOdds the current best lay odds
* @return a GreenUpResult with the lay stake and expected profit per outcome
*/
public GreenUpResult calculate(
BigDecimal backStake,
BigDecimal backOdds,
BigDecimal layOdds) {
if (backStake.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Back stake must be positive");
}
if (backOdds.compareTo(BigDecimal.ONE) <= 0
|| layOdds.compareTo(BigDecimal.ONE) <= 0) {
throw new IllegalArgumentException("Odds must be greater than 1.0");
}
// layStake = (backStake * backOdds) / layOdds
BigDecimal impliedBack = backStake.multiply(backOdds);
BigDecimal rawLayStake = impliedBack.divide(layOdds, STAKE_SCALE, RoundingMode.HALF_UP);
// Round to Betfair's minimum increment and enforce minimum bet
BigDecimal layStake = rawLayStake.max(MIN_BET);
// Locked-in profit = backStake * backOdds - layStake * layOdds
BigDecimal grossBack = backStake.multiply(backOdds);
BigDecimal layLiability = layStake.multiply(layOdds.subtract(BigDecimal.ONE));
// Net profit if selection wins: back profit minus lay liability
BigDecimal profitIfWins = grossBack
.subtract(backStake)
.subtract(layLiability);
// Net profit if selection loses: lay profit minus back stake
BigDecimal layWin = layStake;
BigDecimal profitIfLoses = layWin.subtract(backStake);
boolean isGreen = profitIfWins.compareTo(BigDecimal.ZERO) > 0
&& profitIfLoses.compareTo(BigDecimal.ZERO) > 0;
return new GreenUpResult(layStake, profitIfWins, profitIfLoses, isGreen);
}
/**
* Whether greening up would lock in a profit (price has shortened).
* Returns false if the position has gone against us.
*/
public boolean canGreenForProfit(BigDecimal backOdds, BigDecimal layOdds) {
return layOdds.compareTo(backOdds) < 0;
}
public record GreenUpResult(
BigDecimal layStake,
BigDecimal profitIfWins,
BigDecimal profitIfLoses,
boolean isGreen
) {
public BigDecimal averageProfit() {
return profitIfWins.add(profitIfLoses)
.divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP);
}
}
}
A worked example: back £10 at 5.0, current lay price is 3.5.
layStake = (10 × 5.0) / 3.5 = 50 / 3.5 = £14.29
profitIfWins = (10 × 4.0) - (14.29 × 2.5) = 40.00 - 35.73 = £4.27
profitIfLoses = 14.29 - 10 = £4.29
Both outcomes yield roughly the same profit. The small difference is rounding. That’s the green.
Betfair enforces specific price increments depending on where the price sits on the ladder. For odds between 2.0 and 3.0, the minimum increment is 0.02. Between 3.0 and 4.0 it’s 0.05. The lay stake itself needs rounding to £0.01, but the more important rounding is on the lay price itself — you need to place the lay at a valid ladder price.
public BigDecimal roundToLadderIncrement(BigDecimal price) {
// Simplified — real Betfair increment table has ~8 bands
BigDecimal[] breakpoints = {
new BigDecimal("2.0"), new BigDecimal("3.0"), new BigDecimal("4.0"),
new BigDecimal("6.0"), new BigDecimal("10.0"), new BigDecimal("20.0"),
new BigDecimal("30.0"), new BigDecimal("50.0"), new BigDecimal("100.0")
};
BigDecimal[] increments = {
new BigDecimal("0.01"), new BigDecimal("0.02"), new BigDecimal("0.05"),
new BigDecimal("0.1"), new BigDecimal("0.2"), new BigDecimal("0.5"),
new BigDecimal("1.0"), new BigDecimal("2.0"), new BigDecimal("5.0")
};
for (int i = 0; i < breakpoints.length; i++) {
if (price.compareTo(breakpoints[i]) < 0) {
BigDecimal inc = increments[i];
return price.divide(inc, 0, RoundingMode.DOWN).multiply(inc);
}
}
return price.divide(new BigDecimal("10.0"), 0, RoundingMode.DOWN)
.multiply(new BigDecimal("10.0"));
}
Always round the lay price down (favourably) rather than up. Rounding up means you’re asking for worse odds for yourself.
The trigger logic is where strategy lives. A few patterns I use:
public class GreenUpTrigger {
private static final BigDecimal PROFIT_THRESHOLD = new BigDecimal("0.05"); // 5% move
private static final long SECONDS_BEFORE_OFF_TO_FORCE = 120L;
private final GreenUpCalculator calculator;
public boolean shouldGreenUp(
BigDecimal backOdds,
BigDecimal currentLayOdds,
Instant scheduledOff,
BigDecimal backStake) {
// Always green up in the final 2 minutes — clear the position
long secondsToOff = Duration.between(Instant.now(), scheduledOff).getSeconds();
if (secondsToOff <= SECONDS_BEFORE_OFF_TO_FORCE) {
return calculator.canGreenForProfit(backOdds, currentLayOdds);
}
// Green up if we've achieved target price movement
if (!calculator.canGreenForProfit(backOdds, currentLayOdds)) {
return false; // price has drifted — wait or accept loss
}
BigDecimal priceMoveRatio = backOdds.subtract(currentLayOdds)
.divide(backOdds, 4, RoundingMode.HALF_UP);
return priceMoveRatio.compareTo(PROFIT_THRESHOLD) >= 0;
}
}
The time-based trigger is important. You don’t want to be holding a back position through the off — in-play volatility is brutal and the exchange will suspend the market at the off anyway, potentially leaving you stuck.
Sometimes you don’t want to fully green — you want to take some profit and let part of the position run. A partial green lays off a fraction of the back stake:
public GreenUpResult partialGreen(
BigDecimal backStake,
BigDecimal backOdds,
BigDecimal layOdds,
BigDecimal fractionToGreen) {
if (fractionToGreen.compareTo(BigDecimal.ZERO) <= 0
|| fractionToGreen.compareTo(BigDecimal.ONE) > 0) {
throw new IllegalArgumentException(
"Fraction must be between 0 (exclusive) and 1 (inclusive)");
}
BigDecimal stakeToGreen = backStake.multiply(fractionToGreen)
.setScale(2, RoundingMode.HALF_UP);
return calculator.calculate(stakeToGreen, backOdds, layOdds);
}
I typically use partial greens when the price has moved significantly but I still believe the selection has further to shorten — take half the profit off the table, let the other half ride.
If the current lay odds are above your back odds, greening up locks in a loss on both outcomes. GreenUpResult.isGreen() will be false. In this case you have three options:
The calculator handles this correctly: when layOdds > backOdds, the profitIfWins and profitIfLoses will both be negative, and isGreen will be false. The decision to proceed is a strategy call, not a calculation error.
BigDecimal throughout. double arithmetic produces rounding errors that compound across many trades. This is financial calculation — use exact arithmetic.If you’re building a pre-race Betfair trading system and want to get the execution logic right, get in touch.