- Advertisement -Newspaper WordPress Theme
Algorithm tradingBacktesting.py - An Introductory Guide to Backtesting with Python - AlgoTrading101 Blog

Backtesting.py – An Introductory Guide to Backtesting with Python – AlgoTrading101 Blog


How to get started with Backtesting.py?

To get started with Backtesting.py, you will need to install it via pip with the following command:

To start creating a trading strategy, you will import the Strategy object which is used within your trading algorithm classes:

from backtesting import Strategy

class Algorithm(Strategy):
      ...

For this article, I’ll be using Google Colab. Feel free to use any IDE of your choice. All code will be found on our GitHub and also at the bottom of the article. In the following sections, we’ll test out the main features that backtesting.py has to offer.

How to get data with Backtesting.py?

To import data from Backtesting.py, we will access the test module and obtain a specific asset by passing its symbol. It returns the data as a Pandas data frame.

from backtesting.test import GOOG

GOOG.head()

                Open	High	Low	Close	Volume
2004-08-19	100.00	104.06	95.96	100.34	22351900
2004-08-20	101.01	109.08	100.50	108.31	11428600
2004-08-23	110.75	113.48	109.05	109.40	9137200
2004-08-24	111.24	111.60	103.57	104.87	7631300
2004-08-25	104.96	108.00	103.88	106.00	4598900

Keep in mind that Backtesting only has GOOG and EURUSD for test data. Thus, you should use alternative data providers such as Yahoo Finance or Quandl.

How to use technical indicators with Backtesting.py?

To use technical indicators with Backtesting.py, you will need to import them from the test module by passing their function name. For example, if you want to obtain the Simple Moving Average indicator, you would write:

from backtesting.test import SMA

class SmaCross(Strategy):
    n1 = 20 # period of the first SMA
    n2 = 50 # period of the second SMA

    def init(self):
        close = self.data.Close # close price data
        self.sma1 = self.I(SMA, close, self.n1)
        self.sma2 = self.I(SMA, close, self.n2)

Have in mind that Backtesting.py only offers the SMA as an example, it isn’t an indicators library which means that you should build your own indicators or use a library such as TA-Lib or Tulip. Backtesting.py integrated well with both proposed libraries.

Each technical indicator can be combined with an event such as the cross and crossover.

from backtesting.lib import crossover

def next(self):
      if crossover(self.sma1, self.sma2):
          self.buy()
      elif crossover(self.sma2, self.sma1):
          self.sell()

How to define entries and exits with Backtesting.py?

Entries and exits can be defined with Backtesting.py by using conditions that can trigger a buy or sell order. An example of buy and sell order parameters are the following:

def buy(self, *, size=.9999, limit=None, stop=None, sl=None, tp=None)

def sell(self, *, size=.9999, limit=None, stop=None, sl=None, tp=None)

When an entry or exit is executed, it results in a trade. We can query existing orders through Strategy.orders.

How to code a mean-reversion strategy with Backtesting.py?

To code a mean-reversion strategy with Backtesting.py, we will first need to obtain the data of the asset we plan to trade. Then, we will lay out our strategy logic to make all the steps clear. After that, we will code it out and run the backtest.

The goal of this strategy will be to sell/short the asset if it is trading more than 3 standard deviations above the rolling mean and to buy/long the asset if it is trading more than 3 standard deviations below the rolling mean.

We will use 15-minute candles and a rolling mean of 50. The take profit will be the value of our simple moving average.

Now, let us obtain the data for the HE asset. We will do a date closer to the time of writing due to the yfinance constraints on the 15-minute data.

# Obtain OHLV data for HE
# Obtain OHLV data for HE
he = yf.download("HE", start="2023-01-15", interval="15m")[
    ["Open", "High", "Low", "Close", "Volume"]
]
he.head()

Now, let’s set up the trading strategy and the initialization logic.

from backtesting.test import SMA

def std_3(arr, n):
    return pd.Series(arr).rolling(n).std() * 3

class MeanReversion(Strategy):
    roll = 50

    def init(self):
        self.he = self.data.Close

        self.he_mean = self.I(SMA, self.he, self.roll)
        self.he_std = self.I(std_3, self.he, self.roll)
        self.he_upper = self.he_mean + self.he_std
        self.he_lower = self.he_mean - self.he_std

        self.he_close = self.I(SMA, self.he, 1)

Above, I imported the built-in SMA indicator and coded the 3 STD indicator by hand. You can also use libraries for more complicated indicators. Then, we specified the rolling window and initialized the algorithm.

