Hire Me
← All Writing Betfair

Greening Up — Locking In Profits Before the Off

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.

The Maths

When you back a selection for backStake at backOdds, your position is:

To 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).

The GreenUpCalculator

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.

Rounding to the Minimum Bet Increment

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.

When to Trigger a Green

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.

Partial Greening

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.

The Loss Scenario

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:

  1. Accept the loss and green up anyway — if you’re approaching the off and want a guaranteed small loss rather than risking a full back stake loss.
  2. Do nothing and hope the price shortens back — viable only if there’s time and you have a view.
  3. Cut the loss — fully close for a certain loss by laying at the current price with a stake that minimises the worst-case outcome.

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.

ProTips

If you’re building a pre-race Betfair trading system and want to get the execution logic right, get in touch.