Back Premier League Draws in Games without clear Favourite

Published on April 15, 2020, 1:59 p.m. - Sports: Football

This betting system involves backing the draw in football matches for games without a clear favourite, i.e. both teams have similar odds. We would like to share the Python code that we used to test the strategy as well as the backtesting result.

The idea behind the betting strategy is that odds for the draw in football matches are mispriced (too high) for games where there is no clear favourite. No clear favourite means that both teams have similar odds to win the match. The hypothesis is that in reality such matches are more likely to end in a draw in relation with the odds for the draw. Hence there should be some opportunity for value betting.

The "back the draw betting strategy" was reported on various websites and I recommend you to read the post by D. Sumpter on the pinnacle blog or the article on churchofbetting.com which also includes backtesting results.

Python Code to Test the Strategy

First I started with a Python script to run a quick backtest. The script downloads the relevant data from football-data.co.uk into a pandas dataframe. For each match the probability for the home and away win are derived by taking the inverse of the Pinnacle odds (bookmaker margin is not corrected). Then a back bet is placed on the draw for matches where the difference in probability between home and away win is less than 0.1 (10%). Constant one point staking per match is used:


import numpy as np
import pandas as pd
from typing import Tuple


def get_df(
        seasons: Tuple[str] = ("1213", "1314", "1415", "1516", "1617", "1718", "1819"),
        leagues: Tuple[str] = ("E0", "D1", "I1", "SP1")
) -> pd.DataFrame:
    results = pd.DataFrame()
    for season in seasons:
        for league in leagues:
            df = pd.read_csv(
                f"http://www.football-data.co.uk/mmz4281/{season}/{league}.csv",
                usecols=["HomeTeam", "AwayTeam", "FTHG", "FTAG", "PSH", "PSD", "PSA"],
            )
            df.dropna(inplace=True)
            df["ProbaH"] = 1 / df["PSH"]
            df["ProbaA"] = 1 / df["PSA"]
            df["Season"] = season
            df["League"] = league
            results = pd.concat([results, df], ignore_index=True)
    return results


if __name__ == "__main__":
    df = get_df()
    df["Profit"] = df.apply(
        lambda x: (
            x["PSD"] - 1 if x["FTHG"] == x["FTAG"] else -1
        ) if abs(x["ProbaH"] - x["ProbaA"]) < 0.1 else 0,
        axis=1
    )
    df["Bets"] = np.where(df.profit != 0, 1, 0)
    pt = df.pivot_table(
        index="Season", columns="League", values=["Profit", "Bets"], aggfunc=np.sum
    )

Backtesting Results on Historical Data

The Python script returns a table showing the following results. Number of bets and cumulative profit are shown per season and league (German Bundesliga D1, English Premier League E0, Italian Serie A I1 and Spanish La Liga SP1):

  Bets Profit
League D1 E0 I1 SP1 D1 E0 I1 SP1
Season                
1213 55 48 59 50 11.13 20.60 -3.57 1.81
1314 68 53 61 52 -11.90 -2.00 -15.44 -1.60
1415 66 63 64 54 7.17 11.13 1.62 11.89
1516 62 85 62 55 6.10 28.20 -9.71 4.86
1617 65 52 56 61 3.88 -6.09 -13.63 8.85
1718 70 64 52 63 6.33 15.40 -1.49 1.51
1819 59 71 66 76 -10.28 -15.93 26.44 22.23

Around 60 bets on average were placed per league and season with an average profit of around 3.5 points. For the Premier League the average number of matches is slightly higher with 62 and an average profit of around 7.33 points per season. This aligns well with the original publication in the pinnacle blog where the author claimed that the betting system had nice returns in the season 2015/16 whilst achieving lower yield in the season 2016/17.

The total returns over time for the Premier League are shown in the following backtest:

Bottom line is that the betting strategy had nice returns over the past years. However, recent results are less encouraging with larger losses in the 2018/19 season. Maximum draw down is around 24 points over a time span of 453 days.

Deploying the Strategy as Automated Betting Bot

If you are interested in creating a bot that automatically places bets on the draw for matches with little difference in the winning odds for the two teams then this section might be helpful for you: I will explain how I created a simple basic version of a betting bot and also how it can be deployed to automatically place the bets.

I will focus on Python and the Betfair API to create the betting bot. Adapting it to other programming languages or betting exchanges / bookmakers shouldn't be too complicated if you are familiar with another programming language or betting API.

