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.
Not a great result trading wise! It looks like our super simple strategy need some refining. Who would have thought it?
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
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:
Find This Post Useful?
If this post saved you time and effort, please consider support the site! There are many ways to support us and some won’t even cost you a penny.
Brave
Backtest Rookies is a registered with Brave publisher!
Brave users can drop us a tip.
Alternatively, support us by switching to Brave using this referral link and we will receive some BAT!
Tradingview
Referral Link
Enjoying the content and thinking of subscribing to Tradingview? Support this site by clicking the referral link before you sign up!
PayPal
BTC
3HxNVyh5729ieTfPnTybrtK7J7QxJD9gjD
ETH
0x9a2f88198224d59e5749bacfc23d79507da3d431
LTC
M8iUBnb8KamHR18gpgi2sSG7jopddFJi8S
[…] 5 – Using Analyzers in Backtrader […]
Is it possible, if you have multiple datafeeds, to get separate analyzer reports for each data feed?
I.e., if I’m feeding GBP_USD and USD_JPY feeds, I want to know how they performed separately.
I guess you can run strategy separetly for 2 feeds and compare results, it might be dumb approach, but it would work. Just plug the GBP_USD data and run strategy, save results and repear for USD_JPY feed
Hi Rookies, newbie, just following through your first few tutorials. Great stuff, thanks.
My problem is this: I’ve installed the latest Python 3, BT, etc, all new. And it runs just fine but…
total_lost is returning instead the value for total_won.
So for this particular script (exactly copy/paste into my local file), I get the following results (sorry the columns aren’t lined up perfectly):
Trade Analysis Results:
Total Open Total Closed Total Won Total Lost
0 5 3 3
Strike Rate Win Streak Losing Streak PnL Net
60.0 2 2 -1582.0
SQN: -0.52
Final Portfolio Value: $98418.0
No matter what changes to the script I make (for example going to 40-65 instead of RSI 30-70 to increase the number of trades) I still get the Total Lost == Total Won, even though it’s not so. Similarly, the analyzer.streak.lost.longest is also returning instead the value for longest winning streak.
Has there been a change in analyzer behavior?
Oh, never mind, yes there has. Lost == Win. Looked into the source code and there’s currently a bug introduced in tradeanalyzer.py a month ago in July, but it’s already fixed in the development branch.
That doesn’t explain the difference in actual strategy value lost with this example (between mine and yours) but that could be changes in the Yahoo data (who knows with them).
Thanks again, great series so far! -Paul
Also, in contrast to the Quandl feed for AAPL data in the first tutorial, this Yahoo data currently messes up the candlestick plot badly. It’s all red and the candlesticks are too big. Haven’t dived into the issue, but it’s a mess. The Quandl data also returns portfolio final value that is much closer to what you reported here.
Thanks for the feedback – Also good catch on the recently introduced issue in BT.
Yes, I should get around to updating the data feed in these tutorials. When I first created these articles, Yahoo was still working.
I was thinking to switch all to Quandl but unfortunately the EOD WIKI is no longer continuously updated either.
Updating this with some sensible data is on the todo list.
Hi,
Thanks to share these tutorials.
I copied/pasted the script this evening into the last BT installation 1.9.72.122 (with which I could already run successfully tutorial from BT website / Quick start, and your own previous tutorial)
I had no error up to now, but with this script, I get the following error:
Traceback (most recent call last):
File "analyzer.py", line 113, in
printTradeAnalysis(firstStrat.analyzers.ta.get_analysis())
File "analyzer.py", line 50, in printTradeAnalysis
total_open = analyzer.total.open
File "/home/pierre/.local/lib/python3.7/site-packages/backtrader/utils/autodict.py", line 104, in __getattr__
return self[key]
File "/home/pierre/.local/lib/python3.7/site-packages/backtrader/utils/autodict.py", line 94, in __missing__
raise KeyError
KeyError
(to help, “analyzer.py” is the name of my own file into which I copied your script)
Any idea what could be wrong?
Also, if I may add, please, what is the use of the import:
from collections import OrderedDict
I thank you in advance for your help.
Have a good evening.
Best regards,
Pierre
Pierre, the problem with your analysis is, that the analyzer doesn’t find anything to analyze in your analysis, .i.e., the strategy.analyzers.ta.get_analysis() variable is empty/too small to be analyzed.
Hi, can you please demonstrate in one article to find popular candlestick patterns using backtrader ?
This is a good idea…
I could add it to the suggestion list 🙂
Hi.
Great article.
Unfortunately I’m really stuck on retrieving Analyzer results when Optimizing? Instead of final P&L I’d like to see the Sharpe Ratio for each optimization iteration, but I’m struggling on the how to.
I’ve read the Backtrader docs but I’m a newb and clearly not getting a concept. When I have a single cerebro run, I can access the analyzers no problem once the cerebro.run() has executed.
Yet when optimizing it seems to loop over the optimizing parameters (as expected) and I’m not sure where I should call the get_analysis() in my code. I feel it shuld be in the stop() method, but I can’t get it working.
Does that make sense? Could you give me a clue pls? Much appreciated.