Backtrader: Bracket Orders

In this post, we are going to take a look at bracket orders. Bracket orders are a special type of order which have not been covered on the blog before.  These type of order can greatly simplify entering positions when a stop loss and  take profit are desired.

Bracket Orders

The bracket order allows Backtrader to emulate a broker order where we specify a stop loss and take profit at the same time we enter.  This is quite a common way to enter a position with most brokers and is quite special because:
  • We send 3 orders at the same time. 1 order to enter a position and 2 orders to exit a position
  • The exit orders are accepted by the broker but are not active until the entry order is filled. This stops your exit orders from triggering a trade in the wrong direction!
  • Once we are in a position and one of the exit orders is hit, the other is automatically canceled.
When manually trading using an online platform you will usually see a form with options to add a stop loss and take profit. This gives the impression that we are sending only one order but we are actually sending three. Bracket Order Form On Tradingview In previous versions of Backtrader, we had to simulate the bracket order with multiple separate orders. However, since version 1.9.37.116 we have a specific bracketorder which will manage all of this for us. The official docs do a great job of describing how a bracket order works:
  • The 3 orders are submitted together to avoid having any of them triggered independently
  • The low/high side orders are marked as children of the main side
  • The children are not active until the main side is executed
  • The cancellation of the main side cancels both the low and high side
  • The execution of the main side activates both the low and high side
  • Upon being active
    • The execution or cancellation of any of low/high side orders automatically cancels the other
Source: https://www.backtrader.com/docu/order-creation-execution/bracket/bracket.html?#bracket-orders Note: Relating this to our image above, the main side order would be our market entry order. The low side, would be our stop loss and the high side would be our take profit. If you are new to programming, you may not understand how parents and children relate to our order. It is just a convenient way of indicating hierarchy.  You can think of this like folders on your hard drive. You will have a folder named documents and in that documents folder you might have another folder named work. In this scenario, the workfolder is the child folder of the documentsfolder.

Get The Test Data

To run the examples in this post, download the following test data: TestBrackets.csv As mentioned in other tutorials, using this 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 in this example contains only close data and simply pulsates between $100 and $300.

Scope

Once you know the basic syntax, bracket orders are a breeze to use. The official documentation is solid but only covers a simple usage example using a limit order for entry. As such, we will attempt to add some value by covering more types of order and briefly discussing when you might want to use them.

Market Entry, Stop Loss and Take Profit

A bracket order which uses a market order to enter a position (i.e the mainside) is probably the most common bracket order that readers will have used on other platforms. It is also the same bracket order as shown in the order ticket image above. We can execute this order with the line: self.buy_bracket(limitprice=long_tp, stopprice=long_stop, exectype=bt.Order.Market) Generally, we use this type of order when we have a signal to enter and we do not want to wait to enter the position. We place a market order and we are filled against the best order on the order book.

Limit Entry, Stop Loss and Take Profit

If we do not wish to enter immediately, we could decide to use a limit order to try and get a better price. In this case, we enter using the following line: self.sell_bracket(limitprice=short_tp, price=entry, stopprice=short_stop, exectype=bt.Order.Limit) In our imaginary example below, we are confident that price will bounce a bit before continuing a downward trend. Therefore we place a limit order just above the peak of a recent bounce to try and catch another high before the fall.

Stop Entry, Stop Loss and Take Profit

Sometimes we may want to enter a position at a price which is worse than what we can have right now. We might choose to do this when looking for confirmation that price will continue moving in a certain direction. Another reason might be to try and catch a  breakout. To enter with a stop order we just change the exectype like so: self.sell_bracket(limitprice=short_tp, price=entry, stopprice=short_stop, exectype=bt.Order.Limit) In the example below, at current bar we can see price has bounced back up. We have a bearish view but would like to see price move below previous support to confirm that price will continue to move lower. As such,  we set a stop entry order just a bit lower than the low before the bounce. If price never made a new low, we would be safe as no orders would be triggered. However, in our example, we can see that our stop order is triggered and we eventually are stopped out.

