This is the second post covering the development of sizers in Backtrader. The first post provided an introduction to the basics, looked at the anatomy of a sizer and provided some basic sizing algorithms. This post by contrast, will dive a bit deeper into the subject by taking a look at comminfo and how it can be used to fine-tune your sizing algorithms.
Note: Since this post is primarily focused on how to apply comminfo to sizing algorithms, readers may wish to read the related commission’s scheme post: Backtrader: Commission Schemes
Comminfo
Each time you make a buy or sell call within a strategy, the sizer class is passed the strategy’s comminfo instance. This allows you to access commission attributes (variables) such ascommission
, mult
and margin
as well as comminfo’s own methods (functions). Having access to commission parameters and methods can allow the sizer to account for the exact commission that would be charged to open and close a position.
Some further reading on commissions can be found here:
- Official Docs: https://www.backtrader.com/docu/commission-schemes/commission-schemes.html
- Backtest-Rookies: Backtrader: Commission Scheme
Accounting for Commissions
If we want to properly control our total risk, taking account of commissions is essential. As such, our sizing algorithm should factor in this cost so we don’t induce death by a thousand paper cuts. Additionally, if we don’t factor in these costs, we can easily end up returning a size that we don’t enough cash to open. This results in missed trades and sometimes a bit of head scratching! An example of this is below. If you run the code, with the basic max risk sizer developed in part 1 and crank up the commissions, you will the issue.''' 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 import argparse def parse_args(): parser = argparse.ArgumentParser(description='Sizers Testing') parser.add_argument('--commperc', default=0.01, type=float, help='The percentage commission to apply to a trade') return parser.parse_args() class maxRiskSizer(bt.Sizer): ''' Returns the number of shares rounded down that can be purchased for the max risk tolerance ''' params = (('risk', 0.1),) 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() elif self.rsi > 70: self.close() else: pass args = parse_args() #Variable for our starting cash startcash = 10000 #Create an instance of cerebro cerebro = bt.Cerebro() #Add our strategy cerebro.addstrategy(firstStrategy) data = bt.feeds.Quandl( dataname='F', 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) cerebro.addsizer(maxRiskSizer) cerebro.broker.setcommission(commission=args.commperc) # Run over everything cerebro.run() #Get final portfolio Value portvalue = cerebro.broker.getvalue() pnl = portvalue - startcash #Print out the final result print('Final Portfolio Value: ${}'.format(portvalue)) print('P/L: ${}'.format(pnl)) #Finally plot the end results cerebro.plot(style='candlestick')Important Note: The data in this tutorial uses the Quandl API. With this API you are limited to the number calls you can make per day. To have unlimited access to their free data, sign up for an API key here and add the
apikey
keyword to the quandl data call like so:
data = bt.feeds.Quandl( dataname='F', fromdate = datetime(2016,1,1), todate = datetime(2017,1,1), buffered= True, apikey="INSERT YOUR API KEY" )Reference: https://www.quandl.com/?modal=register Now run the code with the following commands. Take care to replace the file name with whatever you save the code as. Example 1:
python .\sizers-part2-perc-commission-too-high.py
Example 2: python .\sizers-part2-perc-commission-too-high.py --commperc 10
You will see that in the second example, no trades are taken. That is because we don’t have enough cash in the account to pay for the number of shares returned by the sizer when a large commission is added. It is an extreme example, but this can also happen in less extreme circumstances. For example when trying to go “all in” or when you don’t have much cash left due to existing positions in the market.
This slideshow requires JavaScript.
Moving On
Getting back on track, we will now extend themaxRiskSizer
to account for commissions. The maxRiskSizer
is designed to calculate the maximum size position you can take without exceeding a certain percentage of the cash available in your account. It would, therefore, be even better if it could take the commission into account.
Example Code
''' Author: www.backtest-rookies.com MIT License Copyright (c) 2018 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 import argparse def parse_args(): parser = argparse.ArgumentParser(description='Sizers Testing') parser.add_argument('--commsizer', action ='store_true',help=('Use the Sizer ' 'that takes commissions into account')) parser.add_argument('--commtype', default="Percentage", type=str, choices=["Percentage", "Fixed"], help='The type of commission to apply to a trade') parser.add_argument('--commission', default=0.05, type=float, help='The amount of commission to apply to a trade') parser.add_argument('--risk', default=0.01, type=float, help='The percentage of available cash to risk on a trade') parser.add_argument('--debug', action ='store_true', help=('Print Sizer Debugs')) return parser.parse_args() class maxRiskSizer(bt.Sizer): ''' Returns the number of shares rounded down that can be purchased for the max rish tolerance ''' params = (('risk', 0.1), ('debug', True)) def _getsizing(self, comminfo, cash, data, isbuy): max_risk = math.floor(cash * self.p.risk) if isbuy == True: size = max_risk / data[0] else: size = max_risk / data[0] * -1 #Finally round down to the nearest unit size = math.floor(size) if self.p.debug: if isbuy: buysell = 'Buying' else: buysell = 'Selling' print("------------- Sizer Debug --------------") print("Action: {}".format(buysell)) print("Price: {}".format(data[0])) print("Cash: {}".format(cash)) print("Max Risk %: {}".format(self.p.risk)) print("Max Risk $: {}".format(max_risk)) print("Current Price: {}".format(data[0])) print("Size: {}".format(size)) print("----------------------------------------") return size class maxRiskSizerComms(bt.Sizer): ''' Returns the number of shares rounded down that can be purchased for the max risk tolerance ''' params = (('risk', 0.1), ('debug', True)) def _getsizing(self, comminfo, cash, data, isbuy): size = 0 # Work out the maximum size assuming all cash can be used. max_risk = math.floor(cash * self.p.risk) comm = comminfo.p.commission if comminfo.stocklike: # We are using a percentage based commissions # Apply the commission to the price. We can then divide our risk # by this value com_adj_price = data[0] * (1 + (comm * 2)) # *2 for round trip comm_adj_max_risk = "N/A" if isbuy == True: comm_adj_size = max_risk / com_adj_price if comm_adj_size < 0: #Avoid accidentally going short comm_adj_size = 0 else: comm_adj_size = max_risk / com_adj_price * -1 else: #Else is fixed size # Dedecut commission from available cash to invest comm_adj_max_risk = max_risk - (comm *2) # Round trip com_adj_price = "N/A" if comm_adj_max_risk < 0: # Not enough cash return 0 if isbuy == True: comm_adj_size = comm_adj_max_risk / data[0] else: comm_adj_size = comm_adj_max_risk / data[0] * -1 #Finally make sure we round down to the nearest unit. comm_adj_size = math.floor(comm_adj_size) if self.p.debug: if isbuy: buysell = 'Buying' else: buysell = 'Selling' print("------------- Sizer Debug --------------") print("Action: {}".format(buysell)) print("Price: {}".format(data[0])) print("Cash: {}".format(cash)) print("Max Risk %: {}".format(self.p.risk)) print("Max Risk $: {}".format(max_risk)) print("Commission Adjusted Max Risk: {}".format(comm_adj_max_risk)) print("Current Price: {}".format(data[0])) print("Commission: {}".format(comm)) print("Commission Adj Price (Round Trip): {}".format(com_adj_price)) print("Size: {}".format(comm_adj_size)) print("----------------------------------------") return comm_adj_size class FixedCommisionScheme(bt.CommInfoBase): ''' This is a simple fixed commission scheme ''' params = ( ('commission', 5), ('stocklike', False), ('commtype', bt.CommInfoBase.COMM_FIXED), ) def _getcommission(self, size, price, pseudoexec): return self.p.commission class firstStrategy(bt.Strategy): params = (('longshort', False),) 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() elif self.position.size > 0 and self.rsi > 70: self.close() else: pass args = parse_args() #Variable for our starting cash startcash = 10000 #Create an instance of cerebro cerebro = bt.Cerebro() #Add our strategy cerebro.addstrategy(firstStrategy) data = bt.feeds.Quandl( dataname='F', 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 if args.commsizer: cerebro.addsizer(maxRiskSizerComms, debug=args.debug, risk=args.risk) else: cerebro.addsizer(maxRiskSizer, debug=args.debug, risk=args.risk) if args.commtype.lower() == 'percentage': cerebro.broker.setcommission(args.commission) else: #Add the new commissions scheme comminfo = FixedCommisionScheme(commission=args.commission) cerebro.broker.addcommissioninfo(comminfo) # Run over everything cerebro.run() #Get final portfolio Value portvalue = cerebro.broker.getvalue() pnl = portvalue - startcash #Print out the final result print('Final Portfolio Value: ${}'.format(round(portvalue,2))) print('P/L: ${}'.format(round(pnl,2))) #Finally plot the end results cerebro.plot(style='candlestick')
Commentary
The code in the example allows you to run the same script in two modes. To do this, we use theargparse
module to specify different options when running the script (for more on that see: Using argparse to change strategy parameters). In this case, we are using it so that we can easily compare the differences between the position sizes returned from the sizer. We also use it to adjust the commission size to further influence the returned sizes.
Run the script with the “–help” option to find out more about what each option does. The following output shall be returned.
python3 .\sizers-part2-commission.py --help usage: sizers-part2-commission.py [-h] [--commsizer] [--commtype {Percentage,Fixed}] [--commission COMMISSION] [--risk RISK] [--debug] Sizers Testing optional arguments: -h, --help show this help message and exit --commsizer Use the Sizer that takes commissions into account --commtype {Percentage,Fixed} The type of commission to apply to a trade --commission COMMISSION The amount of commission to apply to a trade --risk RISK The percentage of available cash to risk on a trade --debug Print Sizer DebugsNote: To see how the sizer is arriving at its final value running with the
--debug
flag is recommended.
The code example contains two sample sizers, one fixed commission scheme and a strategy. The two sizers have the same objective. That is to provide the maximum size that can be purchased within a certain risk tolerance. Where they differ is that maxRiskSizerComms
has been extended to make an adjustment to the final price based on the amount of commission expected for a round trip (to buy and later sell the instrument). It also is able to change its algorithm depending on whether the commission scheme is stock-like or futures-like (percentage based or a fixed commission). (See: https://www.backtrader.com/docu/commission-schemes/commission-schemes.html#commissions-stocks-vs-futures)
The maxRiskSizerComms
is the focus of this tutorial and where the magic happens. However, the basic sizer from part 1 has been included so that you can easily compare the differences. maxRiskSizersComms
arrives at its sizing calculation using the following steps
- It calculates the maximum cash we are willing to risk
- Then get the current
commission
value. - Next, it will check whether the commission scheme is
stocklike
. - If so, we deem
commission
to be a percentage value. As such, we then apply a percentage based commission to the price. Note that the percentage is doubled to account for the ultimate sale commission of the same size. - If not, we deem
commission
to be a fixed value. As such, we then double the commission (again to account for the sale) and simply deduct it from the total we are willing to risk. - Finally, we divide the amount we are willing to risk by the price (or adjusted price in step 4) of the Instrument.
comminfo
object, one thing to watch out for is to use the parameter shortcut p
when accessing the commission scheme attributes. This one stumped me for a few minutes. E.g to access the commission value, use comminfo.p.commission
instead of comminfo.commission
. If you do the latter, you will raise the following AttributeError
.
AttributeError: 'CommInfoBase' object has no attribute 'commission'
Result
Assuming you shall save the example code assizers-part2-commission.py
, the code can be run with the commands in the following examples.
Note: If you are running on Windows and only have one version of Python installed, you will need to replace python3
with just python
in the examples below.
Example 1
The first example will run the script using the sizer that does not take commissions into account and uses default values forrisk
andcommission
. Note that when the commtype
is not specified, the default commission type is stock-like
.
python3 .\sizers-part2-commission.py --debug
------------- Sizer Debug -------------- Action: Buying Price: 10.487417912902 Cash: 10000.0 Max Risk %: 0.01 Max Risk $: 100 Current Price: 10.487417912902 Size: 9 ---------------------------------------- ------------- Sizer Debug -------------- Action: Buying Price: 11.587057110506 Cash: 10007.77084722578 Max Risk %: 0.01 Max Risk $: 100 Current Price: 11.587057110506 Size: 8 ---------------------------------------- Final Portfolio Value: $10004.82 P/L: $4.82
Example 2
The second example will swap out the sizer with themaxRiskSizerComms
sizer. As such, it will now account for the commission.
python3 .\sizers-part2-commission.py --debug --commsizer
------------- Sizer Debug -------------- Action: Buying Price: 10.487417912902 Cash: 10000.0 Max Risk %: 0.01 Max Risk $: 100 Commission Adjusted Max Risk: N/A Current Price: 10.487417912902 Commission: 0.05 Commission Adj Price (Round Trip): 11.536159704192201 Size: 8 ---------------------------------------- ------------- Sizer Debug -------------- Action: Buying Price: 11.587057110506 Cash: 10006.907419756248 Max Risk %: 0.01 Max Risk $: 100 Commission Adjusted Max Risk: N/A Current Price: 11.587057110506 Commission: 0.05 Commission Adj Price (Round Trip): 12.745762821556601 Size: 7 ---------------------------------------- Final Portfolio Value: $10004.33 P/L: $4.33Notice that we now have some more debug output and the returned size is smaller?
Example 3
The final example changes the commission type to fixed and sets a $5 commission to each trade.python3 .\sizers-part2-commission.py --debug --commsizer --commtype Fixed --commission 5
------------- Sizer Debug -------------- Action: Buying Price: 10.487417912902 Cash: 10000.0 Max Risk %: 0.01 Max Risk $: 100 Commission Adjusted Max Risk: 90.0 Current Price: 10.487417912902 Commission: 5.0 Commission Adj Price (Round Trip): N/A Size: 8 ---------------------------------------- ------------- Sizer Debug -------------- Action: Buying Price: 11.587057110506 Cash: 10006.033120578884 Max Risk %: 0.01 Max Risk $: 100 Commission Adjusted Max Risk: 90.0 Current Price: 11.587057110506 Commission: 5.0 Commission Adj Price (Round Trip): N/A Size: 7 ---------------------------------------- Final Portfolio Value: $10001.83 P/L: $1.83From the output, we can see that the max risk was adjusted from 100 to 90 before the size calculation was made. Also, the commission adjusted price is now ‘N/A’ due to the fact we do not need to make a price adjustment for a fixed commission. Finally, one more thing that might be worth pointing out. Notice that when using
strategy.close()
the sizer is not called? This is to be expected as the framework does not need to consult the sizer to know what the current open position size is. It just takes the current open position size and uses that. This simplifies sizer development as you do not need to code a condition for a close()
method call. 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