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!

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.

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:

To this:

Or this:

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.

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.