The 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

class TestStrategy(bt.Strategy):

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

    def __init__(self):
        print('-'*32,' STRATEGY INIT ','-'*32)
        self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21)

    def next(self):
        date = self.data.datetime.date()
        close = self.data.close[0]

        print('{}: Close: ${}, Position Size: {}'.format(date, close, self.position.size))

        if not self.position:
            long_tp = close + 50
            long_stop = close - 50
            if date == datetime(2018,1,11).date():
                # Enter with a market order
                # Place TP at 50 dollars above
                buy_ord = self.buy_bracket(limitprice=long_tp, stopprice=long_stop, exectype=bt.Order.Market)

            elif date == datetime(2018,1,21).date():
                # Enter with a limit order to go short to try and catch price
                # near the top
                entry = 200
                short_tp = entry - 50
                short_stop = entry + 50
                buy_ord = self.sell_bracket(limitprice=short_tp, price=entry, stopprice=short_stop, exectype=bt.Order.Stop)


            elif date == datetime(2018,2,11).date():
                # Enter with a stop order to go short and catch price on the
                # way down
                entry = 290
                short_tp = entry - 50
                short_stop = entry + 50
                buy_ord = self.sell_bracket(limitprice=short_tp, price=entry, stopprice=short_stop, exectype=bt.Order.Limit)

            elif date == datetime(2018,3,23).date():
                # Invalid Order Test
                # Attempt to set a limit order again
                #   1. Incorrect Exetype
                #   2. Stop Value below price  (incorrect for short)
                #   3. Limit Order above price (incorrect for short)
                entry = 290
                short_tp = 300
                short_stop = 90
                buy_ord = self.sell_bracket(limitprice=short_tp, price=entry, stopprice=short_stop, exectype=bt.Order.Stop)


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

        if order.status == order.Accepted:
            print('-'*32,' NOTIFY ORDER ','-'*32)
            print('Order Accepted')
            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')
            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')
            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')
            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/TestBrackets.csv',
    dtformat=('%m/%d/%Y'),
    datetime=0,
    time=-1,
    high=1,
    low=-1,
    open=1,
    close=1,
    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))

Code Commentary

The code above will create a bracket order for each of the types described above. Further, since we are testing it will do this on a fixed date. There is no intelligence for setting our target prices and stop loss levels. Instead, we just provide some fixed figures based on what we know the data will do. The intention is to use the bracket orders in their simplest form rather than bloat the code with complex entry signals and stop loss calculations. In addition to the order types described above, we also created one invalid order to see how the engine handles such a scenario and help you easily spot when you have setup the order incorrectly.

Running the Code

The data used is assumed to be in a subfolder called datawhich is in the same directory as the script. If you place the data file somewhere else or give it a different name,  be sure to modify the line: dataname='data/TestBrackets.csv', Let’s now take a look at some of the output and how it relates to our trade scenarios.

Market Entry, Stop Loss and Take Profit

The first trade completed produces the following output:
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-01-12, Status 2: Ref: 1, Size: 1, Price: NA
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-01-12, Status 2: Ref: 2, Size: -1, Price: 150.0
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-01-12, Status 2: Ref: 3, Size: -1, Price: 250.0
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-01-12, Status 4: Ref: 1, Size: 1, Price: NA
Created: 2018-01-11 23:59:59.999989 Price: 200.0 Size: 1
--------------------------------------------------------------------------------
2018-01-12: Close: $210.0, Position Size: 1
2018-01-13: Close: $220.0, Position Size: 1
2018-01-14: Close: $230.0, Position Size: 1
2018-01-15: Close: $240.0, Position Size: 1
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-01-16, Status 4: Ref: 3, Size: -1, Price: 250.0
Created: 2018-01-11 23:59:59.999989 Price: 250.0 Size: -1
--------------------------------------------------------------------------------
--------------------------------  NOTIFY ORDER  --------------------------------
Order Canceled
2018-01-16, Status 5: Ref: 2, Size: -1, Price: 150.0
--------------------------------  NOTIFY TRADE  --------------------------------
2018-01-16 23:59:59.999989, Close Price: 210.0, Profit, Gross 40.0, Net 40.0
--------------------------------------------------------------------------------
We can see that all 3 orders are accepted at the same time. The first order Ref: 1 has a Price: NA. This is our Market Order. You will also see that this order also is the only order which has Completedon the same bar that the orders are Accepted. Following this, our position is open for a few bars until the price rises to $250 and our take profit is triggered. You can see that Ref: 3is Completed and Ref: 2is Canceled automatically at the same time. Lovely!

