Skip to main content

sports-betting

ci doc

Category Tools
Development black ruff mypy docformatter
Testing pytest coverage interrogate
Security safety bandit
Automation nox pre-commit
Package version pythonversion downloads
Documentation mkdocs
Communication discussions

Introduction

sports-betting is a set of tools for creating, testing and using sports betting models. You can use it from an AI agent, from Python, or from the command line.

Its two main components are dataloaders and bettors:

  • A dataloader downloads the data and shapes it for modelling. It is built from a statistics source and an odds source. You choose both, so you always know where your data came from.
  • A bettor backtests a betting strategy and predicts the value bets of upcoming events. It wraps any scikit-learn estimator.

Installation

For user installation, sports-betting is currently available on the PyPi's repository, and you can install it via pip:

pip install sports_betting

To drive the library from an AI agent, install the MCP server:

pip install 'sports_betting[mcp]'

Development installation requires to clone the repository and then use PDM to install the project as well as the main and development dependencies:

git clone https://github.com/georgedouzas/sports-betting.git
cd sports-betting
pdm install

Quick start

AI agent

The agent is a first-class way into the library. It reaches everything Python and the command line reach, and it does more besides. It explores the data, tables the results, plots them, writes the model, and does anything else you ask. A betting model is a scikit-learn estimator, and an agent can write one, run it, and tell you whether it was any good.

pip install 'sports_betting[mcp]'
claude mcp add sportsbet -- sportsbet-mcp

What follows is a chat with Claude, using the library through the MCP server, with the real output.

You: What soccer data can I work with?

Claude: 918 league, division and season combinations across 27 leagues, from 1994 to 2026.

England goes deepest, with five divisions. Germany, Italy, France, Spain, Scotland, the Netherlands, Belgium, Portugal, Turkey and Greece have one or two each, and there are single-division feeds for another sixteen countries, from Argentina to the USA.

Every one comes with historical odds from several bookmakers, so a backtest runs against real prices. I have not downloaded anything, since reading the catalogue is free.

You: Find me a strategy that works on the top two divisions of Germany, Italy and France, 2021 to 2024.

Claude: Downloaded 24 season files. Then I searched, rather than guessing.

First, which market is even worth betting. A logistic model on each, at the best odds in the market:

Market Yield per bet Bets per fold
home_win −3.8% 901
draw +3.9% 1,319
away_win −3.6% 1,217
over_2.5 +0.3% 642
under_2.5 −2.2% 702

Only the draw pays. That is not a surprise: the draw is the outcome punters least like backing, so the price drifts.

Then, whether the market is worth searching over. The markets to bet on are a hyperparameter like any other, so I put them in the grid alongside the model's regularisation and let the search pick:

BettorGridSearchCV(
    estimator=ClassifierBettor(classifier, init_cash=10000.0, stake=50.0),
    param_grid={
        'classifier__multioutputclassifier__estimator__C': [0.1, 1.0, 50.0],
        'betting_markets': [['home_win'], ['draw'], ['away_win'], ['home_win', 'draw', 'away_win']],
    },
    cv=TimeSeriesSplit(3),
)

Backtested over five time-ordered folds:

Testing period Bets Yield per bet Final cash
Feb 2021 to Nov 2021 1,407 +1.0% £10,715
Nov 2021 to May 2022 1,406 +8.3% £15,812
May 2022 to Feb 2023 1,406 −0.5% £9,675
Feb 2023 to Nov 2023 1,406 +4.5% £13,161
Nov 2023 to Jun 2024 1,407 −0.2% £9,858

Mean yield +2.6%, positive in three folds of five. Better than the fixed model, which managed +1.2%.

Worth being honest about: I have now tried a dozen configurations on this data. Some of that +2.6% is me searching, not the model knowing. Before you stake anything, hold a season back and check it there.

You: Which league is that draw edge actually in?

Claude: Almost entirely Italy.

League Yield per bet on the draw
Italy +9.0%
Germany +2.9%
France −0.6%

That is a big number and I would treat it with suspicion rather than excitement. It is one league over four seasons, found after I had already looked at several splits.

You: Write me the Italy draw model, then, and show me the folds.

Claude: Here it is, a logistic regression on the two Italian divisions, betting only the draw:

