Skip to content

How to Automate III: Betfair Data Scientists Models


This tutorial is Part three of the How to Automate series and follows on logically from the How to Automate II tutorial we shared previously. If you're still new to Flumine we suggest you take a look at part one and part two before diving into this one. The overall goal of the How to Automate series was to learn how to use Flumine to Automate our own Betting Model. So far we have covered how Flumine works, lets now put it into action with one of Betfair's own data science models which will give us a solid foundation for part four where we automate our own model.

As always please reach out with feedback, suggestions or queries, or feel free to submit a pull request if you catch some bugs or have other improvements!


Context

Betfair’s own Data Scientists have created a few inhouse models which produce ratings that you can use as racing and sporting tips. In our previous monthly meet ups, we learnt how to Backtest these models ourselves, now we will learn how actually automate those models! We will be automating the thoroughbred ratings model and the greyhound's model, but you can easily apply this to any model and if you have a specific angle you want to automate, you can just copy & paste our code and modify it to your liking!


Scrape Today's Model Ratings

Let's quickly scrape the ratings that we want to automate, all of the Betfair Data Science models are found on the Betfair Hub Models page. If we follow the links to the Horse Ratings Model and the Greyhounds Ratings Model we find that URL links behind the ratings download buttons have a consistent URL pattern that is easy to scrape.

horse_model

link_start

You end up with a link like this for the horse ratings model:

https://betfair-data-supplier-prod.herokuapp.com/api/widgets/kash-ratings-model/datasets?date=2022-03-09&presenter=RatingsPresenter&csv=true

and one like this for the greyhounds ratings model:

https://betfair-data-supplier-prod.herokuapp.com/api/widgets/iggy-joey/datasets?date=2022-03-09&presenter=RatingsPresenter&csv=true

We can take advantage of this consistency and use some simple python code to scrape all the ratings into a pandas dataframe by simply changing the date in the url.

import pandas as pd
# Thoroughbred model (named the kash-ratings-model)
kash_url_1 = 'https://betfair-data-supplier-prod.herokuapp.com/api/widgets/kash-ratings-model/datasets?date='
kash_url_2 = pd.Timestamp.now().strftime("%Y-%m-%d") # todays date formatted as YYYY-mm-dd
kash_url_3 = '&presenter=RatingsPresenter&csv=true'

kash_url = kash_url_1 + kash_url_2 + kash_url_3
kash_url
'https://betfair-data-supplier-prod.herokuapp.com/api/widgets/kash-ratings-model/datasets?date=2022-02-25&presenter=RatingsPresenter&csv=true'
# Greyhounds model (named the iggy-joey-model)
iggy_url_1 = 'https://betfair-data-supplier-prod.herokuapp.com/api/widgets/iggy-joey/datasets?date='
iggy_url_2 = pd.Timestamp.now().strftime("%Y-%m-%d")
iggy_url_3 = '&presenter=RatingsPresenter&csv=true'

iggy_url = iggy_url_1 + iggy_url_2 + iggy_url_3
iggy_url
'https://betfair-data-supplier-prod.herokuapp.com/api/widgets/iggy-joey/datasets?date=2022-02-25&presenter=RatingsPresenter&csv=true'

Now that we have the URL, that dynamically changes to the current date, let's scrape the data using pandas and do some quick cleaning. We only really need three variables, the market_id so we know what market to bet on, the selection_id to know which horse/dog the rating is referring to and the most important one the rating.

# Download todays thoroughbred ratings
kash_df = pd.read_csv(kash_url)

## Data clearning
# Rename Columns
kash_df = kash_df.rename(columns={"meetings.races.bfExchangeMarketId":"market_id","meetings.races.runners.bfExchangeSelectionId":"selection_id","meetings.races.runners.ratedPrice":"rating"})
# Only keep columns we need
kash_df = kash_df[['market_id','selection_id','rating']]
# Convert market_id to string
kash_df['market_id'] = kash_df['market_id'].astype(str)
kash_df
market_id selection_id rating
0 1.195173067 4218988 210.17
1 1.195173067 28551850 8.32
2 1.195173067 37261788 6.24
3 1.195173067 20357386 5.60
4 1.195173067 20466004 8.14
... ... ... ...
442 1.195174127 43035476 4.68
443 1.195174127 27077942 188.93
444 1.195174127 26735524 16.85
445 1.195174127 21661954 192.68
446 1.195174127 24265033 13.85