Stop Entry, Stop Loss and Take Profit

Our second example uses stop order for entry and produces the following output:
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-01-22, Status 2: Ref: 4, Size: -1, Price: 200
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-01-22, Status 2: Ref: 5, Size: 1, Price: 250
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-01-22, Status 2: Ref: 6, Size: 1, Price: 150
2018-01-22: Close: $290.0, Position Size: 0
2018-01-23: Close: $280.0, Position Size: 0
2018-01-24: Close: $270.0, Position Size: 0
2018-01-25: Close: $260.0, Position Size: 0
2018-01-26: Close: $250.0, Position Size: 0
2018-01-27: Close: $240.0, Position Size: 0
2018-01-28: Close: $230.0, Position Size: 0
2018-01-29: Close: $220.0, Position Size: 0
2018-01-30: Close: $210.0, Position Size: 0
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-01-31, Status 4: Ref: 4, Size: -1, Price: 200
Created: 2018-01-21 23:59:59.999989 Price: 200 Size: -1
--------------------------------------------------------------------------------
2018-01-31: Close: $200.0, Position Size: -1
2018-02-01: Close: $190.0, Position Size: -1
2018-02-02: Close: $180.0, Position Size: -1
2018-02-03: Close: $170.0, Position Size: -1
2018-02-04: Close: $160.0, Position Size: -1
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-02-05, Status 4: Ref: 6, Size: 1, Price: 150
Created: 2018-01-21 23:59:59.999989 Price: 150 Size: 1
--------------------------------------------------------------------------------
--------------------------------  NOTIFY ORDER  --------------------------------
Order Canceled
2018-02-05, Status 5: Ref: 5, Size: 1, Price: 250
--------------------------------  NOTIFY TRADE  --------------------------------
2018-02-05 23:59:59.999989, Close Price: 200.0, Profit, Gross 50.0, Net 50.0
--------------------------------------------------------------------------------
The key difference here is that all 3 orders are Accepted but none of them are Completed. We create the order, the price is quite high. Following this, the price gradually drops until the stop entry order is triggered at $200. Notice how the stoppricewas set at $250 but did not trigger when the price was above $250 or passing through it. This is because the order was not valid until the mainside (the stop entry) order was completed. That is the beauty of bracket orders.

Limit Entry, Stop Loss and Take Profit