The first step is a function that "talks" to the API. For this I use the Python requests package to call the Betfair API via HTTP protocol:


import os
import requests
from requests import Response


def call_api(json: dict) -> Response:
    headers = {
        'X-Application': os.environ['BETFAIR_APP_KEY'],
        'X-Authentication': os.environ['BETFAIR_TOKEN'],
        'content-type': 'application/json'
    }
    try:
        r: Response = requests.post(
            "https://api.betfair.com/exchange/betting/json-rpc/v1",
            headers=headers,
            json={
                "jsonrpc": "2.0",
                **json,
                "id": 1
            },
        )
        return r.json().get("result", {})
    except Exception as e:
        print(e)

Please note that Betfair application key and token need to be present in an environment variable. You can then use the call_api function to query football markets from Betfair, get the market book and place an order if the selection qualifies.

To get the relevant betting markets, the listMarketCatalogue endpoint is called. Here I am only interested in Premier League matches and that's why I use the eventTypeId filter to only select football matches (football has eventTypeId 1) in the Premier League (competitionId 10932509). Another filter is used to only select the match odds market:


def list_market_catalogue(max_results: int = 100):
    json = {
        "method": "SportsAPING/v1.0/listMarketCatalogue",
        "params": {
            "filter": {
                "eventTypeIds": [1],
                "competitionIds": [10932509],
                "marketCountries": ["GB"],
                "marketTypeCodes": ["MATCH_ODDS"],
            },
            "sort": "FIRST_TO_START",
            "maxResults": max_results,
            "marketProjection": [
                "MARKET_START_TIME",
                "EVENT_TYPE",
                "EVENT",
                "COMPETITION",
                "RUNNER_METADATA"
            ]
        },
    }
    return call_api(json)

For each market you can then query the market book via the listMarketBook endpoint. Here you just need to specify the market_ids you are interested in. The response contains offers on the back and lay side as well as the lastPriceTraded. This might serve as a criteria to either select the match for a bet or not.


from typing import List


def list_market_book(market_ids: List[str]):
    json = {
        "method": "SportsAPING/v1.0/listMarketBook",
        "params": {
            "marketIds": market_ids,
            "priceProjection": {
                "priceData": ['EX_BEST_OFFERS']
            }
        }
    }
    return call_api(json)

The last function missing is the part that places the order: Market ID, selection ID, bet size and price can be specified as an argument. The method then places a simple exchange bet, order will be lapsed when the market is turned in-play:


def place_order(
        market_id: str,
        selection_id: str,
        size: float,
        price: float
):
    json = {
        "method": "SportsAPING/v1.0/placeOrders",
        "params": {
            "marketId": market_id,
            "instructions": {
                "orderType": "LIMIT",
                "selectionId": selection_id,
                "side": "BACK",
                "limitOrder": {
                    "size": size,
                    "price": price,
                    "persistenceType": "LAPSE",
                }
            },
        }
    }
    return call_api(json)

The last missing bit is some logic that puts all the building blocks together. Markets are pulled from the API via list_market_catalogue. For each market the market book is obtained and a bet is only placed if the difference in lastPriceTraded is smaller than a certain threshold:


if __name__ == "__main__":
    threshold = 0.3
    markets = list_market_catalogue()
    for market in markets:
        market_book = list_market_book([market.get("marketId")])[0]
        if abs(
                market_book.get("runners")[0].get("lastPriceTraded") - market_book.get("runners")[2].get("lastPriceTraded")
        ) < threshold:
            place_order(
                market.get("marketId"), market_book.get("runners")[1].get("selectionId"), 
                1.0,
                market.get("marketId"), market_book.get("runners")[1].get("lastPriceTraded"),
            )

Please note that this is a very basic implementation for a betting bot and I would put much more work into it before using it. Various parts still need improvement such as filtering matches by market start time, avoid duplicate orders via customer reference, adding logging and proper error handling to just name a few.

Instead of maintaining your own Betfair client you might also have a look at betfairlightweight or other Betfair clients that could help you getting started with betting or trading via API.

Once you have the complete betting script you can then go ahead and deploy it on a server. We will shortly provide a blog post on how we deploy betting bots in production with more details.

Do you like our content? Please share with your friends!

Share on Facebook Share on Twitter

Comments

No comments published yet.

Please log in to leave a comment.

Similar Strategies
See all Strategies!
Any Questions or Suggestions?

If you would like to learn more about this strategy, please do not hesitate to contact us.

Contact Us!