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.

Once a bettor has found the value bets, execution places them. A venue with an official betting API is placed at by the library. A bookmaker with no API is driven in a browser by your agent, on your own account, with the risk of that stated plainly.

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]'

To place bets as well as find them, add the execution extra. Driving a bookmaker's website also needs a browser:

pip install 'sports_betting[mcp,execution]'
python -m playwright install chromium

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.

You: Say the season is on and there are value bets. How would you actually place them?

Claude: By quoting first and staking only once you have seen the quote. The leagues are in their summer break, so I will show it on two example draws rather than invent live prices. Here is the quote for a £50 stake on each:

total_stake 100.0 | total_exposure 100.0

Running it now, the way you would by default, places nothing:

             match market selection  status  stake  detail
Juventus vs Napoli   draw  Juventus dry_run    0.0  Nothing was staked. To place these bets, pass back the quoted stake of 100.0 and the quoted exposure of 100.0.
     Roma vs Lazio   draw      Roma dry_run    0.0  Nothing was staked. To place these bets, pass back the quoted stake of 100.0 and the quoted exposure of 100.0.

A dry run is not a flag I remembered to set. It is what happens until I pass the quoted figures back exactly. If I pass the wrong ones, it still stakes nothing and tells me the real ones:

             status  detail
refused_unconfirmed  Nothing was staked. The quoted stake is 100.0 and the quoted exposure is 100.0, but 10.0 and 10.0 were passed back.
refused_unconfirmed  Nothing was staked. The quoted stake is 100.0 and the quoted exposure is 100.0, but 10.0 and 10.0 were passed back.

Only the quoted figures place the bets, one at a time, each with a receipt:

             match market selection       status  stake  price
Juventus vs Napoli   draw  Juventus matched_full   50.0   3.10
     Roma vs Lazio   draw      Roma matched_full   50.0   3.25

That was a venue with an official API, which I place at for you. A bookmaker with no API is different: I drive its website on your account, which breaches essentially every bookmaker's terms of service and risks the account and the balance in it. I read the page as something I can act on, and I refuse to click a confirm button the site has disabled rather than pretend the bet went on:

- form "Bet slip" [ref=e2]:
  - textbox "Stake" [ref=e3]: "10.00"
  - button "Place bet" [ref=e4]
  - button "Confirm bet" [disabled] [ref=e5]

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. Placing spends real money, so it refuses until you confirm the quote, and it never evades a venue that blocks automation.

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.15.0.tar.gz (246.4 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.15.0-py3-none-any.whl (156.5 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for sports_betting-0.15.0.tar.gz
Algorithm Hash digest
SHA256 5653c65a8231e3147b06f373a728528c398d9f7362447da4bc63e19a806a4895
MD5 35313ea8fb052c30daa3d50137b4cd22
BLAKE2b-256 413e4e20d377af7089a4796ec6cd1b7ec1f049a3b5cf1a058cb22ac31cc9a03e

See more details on using hashes here.

Provenance

The following attestation bundles were made for sports_betting-0.15.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.15.0-py3-none-any.whl.

File metadata

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

File hashes

Hashes for sports_betting-0.15.0-py3-none-any.whl
Algorithm Hash digest
SHA256 853dc194b2236701da64a61445843af40182b61522c9b0fd882e7c27a446dcd8
MD5 a65c6e81580699f85904ffebe66cef22
BLAKE2b-256 389c43fd28ea3f18178b340388ea4e75228bf7fca67f2569b767d04c92742478

See more details on using hashes here.

Provenance

The following attestation bundles were made for sports_betting-0.15.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