Backtest a trading strategy in Python

In this article, I will introduce a way to backtest trading strategies in Python. All you need for this is a python interpreter, a trading strategy and last but not least: a dataset.

A complete and clean dataset of OHLC (Open High Low Close) candlesticks is pretty hard to find, even more if you are not willing to pay for it! (Quandl is a good place for that)

As an example, I chose to backtest a strategy on Bitcoin as it’s trendy in these recent time. It was really hard to find a clean 15-minutes candlesticks dataset. I spent 4 hours finding the dataset, cleaning it, writing a code that transforms timestamps into datetime and converts 1-minute candles into 15-minutes candles.

There are many programmatical ways to backtest strategies. For example websites like Quantopian are very efficient and provide with good charts and good metrics. Here we will use a Python package: Backtrader. (Amazing documentation on their website).

How do we proceed?

First, install backtrader in a command prompt (Terminal for Mac OSX):

pip install backtrader

Second, implement your logic in a Python file. The strategy I will backtest here is very poor: I will trade the RSI (relative strength index) – but the aim of this article isn’t in finding an outstanding strategy that will generate millions of $.

To make it simple, the RSI is an index going from 0 to 100 that is supposed to indicate whether the product you are currently trading is overbought and oversold.

Here, when the index will exceed 90 (pretty high) we go short and when it drops below 20 we go long. At each long or short position, we place a stop loss order and a take profit order.

Let’s initialize some things :

  • A Cerebro instance: this is the brain of the backtest.
  • The amount of cash in your portfolio.
  • Some preferences and parameters: slippage cost, commission fees, final metrics, number of positions…
  • Feed Cerebro with your dataset.
    • “openinterest=-1” means that I don’t have an open interest column.
    • “openinterest=0” means the first column is the open interest column.
import backtrader as bt 

cerebro = bt.Cerebro()
data = bt.feeds.GenericCSVData(dataname="BTCUSD_15MIN.csv",
                               datetime=0,
                               fromdate=datetime.datetime(2016,1,1),
                               todate=datetime.datetime(2017,10,1),
                               open=1,
                               high=2,
                               low=3,
                               close=4,
                               openinterest=-1,
                               time=-1,
                               volume=-1,
                               timeframe=bt.TimeFrame.Ticks,
                               dtformat="%Y-%m-%d %H:%M:%S")

cerebro.adddata(data)

# 20 000$ cash initialization
cerebro.broker.setcash(20000.0)

#Slippage cost :
cerebro.broker = bt.brokers.BackBroker(slip_perc=0.0) 

#Number of positions fixed:
cerebro.addsizer(bt.sizers.FixedSize, stake=2)

#Commission
cerebro.broker.setcommission(commission=0.005)

#Add Sharpe Ratio:
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="mySharpe", riskfreerate=0.001)
If you made it well, adding these few lines will plot your candle chart:
cerebro.run()
cerebro.plot(style='candlestick', barup='green', bardown='red')
Third, it’s high time to implement your logic. Your logic has to be implemented in a class that derives from bt.Strategy and implement at least :
  • __init__: Where you initialize all your variables, indicators etc…
  • start: The starting state of your strategy.
  • next: What you will do at each time iteration: a big part of your logic goes here.
  • notify_order: What you want to do once an order is placed. This is where we put the logic of stop loss and take profit orders.
  • notify_trade: What you want to do once your position is closed.
  • log: What is the format you want to use for printing in the console at each time iteration. Not mandatory but recommended.

This is the example of my final code, click to extend the code snippet:

import backtrader as bt
import datetime

