Developing Sizers in Backtrader – Part 1

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.

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-applicability

The 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.size
The 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.html

exampleSizer()

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-development

printSizingParams()

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: Sizer Parameters 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.

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 donating a coffee to support the site!  


Dontate with PayPal using any payment method you are comfortable with. 


3PUY12Tgp8xynrMCbBdLE56DShzCbxFG8i

0x9c32a2e1e4a06b0995777ac86745c0db1c13bdfc

LUph5xfqvn2bNfthhEcw9QiVLBYeztacFR