Skip to content

Guides

How do I backtest MLB closing moneylines and run totals in Python?

A day of games with each sportsbook's opening and closing moneyline, run line and total, next to the result. It counts favourite wins and overs.

Code verified October 12, 2026. One call to get_odds, 5 credits.

The call

date is a US Eastern day. Loop it over a range to build a season. A total that lands on the number has over: null, which is a push.

import requests
import pandas as pd

API_KEY = "YOUR_API_KEY"

resp = requests.post(
    "https://mcp.fastballapi.com/v1/get_odds",
    headers={"x-api-key": API_KEY},
    json={
        "date": "2026-06-10",
    },
    timeout=60,
)
resp.raise_for_status()
data = resp.json()["data"]

rows = []
for g in data["games"]:
    r = g.get("result")
    if not r or not g["books"]:
        continue
    books = pd.DataFrame(g["books"]).dropna(subset=["home_close_prob", "away_close_prob"])
    rows.append({
        "game": g["away"]["abbrev"] + " @ " + g["home"]["abbrev"],
        "home_fair": (books["home_close_prob"] / (books["home_close_prob"] + books["away_close_prob"])).median(),
        "total_close": r["median_total"],
        "runs": g["home"]["runs"] + g["away"]["runs"],
        "favourite_won": r["favourite_won"],
        "over": r["over"],
    })
df = pd.DataFrame(rows)
df["fav_prob"] = df["home_fair"].apply(lambda p: max(p, 1 - p))
print(df.head(8).round(3).to_string(index=False))
print("favourite won:", int(df["favourite_won"].sum()), "of", len(df))
print("over:", int((df["over"] == True).sum()), "under:", int((df["over"] == False).sum()), "push:", int(df["over"].isna().sum()))
print(df.groupby(pd.cut(df["fav_prob"], [0.5, 0.55, 0.6, 1.0]), observed=True)["favourite_won"].agg(["mean", "count"]).round(2))

Replace YOUR_API_KEY with a key from your dashboard. Python needs requests and pandas (pip install requests pandas). JavaScript runs on Node 18 or newer, or in the browser.

What comes back

The data object of the response, trimmed to the first rows. Every field is real output from the run above.

{
  "games": [
    {
      "game_id": "401815697",
      "season": 2026,
      "season_type": "regular",
      "date": "2026-06-10",
      "starts_at": "2026-06-10T17:10:00.000Z",
      "status": "final",
      "home": {
        "team_id": "30",
        "abbrev": "TB",
        "name": "Tampa Bay Rays",
        "runs": 7,
        "hits": 13,
        "errors": 0
      },
      "away": {
        "team_id": "2",
        "abbrev": "BOS",
        "name": "Boston Red Sox",
        "runs": 5,
        "hits": 6,
        "errors": 0
      },
      "innings": 9,
      "extra_innings": false,
      "venue": "Tropicana Field",
      "city": "St. Petersburg",
      "attendance": 16504,
      "books": [
        {
          "provider": "BetMGM",
          "home_ml_open": -155,
          "home_ml_close": -165,
          "away_ml_open": 125,
          "away_ml_close": 135,
          "home_close_prob": 0.623,
          "away_close_prob": 0.426,
          "home_run_line_open": -1.5,
          "home_run_line_close": -1.5,
          "run_line_home_odds": 135,
          "run_line_away_odds": -160,
          "total_open": 7.5,
          "total_close": 7.5,
          "over_odds": 105,
          "under_odds": -125,
          "captured_at": "2026-06-10T17:00:36.000Z"
        },
        "...7 more"
      ],
      "result": {
        "median_home_ml": -162,
        "median_total": 7.5,
        "favourite_won": true,
        "over": true
      }
    },
    {
      "game_id": "401815700",
      "season": 2026,
      "season_type": "regular",
      "date": "2026-06-10",
      "starts_at": "2026-06-10T17:10:00.000Z",
      "status": "final",
      "home": {
        "team_id": "5",
        "abbrev": "CLE",
        "name": "Cleveland Guardians",
        "runs": 4,
        "hits": 6,
        "errors": 2
      },
      "away": {
        "team_id": "10",
        "abbrev": "NYY",
        "name": "New York Yankees",
        "runs": 8,
        "hits": 11,
        "errors": 1
      },
      "innings": 9,
      "extra_innings": false,
      "venue": "Progressive Field",
      "city": "Cleveland",
      "attendance": 31586,
      "books": [
        {
          "provider": "BetMGM",
          "home_ml_open": -118,
          "home_ml_close": -120,
          "away_ml_open": -102,
          "away_ml_close": 100,
          "home_close_prob": 0.545,
          "away_close_prob": 0.5,
          "home_run_line_open": 1.5,
          "home_run_line_close": -1.5,
          "run_line_home_odds": 165,
          "run_line_away_odds": -220,
          "total_open": 8,
          "total_close": 7.5,
          "over_odds": -118,
          "under_odds": -102,
          "captured_at": "2026-06-10T17:00:36.000Z"
        },
        "...7 more"
      ],
      "result": {
        "median_home_ml": -119,
        "median_total": 7.5,
        "favourite_won": false,
        "over": true
      }
    },
    "...13 more"
  ],
  "count": 120
}

What to do next

  • get_line_movement (10 credits). Snapshots of a game's line before first pitch, to test a rule on the open instead of the close.
  • get_ratings (2 credits). Our team Elo ratings, to set a model's win chance beside the implied probability.

More guides

Start with 500 free credits every month

No card. REST or MCP.