# Your logic is inside this class :
class MyStrategy(bt.Strategy):
    params = (
        ('stop_loss', 0.02),
        ('take_profit', 0.04),
        ('period_rsi', 14),
        ('low_rsi', 20),
        ('high_rsi', 80)
    )

    def log(self, txt, dt=None):
        ''' Logging function for this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        time = self.datas[0].datetime.time()
        print('%s - %s, %s' % (dt.isoformat(), time, txt))

    def __init__(self):
        # Keep a reference to the "close"
        self.dataclose = self.datas[0].close

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # Add a RSI indicator
        self.rsi = bt.indicators.RelativeStrengthIndex(
            self.datas[0], period=self.params.period_rsi, safediv=True)

        self.price_at_signal = 0
        self.trades = 0

    def start(self):
        self.trades = 0

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def notify_order(self, order):
        if order.status in [order.Margin, order.Rejected]:
            pass

        if order.status in [order.Submitted, order.Accepted]:
            # Order accepted by the broker. Do nothing.
            return

        elif order.status == order.Cancelled:
            self.log(' '.join(map(str, [
                'CANCEL ORDER. Type :', order.info['name'], "/ DATE :",
                self.data.num2date(order.executed.dt).date().isoformat(),
                "/ PRICE :",
                order.executed.price,
                "/ SIZE :",
                order.executed.size,
            ])))

        elif order.status == order.Completed:
            # If a stop loss or take profit is triggered:
            if 'name' in order.info:
                self.log("%s: REF : %s / %s / PRICE : %.3f / SIZE : %.2f / COMM : %.2f" %
                         (order.info['name'], order.ref,
                          self.data.num2date(order.executed.dt).date().isoformat(),
                          order.executed.price,
                          order.executed.size,
                          order.executed.comm))

            else:
                if order.isbuy():
                    # Initialize our take profit and stop loss orders :
                    stop_loss = order.executed.price * (1.0 - self.params.stop_loss)
                    take_profit = order.executed.price * (1.0 + self.params.take_profit)

                    stop_order = self.sell(exectype=bt.Order.StopLimit,
                                           price=stop_loss)
                    stop_order.addinfo(name="STOP")

                    #OCO : One cancels the Other => The execution of one instantaneously cancels the other
                    takeprofit_order = self.sell(exectype=bt.Order.Limit,
                                                 price=take_profit,
                                                 oco=stop_order)
                    takeprofit_order.addinfo(name="PROFIT")

                    self.log("SignalPrice : %.3f Buy: %.3f, Stop: %.3f, Profit : %.3f"
                             % (self.price_at_signal,
                                order.executed.price,
                                stop_loss,
                                take_profit))

                elif order.issell():
                    # As before, we initialize our stop loss and take profit here
                    stop_loss = order.executed.price * (1.0 + self.params.stop_loss)
                    take_profit = order.executed.price * (1.0 - self.params.take_profit)

                    stop_order = self.buy(exectype=bt.Order.StopLimit,
                                          price=stop_loss)
                    stop_order.addinfo(name="STOP")

                    #OCO !
                    takeprofit_order = self.buy(exectype=bt.Order.Limit,
                                                price=take_profit,
                                                oco=stop_order)
                    takeprofit_order.addinfo(name="PROFIT")

                    self.log("SignalPrice: %.3f Sell: %.3f, Stop: %.3f, Profit : %.3f"
                             % (self.price_at_signal,
                                order.executed.price,
                                stop_loss,
                                take_profit))

    def next(self):
        # Simply log the closing price and the current RSI value
        self.log('Close, %.3f / RSI : %.2f' % (self.dataclose[0], float(self.rsi[0])))

        # If I already have a pending order, I do nothing :
        if self.order:
            return

        # If I don't have any position I can take one:
        if self.position.size == 0:
            if self.rsi[0] >= self.params.high_rsi:
                # Sell short :
                self.sell()
                self.price_at_signal = self.dataclose[0]
                self.log('Sell order : %.3f' % self.dataclose[0])
                self.trades += 1

            elif self.rsi[0] <= self.params.low_rsi:
                # Go long :
                self.buy()
                self.price_at_signal = self.dataclose[0]
                self.log('Buy order : %.3f' % self.dataclose[0])
                self.trades += 1

            else:
                self.log("Nothing, wait.")

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    # Setting my parameters : Stop loss at 1%, take profit at 4%, go short when rsi is 90 and long when 20.
    cerebro.addstrategy(strategy=MyStrategy, stop_loss=0.01, take_profit=0.04, high_rsi=90, low_rsi=20)

    data = bt.feeds.GenericCSVData(dataname="BTCUSD_15MIN.csv",
                                   datetime=0,
                                   fromdate=datetime.datetime(2016, 1, 1),
                                   todate=datetime.datetime(2017, 10, 1),
                                   open=1,
                                   high=2,
                                   low=3,
                                   close=4,
                                   openinterest=-1,
                                   time=-1,
                                   volume=-1,
                                   timeframe=bt.TimeFrame.Minutes,
                                   compression=15,
                                   dtformat="%Y-%m-%d %H:%M:%S")

    cerebro.adddata(data)

    # no slippage
    cerebro.broker = bt.brokers.BackBroker(slip_perc=0.0)

    # 20 000$ cash initialization
    cerebro.broker.setcash(20000.0)

    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=2)

    # Set the fees
    cerebro.broker.setcommission(commission=0.00005)

    # add analyzers
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="mySharpe", riskfreerate=0.001)
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name="myDrawDown")

    # Print out the starting conditions
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    backtest = cerebro.run()

    # Print out the final result
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

    print('Sharpe Ratio:', backtest[0].analyzers.mySharpe.get_analysis())
    print('Drawdown :', backtest[0].analyzers.myDrawDown.get_analysis())

    cerebro.plot(style='candlestick', barup='green', bardown='red')

The result

The most interesting part, what is the result?
Here is the final complex chart generated by backtrader (it uses matplotlib) :

backtest

2-year backtest on BTCUSD. Starting value: 20 000$. Ending value: 22 123.9$. Sharpe ratio: 1.167

This is how to read it:

  • First upper chart accounts for your cash value.
  • The second accounts for all your trades’ P&L (blue for the positive and red for negative).
  • The third is the candle chart with all your entry and exit points.
  • The last one is the RSI.

Here is a zoom on a period:

backtest_zoom

This zoom shows how I go long when RSI is low and exit with a take profit.

Get a record of all your trades

It is very likely that you will be willing to get a track on every trades, and you can do so by inserting this line :

cerebro.addwriter(bt.WriterFile, csv=True, out="your_file.csv")

It will create a CSV file with all the iterations!


Conclusion

We have seen in this article how to backtest a trading strategy on Python. However, there are a lot of biases to consider and pay attention: lookahead bias, optimization bias, cognitive bias (really important!) and so on. All of them are described in “Successful Algorithmic Trading” by Michael L.Halls-Moore (founder of QuantStart).

A successful 2 year backtest will never certify that your strategy will be successful in the future. Also, and this is what cognitive bias is all about, do not trade discretionary when trying your strategy in real since it will screw up your potential result.

Finally, the strategy implemented here was a really simple one for an educational aim. An interesting feature of backtrader is that you can optimize your strategy. You just have to backtest it while varying some parameters within a chosen range, as a result, backtrader will find you which set of parameters give you the best performance over the period. See more on backtrader.com.

Do not hesitate to contact me if you have any questions about this article or my code!

Thank you and please vote below!

 

2 thoughts on “Backtest a trading strategy in Python

  1. You could definitely see your skills in the paintings you write. The world hopes for even more passionate writers such as you who aren’t afraid to mention how they believe. Always follow your heart.

    Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s