Build a football dashboard in Python
A guided path from the authenticated client to three charts, with short copyable code at every step and the full script to download.
1 Final result
Build a football dashboard with the upcoming matches, the available markets and the verified history of your competition.
You will produce, locally, three views from a single competition:
- a table of 1X2 probabilities with the main pick and its confidence;
- the BTTS, Over/Under, double chance and DNB markets when the response contains them;
- verified history with the cumulative success of the pick published before kickoff.
The main path uses only two API calls when FORES_LEAGUE_CODE is set: one for upcoming matches, one for history.
2 Prerequisites
Install the official SDK and matplotlib. foresportia is the official Python SDK for the API; matplotlib is only used for the local visualizations.
python -m pip install --upgrade foresportia matplotlib3 Add the API key
Windows PowerShell (current session):
$env:FORES_API_KEY = "your_key"
$env:FORES_LEAGUE_CODE = "SUE"Persistent Windows variable:
[Environment]::SetEnvironmentVariable(
"FORES_API_KEY",
"your_key",
"User"
)macOS / Linux:
export FORES_API_KEY="your_key"
export FORES_LEAGUE_CODE="SUE"4 Initialize the client
ForesportiaClient.from_env() reads the key from the environment. The SDK handles authentication, typed responses, quotas and HTTP errors.
from foresportia import ForesportiaClient
client = ForesportiaClient.from_env()5 Choose the competition
With a Developer key, a single competition is tied to the key. With Starter, several competitions may be accessible. The function below uses FORES_LEAGUE_CODE when set, otherwise detects the only accessible competition with list_leagues().
def choose_league(client: ForesportiaClient) -> str:
"""Use FORES_LEAGUE_CODE or detect the only accessible competition."""
if LEAGUE_CODE:
return LEAGUE_CODE.upper()
leagues = client.list_leagues().data
if len(leagues) == 1:
return leagues[0].code
if not leagues:
raise RuntimeError("No competition is accessible with this key.")
codes = ", ".join(league.code for league in leagues)
raise RuntimeError(
"Several competitions are accessible. "
f"Set FORES_LEAGUE_CODE to one of: {codes}"
)6 Load the upcoming matches
A single call returns the upcoming matches of the competition. Here UPCOMING_DAYS is 14 and UPCOMING_LIMIT is 5.
upcoming_response = client.list_league_matches(
league_code,
include="upcoming",
days=UPCOMING_DAYS,
limit=UPCOMING_LIMIT,
)Each match exposes the fields the dashboard needs:
match.probabilities
match.pick
match.markets
match.confidence7 Load the history
The dedicated list_league_history() method returns the verified history of the competition. The window adapts automatically to the contract.
history_response = client.list_league_history(league_code)
history = history_response.data- Developer: 7 days; Starter: up to 31 days per request over a 90-day entitlement.
- A period can legitimately be empty: that is a normal response, not an error.
- Probabilities and pick are those published before kickoff.
- status="final" and result_score provide the verified result.
8 Evaluate the 1X2 pick
Two short functions are enough: actual_outcome() turns a final score into home/draw/away, and pick_is_correct() compares the published pick with the verified result.
def actual_outcome(score: str | None) -> str | None:
"""Turn a final score such as '2-1' into home/draw/away."""
if not score:
return None
try:
home, away = map(int, score.split("-"))
except ValueError:
return None
return "home" if home > away else "away" if away > home else "draw"
def pick_is_correct(match: MatchSummary) -> bool | None:
"""Compare the published 1X2 pick with the verified final score."""
actual = actual_outcome(match.result_score) if match.is_final else None
predicted = match.predicted_outcome
return None if actual is None or predicted is None else actual == predicted9 Build the three charts
1X2 probabilities
Goal: read at a glance the home / draw / away split, the main pick and the confidence.
Data: match.probabilities, match.pick, match.predicted_outcome and match.confidence from the upcoming matches.
def plot_upcoming(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
"""1X2 probabilities, main pick and confidence."""
figure, axis = plt.subplots(figsize=(13, max(4.8, len(matches) * 0.9)))
if not matches:
axis.axis("off")
axis.text(0.5, 0.5, "No upcoming published match.", ha="center", va="center")
return figure
y = list(range(len(matches)))
home = [m.probabilities["home"] * 100 for m in matches]
draw = [m.probabilities["draw"] * 100 for m in matches]
away = [m.probabilities["away"] * 100 for m in matches]
axis.barh(y, home, label="1 · Home")
axis.barh(y, draw, left=home, label="X · Draw")
axis.barh(y, away, left=[h + d for h, d in zip(home, draw)], label="2 · Away")
axis.set_yticks(y, [f"{date_label(m)} · {m.home_team} – {m.away_team}" for m in matches])
axis.set_xlim(0, 140)
axis.set_xticks((0, 20, 40, 60, 80, 100))
axis.set_xlabel("1X2 probability (%)")
axis.set_title(f"{league_name} — upcoming matches")
axis.grid(axis="x", alpha=0.25)
axis.invert_yaxis()
axis.legend(ncol=3, loc="upper center", bbox_to_anchor=(0.42, -0.12))
for index, match in enumerate(matches):
probability = (match.pick or {}).get("probability")
probability_text = f"{probability:.0%}" if isinstance(probability, (int, float)) else ""
axis.text(
102,
index,
f"★ {match.predicted_outcome or 'no pick'} {probability_text} · {confidence_text(match)}",
va="center",
fontsize=9,
)
figure.tight_layout()
return figure
Reading: a long home bar signals a clear favorite; the starred pick recalls the published choice and its confidence.
Available markets
Goal: see, as a matrix, the markets actually present in the response.
Data: match.markets, filtered to the keys actually returned (BTTS, Over/Under, double chance, DNB).
def plot_markets(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
"""Matrix of the markets actually present in the response."""
available = {key for match in matches for key in (match.markets or {})}
columns = [(key, label) for key, label in MARKETS if key in available]
figure, axis = plt.subplots(
figsize=(max(10, len(columns) * 1.4), max(4.8, len(matches) * 0.9))
)
if not matches or not columns:
axis.axis("off")
axis.text(0.5, 0.5, "No additional market available.", ha="center", va="center")
return figure
matrix = [[(m.markets or {}).get(key, float("nan")) for key, _ in columns] for m in matches]
image = axis.imshow(matrix, aspect="auto", vmin=0, vmax=1)
axis.set_xticks(range(len(columns)), [label for _, label in columns])
axis.set_yticks(range(len(matches)), [f"{m.home_team} – {m.away_team}" for m in matches])
axis.set_title(f"{league_name} — available markets")
for row_index, row in enumerate(matrix):
for column_index, probability in enumerate(row):
if probability == probability: # NaN-safe
axis.text(column_index, row_index, f"{probability:.0%}", ha="center", va="center")
figure.colorbar(image, ax=axis, label="Probability")
figure.tight_layout()
return figure
Reading: an empty cell means the market is not provided for that match, not that it is zero.
Verified history
Goal: track the cumulative success of the pick over matches already played.
Data: the history returned by list_league_history(), limited to matches with a verified result.
def plot_history(league_name: str, history: list[MatchSummary]) -> plt.Figure:
"""Cumulative success curve with result and confidence."""
decided = [match for match in history if pick_is_correct(match) is not None]
figure, axis = plt.subplots(figsize=(12, 6))
if not decided:
axis.axis("off")
axis.text(0.5, 0.5, "No verified result in the history window.", ha="center", va="center")
return figure
successes = 0
cumulative = []
for index, match in enumerate(decided, start=1):
successes += int(pick_is_correct(match) is True)
cumulative.append(successes / index)
x = list(range(1, len(decided) + 1))
axis.plot(x, cumulative, marker="o")
axis.axhline(0.5, linestyle="--", linewidth=1)
axis.set_ylim(0, 1)
axis.set_xlabel("Historical match")
axis.set_ylabel("Cumulative success rate")
axis.set_title(
f"{league_name} — history: {successes}/{len(decided)} correct "
f"({cumulative[-1]:.1%})"
)
axis.grid(alpha=0.25)
for index, match in enumerate(decided):
verdict = "✓" if pick_is_correct(match) else "✗"
axis.annotate(
f"{verdict} {confidence_text(match)}\n{match.result_score}",
(x[index], cumulative[index]),
xytext=(0, 10),
textcoords="offset points",
ha="center",
fontsize=8,
)
if len(decided) < 20:
axis.text(0.01, 0.03, "Small sample: descriptive reading only.", transform=axis.transAxes)
figure.tight_layout()
return figure
Reading: the curve is descriptive. On a small sample it does not prove general performance.
10 Run the script
Run the script from the folder that contains it. SAVE_PNG controls image saving: when True, the three charts are written to the foresportia_charts folder.
python foresportia_tutorial.py11 Full code
The block below is the entire content of the canonical file, identical to the downloadable script. Copy it or download the .py.
Show the full code
#!/usr/bin/env python3
"""Foresportia tutorial: upcoming matches, markets and verified history.
This script is the canonical source used by the Foresportia documentation
tutorial at /api/docs/tutorials/football-dashboard.html. The page code blocks
and the downloadable file are both generated from this exact file, so they
never diverge. The ``# region`` / ``# endregion`` markers are non-functional
comments used by the documentation generator to slice out each step.
Install:
python -m pip install --upgrade foresportia matplotlib
API key — Windows PowerShell:
$env:FORES_API_KEY = "your_key" # current session
[Environment]::SetEnvironmentVariable(
"FORES_API_KEY", "your_key", "User"
) # persistent
API key — macOS / Linux:
export FORES_API_KEY="your_key" # current session
echo 'export FORES_API_KEY="your_key"' >> ~/.zshrc # macOS/zsh
echo 'export FORES_API_KEY="your_key"' >> ~/.bashrc # Linux/bash
To keep the main path at two API calls, also set the competition:
$env:FORES_LEAGUE_CODE = "SUE" # Windows PowerShell
export FORES_LEAGUE_CODE="SUE" # macOS / Linux
Without FORES_LEAGUE_CODE, a single-competition Developer key can be detected
with list_leagues(), at the cost of one extra request.
Run:
python foresportia_tutorial.py
"""
from __future__ import annotations
import os
from datetime import datetime
from pathlib import Path
import matplotlib.pyplot as plt
from foresportia import ForesportiaAPIError, ForesportiaClient, ForesportiaRateLimitError
from foresportia.models import MatchSummary
LEAGUE_CODE = os.getenv("FORES_LEAGUE_CODE")
UPCOMING_DAYS = 14
UPCOMING_LIMIT = 5
SAVE_PNG = False
OUTPUT_DIR = Path("foresportia_charts")
MARKETS = (
("btts", "BTTS"),
("over_2_5", "Over 2.5"),
("under_2_5", "Under 2.5"),
("double_chance_1x", "1X"),
("double_chance_x2", "X2"),
("double_chance_12", "12"),
("dnb_home", "DNB 1"),
("dnb_away", "DNB 2"),
)
# region choose-league
def choose_league(client: ForesportiaClient) -> str:
"""Use FORES_LEAGUE_CODE or detect the only accessible competition."""
if LEAGUE_CODE:
return LEAGUE_CODE.upper()
leagues = client.list_leagues().data
if len(leagues) == 1:
return leagues[0].code
if not leagues:
raise RuntimeError("No competition is accessible with this key.")
codes = ", ".join(league.code for league in leagues)
raise RuntimeError(
"Several competitions are accessible. "
f"Set FORES_LEAGUE_CODE to one of: {codes}"
)
# endregion choose-league
# region evaluate
def actual_outcome(score: str | None) -> str | None:
"""Turn a final score such as '2-1' into home/draw/away."""
if not score:
return None
try:
home, away = map(int, score.split("-"))
except ValueError:
return None
return "home" if home > away else "away" if away > home else "draw"
def pick_is_correct(match: MatchSummary) -> bool | None:
"""Compare the published 1X2 pick with the verified final score."""
actual = actual_outcome(match.result_score) if match.is_final else None
predicted = match.predicted_outcome
return None if actual is None or predicted is None else actual == predicted
# endregion evaluate
def confidence_text(match: MatchSummary) -> str:
"""Show the badge when present, otherwise the confidence score."""
confidence = match.confidence or {}
if confidence.get("label"):
return str(confidence["label"])
if confidence.get("badge"):
return str(confidence["badge"])
score = confidence.get("score")
return f"confidence {score:.2f}" if isinstance(score, (int, float)) else "not provided"
def date_label(match: MatchSummary) -> str:
kickoff = match.kickoff_local or match.kickoff
try:
return datetime.fromisoformat(kickoff.replace("Z", "+00:00")).strftime("%d/%m")
except (AttributeError, ValueError):
return "--/--"
# region plot-upcoming
def plot_upcoming(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
"""1X2 probabilities, main pick and confidence."""
figure, axis = plt.subplots(figsize=(13, max(4.8, len(matches) * 0.9)))
if not matches:
axis.axis("off")
axis.text(0.5, 0.5, "No upcoming published match.", ha="center", va="center")
return figure
y = list(range(len(matches)))
home = [m.probabilities["home"] * 100 for m in matches]
draw = [m.probabilities["draw"] * 100 for m in matches]
away = [m.probabilities["away"] * 100 for m in matches]
axis.barh(y, home, label="1 · Home")
axis.barh(y, draw, left=home, label="X · Draw")
axis.barh(y, away, left=[h + d for h, d in zip(home, draw)], label="2 · Away")
axis.set_yticks(y, [f"{date_label(m)} · {m.home_team} – {m.away_team}" for m in matches])
axis.set_xlim(0, 140)
axis.set_xticks((0, 20, 40, 60, 80, 100))
axis.set_xlabel("1X2 probability (%)")
axis.set_title(f"{league_name} — upcoming matches")
axis.grid(axis="x", alpha=0.25)
axis.invert_yaxis()
axis.legend(ncol=3, loc="upper center", bbox_to_anchor=(0.42, -0.12))
for index, match in enumerate(matches):
probability = (match.pick or {}).get("probability")
probability_text = f"{probability:.0%}" if isinstance(probability, (int, float)) else ""
axis.text(
102,
index,
f"★ {match.predicted_outcome or 'no pick'} {probability_text} · {confidence_text(match)}",
va="center",
fontsize=9,
)
figure.tight_layout()
return figure
# endregion plot-upcoming
# region plot-markets
def plot_markets(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
"""Matrix of the markets actually present in the response."""
available = {key for match in matches for key in (match.markets or {})}
columns = [(key, label) for key, label in MARKETS if key in available]
figure, axis = plt.subplots(
figsize=(max(10, len(columns) * 1.4), max(4.8, len(matches) * 0.9))
)
if not matches or not columns:
axis.axis("off")
axis.text(0.5, 0.5, "No additional market available.", ha="center", va="center")
return figure
matrix = [[(m.markets or {}).get(key, float("nan")) for key, _ in columns] for m in matches]
image = axis.imshow(matrix, aspect="auto", vmin=0, vmax=1)
axis.set_xticks(range(len(columns)), [label for _, label in columns])
axis.set_yticks(range(len(matches)), [f"{m.home_team} – {m.away_team}" for m in matches])
axis.set_title(f"{league_name} — available markets")
for row_index, row in enumerate(matrix):
for column_index, probability in enumerate(row):
if probability == probability: # NaN-safe
axis.text(column_index, row_index, f"{probability:.0%}", ha="center", va="center")
figure.colorbar(image, ax=axis, label="Probability")
figure.tight_layout()
return figure
# endregion plot-markets
# region plot-history
def plot_history(league_name: str, history: list[MatchSummary]) -> plt.Figure:
"""Cumulative success curve with result and confidence."""
decided = [match for match in history if pick_is_correct(match) is not None]
figure, axis = plt.subplots(figsize=(12, 6))
if not decided:
axis.axis("off")
axis.text(0.5, 0.5, "No verified result in the history window.", ha="center", va="center")
return figure
successes = 0
cumulative = []
for index, match in enumerate(decided, start=1):
successes += int(pick_is_correct(match) is True)
cumulative.append(successes / index)
x = list(range(1, len(decided) + 1))
axis.plot(x, cumulative, marker="o")
axis.axhline(0.5, linestyle="--", linewidth=1)
axis.set_ylim(0, 1)
axis.set_xlabel("Historical match")
axis.set_ylabel("Cumulative success rate")
axis.set_title(
f"{league_name} — history: {successes}/{len(decided)} correct "
f"({cumulative[-1]:.1%})"
)
axis.grid(alpha=0.25)
for index, match in enumerate(decided):
verdict = "✓" if pick_is_correct(match) else "✗"
axis.annotate(
f"{verdict} {confidence_text(match)}\n{match.result_score}",
(x[index], cumulative[index]),
xytext=(0, 10),
textcoords="offset points",
ha="center",
fontsize=8,
)
if len(decided) < 20:
axis.text(0.01, 0.03, "Small sample: descriptive reading only.", transform=axis.transAxes)
figure.tight_layout()
return figure
# endregion plot-history
def main() -> int:
try:
with ForesportiaClient.from_env(timeout=20.0, max_retries=0) as client:
league_code = choose_league(client)
# region upcoming-call
upcoming_response = client.list_league_matches(
league_code,
include="upcoming",
days=UPCOMING_DAYS,
limit=UPCOMING_LIMIT,
)
# endregion upcoming-call
# region history-call
history_response = client.list_league_history(league_code)
# endregion history-call
upcoming = upcoming_response.data[:UPCOMING_LIMIT]
history = history_response.data
reference = upcoming[0] if upcoming else history[0] if history else None
league_name = f"{reference.league.name} ({reference.league.code})" if reference else league_code
decided = [match for match in history if pick_is_correct(match) is not None]
successes = sum(pick_is_correct(match) is True for match in decided)
print(f"\n{league_name}")
print(f"Upcoming matches: {len(upcoming)}")
print(f"History received: {len(history)}")
if decided:
print(f"Success: {successes}/{len(decided)} ({successes / len(decided):.1%})")
print("History window:", history_response.payload.get("date_range"))
print("Entitlement:", history_response.history_entitlement_days, "day(s)")
figures = [
("01_upcoming_1x2.png", plot_upcoming(league_name, upcoming)),
("02_markets.png", plot_markets(league_name, upcoming)),
("03_history.png", plot_history(league_name, history)),
]
if SAVE_PNG:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
for filename, figure in figures:
path = OUTPUT_DIR / filename
figure.savefig(path, dpi=170, bbox_inches="tight", pad_inches=0.2)
print("Saved:", path.resolve())
plt.show()
return 0
except ForesportiaRateLimitError as error:
print("Temporary quota reached. Retry-After:", error.retry_after)
return 2
except ForesportiaAPIError as error:
print("API error:", error.status_code, error.error_code, error.endpoint)
return 1
except RuntimeError as error:
print("Error:", error)
return 1
if __name__ == "__main__":
raise SystemExit(main())
Create your Developer key
Developer is free and uses the same endpoints as Starter. Activate a key, set FORES_API_KEY, then run the script again.