This is part 1 of a look into the world of sizers. Originally, I intended this to be a single post but as I dived deeper into the subject and I realized there would be too much content to keep it at a reasonable length. As such the first post will cover the fundamentals of sizer development and provide a couple of simple sizer examples. The second post will dive a bit deeper and take a look at comminfo. Since comminfo is a class, it has numerous methods that can be used to make your sizing algorithms take commissions, interest and existing open positions into account.
The first parameter printed from the code is an example of data which can be accessed via the strategy class using self.strategy.getposition(). You may notice straight away that “pos” is a position object rather than a simple value like “cash“. Similarly the “account value” variable provides an example of data that can be access easily through self.broker().
Note that if you plan to perform live trading, be sure to read which methods your live broker of choice supports in Backtrader’s documentation. I had to implement some broker workarounds for methods that were not available when live trading with Oanda.
Sizers
Sizers are classes that can be loaded into cerebro that are used to decide how many shares, units, contracts etc to buy or sell whenever self.buy() or self.sell() is called. Sizers will only be used for a calculation when no size is given. In other words, if your script contains a buy call like self.buy(size=100), the sizer will not be called. However if you call just self.buy(), cerebro will ask the sizer for the size to buy.Why would we want to do that?
Some people may prefer to explicitly state the size and perform some type of sizing logic in their strategy code instead. If you prefer that method of working that is fine. There is more than one way to skin a cat after all! However, from my perspective, there are some advantages to using sizers. If you like to have compartmentalized code, then sizers are for you. In addition sizers allow you to make some subtle or not so subtle changes to the logic of a strategy without actually having to touch the strategy code. The Backtrader documentation has a good example of this where a sizer is used to turn a long/short strategy into a long only strategy simply by using a different sizer. Now take that one step further and it is easy to imagine that you can have a library of sizers to allow you to deploy the same strategy in different markets with different commission schemes and different trading conditions without having to alter the main strategy code at all. Reference: https://www.backtrader.com/docu/sizers/sizers.html#practical-sizer-applicabilityThe anatomy of a sizer
A sizer is a sub-class of backtrader.Sizer. If you are new to programing, sub-classing allows us to build an object according the the blueprints of main class. The object then inherits all of the features and functionality of the main class without having to copy and paste the code into our own class. We can then simply change the parts of the code we want buy re-writing a method (a class function), attribute (a class variable) or adding something new. All the parts left untouched will continue to function in the same was as written in the parent class. In the code below you will see backtrader.Sizer written as bt.Sizer since I generally import backtrader as bt!class exampleSizer(bt.Sizer): params = (('size',1),) def _getsizing(self, comminfo, cash, data, isbuy): return self.p.sizeThe code above contains an example sizer in it’s simplest form. This will allow us to deconstruct the various key parts of a sizer.
params tuple
Sizers, just like strategies and indicators can contain a parameters tuple. Having a a set of parameters can offer some flexibility when loading the sizer into cerebro and provide data to the sizer that may not otherwise be available._getsizing()
Next we have a the method _getsizing(). This method is called every time a strategy makes a self.buy() or self.sell() call without stating the size of the order. The _getsizing() method is passed a series of parameters by the Backtrader framework. These are:- comminfo: Provides access to various methods which allow you to access broker commission data. This will allow you to take into account all fee’s related to the trade before deciding on the size. A future second part to this tutorial will look at comminfo specifically in more detail.
- cash: Provides the amount of cash available in the account.
- data: Provides access to the data feed being called. For example we can access the latest close price from here.
- isbuy: Is a boolean value (True/False) that tells us whether the order is a buy order. If it is false, then we know the order is a sell order.
Strategy and broker
Two classes that are accessible but not seen in the code above are self.strategy and self.broker. Between these two objects, you have access to pretty much everything needed to create complex sizing algorithms. A note of warning though. If you make calculations based on strategy attributes, try to make sure they are standard attributes/variable in the framework. In other words, available in all strategies (instead of custom attributes added to the code by yourself). Otherwise you will sacrifice portability of the sizer as it will only work with scripts where you have coded the same attribute.Don’t forget to return something
Finally, we must remember to return a value at the end of the calculation. If you forget this, your strategy will not place any orders.The Code
The code in this post contains 3 example sizers. The first of the examples is the fixed size sizer shown in the code block above. The second is an example sizer that prints the all of the _getsizing() method parameters except for comminfo (which we will look at in more detail later). The final example provides a practical sizer implementation to limit the size of a trade to a percentage of the total cash in the account. This is a common sizing algorithm that many strategies use to limit risk.''' 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 import math class exampleSizer(bt.Sizer): params = (('size',1),) def _getsizing(self, comminfo, cash, data, isbuy): return self.p.size class printSizingParams(bt.Sizer): ''' Prints the sizing parameters and values returned from class methods. ''' def _getsizing(self, comminfo, cash, data, isbuy): #Strategy Method example pos = self.strategy.getposition(data) #Broker Methods example acc_value = self.broker.getvalue() #Print results print('----------- SIZING INFO START -----------') print('--- Strategy method example') print(pos) print('--- Broker method example') print('Account Value: {}'.format(acc_value)) print('--- Param Values') print('Cash: {}'.format(cash)) print('isbuy??: {}'.format(isbuy)) print('data[0]: {}'.format(data[0])) print('------------ SIZING INFO END------------') return 0 class maxRiskSizer(bt.Sizer): ''' Returns the number of shares rounded down that can be purchased for the max rish tolerance ''' params = (('risk', 0.03),) def __init__(self): if self.p.risk > 1 or self.p.risk < 0: raise ValueError('The risk parameter is a percentage which must be' 'entered as a float. e.g. 0.5') def _getsizing(self, comminfo, cash, data, isbuy): if isbuy == True: size = math.floor((cash * self.p.risk) / data[0]) else: size = math.floor((cash * self.p.risk) / data[0]) * -1 return size 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() else: if self.rsi > 70: self.close() def notify_trade(self, trade): if trade.justopened: print('----TRADE OPENED----') print('Size: {}'.format(trade.size)) elif trade.isclosed: print('----TRADE CLOSED----') print('Profit, Gross {}, Net {}'.format( round(trade.pnl,2), round(trade.pnlcomm,2))) else: return #Variable for our starting cash startcash = 10000 #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(2016,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 sizer cerebro.addsizer(printSizingParams) # Run over everything cerebro.run() #Get final portfolio Value portvalue = cerebro.broker.getvalue() pnl = portvalue - startcash #Print out the final result print('----SUMMARY----') print('Final Portfolio Value: ${}'.format(portvalue)) print('P/L: ${}'.format(pnl)) #Finally plot the end results cerebro.plot(style='candlestick')
Commentary
First of all, given that we have multiple examples in the code above, it is worth noting how to switch between them. To change the sizer being used, simply change this line:#add the sizer cerebro.addsizer(exampleSizer, size=50)To this:
#add the sizer cerebro.addsizer(printSizingParams)Or this:
#add the sizer cerebro.addsizer(maxRiskSizer, risk=0.2)
import math
I have included an extra python module in this tutorial. The math module is providing math.floor() which makes it easy to always round down to the nearest number. With a max risk algorithm, we never want to round up as it can potentially take us over our risk limit. Rounding down will never do that. Official Python documentation for the math module can be found here: https://docs.python.org/3/library/math.htmlexampleSizer()
The example sizer was included only to discuss the anatomy of a sizer. However it also provides an example of a sizer that uses a fixed stake size. An almost identical version appears in Backtrader’s official documentation here: https://www.backtrader.com/docu/sizers/sizers.html#sizer-developmentprintSizingParams()
Seeing is believing and thus I chose to include a simple sizer which prints out the contents of the sizers parameters. Usually I find this helps me to understand exactly what the parameter is doing and how I can use it when I can see exactly what is returned. It doubly comes in handy when I am having trouble reading the documentation. Running the code should provide output as follows:
maxRiskSizer()
The maxRiskSizer, simply calculates the maximum size position you can take without exceeding a certain percentage of the cash available in your account. The percentage is set through the “risk” parameter in the params tuple. The percentage is entered as a float between 0 and 1 and the default value is 3% but can be set to a different value when loaded into cerebro. For those of you like myself that are not math wizards, the * -1 in the line below changes the size value from positive to negative. We need a negative size for a sell order.size = math.floor((cash * self.p.risk) / data[0]) * -1
maxRiskSizer buy.sell() warning
If you are in the habit of closing a position with a fixed stake size using buy.sell(), you need to be aware that using the maxRiskSizer can, and will, result in positions not being closed and unwanted entries. This is because your cash levels are changing when trades are opened and closed so x% is now x% of a different total. To compound the issue, the instrument value is changing so that x% will result in a different amount of shares / contracts bought. The simple solution is to close positions with self.close(). This will calculate the correct size needed to fully close a position.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
Awesome content ! I am new to backtracker and am following all your posts about Backtrader. Keep it up. Thank you for sharing
Thanks. Much appreciated!
Thank you BTR for all the educational content about backtrader.
Regarding maxRiskSizer(), more often than not, the risk is defined as a maximum amount ready to be lost by getting stopped out of the trade, without waiting to lose the entire stake put on the trade.
In that case, rather than a % of portfolio value, max loss could be defined as maximum units in a trade that’s acceptable to break out a defined range / volatility based on indicators such as ATR / Bollinger Bands.
From my limited understanding of bt, in such a case, sizers have to be embedded in the strategy itself rather than through _getsizing() method. Or do you see a better way to handle such a scenario?
Thanks.
Hi,
shouldn’t we set checksubmit=True?