Backtrader: Target Orders and Stop Losses

Today we are going to take a look at how to use target orders as part of an almost “all in” strategy whilst sending stop losses (or take profits) with the correct size. It might sound simple enough on paper but, when sizing positions, there are a few options available to us. If you go down the wrong route, you can be left scratching your head. Why do we say almost “all in”? Well, going “all In” is not as simple as dividing our available cash by the current price when we send our orders. Unless we use Backtraders cheat on open functionality, our size is calculated using the close price of our data feed. However, our entry price will actually be determined at the open of the following bar. During this time, price could easily gap up/down leaving you without enough cash to complete the trade. As such we, we will not try and go all in with 100% of our cash. Instead, we will reserve a small amount to account for gapping. In my opinion, this is a good habit for when we progress to the real world as price is always bouncing around and it is impossible to know where the price will be by the time your order is filled by the broker.

Other Challenges

Gapping is not the only trading hurdle we must jump over to meet the objectives of this post. Reversing a position will require a completely different size to opening a position from flat. When we are reversing positions, we are required to submit enough size to close the position and then further size to go “all In” in the opposite direction. Backtrader will not do this for you if you simply use self.buy()orself.sell(). Fortunately, though, Backtrader does have a tool for the job which we will look at in a bit. Another challenge we will face is that if we do reverse a position and want to create a stop loss or take profit, we need to ensure that our stop loss or take profit size is not the same as the reversed position size. That would be too large. The stop and profit orders must exit a position and not reverse it again.

Get The Test Data

Before we go further, to run the examples in this post, download the following test data: TestData The data supplied is simple artificial test data created in Excel and exported to CSV. Using artificial data allows us to know exactly what is happening and when. We are able to control price movements in a predictable pattern to make testing and calculations easier. The test data has the following key data changes:
  • From Jan 1st to May 1st, price will nicely rise and fall every 10 bars in even steps.
  • Following this, from May 1st 2018 price will be offset by $50 and start gapping by $10 at the open.
  • On 28th September price will revert to normal.
  • Then from the 28th December price will be offset by another $50

The Strategy

We will start with a simple base strategy and build on it throughout the examples. The strategy has been tailored to the test data and simply takes action on certain dates. This allows us to know ahead of time what should happen. In other words, it will allow us to focus on Backtrader’s mechanics.
'''
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

class TestStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Will not be hit
        short_stop = self.data.close[0] + 50 # Will not be hit
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Price closes at $100 and following opens at $100
                # Base test no gapping
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2018,12,27).date():
                # Price closes at $100 and following opens at $150
                # Start of first position reversing test
                # Still flat at the moment
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTE oco=self.sl_ord is needed to cancel stop losses already in the market
            if date == datetime(2018,1,25).date():
                #Price Closes at $140 and following opens at $140
                # Close Tests first - $40 Dollar Profit
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,1).date():
                #Price closes at $190 and following opens at $190
                # First Position to Reverse
                # We are selling from net longself. Desired position size
                # Is now net short.
                sell_ord = self.sell(oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,6).date():
                # We are already net short
                # Price closes at $150 and following opens at $150
                # NOTE oco=self.sl_ord is needed to cancel the stop loss already in the market
                # First buy to actually reverse a sell
                buy_ord = self.buy(oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,11).date():
                #Price closes at $190 and following opens at $190
                # Close to finish
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(TestStrategy)

# Create a Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 means not used
    )

# Add the data
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# 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')
The eagle-eyed may notice that despite the title of this post, there is no target_order()call in the strategy code. Don’t worry, we will add that later as the post progresses. Note: The data downloaded from above is assumed to be in a folder called datain the same folder as the script. if you place the two files in the same folder, be sure to modify line 160. Running the base strategy as is will result in a trade size of 1 for every trade. The final chart should look like this: Showing Output Chart from Target Orders base code

Percent Sizers

So we now have a base strategy for buying and selling with a size of 1 every time. It is time to move onto the next objective and try to make the strategy go almost “all in”. If you head to the docs and take a look at the available sizers, you might see the PercentSizerand think it is the right (wo)man for the job. So let’s take a look at what happens when we add one.

Gapping Example With a Percent Sizer

Now we are about to use a percentage based sizer, it would be a good time to show an example of the gapping issue that can arise when going fully “all in”. As mentioned, with this setup, we run the risk of missing trades. The first example will demonstrate just that. We have simply added a PercentSizer to the script and set percentsto 100.
'''
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

class TestStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Will not be hit
        short_stop = self.data.close[0] + 50 # Will not be hit
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Price closes at $100 and following opens at $100
                # Base test no gapping
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2018,12,27).date():
                # Price closes at $100 and following opens at $150
                # Start of first position reversing test
                # Still flat at the moment
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTE oco=self.sl_ord is needed to cancel stop losses already in the market
            if date == datetime(2018,1,25).date():
                #Price Closes at $140 and following opens at $140
                # Close Tests first - $40 Dollar Profit
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,1).date():
                #Price closes at $190 and following opens at $190
                # First Position to Reverse
                # We are selling from net longself. Desired position size
                # Is now net short.
                sell_ord = self.sell(oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,6).date():
                # We are already net short
                # Price closes at $150 and following opens at $150
                # NOTE oco=self.sl_ord is needed to cancel the stop loss already in the market
                # First buy to actually reverse a sell
                buy_ord = self.buy(oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,11).date():
                #Price closes at $190 and following opens at $190
                # Close to finish
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        elif order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        elif order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        elif order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(TestStrategy)

# Create a Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 means not used
    )

# Add the data
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# Add a sizer
cerebro.addsizer(bt.sizers.PercentSizer, percents=100)

# 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')
If you run the script and compare the results to the previous version, you will notice that the long market entry order on the 28th Dec 2018 is not completed. This is due to price gapping up. Scroll up to compare to the base strategy. Example output chart showing missed trade If you reduce percentsto 50 then the trade will re-appear as you will have enough cash in the account to complete the order.

Percent Sizer – Reversing Size

In the next PercentSizer example, we have moved some of the entry/exit dates in the script. Therefore, be sure to copy the updated code below. This has been done to avoid the huge gap and set a more reasonable percentage level. Of course, what is reasonable depends on what time-frame and asset class you are looking at. For example, if you are on the minute chart for a Forex pair you could probably go almost “all in” with only a very small amount reserved to cover gapping. Conversely, if you are trading equities on the daily time-frame, gapping can be much more extreme!
'''
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

class TestStrategy(bt.Strategy):

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Will not be hit
        short_stop = self.data.close[0] + 50 # Will not be hit
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Price closes at $100 and following opens at $100
                # Base test no gapping
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Price closes at $100 and following opens at $150
                # Start of first position reversing test
                # Still flat at the moment
                buy_ord = self.buy()
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTE oco=self.sl_ord is needed to cancel stop losses already in the market
            if date == datetime(2018,1,25).date():
                #Price Closes at $140 and following opens at $140
                # Close Tests first - $40 Dollar Profit
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,11).date():
                #Price closes at $190 and following opens at $190
                # First Position to Reverse
                # We are selling from net longself. Desired position size
                # Is now net short.
                sell_ord = self.sell(oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,16).date():
                # We are already net short
                # Price closes at $150 and following opens at $150
                # NOTE oco=self.sl_ord is needed to cancel the stop loss already in the market
                # First buy to actually reverse a sell
                buy_ord = self.buy(oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,21).date():
                #Price closes at $190 and following opens at $190
                # Close to finish
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        elif order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        elif order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        elif order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(TestStrategy)

# Create a Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 means not used
    )

# Add the data
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# Add a sizer
cerebro.addsizer(bt.sizers.PercentSizer, percents=80)

# 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')
If you read over the code, you will see thatpercentshas now been set to 80. However, if you run the script and take a look at the output. We can see that something is not quite right with the size of the order when attempting to reverse a position. Showing short size from sell function

Target Orders

Ok, so the built-in percent sizer doesn’t meet our objectives out of the box. Fortunately, Backtrader has many features and options available to us if we dig around the documentation. One such option is to use target order. A target order allows you to specify a target size, value or percentage of cash to use for the final position. The key difference with a normal buy()or sell()order is that if you are in a position, it will adjust the size accordingly to close the existing position and finish with the correct target size. Official docs are here: https://www.backtrader.com/docu/order_target/order_target.html After reading the docs, the most relevant target order for our goal is order_target_percent. This also happens to be the order target type which has the least information/examples in the documentation. (If you are wondering, how to specify a short position, we just put a negative target percent. Even though to some it might seem unintuitive to target using minus 90% of your cash, it actually follows the same convention as size and value and keeps things consistent. ) The following example is split into two parts. First we will take a look at what happens when you just use target orders to simply replace the self.buy()and self.sell()calls. Then we will finally we will provide a complete working example.

Replacing buy() and sell() with order_target_percent()

As the heading suggests, in the first example we shall just simply replace our buy()and sell()market orders with a call to order_target_percent()and see what happens.
'''
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