447 rows × 3 columns

Let's also set up the Dataframe so that we can easily get our Rating when given market_id and selection_id:

# Set market_id and selection_id as index for easy referencing
kash_df = kash_df.set_index(['market_id','selection_id'])
kash_df

# e.g. can reference like this: 
    # df.loc['1.195173067'].loc['4218988']
    # to return 210.17
rating
market_id selection_id
1.195173067 4218988 210.17
28551850 8.32
37261788 6.24
20357386 5.60
20466004 8.14
... ... ...
1.195174127 43035476 4.68
27077942 188.93
26735524 16.85
21661954 192.68
24265033 13.85

447 rows × 1 columns

Let's do the same thing for the greyhound model

# Download todays greyhounds ratings
iggy_df = pd.read_csv(iggy_url)

## Data clearning
# Rename Columns
iggy_df = iggy_df.rename(columns={"meetings.races.bfExchangeMarketId":"market_id","meetings.races.runners.bfExchangeSelectionId":"selection_id","meetings.races.runners.ratedPrice":"rating"})
# Only keep columns we need
iggy_df = iggy_df[['market_id','selection_id','rating']]
# Convert market_id to string
iggy_df['market_id'] = iggy_df['market_id'].astype(str)
iggy_df

# Set market_id and selection_id as index for easy referencing
iggy_df = iggy_df.set_index(['market_id','selection_id'])
iggy_df
rating
market_id selection_id
1.195241412 43066395 12.80
43066396 12.17
43066397 11.10
43066398 5.85
42256274 2.85
... ... ...
1.195242071 42424961 7.73
42588368 18.47
35695588 7.33
42334315 7.14
38830169 41.51

960 rows × 1 columns


Automate Todays Model Ratings

Now that we have all of our ratings in a nice clean DataFrame we can easily automate them in Flumine.

Log into Flumine

# Import libraries for logging in
import betfairlightweight
from flumine import Flumine, clients

# Credentials to login and logging in 
trading = betfairlightweight.APIClient('username','password',app_key='appkey')
client = clients.BetfairClient(trading, interactive_login=True)

# Login
framework = Flumine(client=client)

# Code to login when using security certificates
# trading = betfairlightweight.APIClient('username','password',app_key='appkey', certs=r'C:\Users\zhoui\openssl_certs')
# client = clients.BetfairClient(trading)

# framework = Flumine(client=client)

Create Our Strategy in Flumine

Strategy for Thoroughbreds

This is where you can come up with exciting and innovative strategies!

But because we are lame, let's start off with a simple strategy that checks prices 60 seconds before the jump and Backs anything greater than our ratings and Lays anything less than our ratings all with a flat stake size of $5

If you get confused about the code structure for how Flumine and BaseStrategy works, be sure to take a check out How to Automate I as it dives right into how to use Flumine.

Our main logic is:

  • Check how far out from the jump we are
  • If we are within 60 seconds from the jump check each horse's market prices
    • If best Back price > model price then place a $5 Back bet
    • If best Lay price < model price then place a $5 Lay bet
# Import libraries and logging
from flumine import BaseStrategy 
from flumine.order.trade import Trade
from flumine.order.order import LimitOrder, OrderStatus
from flumine.markets.market import Market
from betfairlightweight.filters import streaming_market_filter
from betfairlightweight.resources import MarketBook
import pandas as pd
import logging

# Will create a file called how_to_automate_3.log in our current working directory
logging.basicConfig(filename = 'how_to_automate_3.log', level=logging.INFO, format='%(asctime)s:%(levelname)s:%(message)s')


# New strategy called FlatKashModel for the Thoroughbreds model
class FlatKashModel(BaseStrategy):

    def start(self) -> None:
        print("starting strategy 'FlatKashModel'")

    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:
        # process_market_book only executed if this returns True
        if market_book.status != "CLOSED":
            return True

    # If check_market_book returns true i.e. the market is open and not closed then we will run process_market_book once initially
    # After the first inital time process_market_book has been run, every single time the market ticks, process_market_book will run again
    def process_market_book(self, market: Market, market_book: MarketBook) -> None:
        # If time is less than 1min and we haven't placed a bet yet then look at our ratings and place bet
        if market.seconds_to_start < 60 and market_book.inplay == False:
            for runner in market_book.runners:
                # Check runner hasn't scratched and that first layer of back or lay price exists
                if runner.status == "ACTIVE" and runner.ex.available_to_back[0] and runner.ex.available_to_lay[0]:
                    # If best available to back price is > rated price then flat $5 bet
                    if runner.ex.available_to_back[0]['price'] > kash_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="BACK", order_type=LimitOrder(price=runner.ex.available_to_back[0]['price'], size=5.00)
                        )
                        market.place_order(order)
                    # If best available to lay price is < rated price then flat $5 lay
                    if runner.ex.available_to_lay[0]['price'] < kash_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="LAY", order_type=LimitOrder(price=runner.ex.available_to_lay[0]['price'], size=5.00)
                        )
                        market.place_order(order)

