Using Analyzers in Backtrader

Once you have figured out how to write a basic strategy, you then need to be able to quantify whether it is very good. Backtrader has a rich library of analyzers that can provide you metrics from simply tracking wins and losses to more complex Sharpe ratio’s and drawdown analysis.

What are Backtrader Analyzers?

Simply put they are objects that you load into cerebro (the Backtrader engine) that monitor your strategy as it runs. After cerebro has finished running the analyzers can be accessed through strategy objects that are returned by cerebro after running. The analyzer objects themselves have a special method (function) that return a dictionary containing all the statistics that the analyzer is tracking. Sometimes this is a lot of information and other times just one or two statistics. If that does not make sense right now, don’t worry, it will become clearer once you see the code. There are a whole library of different analyzers in Backtrader. Once you have worked through this script, have a look at the documentation to see which analyzers interest you and tweak the code to include them.

Scope

This post is intended to show how to setup and analyzer and print the results. It follows the Backtrader: First Script post and forms part of the getting started series to Backtrader. To see the rules of the strategy and explanation of the code, take a look at that post.

Background

There a are a couple of backtesting / trading metrics that crop into this post. Here is a glossary:
  • Strike Rate: This is the a percentage that represents the number of times you win vs the total number of trades you placed (win rate / total trades). It can help identify to whether you have an edge in the market. Some people aim for to get the strike rate as high as possible. Usually with lots of small wins. Others are happy with lower strike rates but aim for big wins and small losses.
  • SQN: System Quality Number, this was defined by Dr Van Tharp of the Van Tharp institute. It basically gives your strategy a score. A more academic explanation of SQN from the Van Tharp website is below:
SQN measures the relationship between the mean (expectancy) and the standard deviation of the R-multiple distribution generated by a trading system. It also makes an adjustment for the number of trades involved.

For more information see: http://www.vantharp.com/tharp-concepts/sqn.asp

  • Note: The Backtrader documentation provides a helpful ranking system for SQN:
    • 1.6 – 1.9 Below average
    • 2.0 – 2.4 Average
    • 2.5 – 2.9 Good
    • 3.0 – 5.0 Excellent
    • 5.1 – 6.9 Superb
    • 7.0 – Holy Grail?

The Code

'''
Author: www.backtest-rookies.com

MIT License

Copyright (c) 2017 backtest-rookies.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''

import backtrader as bt
from datetime import datetime
from collections import OrderedDict

class firstStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        if not self.position:
            if self.rsi < 30:
                self.buy(size=100)
        else:
            if self.rsi > 70:
                self.sell(size=100)


def printTradeAnalysis(analyzer):
    '''
    Function to print the Technical Analysis results in a nice format.
    '''
    #Get the results we are interested in
    total_open = analyzer.total.open
    total_closed = analyzer.total.closed
    total_won = analyzer.won.total
    total_lost = analyzer.lost.total
    win_streak = analyzer.streak.won.longest
    lose_streak = analyzer.streak.lost.longest
    pnl_net = round(analyzer.pnl.net.total,2)
    strike_rate = (total_won / total_closed) * 100
    #Designate the rows
    h1 = ['Total Open', 'Total Closed', 'Total Won', 'Total Lost']
    h2 = ['Strike Rate','Win Streak', 'Losing Streak', 'PnL Net']
    r1 = [total_open, total_closed,total_won,total_lost]
    r2 = [strike_rate, win_streak, lose_streak, pnl_net]
    #Check which set of headers is the longest.
    if len(h1) > len(h2):
        header_length = len(h1)
    else:
        header_length = len(h2)
    #Print the rows
    print_list = [h1,r1,h2,r2]
    row_format ="{:<15}" * (header_length + 1)
    print("Trade Analysis Results:")
    for row in print_list:
        print(row_format.format('',*row))

def printSQN(analyzer):
    sqn = round(analyzer.sqn,2)
    print('SQN: {}'.format(sqn))

#Variable for our starting cash
startcash = 100000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(firstStrategy)

#Get Apple data from Yahoo Finance.
data = bt.feeds.YahooFinanceData(
    dataname='AAPL',
    fromdate = datetime(2009,1,1),
    todate = datetime(2017,1,1),
    buffered= True
    )

#Add the data to Cerebro
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# Add the analyzers we are interested in
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="ta")
cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn")

# Run over everything
strategies = cerebro.run()
firstStrat = strategies[0]

# print the analyzers
printTradeAnalysis(firstStrat.analyzers.ta.get_analysis())
printSQN(firstStrat.analyzers.sqn.get_analysis())

#Get final portfolio Value
portvalue = cerebro.broker.getvalue()

#Print out the final result
print('Final Portfolio Value: ${}'.format(portvalue))

#Finally plot the end results
cerebro.plot(style='candlestick')

Commentary

Lets start at the setup. Adding an analyzer to the strategy is as easy as calling the addanaylzer() function.
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="ta")
Here we are loading the TradeAnalyzer and giving it a name. The name makes accessing it later much easier. After cerebro has finished running it will return a list of strategy objects. In our case we only loaded one strategy into cerebro. Even so, the one strategy will still be returned in a list. Therefore we need extract our strategy from the list by using a list index position of [0]. Once we have that, we have our strategy.
# Run over everything
strategies = cerebro.run()
firstStrat = strategies[0]
Next up we need to access our data…. Analyzers can be accessed from inside the strategy object and have a built in method (function) for returning a dictionary that contains the results. Below we access the analyzer “ta” (which is the name we gave it when loading the analyzer into cerebro) from the firstStrat strategy object and call the “get_analysis()” method.
firstStrat.analyzers.ta.get_analysis()
So now we have a dictionary, we want to get the data of interest out of it and do something with it. In this case, I am just going to take some of the metrics and print them to the terminal. However, you might want to expand this and export the results to a CSV. For this exercise I am going to look at:
  • Total trades still open
  • Total closed trades
  • Total trades won
  • Total trades lost
  • My Strike rate (which I will calculate from the data given)
  • My best winning streak
  • My worst losing streak
  • My profit or loss.
  • The SQN number of the strategy
To do this I have created two special print functions. This is so the code can then be reused (cut + paste) for future scripts. The print function worth commenting further on is:
printTradeAnalysis(analyzer):
I fear this function might be overly complex for a beginners post due to the line shown below. I just happen to like the clean output it produces in the terminal. It contains some more advanced python techniques for formatting text. I am talking about the line which looks like this:
row_format ="{:<15}" * (header_length + 1)
This line allows me to print the output evenly spaced without having to install another python module like TextTable. To find out more about data formatting in Python see the docs here: https://docs.python.org/3/library/string.html#formatspec An alternative (and a much simpler option) is to use the built-in analyzer print()method. This prints every value inside the analyzer on a separate line. Whether you want to do this or not is a matter of personal preference. I.e whether you want to have some alternative formatting, pick and choose certain statistics of interest or further process the data. An example of how to do this is below.
# Run over everything
strategies = cerebro.run()
firstStrat = strategies[0]

for x in firstStrat.analyzers:
    x.print()

The result

Fire up the script and you should see something that looks like this: Terminal output with Analyzer from bactrader Not a great result trading wise! It looks like our super simple strategy need some refining. Who would have thought it?

Find This Post Useful?

If this post saved you time and effort, please consider donating a coffee to support the site!  

3PUY12Tgp8xynrMCbBdLE56DShzCbxFG8i

0xb90252f1a0af77a43c499102be8a08ce5e190e01

Le3ykk29k2TjD3ZFoEyWTFzJgUu9Q9v6Fq