How to add takeprofit / stoploss?



  • Hello,

    In my broker when sending an order, I can set stop-loss and take-profit levels on it. Such that e.g. I can send a limit buy order, with stoploss and takeprofit levels and forget about it. It will either remain open, or close with profit/loss. Simulating this with backtrader appears to result in errors.

    In order notification, if buy order is executed I execute two sell orders: one limit order with stop loss, and one stop order with take profit. This eventually fails and results in stategy executing both of the sell orders. Ideally if one of these sell orders is executed, i would like the other one to be instantly killed.

    How should I code takeprofit/stoploss on my positions?

    Should I extend the bbroker itself to support stoploss/takeprofit?

    Regards,

    Dimitri.


  • administrators

    "Simulating this with backtrader appears to result in errors."

    backtrader is only doing what you are telling it to do. That isn't an error.

    In order notification, if buy order is executed I execute two sell orders: one limit order with stop loss, and one stop order with take profit. This eventually fails and results in strategy executing both of the sell orders. Ideally if one of these sell orders is executed, i would like the other one to be instantly killed.

    It doesn't fail. It simply doesn't fulfill your own expectations (it is nowhere stated in the documentation that one order will be cancelled because the other is executed)

    You are looking for OCO (Order Cancels Order) functionality and this is not implemented in backtrader

    How should I code takeprofit/stoploss on my positions?

    In the absence of OCO, a dictionary can do the trick, by adding two entries. In one the stop-loss order is the key and the take-profit order is the value. In the second entry the roles are reversed.

    Upon being notified of the execution of one order, inspection of the dictionary allows cancelling the pending order

    Should I extend the bbroker itself to support stoploss/takeprofit?

    The platform is open.



  • Hi Guys,

    I used dictionary with orders to implement stoploss/takeprofit in a strategy following the advice above and it works but I am facing an issue in a specific scenario.

    If both limit and stop orders execution prices are between high and low on the next bar, they both get executed and I am unable to cancel the other one.

    # excerpt from notify_order
    ...
            elif order.status == order.Completed:
                if 'name' in order.info:
                    self.broker.cancel(self.order_dict[order.ref])                           
                else:
                    if order.isbuy():
                        stop_loss = order.executed.price*(1.0 - (self.p.stoploss)/100.0)
                        take_profit = order.executed.price*(1.0 + 3*(self.p.stoploss)/100.0)
    
                        sl_ord = self.sell(exectype=bt.Order.Stop,
                                           price=stop_loss)
                        sl_ord.addinfo(name="Stop")
    
                        tkp_ord = self.sell(exectype=bt.Order.Limit,
                                            price=take_profit)
                        tkp_ord.addinfo(name="Prof")
    
                        self.order_dict[sl_ord.ref] = tkp_ord
                        self.order_dict[tkp_ord.ref] = sl_ord
    ...
    

    Here is an execution log.

    Starting Portfolio Value: 10000.00
    SignalPrice: 1155.12 Buy: 1155.17, Stop: 1154.01, Prof: 1158.64
    Open: 1 2002-01-10 1155.17 1.00 0.00
    TRADE,OPEN,2002-01-10,1155.17,0.0,0.0
    Stop: 2 2002-01-10 1154.01 -1.00 0.00
    TRADE PROFIT, GROSS -1.16, NET -1.16
    CANCELL,Prof,3,2002-01-10,0.0,0,0.0
    SignalPrice: 1132.83 Buy: 1132.94, Stop: 1131.81, Prof: 1136.34
    Open: 4 2002-01-17 1132.94 1.00 0.00
    TRADE,OPEN,2002-01-17,1132.94,0.0,0.0
    Stop: 5 2002-01-17 1131.81 -1.00 0.00
    TRADE PROFIT, GROSS -1.13, NET -1.13
    CANCELL,Prof,6,2002-01-17,0.0,0,0.0
    SignalPrice: 1119.31 Buy: 1120.13, Stop: 1119.01, Prof: 1123.49
    Open: 7 2002-01-23 1120.13 1.00 0.00
    TRADE,OPEN,2002-01-23,1120.13,0.0,0.0
    Stop: 8 2002-01-23 1119.01 -1.00 0.00
    Prof: 9 2002-01-23 1123.49 -1.00 0.00
    TRADE PROFIT, GROSS -1.12, NET -1.12
    TRADE,OPEN,2002-01-23,-1123.49039,0.0,0.0
    

    And the corresponding data:
    On first bar is getting the signal, on second bar is buy with market order and on third bar is executing both stop and limit orders.

    2002-01-22 16:00:00,1119.34,1119.42,1119.3,1119.31,0,1119.31
    2002-01-23 09:00:00,1120.13,1123.39,1120.11,1120.55,0,1120.55
    2002-01-23 10:00:00,1120.62,1124.16,1117.43,1124.16,0,1124.16
    

    BR,
    dpetrov


  • administrators

    That case needs real OCO emulation (as real as you can get in an emulation), because both orders meet the condition during the same bar evaluation in the broker.



  • To correctly backtest such orders you need to use lower data time frame.



  • In my playground profit_percents calculates in Super of this strategy. Then strategy check:

    if 3 < profit_percents or profit_percents < -1:
        self.close() # sell/buy according to open order sign
    

    Nice idea about dictionary in orders.



  • @Maxim-Korobov

    Good idea, but I think this apporach will have 1 bar delay.

    According to the docs:

    • Stop and Limit orders will execute as soon as the price touches the order price
    • Close will execute using the close price of the next barwhen the next bar actually CLOSES

    I am going to test it today and compare.

    Otherwise I can use data with higher frequency as mentioned by @ab_trader.
    Is this going to change the strategy decisions if it is based on technical indicators?

    BR,
    dpetrov



  • If you use higher frequency to simulate execution of stop loss and take profit orders, and use your regular timeframe for indicators, than your base line data will be higher frequency data and you will use data resampling to get new data set for indicator calculations.

    Data resampling - https://www.backtrader.com/docu/data-resampling/data-resampling.html

    Also I was thinking about another approach without going to lower timeframes - check execution of the stop loss first, and then check execution of the take profit. This will be conservative, and I believe some additional coding be required, it will not be simple order sending.



  • @dimitar-petrov said in How to add takeprofit / stoploss?:

    excerpt from notify_order

    ...
    elif order.status == order.Completed:
    if 'name' in order.info:
    self.broker.cancel(self.order_dict[order.ref])
    else:
    if order.isbuy():
    stop_loss = order.executed.price*(1.0 - (self.p.stoploss)/100.0)
    take_profit = order.executed.price*(1.0 + 3*(self.p.stoploss)/100.0)

                    sl_ord = self.sell(exectype=bt.Order.Stop,
                                       price=stop_loss)
                    sl_ord.addinfo(name="Stop")
    
                    tkp_ord = self.sell(exectype=bt.Order.Limit,
                                        price=take_profit)
                    tkp_ord.addinfo(name="Prof")
    
                    self.order_dict[sl_ord.ref] = tkp_ord
                    self.order_dict[tkp_ord.ref] = sl_ord
    

    ...

    Can you please share the complete code



  • @Usct here you go,

    from __future__ import (absolute_import, division, print_function,
                            unicode_literals)
    
    
    import backtrader as bt
    import backtrader.indicators as btind
    
    
    class BBands(bt.Strategy):
        params = (
            ('stoploss', 0.001),
            ('profit_mult', 2),
            ('prdata', False),
            ('prtrade', False),
            ('prorder', False),
        )
    
        def __init__(self):
            # import ipdb; ipdb.set_trace()
            self.bbands = bt.talib.BBANDS(self.data,
                                          timeperiod=25,
                                          plotname='TA_BBANDS')
    
            self.order_dict = {}
            self.buy_sig = btind.CrossOver(self.data, self.bbands.line2)
            self.sell_sig = btind.CrossOver(self.bbands.line0, self.data)
            self.trades = 0
            self.order = None
    
        def start(self):
            self.trades = 0
    
        def next(self):
            if self.position:
                return
            else:
                if self.buy_sig > 0:
                    # import ipdb; ipdb.set_trace()
                    self.buy(exectype=bt.Order.Market)
                    self.last_sig_price = self.data.close[0]
                    self.trades += 1
                elif self.sell_sig > 0:
                    self.sell(exextype=bt.Order.Market)
                    self.trades += 1
    
                    if self.p.prdata:
                        print(','.join(str(x) for x in
                        ['DATA', 'OPEN',
                            self.data.datetime.date().isoformat(),
                            self.data.close[0],
                            self.buy_sig[0]]))
    
    
        def notify_order(self, order):
            # import ipdb; ipdb.set_trace()
            if order.status in [order.Margin, order.Rejected]:
                pass
            elif order.status == order.Cancelled:
                if self.p.prorder:
                    print(','.join(map(str, [
                        'CANCELL', order.info['OCO'], order.ref,
                        self.data.num2date(order.executed.dt).date().isoformat(),
                        order.executed.price,
                        order.executed.size,
                        order.executed.comm,
                    ]
                    )))
            elif order.status == order.Completed:
                if 'name' in order.info:
                    self.broker.cancel(self.order_dict[order.ref])
                    self.order = None
                    if self.p.prorder:
                        print("%s: %s %s %.2f %.2f %.2f" %
                            (order.info['name'], order.ref,
                            self.data.num2date(order.executed.dt).date().isoformat(),
                            order.executed.price,
                            order.executed.size,
                            order.executed.comm))
                else:
                    if order.isbuy():
                        stop_loss = order.executed.price*(1.0 - (self.p.stoploss))
                        take_profit = order.executed.price*(1.0 + self.p.profit_mult*(self.p.stoploss))
    
                        sl_ord = self.sell(exectype=bt.Order.Stop,
                                           price=stop_loss)
                        sl_ord.addinfo(name="Stop")
    
                        tkp_ord = self.sell(exectype=bt.Order.Limit,
                                            price=take_profit)
                        tkp_ord.addinfo(name="Prof")
    
                        self.order_dict[sl_ord.ref] = tkp_ord
                        self.order_dict[tkp_ord.ref] = sl_ord
    
                        if self.p.prorder:
                            print("SignalPrice: %.2f Buy: %.2f, Stop: %.2f, Prof: %.2f"
                                  % (self.last_sig_price,
                                     order.executed.price,
                                     stop_loss,
                                     take_profit))
    
                    elif order.issell():
                        stop_loss = order.executed.price*(1.0 + (self.p.stoploss))
                        take_profit = order.executed.price*(1.0 - 3*(self.p.stoploss))
    
                        sl_ord = self.buy(exectype=bt.Order.Stop,
                                          price=stop_loss)
                        sl_ord.addinfo(name="Stop")
    
                        tkp_ord = self.buy(exectype=bt.Order.Limit,
                                            price=take_profit)
                        tkp_ord.addinfo(name="Prof")
    
                        self.order_dict[sl_ord.ref] = tkp_ord
                        self.order_dict[tkp_ord.ref] = sl_ord
    
                    if self.p.prorder:
                        print("Open: %s %s %.2f %.2f %.2f" %
                            (order.ref,
                             self.data.num2date(order.executed.dt).date().isoformat(),
                            order.executed.price,
                            order.executed.size,
                            order.executed.comm))
    
        def notify_trade(self, trade):
            # import ipdb; ipdb.set_trace()
            if self.p.prtrade:
                if trade.isclosed:
                    print('TRADE PROFIT, GROSS %.2f, NET %.2f' %
                        (trade.pnl, trade.pnlcomm))
                elif trade.justopened:
                            print(','.join(map(str, [
                                'TRADE', 'OPEN',
                                self.data.num2date(trade.dtopen).date().isoformat(),
                                trade.value,
                                trade.pnl,
                                trade.commission,
                            ]
                            )))
    
        def stop(self):
            print('(Stop Loss Pct: %2f, S/P Multiplier: %2f) Ending Value %.2f (Num Trades: %d)' %
                     (self.params.stoploss, self.params.profit_mult, self.broker.getvalue(), self.trades))
    


  • Thanks a lot!



  • CAn this be achieved with bracket orders?


  • administrators

    That's the point of bracket orders which exactly implement this concept. But beware: only Interactive Brokers has native support for it.



  • @backtrader So in the case of Oanda, i would have to implement the bracket orders manually like so?

    (From your code:)

    mainside = self.buy(price=13.50, exectype=bt.Order.Limit, transmit=False)
    lowside  = self.sell(price=13.00, size=mainsize.size, exectype=bt.Order.Stop,
                         transmit=False, parent=mainside)
    highside = self.sell(price=14.00, size=mainsize.size, exectype=bt.Order.Limit,
                         transmit=True, parent=mainside)

  • administrators

    No. It means (unless it is somewhere well hidden in the docs) that model is not supported with Oanda.

    There may be something similar by directly specifying the prices, but it will have to be looked into.


  • administrators

    You may play with this commit:

    It uses the built-in stoploss and takeprofit price attributes of an order in Oanda to make a complete Bracket order simulation.

    • You can use buy_bracket / sell_bracket or the manual pattern from above (the former two would be recommended)

      The execution type for the stop-side and profit-side cannot be set. They are controlled by Oanda. Only the prices can be set.

      You can still set the execution type for the main-side (Market, Limit, ...)

    Even if Oanda manages everything as a single order, inside backtrader will be 3 orders and cancellation of any order will cancel the others. There will be individual notifications for each of the orders.

    The commit adds also

    • StopTrail support for Oanda


  • @backtrader Oh man, you're awsome. Thanks for adding that code so quickly.


Log in to reply
 

Looks like your connection to Backtrader Community was lost, please wait while we try to reconnect.