Strategy for Greyhounds

Let's create the same strategy for the Greyhounds model. The only difference here is that when we reference the model price, we are referencing the DataFrame iggy_df instead of kash_df. (An alternate way we could do this if we wanted would be to combine both strategies together and just append the dataframes together, but lets just keep it nice and simple for now)

# New strategy called FlatIggyModel for the Greyhound model
class FlatIggyModel(BaseStrategy):

    def start(self) -> None:
        print("starting strategy 'FlatIggyModel'")

    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:
        # process_market_book only executed if this returns True
        if market_book.status != "CLOSED":
            return True

    # If check_market_book returns true i.e. the market is open and not closed then we will run process_market_book once initially
    # After the first inital time process_market_book has been run, every single time the market ticks, process_market_book will run again
    def process_market_book(self, market: Market, market_book: MarketBook) -> None:

        # If time is less than 1min and we haven't placed a bet yet then look at our ratings and place bet
        if market.seconds_to_start < 60 and market_book.inplay == False:
            for runner in market_book.runners:
                # Check runner hasn't scratched and that first layer of back or lay price exists
                if runner.status == "ACTIVE" and runner.ex.available_to_back[0] and runner.ex.available_to_lay[0]:
                    # If best available to back price is > rated price then flat $5 bet
                    if runner.ex.available_to_back[0]['price'] > iggy_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="BACK", order_type=LimitOrder(price=runner.ex.available_to_back[0]['price'], size=5.00)
                        )
                        market.place_order(order)
                    # If best available to lay price is < rated price then flat $5 lay
                    if runner.ex.available_to_lay[0]['price'] < iggy_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="LAY", order_type=LimitOrder(price=runner.ex.available_to_lay[0]['price'], size=5.00)
                        )
                        market.place_order(order)

Running our strategy

Now that we have created two strategies we need to add both to the frame work along with the auto-terminate and bet logging we made in How to Automate II

thoroughbreds_strategy = FlatKashModel(
    market_filter=streaming_market_filter(
        event_type_ids=["7"], # Horse Racing
        country_codes=["AU"], # Australian Markets
        market_types=["WIN"], # Win Markets 
    ),
    max_order_exposure= 50, # Max bet sizes of $50
    max_trade_count=1, # Max of trade/bet attempt per selection
    max_live_trade_count=1, # Max of 1 unmatched Bet per selection
)

greyhounds_strategy = FlatIggyModel(
    market_filter=streaming_market_filter(
        event_type_ids=["4339"], # Greyhound Racing
        country_codes=["AU"], # Australian Markets
        market_types=["WIN"], # Win Markets
    ),
    max_order_exposure= 50, # Max bet sizes of $50
    max_trade_count=1, # Max of trade/bet attempt per selection
    max_live_trade_count=1, # Max of 1 unmatched Bet per selection
)

framework.add_strategy(thoroughbreds_strategy) # Add horse racing strategy to our framework
framework.add_strategy(greyhounds_strategy) # Add greyhound racing strategy to our framework
# import logging
import datetime
from flumine.worker import BackgroundWorker
from flumine.events.events import TerminationEvent

# logger = logging.getLogger(__name__)

"""
Worker can be used as followed:
    framework.add_worker(
        BackgroundWorker(
            framework,
            terminate,
            func_kwargs={"today_only": True, "seconds_closed": 1200},
            interval=60,
            start_delay=60,
        )
    )
This will run every 60s and will terminate 
the framework if all markets starting 'today' 
have been closed for at least 1200s
"""


