Backtest opening vs closing odds
A season of results, each with the price Bet365 opened at and the price it closed at. Settle the same simple bets at both prices and compare what they returned.
The finished backtest
Sample output: the Premier League 25/26 season, one unit staked on every match.
| One unit on | Return at the opening price | Return at the closing price |
|---|---|---|
| Every home team | -9.7% | -8.5% |
| Every draw | +3.0% | +6.4% |
| Every away team | -9.0% | -11.4% |
| Every favourite | -10.9% | -8.6% |
Premier League 25/26, 380 matches, Bet365 1X2 prices. One season is a small sample. Captured 8 October 2026 from
GET /v1/leagues/{id}/fixtures?season=25/26&include=odds
The API calls behind it
-
1
GET /v1/leagues?search=Premier League&country=GB-ENGFind the league id. Look it up once and keep it. Reference →
-
2
GET /v1/leagues/{id}/fixtures?season=25/26&status=finished&include=odds&per_page=50A season of results, 50 a page, each row with its opening and closing odds in place. Reference →
What it takes to run
- Odds on a fixture list (include=odds) are Pro and above, $5/mo. A free key reads the same prices one match at a time from /v1/fixtures/{id}/odds.
- Nine requests for a 380-match season: one to find the league, eight pages of 50 matches.
- Pro reads the last 12 months, so it returns the part of last season inside that window. Ultra returns the whole season and reaches back to 2014.
The code
Install the client, save the script, run it with your key.
pip install fivedollarfootballimport os
from fivedollarfootball import Client
client = Client(os.environ["FIVEDOLLARFOOTBALL_API_KEY"])
SEASON = "25/26"
leagues = client.leagues(search="Premier League", country="GB-ENG")
league = next(l for l in leagues if l["name"] == "England Premier League")
# 1. A season of results, each row with its opening and closing 1X2 odds.
matches = client.iter_all(
client.league_fixtures, league_id=league["id"], season=SEASON,
status="finished", include="odds", per_page=50,
)
rows = [m for m in matches if m["odds"]["1x2"]["opening"] and m["odds"]["1x2"]["closing"]]
def result(m):
home, away = m["goals"]["home"], m["goals"]["away"]
return "home" if home > away else "away" if away > home else "draw"
# 2. Four simple bets, one unit on every match.
bets = {
"home team": lambda price: "home",
"draw": lambda price: "draw",
"away team": lambda price: "away",
"favourite": lambda price: min(price, key=price.get),
}
# 3. Settle each bet twice: at the opening price and at the closing price.
print(f'{league["name"]} {SEASON}: {len(rows)} matches, one unit on each')
print(f'{"Back every":<12}{"At opening":>12}{"At closing":>12}')
for name, pick in bets.items():
returns = []
for stage in ("opening", "closing"):
profit = 0
for m in rows:
price = m["odds"]["1x2"][stage]
side = pick(price)
profit += price[side] - 1 if side == result(m) else -1
returns.append(profit / len(rows))
print(f'{name:<12}{returns[0]:>+12.1%}{returns[1]:>+12.1%}')
England Premier League 25/26: 380 matches, one unit on each
Back every At opening At closing
home team -9.7% -8.5%
draw +3.0% +6.4%
away team -9.0% -11.4%
favourite -10.9% -8.6%
FIVEDOLLARFOOTBALL_API_KEY=fb_live_your_key python odds_backtest.pyThe full project is on GitHub: 5dollarfootball-api/football-odds-backtest →
Take it further
- Run several seasons and leagues before reading anything into a number: one season is 380 matches.
- Add your own rule to the bets: it only has to return "home", "draw" or "away" for a match.
- Each row also carries the Asian handicap, goal line and corner line at opening and closing: bet only when a line moved, and compare.
- To compare the two prices as predictions instead of as bets, score them with a Brier score or log loss.
- For the price at any moment in between, not just the two ends, read the tick history from /v1/fixtures/{id}/odds/history (Ultra).
The data behind this example
Football Odds API
One endpoint for European 1X2, Asian handicap, goal line, corner and card odds —...
Asian Handicap Odds API
Asian handicap lines with opening, closing and in-play values — for goals and co...
Goal Line Odds API
Over/under goal lines with opening, closing and in-play values across thousands...
Frequently asked questions
Pro, at $5/mo. Reading odds for a whole fixture list in one call (include=odds) starts on Pro, and Pro reaches back 12 months. Ultra returns complete past seasons back to 2014. A free key can read the same opening and closing prices one match at a time.
One unit is staked on every match. A winning bet pays the price minus the stake and a losing bet loses the stake. The figure is the total profit divided by the number of matches, so -8.6% means 8.6 units lost for every 100 staked.
Yes. For a finished match, closing is the last pre-match price Bet365 quoted and opening is the first. Settling at the closing price, not at a later in-play price, is what keeps a backtest honest.
Start building for free
Get a working API key in under a minute. No credit card, no sales call.