# Check the market margin at the best prices

Add up the best price on every side of a market and see how much margin is left.

## The idea

For a market whose outcomes cover every result (home, draw and away, or over and under on one line), the implied probabilities of one bookmaker's prices add up to more than 1. The amount over 1 is that bookmaker's margin.

Take the best price for each outcome from different bookmakers and add up `1 / price` again. The sum usually drops, because you are picking the best of several prices on each side. If the sum falls below 1, splitting a stake across every outcome in the right proportions would return more than the total staked whatever the result, but only if every bet is accepted at those exact prices. That is what people call an arbitrage.

With real prices a sum under 1 is rare and short lived. Prices move within seconds to minutes, a price can be gone by the time you act on it, and bookmakers may limit or close accounts that bet this way. Treat this program as a way to measure how tight a market is, not as a source of profit.

## The program

It reads the market catalogue for the league once, to learn which sides make up each market, then reads current prices for the league's games. Each market appears once with every bookmaker's prices on each outcome, sorted best first, so the best price per side is the outcome's first price. For each market the program prints those best prices, their sum and the margin. A market where any side has no current price is skipped, because a sum over part of a market means nothing.

```javascript
const BASE = "https://api.sockodds.com/v1";
const headers = { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` };

async function getAll(url) {
  const rows = [];
  while (url) {
    const body = await (await fetch(url, { headers })).json();
    rows.push(...body.data);
    url = body.links.next; // follow links.next unchanged until it is null
  }
  return rows;
}

// Which sides make up each market in this league (from the catalogue; cache this).
const sides = new Map();
for (const market of await getAll(`${BASE}/markets?league=epl&limit=100`)) {
  if (Array.isArray(market.sides)) sides.set(market.key, market.sides);
}

for (const event of await getAll(`${BASE}/odds?league=epl&limit=25&starts_after=${new Date().toISOString().slice(0, 19)}Z`)) {
  console.log(event.name);
  for (const market of event.markets) {
    // Each outcome's prices are sorted best first, so prices[0] is the best price on that side.
    const outcomes = new Map(market.outcomes.map((outcome) => [outcome.side, outcome]));
    const keys = sides.get(market.key);
    if (!keys || !keys.every((side) => outcomes.get(side)?.prices.length)) continue; // not every side is priced
    const first = outcomes.get(keys[0]);
    const label = market.name + (first.line === null ? "" : ` ${first.line}`) + ` (${market.period})`;
    const sum = keys.reduce((total, side) => total + 1 / outcomes.get(side).prices[0].decimal_odds, 0);
    console.log(`  ${label}`);
    for (const side of keys) {
      const best = outcomes.get(side).prices[0];
      console.log(`    ${side}: ${best.decimal_odds.toFixed(2)} at ${best.bookmaker}`);
    }
    const note = sum < 1 ? "under 1: check every price is still up" : "over 1: no arbitrage";
    console.log(`    sum ${sum.toFixed(4)}, margin ${((sum - 1) * 100).toFixed(2)}%, ${note}`);
  }
}
```

```python
import json, os, urllib.request
from datetime import datetime, timezone

BASE = "https://api.sockodds.com/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]}


def get_all(url):
    rows = []
    while url:
        with urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS)) as response:
            body = json.load(response)
        rows.extend(body["data"])
        url = body["links"]["next"]  # follow links.next unchanged until it is null
    return rows


# Which sides make up each market in this league (from the catalogue; cache this).
sides = {}
for market in get_all(BASE + "/markets?league=epl&limit=100"):
    if isinstance(market.get("sides"), list):
        sides[market["key"]] = market["sides"]

for event in get_all(BASE + f"/odds?league=epl&limit=25&starts_after={datetime.now(timezone.utc):%Y-%m-%dT%H:%M:%SZ}"):
    print(event["name"])
    for market in event["markets"]:
        # Each outcome's prices are sorted best first, so prices[0] is the best price on that side.
        outcomes = {outcome["side"]: outcome for outcome in market["outcomes"]}
        keys = sides.get(market["key"])
        if not keys or not all(side in outcomes and outcomes[side]["prices"] for side in keys):
            continue  # not every side is priced
        first = outcomes[keys[0]]
        label = market["name"] + ("" if first["line"] is None else f" {first['line']}") + f" ({market['period']})"
        total = sum(1 / outcomes[side]["prices"][0]["decimal_odds"] for side in keys)
        print(f"  {label}")
        for side in keys:
            best = outcomes[side]["prices"][0]
            print(f"    {side}: {best['decimal_odds']:.2f} at {best['bookmaker']}")
        note = "under 1: check every price is still up" if total < 1 else "over 1: no arbitrage"
        print(f"    sum {total:.4f}, margin {(total - 1) * 100:.2f}%, {note}")
```

Output (from a local copy of the API with invented test prices):

```text
AFC Bournemouth v Arsenal
  Result (with the draw) (full)
    home: 2.15 at swiftbet
    draw: 3.57 at swiftbet
    away: 3.78 at swiftbet
    sum 1.0098, margin 0.98%, over 1: no arbitrage
  Both teams to score (full)
    yes: 1.89 at swiftbet
    no: 2.05 at swiftbet
    sum 1.0169, margin 1.69%, over 1: no arbitrage
  Total goals 2.5 (full)
    over: 1.96 at swiftbet
    under: 2.03 at swiftbet
    sum 1.0028, margin 0.28%, over 1: no arbitrage
```

## Reading the output

- **sum** is the total of `1 / price` over the best price on each side. Above 1, the best prices still carry a margin. Below 1, they do not, at the moment those prices were seen.
- **margin** is the same number as a percentage over 1. A smaller margin means a tighter market.
- Each best price names its bookmaker by key. Different sides often come from different bookmakers, so acting on a sum under 1 means placing several bets at several bookmakers, at the same time, before any price moves.

## Before you rely on a sum under 1

- **Check the time.** Every price carries `observed_at`. A quote older than ten minutes is withheld from current responses, but a price seen nine minutes ago may already have changed. Compare `observed_at` across the sides.
- **Check the market is the same.** Each market in the response has one id, which already pins the market type, period and line, and holds every bookmaker's prices for it. Two bookmakers can still settle a market differently (for example, whether extra time counts), so read each bookmaker's rules.
- **Expect limits.** Bookmakers may cap your stake, cancel a bet placed at a price they call an error, or close an account.
- **A missing price is not a zero.** A side without a current price means we have no fresh quote for it, not that no bookmaker offers it.

## Make it yours

- Change `league=epl` to any league key on [the leagues page](https://sockodds.com/leagues/). Two-way markets such as `winner` work the same way as three-way markets.
- Use [`/v1/events/{event_id}/odds`](https://sockodds.com/docs/#getEventOdds) to check one game, or `markets=` to check fewer markets and use fewer objects.
- Add `bookmakers=` to compare only the bookmakers you hold accounts with.

The output above comes from a local copy of the API with invented test prices.