class TestStrategy(bt.Strategy):

    params = (('percents', 0.9),) # Float: 1 == 100%

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Will not be hit
        short_stop = self.data.close[0] + 50 # Will not be hit
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Price closes at $100 and following opens at $100
                # Base test no gapping
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Price closes at $100 and following opens at $150
                # Start of first position reversing test
                # Still flat at the moment
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTE oco=self.sl_ord is needed to cancel stop losses already in the market
            if date == datetime(2018,1,25).date():
                #Price Closes at $140 and following opens at $140
                # Close Tests first - $40 Dollar Profit
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,11).date():
                #Price closes at $190 and following opens at $190
                # First Position to Reverse
                # We are selling from net longself. Desired position size
                # Is now net short.
                sell_ord = self.order_target_percent(target=-self.p.percents, oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                self.sl_ord = self.buy(exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,16).date():
                # We are already net short
                # Price closes at $150 and following opens at $150
                # NOTE oco=self.sl_ord is needed to cancel the stop loss already in the market
                # First buy to actually reverse a sell
                buy_ord = self.order_target_percent(target=self.p.percents, oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                self.sl_ord = self.sell(exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,21).date():
                #Price closes at $190 and following opens at $190
                # Close to finish
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(TestStrategy)

# Create a Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 means not used
    )

# Add the data
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# 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')
If we look at the output and, we can see that we are reversing positions nicely! However, if we take a closer look at the output we can see something is not quite right. Our stop loss size is incorrect! Without using a sizer, the default size for a buy() or sell()order is 1. As such, this means we need to make one more adjustment to finish things off. Image Showing Incorrect Stop Loss Sizes

Final Example

Moving into the final example, we now just need to make sure we set our stop loss size correctly. Initially, the problem may seem complex because we need to be flexible and account for times when we both reverse position and times where we open a position from flat (0). However, there is a simple solution: stop_size = abs(sell_ord.size) - abs(self.position.size) We just take the size from the order object returned when we create a order_target_percent() order. We then simply deduct the current position size from it. If we are flat, we will be deducting 0and therefore, the size will be the same as the entry size. Conversely, if we are reversing, we deduct the current position size, which will result in the same real size of the new position.
'''
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

class TestStrategy(bt.Strategy):

    params = (('percents', 0.9),) # Float: 1 == 100%

    def __init__(self):
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        date = self.data.datetime.date()
        long_stop = self.data.close[0] - 50 # Will not be hit
        short_stop = self.data.close[0] + 50 # Will not be hit
        if not self.position:
            if date == datetime(2018,1,11).date():
                # Price closes at $100 and following opens at $100
                # Base test no gapping
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                stop_size = buy_ord.size - abs(self.position.size)
                self.sl_ord = self.sell(size=stop_size, exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,6).date():
                # Price closes at $100 and following opens at $150
                # Start of first position reversing test
                # Still flat at the moment
                buy_ord = self.order_target_percent(target=self.p.percents)
                buy_ord.addinfo(name="Long Market Entry")
                stop_size = buy_ord.size - abs(self.position.size)
                self.sl_ord = self.sell(size=stop_size, exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
        else:
            # NOTE oco=self.sl_ord is needed to cancel stop losses already in the market
            if date == datetime(2018,1,25).date():
                #Price Closes at $140 and following opens at $140
                # Close Tests first - $40 Dollar Profit
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")
            elif date == datetime(2019,1,11).date():
                #Price closes at $190 and following opens at $190
                # First Position to Reverse
                # We are selling from net longself. Desired position size
                # Is now net short.
                sell_ord = self.order_target_percent(target=-self.p.percents, oco=self.sl_ord)
                sell_ord.addinfo(name="Short Market Entry")
                stop_size = abs(sell_ord.size) - abs(self.position.size)
                self.sl_ord = self.buy(size=stop_size, exectype=bt.Order.Stop, price=short_stop)
                self.sl_ord.addinfo(name='Short Stop Loss')
            elif date == datetime(2019,1,16).date():
                # We are already net short
                # Price closes at $150 and following opens at $150
                # NOTE oco=self.sl_ord is needed to cancel the stop loss already in the market
                # First buy to actually reverse a sell
                buy_ord = self.order_target_percent(target=self.p.percents, oco=self.sl_ord)
                buy_ord.addinfo(name="Long Market Entry")
                stop_size = buy_ord.size - abs(self.position.size)
                self.sl_ord = self.sell(size=stop_size, exectype=bt.Order.Stop, price=long_stop)
                self.sl_ord.addinfo(name='Long Stop Loss')
            elif date == datetime(2019,1,21).date():
                #Price closes at $190 and following opens at $190
                # Close to finish
                cls_ord = self.close(oco=self.sl_ord)
                cls_ord.addinfo(name="Close Market Order")

    def notify_order(self, order):
        date = self.data.datetime.datetime().date()

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Accepted'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))


        if order.status == order.Completed:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Completed'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
            print('-'*80)

        if order.status == order.Canceled:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('{} Order Canceled'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))

        if order.status == order.Rejected:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('WARNING! {} Order Rejected'.format(order.info['name']))
            print('{}, Status {}: Ref: {}, Size: {}, Price: {}'.format(
                                                        date,
                                                        order.status,
                                                        order.ref,
                                                        order.size,
                                                        'NA' if not order.price else round(order.price,5)
                                                        ))
            print('-'*80)

    def notify_trade(self, trade):
        date = self.data.datetime.datetime()
        if trade.isclosed:
            print('-'*32,' NOTIFY TRADE ','-'*32)
            print('{}, Close Price: {}, Profit, Gross {}, Net {}'.format(
                                                date,
                                                trade.price,
                                                round(trade.pnl,2),
                                                round(trade.pnlcomm,2)))
            print('-'*80)

startcash = 10000

#Create an instance of cerebro
cerebro = bt.Cerebro()

#Add our strategy
cerebro.addstrategy(TestStrategy)

# Create a Data Feed
data = bt.feeds.GenericCSVData(
    timeframe=bt.TimeFrame.Days,
    compression=1,
    dataname='data/TestData.csv',
    nullvalue=0.0,
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=2,
    low=3,
    open=1,
    close=4,
    volume=-1,
    openinterest=-1 #-1 means not used
    )

# Add the data
cerebro.adddata(data)

# Set our desired cash start
cerebro.broker.setcash(startcash)

# 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')
If we review the final output, we can see that the stop loss size is set correctly. And on the chart, we can see all positions are executed. Final Chart Output from correct solution  

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