# Function that stops automation running at the end of the day
def terminate(
    context: dict, flumine, today_only: bool = True, seconds_closed: int = 600
) -> None:
    """terminate framework if no markets
    live today.
    """
    markets = list(flumine.markets.markets.values())
    markets_today = [
        m
        for m in markets
        if m.market_start_datetime.date() == datetime.datetime.utcnow().date()
        and (
            m.elapsed_seconds_closed is None
            or (m.elapsed_seconds_closed and m.elapsed_seconds_closed < seconds_closed)
        )
    ]
    if today_only:
        market_count = len(markets_today)
    else:
        market_count = len(markets)
    if market_count == 0:
        # logger.info("No more markets available, terminating framework")
        flumine.handler_queue.put(TerminationEvent(flumine))

# Add the stopped to our framework
framework.add_worker(
    BackgroundWorker(
        framework,
        terminate,
        func_kwargs={"today_only": True, "seconds_closed": 1200},
        interval=60,
        start_delay=60,
    )
)
import os
import csv
import logging
from flumine.controls.loggingcontrols import LoggingControl
from flumine.order.ordertype import OrderTypes

logger = logging.getLogger(__name__)

FIELDNAMES = [
    "bet_id",
    "strategy_name",
    "market_id",
    "selection_id",
    "trade_id",
    "date_time_placed",
    "price",
    "price_matched",
    "size",
    "size_matched",
    "profit",
    "side",
    "elapsed_seconds_executable",
    "order_status",
    "market_note",
    "trade_notes",
    "order_notes",
]


class LiveLoggingControl(LoggingControl):
    NAME = "BACKTEST_LOGGING_CONTROL"

    def __init__(self, *args, **kwargs):
        super(LiveLoggingControl, self).__init__(*args, **kwargs)
        self._setup()

    # Changed file path and checks if the file orders_hta_3.csv already exists, if it doens't then create it
    def _setup(self):
        if os.path.exists("orders_hta_3.csv"):
            logging.info("Results file exists")
        else:
            with open("orders_hta_3.csv", "w") as m:
                csv_writer = csv.DictWriter(m, delimiter=",", fieldnames=FIELDNAMES)
                csv_writer.writeheader()

    def _process_cleared_orders_meta(self, event):
        orders = event.event
        with open("orders_hta_3.csv", "a") as m:
            for order in orders:
                if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
                    size = order.order_type.size
                else:
                    size = order.order_type.liability
                if order.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
                    price = None
                else:
                    price = order.order_type.price
                try:
                    order_data = {
                        "bet_id": order.bet_id,
                        "strategy_name": order.trade.strategy,
                        "market_id": order.market_id,
                        "selection_id": order.selection_id,
                        "trade_id": order.trade.id,
                        "date_time_placed": order.responses.date_time_placed,
                        "price": price,
                        "price_matched": order.average_price_matched,
                        "size": size,
                        "size_matched": order.size_matched,
                        "profit": 0 if not order.cleared_order else order.cleared_order.profit,
                        "side": order.side,
                        "elapsed_seconds_executable": order.elapsed_seconds_executable,
                        "order_status": order.status.value,
                        "market_note": order.trade.market_notes,
                        "trade_notes": order.trade.notes_str,
                        "order_notes": order.notes_str,
                    }
                    csv_writer = csv.DictWriter(m, delimiter=",", fieldnames=FIELDNAMES)
                    csv_writer.writerow(order_data)
                except Exception as e:
                    logger.error(
                        "_process_cleared_orders_meta: %s" % e,
                        extra={"order": order, "error": e},
                    )

        logger.info("Orders updated", extra={"order_count": len(orders)})

    def _process_cleared_markets(self, event):
        cleared_markets = event.event
        for cleared_market in cleared_markets.orders:
            logger.info(
                "Cleared market",
                extra={
                    "market_id": cleared_market.market_id,
                    "bet_count": cleared_market.bet_count,
                    "profit": cleared_market.profit,
                    "commission": cleared_market.commission,
                },
            )

framework.add_logging_control(
    LiveLoggingControl()
)
framework.run() # run all our strategies
starting strategy 'FlatKashModel'
starting strategy 'FlatIggyModel'


Conclusion and next steps

There we have it. We now have a bot that you can turn on at the start of a day by hitting the run all button. It will automatically scrape all the data online, place bets throughout the day and at the end of the day stop itself.

Then on the next day you can hit run all again, without needing to update anything.

While Betfair's own Data Science models are easy to automate you're also not likely to become rich. They are also available to everyone freely online, so any edge is likely already priced in.

