Source code for penaltyblog.backtest.backtest

"""
Backtest module

Used to backtest different betting strategies.
"""

from typing import Callable, Optional

import numpy as np
import pandas as pd
from tqdm.auto import tqdm

from .account import Account
from .context import Context


[docs] class Backtest: """Used to backtest different betting strategies. Methods ------- start(bankroll, logic, trainer) Runs the backtest using the logic function (and optionally the trainer function) results() Calculates how well the backtest has performed """ def __init__( self, data: pd.DataFrame, start_date: str, end_date: str, stop_at_negative: bool = False, ): """ Parameters ---------- data : pd.DataFrame A dataframe containing the data to run the backtest over. Must contain a column called `date` start_date : str A string containing the date for the start of the test window stop_at_negative : bool If True then the backtest will stop as soon as the bankroll goes below zero """ self.stop_at_negative = stop_at_negative self.start_date = pd.to_datetime(start_date).to_pydatetime().date() self.end_date = pd.to_datetime(end_date).to_pydatetime().date() # validate the dataframe we are passed if not isinstance(data, pd.DataFrame): raise ValueError("Data must be a pandas dataframe") if "date" not in data.columns: raise ValueError("Data must contain a column called `date`") self.df = data try: self.df["date"] = pd.to_datetime(self.df.date).dt.date except ValueError: pass # get the unique dates during the test window self.window = ( self.df[ (self.df["date"] >= self.start_date) & (self.df["date"] <= self.end_date) ]["date"] .sort_values() .unique() )
[docs] def start( self, bankroll: float, logic: Callable, trainer: Optional[Callable] = None ): """ Parameters ---------- bankroll : float The initial starting value for the bankroll logic : callable The function to apply to each individual fixture. The function should have one argument called `ctx`, which contains the the information required to run the strategy. See the example notebooks for more examples of the `logic` function and how to use the `ctx` object. `ctx` will contain an instance of the `Account` class, which contains functions for placing virtual bets, `lookback` which contains all the fuxtures prior to the date of the current fixture, `fixture` which is the current fixture being processed, and optionally `model` if a trainer function is used. trainer : callable The function used to train a model, which is then added to the `ctx` object passed to the `logic` function. This function should have one argument called `ctx`, which contains the the information required to train the model and should return the trained model. See the example notebooks for more examples of the `trainer` function and how to use the `ctx` object. The trainer function gets called once per unique date and then is made available to all fixtures for that date. Returns ------- None """ self.account = Account(bankroll) # for date in self.window: for date in tqdm(self.window): self.account.current_date = date lookback = self.df[self.df["date"] < date] test = self.df[self.df["date"] == date] ctx = Context(self.account, lookback, None) if trainer is not None: ctx.model = trainer(ctx) for _, row in test.iterrows(): ctx.fixture = row logic(ctx) if self.stop_at_negative and self.account.current_bankroll < 0: return None return None
[docs] def results(self) -> dict: """ Calculates the results of the backtest and returns them as a dict Returns ------- Dictionary containing metrics about the backtest """ total_bets = len(self.account.history) total_profit = self.account.current_bankroll - self.account.bankroll successful_bets = sum([x["outcome"] for x in self.account.history]) successful_bet_pc = 0 if total_bets == 0 else successful_bets / total_bets * 100 max_bankroll = ( None if len(self.account.tracker) == 0 else np.max(self.account.tracker) ) min_bankroll = ( None if len(self.account.tracker) == 0 else np.min(self.account.tracker) ) roi = total_profit / self.account.bankroll * 100 output = { "Total Bets": total_bets, "Successful Bets": successful_bets, "Successful Bet %": successful_bet_pc, "Max Bankroll": max_bankroll, "Min Bankroll": min_bankroll, "Profit": total_profit, "ROI": roi, } return output