# Football Odds Comparison & Line Movement (`smoked_drift/football-odds-comparison`) Actor

Compare football odds across 9+ bookmakers for 22 European leagues. Best available price per outcome, best-price overround, average bookmaker margin, how much line shopping saves you, and open-to-close line movement.

- **URL**: https://apify.com/smoked\_drift/football-odds-comparison.md
- **Developed by:** [Titouan MARTY](https://apify.com/smoked_drift) (community)
- **Categories:** Sports
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 market signals

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Football Odds Comparison & Line Movement

**Compare football odds across 9+ bookmakers** for 22 European leagues. For every match you get the best available price on each outcome, the real cost of that market once you shop every line, how much shopping actually saves you, and how far each bookmaker moved its prices between opening and closing.

This is a **football odds API** for anyone who cares about price rather than prediction: bettors comparing lines, modellers checking whether their edge survives the margin, and analysts studying how information reaches a market.

***

### The number this Actor exists to produce

I measured the 2025/26 season across the big five leagues:

| What you pay | Average margin |
| --- | ---: |
| A typical single bookmaker | **4.90%** |
| The best available line, every outcome | **0.40%** |

Shopping every line instead of accepting one bookmaker's prices cuts the cost of the same bet by roughly **twelve times**. On 1,000 bets that is the difference between paying 49 units of stake and paying 4.

That gap is not an edge and it is not a prediction. It is arithmetic, and it is the single largest controllable number in most people's betting P\&L.

***

### What you get

One dataset item per match:

| Group | Fields |
| --- | --- |
| **Match** | date, kick-off, home, away, status, league details |
| **Best odds** | the best available price per outcome across every quoting bookmaker, with obvious feed errors filtered out |
| **Best-price overround** | the implied-probability sum using those best prices. Negative means backing every outcome returns more than it costs |
| **Average margin** | what a typical bookmaker charges on the same match |
| **Line-shopping gain** | the difference between the two: your saving from comparing lines |
| **Line movement** | the largest open-to-close change, with the bookmaker and the outcome it happened on, plus whether the price shortened or drifted |
| **Signal** | a single ranked descriptor of what makes the match interesting |

***

### Sample output

Real row, Ligue 1, Monaco vs Marseille. Trimmed to the interesting parts:

```json
{
  "date": "2026-08-30",
  "home": "Monaco",
  "away": "Marseille",
  "leagueInfo": { "code": "F1", "name": "Ligue 1", "country": "France", "tier": 1 },
  "status": "finished",
  "market": {
    "bestPrice":         { "home": 2.32, "draw": 3.80, "away": 3.40 },
    "bookmakerCount":    9,
    "closingAvailable":  true,
    "impliedProbability":{ "home": 0.4309, "draw": 0.2684, "away": 0.3007 },
    "averageMargin":     0.0463,
    "fairPrice":         { "home": 2.321, "draw": 3.726, "away": 3.326 },
    "lineMovement": {
      "B365.home": -0.2, "B365.draw": -0.2, "B365.away": 0.45,
      "Avg.home": -0.18, "Avg.draw": -0.18, "Avg.away": 0.47,
      "Max.home": -0.25, "Max.away": 0.45
    }
  },
  "signal": {
    "bookmakerCount":      9,
    "averageMargin":       0.0463,
    "closingAvailable":    true,
    "bestOdds":            { "home": 2.32, "draw": 3.80, "away": 3.40 },
    "bestPriceOverround":  -0.0117,
    "isArbitrage":         false,
    "shoppingGain":        0.058,
    "biggestMovement":     { "book": "Avg", "side": "away", "change": 0.47, "direction": "drifted" },
    "absMovement":         0.47
  },
  "opportunity": { "kind": "shopping-gain", "score": 0.058, "label": "best line saves 5.80 pts vs average book" },
  "source": { "provider": "football-data.co.uk", "league": "F1", "season": "2627", "url": "https://www.football-data.co.uk/mmz4281/2627/F1.csv" }
}
```

Read it as: the average book charged **4.63%**, the best available line left **−1.17%**, so line shopping was worth **5.80 points** on this match — and the away price drifted **+0.47** from open to close.

***

### Why the "best price" is filtered

A raw maximum across ten bookmakers is not trustworthy. On Mallorca vs Barcelona the unfiltered best prices produced a **58% arbitrage** — impossible in a top division, and in reality one bookmaker's feed had gone stale on a long shot.

Any price more than 1.6× the median for that outcome is therefore discarded as a feed error. After filtering, the same match shows −0.38%, which is plausible. Filters that make a product look less exciting but more correct are the right trade.

***

### Usage

#### Compare the big five leagues

```json
{ "leagues": ["big-five"], "seasonsBack": 1 }
```

#### Only genuine arbitrage

```json
{ "leagues": ["all"], "onlyArbitrage": true, "arbitrageThreshold": -0.02, "minBookmakers": 6 }
```

#### Where the money moved

```json
{ "leagues": ["england"], "onlyMovement": true, "minMovement": 0.5 }
```

#### Upcoming fixtures and their current prices

```json
{ "leagues": ["big-five"], "includeFixtures": true, "seasonsBack": 1 }
```

***

### Leagues

`E0` `E1` `E2` `E3` `EC` (England) · `SC0`–`SC3` (Scotland) · `D1` `D2` (Germany) · `I1` `I2` (Italy) · `SP1` `SP2` (Spain) · `F1` `F2` (France) · `N1` (Netherlands) · `B1` (Belgium) · `P1` (Portugal) · `T1` (Turkey) · `G1` (Greece)

**Presets:** `big-five` · `big-five-plus-second` · `france` · `england` · `all`

***

### Pricing

| Event | Charged | Covers |
| --- | --- | --- |
| `market-signal` | once per match returned | best odds, overround, margin, shopping gain, movement |
| `line-movement` | once per match that actually moved | the movement detail |

`minBookmakers` (default 4) and `maxItems` both protect your budget. Tighten either to spend less.

***

### Reliability

Built on the same engine as the [Football Data API](https://apify.com/smoked_drift/european-football-results-odds) Actor, which means:

- The source is a **downloadable CSV**, not a web page. No browser, no proxy, no selectors to break.
- Tested against **1,482 real matches** before release: median best-price overround 0.56%, median line-shopping gain 4.28 points.
- Every row carries its source URL.

***

### Notes and limits

- **A negative overround is not free money.** Getting the best price on all three outcomes needs accounts at many bookmakers; the odds are snapshots rather than simultaneous quotes; and exchange prices carry commission this figure does not account for. Measured across 1,522 matches, the median best-price overround is **+0.40%** and roughly 7% of matches fall below −0.5%. That is why the default arbitrage threshold sits at **−2%**, not zero.
- **Opening lines only for fixtures.** A closing price does not exist before the market settles, so `closingAvailable` is `false` for upcoming matches.
- **Bookmaker coverage varies by division.** `bookmakerCount` tells you exactly how many fed each row.
- This Actor provides **market data only**. It is not betting advice and it does not predict outcomes.

***

### Related

From the same publisher, on the same engine and the same 22 leagues:

- [Football Data API](https://apify.com/smoked_drift/european-football-results-odds) — the underlying match dataset: scores, statistics, opening and closing odds, Elo and form
- [Football Stats & Elo Ratings](https://apify.com/smoked_drift/football-team-stats-elo) — one row per club: Elo, record, home and away splits, form and scoring profile

Want to add location or social context to a fixture? [Google Maps Scraper](https://apify.com/compass/crawler-google-places) and [Instagram Scraper](https://apify.com/apify/instagram-scraper) pair well with this.

***

### Support

Report a broken row or a missing bookmaker in the **Issues** tab of this Actor.

# Actor input Schema

## `leagues` (type: `array`):

Competitions to compare. Use a preset ('big-five', 'big-five-plus-second', 'france', 'england', 'all') or explicit codes: E0-E3, EC, SC0-SC3, D1-D2, I1-I2, SP1-SP2, F1-F2, N1, B1, P1, T1, G1.

## `seasonsBack` (type: `integer`):

How many seasons of history to compare, starting from the current one.

## `includeFixtures` (type: `boolean`):

Also compare the prices on matches that have not kicked off yet. These carry opening lines only, since no closing price exists before the market settles.

## `onlyArbitrage` (type: `boolean`):

Keep only matches where backing every outcome at the best available price returns more than it costs, after the safety threshold below.

## `arbitrageThreshold` (type: `number`):

How far below zero the best-price overround must fall to count as an arbitrage. -0.02 means a genuine 2% edge. Values just below zero are normal: taking the best of ten bookmakers goes negative more often than a real arbitrage exists.

## `minShoppingGain` (type: `number`):

Only keep matches where the best available line saves at least this much margin versus a typical single bookmaker. 0.02 means two percentage points.

## `minMovement` (type: `number`):

Only keep matches where some bookmaker moved a price by at least this much between opening and closing. 0.5 means half a point of decimal odds.

## `onlyMovement` (type: `boolean`):

Drop matches where no bookmaker moved a price at all.

## `minBookmakers` (type: `integer`):

Skip matches quoted by fewer than this many bookmakers. Comparing three books is not a market; four is the default floor.

## `teams` (type: `array`):

Optional. Keep only matches involving these teams. Case-insensitive partial match.

## `maxItems` (type: `integer`):

Hard cap on results. Protects your budget on wide scopes.

## Actor input object example

```json
{
  "leagues": [
    "big-five"
  ],
  "seasonsBack": 1,
  "includeFixtures": true,
  "onlyArbitrage": false,
  "arbitrageThreshold": -0.02,
  "minShoppingGain": 0,
  "minMovement": 0,
  "onlyMovement": false,
  "minBookmakers": 4,
  "maxItems": 1000
}
```

# Actor output Schema

## `matches` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "leagues": [
        "big-five"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("smoked_drift/football-odds-comparison").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "leagues": ["big-five"] }

# Run the Actor and wait for it to finish
run = client.actor("smoked_drift/football-odds-comparison").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "leagues": [
    "big-five"
  ]
}' |
apify call smoked_drift/football-odds-comparison --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,smoked_drift/football-odds-comparison"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/lrb8xXI6n6Ah29Eve/builds/vJbC43NogIxMSLZrz/openapi.json