That's why for the next part of this series we will be learning:

  • Part IV - How to Automate your own model
  • Part V - How to simulate the Exchange to backtest and optimise our strategies

Complete code

Run the code from your ide by using py <filename>.py, making sure you amend the path to point to your input data.

Download from Github

import pandas as pd
from flumine import BaseStrategy 
from flumine.order.trade import Trade
from flumine.order.order import LimitOrder, OrderStatus
from flumine.markets.market import Market
from betfairlightweight.filters import streaming_market_filter
from betfairlightweight.resources import MarketBook
import logging
import betfairlightweight
from flumine import Flumine, clients
import datetime
from flumine.worker import BackgroundWorker
from flumine.events.events import TerminationEvent
import os
import csv
from flumine.controls.loggingcontrols import LoggingControl
from flumine.order.ordertype import OrderTypes

# Will create a file called how_to_automate_3.log in our current working directory
logging.basicConfig(filename = 'how_to_automate_3.log', level=logging.INFO, format='%(asctime)s:%(levelname)s:%(message)s')

# Thoroughbred model (named the kash-ratings-model)
kash_url_1 = 'https://betfair-data-supplier-prod.herokuapp.com/api/widgets/kash-ratings-model/datasets?date='
kash_url_2 = pd.Timestamp.now().strftime("%Y-%m-%d") # todays date formatted as YYYY-mm-dd
kash_url_3 = '&presenter=RatingsPresenter&csv=true'

kash_url = kash_url_1 + kash_url_2 + kash_url_3
kash_url

# Greyhounds model (named the iggy-joey-model)
iggy_url_1 = 'https://betfair-data-supplier-prod.herokuapp.com/api/widgets/iggy-joey/datasets?date='
iggy_url_2 = pd.Timestamp.now().strftime("%Y-%m-%d")
iggy_url_3 = '&presenter=RatingsPresenter&csv=true'

iggy_url = iggy_url_1 + iggy_url_2 + iggy_url_3
iggy_url

# Download todays thoroughbred ratings
kash_df = pd.read_csv(kash_url)

## Data clearning
# Rename Columns
kash_df = kash_df.rename(columns={"meetings.races.bfExchangeMarketId":"market_id","meetings.races.runners.bfExchangeSelectionId":"selection_id","meetings.races.runners.ratedPrice":"rating"})
# Only keep columns we need
kash_df = kash_df[['market_id','selection_id','rating']]
# Convert market_id to string
kash_df['market_id'] = kash_df['market_id'].astype(str)
kash_df

# Set market_id and selection_id as index for easy referencing
kash_df = kash_df.set_index(['market_id','selection_id'])
kash_df

# e.g. can reference like this: 
    # df.loc['1.195173067'].loc['4218988']
    # to return 210.17

# Download todays greyhounds ratings
iggy_df = pd.read_csv(iggy_url)

## Data clearning
# Rename Columns
iggy_df = iggy_df.rename(columns={"meetings.races.bfExchangeMarketId":"market_id","meetings.races.runners.bfExchangeSelectionId":"selection_id","meetings.races.runners.ratedPrice":"rating"})
# Only keep columns we need
iggy_df = iggy_df[['market_id','selection_id','rating']]
# Convert market_id to string
iggy_df['market_id'] = iggy_df['market_id'].astype(str)
iggy_df

# Set market_id and selection_id as index for easy referencing
iggy_df = iggy_df.set_index(['market_id','selection_id'])
iggy_df

# Import libraries for logging in
import betfairlightweight
from flumine import Flumine, clients

# Credentials to login and logging in 
trading = betfairlightweight.APIClient('username','password',app_key='appkey')
client = clients.BetfairClient(trading, interactive_login=True)

# Login
framework = Flumine(client=client)

# Code to login when using security certificates
# trading = betfairlightweight.APIClient('username','password',app_key='appkey', certs=r'C:\Users\zhoui\openssl_certs')
# client = clients.BetfairClient(trading)

# framework = Flumine(client=client)

# Will create a file called how_to_automate_3.log in our current working directory
logging.basicConfig(filename = 'how_to_automate_3.log', level=logging.INFO, format='%(asctime)s:%(levelname)s:%(message)s')

