DEV Community

Cover image for Tracking Football Odds Movement in Python — the Full Tick History, Not Just a Snapshot
John Ellis
John Ellis

Posted on

Tracking Football Odds Movement in Python — the Full Tick History, Not Just a Snapshot

Most football data APIs will tell you what the odds are right now. A few keep yesterday around. Almost none will show you the full journey — every price change from the moment a bookmaker opened the line to the final whistle.

That journey is where the interesting information lives. A 1X2 price drifting from 2.10 to 1.85 over three days tells you which way the money went. A corner line jumping half a goal at kickoff tells you how the market read the starting lineups. If you've ever wanted to study closing line value, detect steam moves, or just chart how a price evolved, you need ticks, not snapshots.

This tutorial builds a small odds-movement tracker in Python. We'll use 5DollarFootballAPI, which stores the complete tick history for every price it has recorded since 2014 — including corner and card lines, which most APIs don't carry at all.

Disclosure: I build that API. The technique here isn't specific to it — anything that serves you a price history works the same way. I'm using mine because I know exactly what's in it.

Setup

pip install fivedollarfootball
Enter fullscreen mode Exit fullscreen mode

Grab an API key at 5dollarfootballapi.com. Worth knowing before you start, so nothing in this tutorial surprises you at the paywall:

  • Free (no card): fixtures, live scores, standings — enough for Step 1.
  • $5/mo: current odds, which is Step 2.
  • $25/mo: the tick history in Steps 3–5. Storing every price change for every market is the expensive part, and it's the tier this tutorial really needs.
from fivedollarfootball import Client

client = Client("fb_live_your_key")
Enter fullscreen mode Exit fullscreen mode

Step 1: find today's matches

The fixtures endpoint defaults to a today-UTC kickoff window:

for match in client.fixtures():
    teams = match["teams"]
    print(match["id"], teams["home"]["name"], "vs", teams["away"]["name"], "-", match["status"])
Enter fullscreen mode Exit fullscreen mode

Pick a fixture id from the output — ideally one a few hours from kickoff, so the market has had time to move.

Step 2: what do the odds look like right now?

odds = client.fixture_odds(FIXTURE_ID, bookmakers=["bet365"], market="1x2")
print(odds)
Enter fullscreen mode Exit fullscreen mode

This is the snapshot every API gives you. Now for the part most of them can't do.

Step 3: pull the full movement history

history = client.odds_history(FIXTURE_ID, market="1x2", bookmaker="bet365")

for tick in history:
    print(tick["recorded_at"], tick["home"], tick["draw"], tick["away"])
Enter fullscreen mode Exit fullscreen mode

Each tick carries the price at that moment, the running score, the match minute (None before kickoff), and a UTC timestamp:

{
  "minute": null,
  "home": 2.05,
  "draw": 3.40,
  "away": 3.60,
  "score": { "home": null, "away": null },
  "recorded_at": "2026-08-18T14:32:07+00:00"
}
Enter fullscreen mode Exit fullscreen mode

The endpoint is paginated; iter_all walks every page for you:

ticks = list(client.iter_all(client.odds_history, fixture_id=FIXTURE_ID, market="1x2"))
Enter fullscreen mode Exit fullscreen mode

Step 4: find the biggest pre-match moves

Let's answer a concrete question: which side did the market move against before kickoff?

prematch = [t for t in ticks if t["minute"] is None and t["home"] is not None]

if len(prematch) >= 2:
    opening, closing = prematch[0], prematch[-1]
    for side in ("home", "draw", "away"):
        move = (closing[side] - opening[side]) / opening[side] * 100
        arrow = "▼ backed" if move < 0 else "▲ drifted"
        print(f"{side:>5}: {opening[side]:.2f}{closing[side]:.2f}  {arrow} {abs(move):.1f}%")
Enter fullscreen mode Exit fullscreen mode

Sample output:

 home: 2.05 → 1.87  ▼ backed 8.8%
 draw: 3.40 → 3.55  ▲ drifted 4.4%
 away: 3.60 → 4.10  ▲ drifted 13.9%
Enter fullscreen mode Exit fullscreen mode

A shortening price means money arrived on that side. Sum this over a season and you're measuring closing line value — the metric most serious bettors and modelers care about more than win rate.

Step 5: the same trick works on corner lines

This is the fun part. Corner totals move too, and that market is far less efficient than 1X2:

corner_ticks = list(client.iter_all(client.odds_history, fixture_id=FIXTURE_ID, market="corner"))

for tick in corner_ticks[-5:]:
    print(tick["recorded_at"], "line", tick["line"], "over", tick["over"], "under", tick["under"])
Enter fullscreen mode Exit fullscreen mode

Line moves (say 9.5 → 10) are a stronger signal than price moves — a bookmaker only reprices the whole market when the flow forces them to.

Where to go from here

  • Store ticks in SQLite and chart them with matplotlib — one fixture is a few hundred rows at most.
  • Compare opening vs closing across a whole league season (league_fixtures + a loop).
  • Watch live: in-play ticks carry the match minute and running score, so you can line odds moves up against goals and red cards.

Full endpoint reference: 5dollarfootballapi.com/docs. The Python client is on PyPI, and there's a matching Node.js client on npm if JavaScript is more your thing.

Questions or feedback? I'm happy to answer in the comments.

Top comments (0)