Our third example uses a limit order for entry and produces the following output:
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-02-12, Status 2: Ref: 7, Size: -1, Price: 290
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-02-12, Status 2: Ref: 8, Size: 1, Price: 340
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-02-12, Status 2: Ref: 9, Size: 1, Price: 240
2018-02-12: Close: $120.0, Position Size: 0
2018-02-13: Close: $130.0, Position Size: 0
2018-02-14: Close: $140.0, Position Size: 0
2018-02-15: Close: $150.0, Position Size: 0
2018-02-16: Close: $160.0, Position Size: 0
2018-02-17: Close: $170.0, Position Size: 0
2018-02-18: Close: $180.0, Position Size: 0
2018-02-19: Close: $190.0, Position Size: 0
2018-02-20: Close: $200.0, Position Size: 0
2018-02-21: Close: $210.0, Position Size: 0
2018-02-22: Close: $220.0, Position Size: 0
2018-02-23: Close: $230.0, Position Size: 0
2018-02-24: Close: $240.0, Position Size: 0
2018-02-25: Close: $250.0, Position Size: 0
2018-02-26: Close: $260.0, Position Size: 0
2018-02-27: Close: $270.0, Position Size: 0
2018-02-28: Close: $280.0, Position Size: 0
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-03-01, Status 4: Ref: 7, Size: -1, Price: 290
Created: 2018-02-11 23:59:59.999989 Price: 290 Size: -1
--------------------------------------------------------------------------------
2018-03-01: Close: $290.0, Position Size: -1
2018-03-02: Close: $300.0, Position Size: -1
2018-03-03: Close: $290.0, Position Size: -1
2018-03-04: Close: $280.0, Position Size: -1
2018-03-05: Close: $270.0, Position Size: -1
2018-03-06: Close: $260.0, Position Size: -1
2018-03-07: Close: $250.0, Position Size: -1
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-03-08, Status 4: Ref: 9, Size: 1, Price: 240
Created: 2018-02-11 23:59:59.999989 Price: 240 Size: 1
--------------------------------------------------------------------------------
--------------------------------  NOTIFY ORDER  --------------------------------
Order Canceled
2018-03-08, Status 5: Ref: 8, Size: 1, Price: 340
--------------------------------  NOTIFY TRADE  --------------------------------
2018-03-08 23:59:59.999989, Close Price: 290.0, Profit, Gross 50.0, Net 50.0
--------------------------------------------------------------------------------
The sequence of events when using a limitorder for entry does not change at all from the stoporder. The only difference is that the price must move up to the target entry price (because we want to go short) instead of down through it. In other words, the price we enter at, must be better than what the price was when the order was created.

Invalid Settings

Our final order uses invalid settings. Specially:
  • We provide an incorrect exectype. It should be Limitbut we entered Stop
  • Our stopprice value is set below our entry price (incorrect for short)
  • The limitprice is set above our entry price (incorrect for short)
Let’s take a look at what happened:
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-03-24, Status 2: Ref: 10, Size: -1, Price: 290
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-03-24, Status 2: Ref: 11, Size: 1, Price: 90
--------------------------------  NOTIFY ORDER  --------------------------------
Order Accepted
2018-03-24, Status 2: Ref: 12, Size: 1, Price: 300
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-03-24, Status 4: Ref: 10, Size: -1, Price: 290
Created: 2018-03-23 23:59:59.999989 Price: 290 Size: -1
--------------------------------------------------------------------------------
2018-03-24: Close: $120.0, Position Size: -1
--------------------------------  NOTIFY ORDER  --------------------------------
Order Completed
2018-03-25, Status 4: Ref: 11, Size: 1, Price: 90
Created: 2018-03-23 23:59:59.999989 Price: 90 Size: 1
--------------------------------------------------------------------------------
--------------------------------  NOTIFY ORDER  --------------------------------
Order Canceled
2018-03-25, Status 5: Ref: 12, Size: 1, Price: 300
--------------------------------  NOTIFY TRADE  --------------------------------
2018-03-25 23:59:59.999989, Close Price: 120.0, Profit, Gross -10.0, Net -10.0
--------------------------------------------------------------------------------
Finishing off the examples, we can see here that our entry order is Completedimmediately. This is because the price of the asset is already below our stopentry level. After this order is Completed, our stop loss is immedaitely Completed on the next bar. The order became active when our entry order Completedand is then also filled immediately because the current price is above the stopprice that we set. (Note: Remember this is a short 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

0xb90252f1a0af77a43c499102be8a08ce5e190e01

Le3ykk29k2TjD3ZFoEyWTFzJgUu9Q9v6Fq