# New strategy called FlatKashModel for the Thoroughbreds model
class FlatKashModel(BaseStrategy):

    def start(self) -> None:
        print("starting strategy 'FlatKashModel'")

    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:
        # process_market_book only executed if this returns True
        if market_book.status != "CLOSED":
            return True

    # If check_market_book returns true i.e. the market is open and not closed then we will run process_market_book once initially
    # After the first inital time process_market_book has been run, every single time the market ticks, process_market_book will run again
    def process_market_book(self, market: Market, market_book: MarketBook) -> None:
        # If time is less than 1min and we haven't placed a bet yet then look at our ratings and place bet
        if market.seconds_to_start < 60 and market_book.inplay == False:
            for runner in market_book.runners:
                # Check runner hasn't scratched and that first layer of back or lay price exists
                if runner.status == "ACTIVE" and runner.ex.available_to_back[0] and runner.ex.available_to_lay[0]:                     
                    # If best available to back price is > rated price then flat $5 bet                
                    if runner.ex.available_to_back[0]['price'] > kash_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="BACK", order_type=LimitOrder(price=runner.ex.available_to_back[0]['price'], size=5.00)
                        )
                        market.place_order(order)
                    # If best available to lay price is < rated price then flat $5 lay
                    if runner.ex.available_to_lay[0]['price'] < kash_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="LAY", order_type=LimitOrder(price=runner.ex.available_to_lay[0]['price'], size=5.00)
                        )
                        market.place_order(order)

# New strategy called FlatIggyModel for the Greyhound model
class FlatIggyModel(BaseStrategy):

    def start(self) -> None:
        print("starting strategy 'FlatIggyModel'")

    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:
        # process_market_book only executed if this returns True
        if market_book.status != "CLOSED":
            return True

    # If check_market_book returns true i.e. the market is open and not closed then we will run process_market_book once initially
    # After the first inital time process_market_book has been run, every single time the market ticks, process_market_book will run again
    def process_market_book(self, market: Market, market_book: MarketBook) -> None:

        # If time is less than 1min and we haven't placed a bet yet then look at our ratings and place bet
        if market.seconds_to_start < 60 and market_book.inplay == False:
            for runner in market_book.runners:
                # Check runner hasn't scratched and that first layer of back or lay price exists
                if runner.status == "ACTIVE" and runner.ex.available_to_back[0] and runner.ex.available_to_lay[0]:                
                # If best available to back price is > rated price then flat $5 bet
                    if runner.ex.available_to_back[0]['price'] > iggy_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="BACK", order_type=LimitOrder(price=runner.ex.available_to_back[0]['price'], size=5.00)
                        )
                        market.place_order(order)
                    # If best available to lay price is < rated price then flat $5 lay
                    if runner.ex.available_to_lay[0]['price'] < iggy_df.loc[market_book.market_id].loc[runner.selection_id].item():
                        trade = Trade(
                        market_id=market_book.market_id,
                        selection_id=runner.selection_id,
                        handicap=runner.handicap,
                        strategy=self,
                        )
                        order = trade.create_order(
                            side="LAY", order_type=LimitOrder(price=runner.ex.available_to_lay[0]['price'], size=5.00)
                        )
                        market.place_order(order)

logger = logging.getLogger(__name__)

"""
Worker can be used as followed:
    framework.add_worker(
        BackgroundWorker(
            framework,
            terminate,
            func_kwargs={"today_only": True, "seconds_closed": 1200},
            interval=60,
            start_delay=60,
        )
    )
This will run every 60s and will terminate 
the framework if all markets starting 'today' 
have been closed for at least 1200s
"""

# Function that stops automation running at the end of the day
def terminate(
    context: dict, flumine, today_only: bool = True, seconds_closed: int = 600
) -> None:
    """terminate framework if no markets
    live today.
    """
    markets = list(flumine.markets.markets.values())
    markets_today = [
        m
        for m in markets
        if m.market_start_datetime.date() == datetime.datetime.utcnow().date()
        and (
            m.elapsed_seconds_closed is None
            or (m.elapsed_seconds_closed and m.elapsed_seconds_closed < seconds_closed)
        )
    ]
    if today_only:
        market_count = len(markets_today)
    else:
        market_count = len(markets)
    if market_count == 0:
        # logger.info("No more markets available, terminating framework")
        flumine.handler_queue.put(TerminationEvent(flumine))

logger = logging.getLogger(__name__)

FIELDNAMES = [
    "bet_id",
    "strategy_name",
    "market_id",
    "selection_id",
    "trade_id",
    "date_time_placed",
    "price",
    "price_matched",
    "size",
    "size_matched",
    "profit",
    "side",
    "elapsed_seconds_executable",
    "order_status",
    "market_note",
    "trade_notes",
    "order_notes",
]