from sklearn.compose import make_column_transformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.multioutput import MultiOutputClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from sportsbet.evaluation import ClassifierBettor

classifier = make_pipeline(
    make_column_transformer(
        (OneHotEncoder(handle_unknown='ignore'), ['league', 'home_team', 'away_team']), remainder='passthrough'
    ),
    SimpleImputer(),
    MultiOutputClassifier(LogisticRegression(solver='liblinear', class_weight='balanced')),
)
bettor = ClassifierBettor(classifier, betting_markets=['draw'], init_cash=10000.0, stake=50.0)
Testing period Bets Yield per bet Final cash
Mar 2021 to Nov 2021 440 +1.7% £10,365
Nov 2021 to Aug 2022 461 +12.7% £12,934
Aug 2022 to Mar 2023 487 +4.5% £11,097
Mar 2023 to Nov 2023 470 +8.9% £12,085
Nov 2023 to Jun 2024 485 +17.2% £14,171

Positive in all five folds, mean +9.0%. That is the kind of result I would want to distrust hardest. I found it by narrowing to the league that already looked good. Give me a season I have never touched and I will tell you if it holds.

You: Fine. Any value bets in the upcoming fixtures?

Claude: None right now. extract_fixtures_data comes back empty for Germany, Italy and France: their seasons are in the summer break, so there is no upcoming match to price.

The mechanism is worth seeing. param_grid chose the seasons to train on, and those are all played. The fixtures come from a separate download, the current season of the leagues you selected, so training and fixtures never overlap.

dataloader = DataLoader(
    param_grid={'league': ['Germany', 'Italy', 'France'], 'division': [1, 2], 'year': [2021, 2022, 2023, 2024]},
    stats=FootballDataStats(), odds=FootballDataOdds(),
)
X, Y, O = dataloader.extract_train_data(odds_type='market_maximum')   # the seasons above, to train on
X_fix, _, O_fix = dataloader.extract_fixtures_data()                  # the current season's upcoming matches

They share their columns, so when the season restarts the model trained on the history bets on the fixtures. To bet on another league, select it. Given what we found I would hold off staking: the edge is Italy alone, four seasons found after a dozen tries. Hold a season back and check it there first.

The agent names the environment variable holding your API key, never the key itself. Extracting the data downloads it, so a paid odds feed spends only when you extract. Do it once and save the dataloader rather than re-extracting.

Python API

The same functionality, in code. A dataloader is created by a statistics source and an odds source:

from sportsbet.dataloaders import DataLoader
from sportsbet.sources import FootballDataOdds, FootballDataStats

dataloader = DataLoader(
    param_grid={'league': ['Germany', 'Italy', 'France'], 'division': [1, 2], 'year': [2021, 2022, 2023, 2024]},
    stats=FootballDataStats(),
    odds=FootballDataOdds(),
)
X_train, Y_train, O_train = dataloader.extract_train_data(odds_type='market_maximum')
X_fix, _, O_fix = dataloader.extract_fixtures_data()

A betting model is any scikit-learn estimator wrapped in a bettor. Here we backtest a bettor that wraps a logistic regression classifier:

from sklearn.compose import make_column_transformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import TimeSeriesSplit
from sklearn.multioutput import MultiOutputClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from sportsbet.evaluation import ClassifierBettor, backtest

classifier = make_pipeline(
    make_column_transformer(
        (OneHotEncoder(handle_unknown='ignore'), ['league', 'home_team', 'away_team']), remainder='passthrough'
    ),
    SimpleImputer(),
    MultiOutputClassifier(LogisticRegression(solver='liblinear', random_state=7, class_weight='balanced')),
)
bettor = ClassifierBettor(classifier, betting_markets=['draw'], init_cash=10000.0, stake=50.0)
backtest(bettor, X_train, Y_train, O_train, cv=TimeSeriesSplit(5))
                                                       Number of betting days  Number of bets  Yield percentage per bet  ROI percentage  Final cash