You can also see the first “hack” that needed to be introduced to adequately compare the indicator to the candle closing price. Essentially, it needed to be transformed into an indicator. There might be a better way of doing this but accessing the prices from self.he was broken at the time of writing.

Now, we will code the trading logic.

def next(self):

        if self.he_close < self.he_lower:
            self.buy(
                tp = self.he_mean,
            )

        if self.he_close > self.he_upper:
            self.sell(
                tp = self.he_mean,
            )

Now, we will wrap everything up and plot the strategy and print out its stats.

How to perform backtesting with Backtesting.py?

To perform backtesting with Backtesting.py, you will need to import the Backtest module and pass it the data, the strategy class, set initial cash, and the trade commission value. Below is an example of how to run a backtest and view its results.

from backtesting import Backtest

bt = Backtest(he, MeanReversion, cash=10000, commission=0.002)
stats = bt.run()
bt.plot()
stats
Start                     2023-01-17 09:30...
End                       2023-03-08 16:00...
Duration                     50 days 06:30:00
Exposure Time [%]                   43.436499
Equity Final [$]                 10104.838029
Equity Peak [$]                  10291.717912
Return [%]                            1.04838
Buy & Hold Return [%]               -7.059514
Return (Ann.) [%]                    7.573549
Volatility (Ann.) [%]               18.320732
Sharpe Ratio                         0.413387
Sortino Ratio                        0.748415
Calmar Ratio                         1.805436
Max. Drawdown [%]                   -4.194859
Avg. Drawdown [%]                   -0.888331
Max. Drawdown Duration        8 days 02:45:00
Avg. Drawdown Duration        1 days 05:46:00
# Trades                                    3
Win Rate [%]                        66.666667
Best Trade [%]                       1.008354
Worst Trade [%]                     -0.502406
Avg. Trade [%]                        0.34971
Max. Trade Duration          13 days 02:45:00
Avg. Trade Duration           6 days 19:20:00
Profit Factor                        3.100131
Expectancy [%]                       0.351706
SQN                                  0.776997

Have in mind that this is just an example strategy for showcasing the library. It shouldn’t be used for real trading.

How to perform optimizations with Backtesting.py?

To perform optimizations with Backtesting.py, we can utilize the optimize function that can receive parameters to optimize and the metric to optimize for. For example, let’s optimize the rolling window size for our previous strategy.

stats = bt.optimize(
    roll=range(10, 60, 5),
    maximize="Equity Final [$]",
    constraint=lambda p: p.roll > 10,
)
stats
Start                     2023-01-17 09:30...
End                       2023-03-08 16:00...
Duration                     50 days 06:30:00
Exposure Time [%]                    4.802561
Equity Final [$]                 10149.401675
Equity Peak [$]                  10149.401675
Return [%]                           1.494017
Buy & Hold Return [%]               -7.059514
Return (Ann.) [%]                   10.938703
Volatility (Ann.) [%]                3.333441
Sharpe Ratio                         3.281505
Sortino Ratio                             inf
Calmar Ratio                         8.567644
Max. Drawdown [%]                   -1.276746
Avg. Drawdown [%]                   -0.428759
Max. Drawdown Duration        0 days 04:30:00
Avg. Drawdown Duration        0 days 01:45:00
# Trades                                    2
Win Rate [%]                            100.0
Best Trade [%]                       1.041394
Worst Trade [%]                       0.45102
Avg. Trade [%]                       0.745774
Max. Trade Duration           0 days 05:30:00
Avg. Trade Duration           0 days 05:23:00
Profit Factor                             NaN
Expectancy [%]                       0.746207
SQN                                   2.52049

To access the final value the strategy landed on, we can access the stats['_strategy']:

<Strategy MeanReversion(roll=15)>

Be careful to not overfit the strategy and play with different buy/sell order settings.

To learn more about overfitting, check out: https://algotrading101.com/learn/what-is-overfitting-in-trading/

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Subscribe Today

GET EXCLUSIVE FULL ACCESS TO PREMIUM CONTENT

SUPPORT NONPROFIT JOURNALISM

EXPERT ANALYSIS OF AND EMERGING TRENDS IN CHILD WELFARE AND JUVENILE JUSTICE

TOPICAL VIDEO WEBINARS

Get unlimited access to our EXCLUSIVE Content and our archive of subscriber stories.

Exclusive content

- Advertisement -Newspaper WordPress Theme

Latest article

More article

- Advertisement -Newspaper WordPress Theme