class LiveLoggingControl(LoggingControl):
    NAME = "BACKTEST_LOGGING_CONTROL"

    def __init__(self, *args, **kwargs):
        super(LiveLoggingControl, self).__init__(*args, **kwargs)
        self._setup()

    # Changed file path and checks if the file orders_hta_2.csv already exists, if it doens't then create it
    def _setup(self):
        if os.path.exists("orders_hta_3.csv"):
            logging.info("Results file exists")
        else:
            with open("orders_hta_3.csv", "w") as m:
                csv_writer = csv.DictWriter(m, delimiter=",", fieldnames=FIELDNAMES)
                csv_writer.writeheader()

    def _process_cleared_orders_meta(self, event):
        orders = event.event
        with open("orders_hta_3.csv", "a") as m:
            for order in orders:
                if order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
                    size = order.order_type.size
                else:
                    size = order.order_type.liability
                if order.order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
                    price = None
                else:
                    price = order.order_type.price
                try:
                    order_data = {
                        "bet_id": order.bet_id,
                        "strategy_name": order.trade.strategy,
                        "market_id": order.market_id,
                        "selection_id": order.selection_id,
                        "trade_id": order.trade.id,
                        "date_time_placed": order.responses.date_time_placed,
                        "price": price,
                        "price_matched": order.average_price_matched,
                        "size": size,
                        "size_matched": order.size_matched,
                        "profit": 0 if not order.cleared_order else order.cleared_order.profit,
                        "side": order.side,
                        "elapsed_seconds_executable": order.elapsed_seconds_executable,
                        "order_status": order.status.value,
                        "market_note": order.trade.market_notes,
                        "trade_notes": order.trade.notes_str,
                        "order_notes": order.notes_str,
                    }
                    csv_writer = csv.DictWriter(m, delimiter=",", fieldnames=FIELDNAMES)
                    csv_writer.writerow(order_data)
                except Exception as e:
                    logger.error(
                        "_process_cleared_orders_meta: %s" % e,
                        extra={"order": order, "error": e},
                    )

        logger.info("Orders updated", extra={"order_count": len(orders)})

    def _process_cleared_markets(self, event):
        cleared_markets = event.event
        for cleared_market in cleared_markets.orders:
            logger.info(
                "Cleared market",
                extra={
                    "market_id": cleared_market.market_id,
                    "bet_count": cleared_market.bet_count,
                    "profit": cleared_market.profit,
                    "commission": cleared_market.commission,
                },
            )

thoroughbreds_strategy = FlatKashModel(
    market_filter=streaming_market_filter(
        event_type_ids=["7"], # Horse Racing
        country_codes=["AU"], # Australian Markets
        market_types=["WIN"], # Win Markets 
    ),
    max_order_exposure= 50, # Max bet sizes of $50
    max_trade_count=1, # Max of trade/bet attempt per selection
    max_live_trade_count=1, # Max of 1 unmatched Bet per selection
)

greyhounds_strategy = FlatIggyModel(
    market_filter=streaming_market_filter(
        event_type_ids=["4339"], # Greyhound Racing
        country_codes=["AU"], # Australian Markets
        market_types=["WIN"], # Win Markets
    ),
    max_order_exposure= 50, # Max bet sizes of $50
    max_trade_count=1, # Max of trade/bet attempt per selection
    max_live_trade_count=1, # Max of 1 unmatched Bet per selection
)

framework.add_strategy(thoroughbreds_strategy) # Add horse racing strategy to our framework
framework.add_strategy(greyhounds_strategy) # Add greyhound racing strategy to our framework

# Add the stopped to our framework
framework.add_worker(
    BackgroundWorker(
        framework,
        terminate,
        func_kwargs={"today_only": True, "seconds_closed": 1200},
        interval=60,
        start_delay=60,
    )
)

framework.add_logging_control(
    LiveLoggingControl()
)

framework.run() # run all our strategies

Disclaimer

Note that whilst models and automated strategies are fun and rewarding to create, we can't promise that your model or betting strategy will be profitable, and we make no representations in relation to the code shared or information on this page. If you're using this code or implementing your own strategies, you do so entirely at your own risk and you are responsible for any winnings/losses incurred. Under no circumstances will Betfair be liable for any loss or damage you suffer.