Training start Training end Testing start Testing end
2020-08-21     2021-02-27   2021-02-27    2021-11-06                     625            1247                       3.6            22.3     12234.0
               2021-11-06   2021-11-06    2022-05-14                     645            1305                       3.4            22.0     12205.0
               2022-05-14   2022-05-15    2023-02-25                     639            1349                      -2.3           -15.8      8420.0
               2023-02-25   2023-02-26    2023-11-06                     637            1344                       6.7            44.9     14491.5
               2023-11-06   2023-11-06    2024-06-02                     659            1348                       8.0            54.1     15409.5

Fit it with bettor.fit(X_train, Y_train, O_train), then bettor.bet(X_fix, O_fix) returns the value bets of the upcoming matches.

The fixtures are downloaded separately by extract_fixtures_data, which returns the upcoming betting events. The training and fixtures data share their columns, so the model trained on the history can bet on the fixtures.

CLI

The same scenario from the command line, which mirrors the API. dataloader extracts the data and evaluation works a model on it. dataloader train extract downloads the seasons once and saves the dataloader. The evaluation commands read that file, so the data is downloaded once and the model is trained once.

# Download the seasons once and save the dataloader
sportsbet dataloader train extract --stats football-data --odds football-data \
  --league Germany --league Italy --league France --division 1 --division 2 \
  --year 2021 --year 2022 --year 2023 --year 2024 \
  --odds-type market_maximum -o dataloader.pkl

# Backtest a model on the saved data
sportsbet evaluation backtest --dataloader dataloader.pkl --model logistic \
  --betting-market home_win --betting-market draw --betting-market away_win \
  --init-cash 10000 --stake 50 --cv 5

# Fit the model once and save it
sportsbet evaluation fit --dataloader dataloader.pkl --model logistic \
  --betting-market home_win --betting-market draw --betting-market away_win \
  -o model.pkl

# Value bets for the upcoming matches, through the fitted model
sportsbet evaluation bet --dataloader dataloader.pkl --bettor model.pkl

The last command prints the value bets of the upcoming matches. --stats and --odds say where the data comes from, and dataloader train extract downloads it. The ready-made models cover the common cases. A model of your own is Python, so you write it in a file and name it, as in --model models.py:bettor, where bettor is the object below.

# models.py
from sklearn.compose import make_column_transformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.multioutput import MultiOutputClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from sportsbet.evaluation import ClassifierBettor

classifier = make_pipeline(
    make_column_transformer(
        (OneHotEncoder(handle_unknown='ignore'), ['league', 'home_team', 'away_team']), remainder='passthrough'
    ),
    SimpleImputer(),
    MultiOutputClassifier(LogisticRegression(solver='liblinear', random_state=7, class_weight='balanced')),
)
bettor = ClassifierBettor(classifier, betting_markets=['draw'], init_cash=10000.0, stake=50.0)

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sports_betting-0.14.0.tar.gz (176.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sports_betting-0.14.0-py3-none-any.whl (144.8 kB view details)

Uploaded Python 3

File details

Details for the file sports_betting-0.14.0.tar.gz.

File metadata

  • Download URL: sports_betting-0.14.0.tar.gz
  • Upload date:
  • Size: 176.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sports_betting-0.14.0.tar.gz
Algorithm Hash digest
SHA256 1a09ce228e69da4e3a7e9e623c3624f13bd4554114c234069d4ac29debb67119
MD5 2e6ac3d6e26a89129786ccd69f4ce3c7
BLAKE2b-256 469af0590ddb1256a2f28ffd6a1a2ee2250b3f74d8f7e088040155198c80c3cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for sports_betting-0.14.0.tar.gz:

Publisher: release.yml on georgedouzas/sports-betting

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sports_betting-0.14.0-py3-none-any.whl.

File metadata

  • Download URL: sports_betting-0.14.0-py3-none-any.whl
  • Upload date:
  • Size: 144.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sports_betting-0.14.0-py3-none-any.whl
Algorithm Hash digest
SHA256 64c0a4f56b95a5fc623a972d25b46f134294faadaec7b0b0b046d7eefa3984f8
MD5 a58d0d277e6a5e0224da2c7f5b7215e8
BLAKE2b-256 5515f4317e565ad4ac4c4d4618a1777b872f204254561ef70d39046170bf87bb

See more details on using hashes here.

Provenance

The following attestation bundles were made for sports_betting-0.14.0-py3-none-any.whl:

Publisher: release.yml on georgedouzas/sports-betting

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page