Trailing stop loss



  • Hi,

    I am very new to BackTrader. I have worked through the docs and I am specifically looking for a way to implement a trailing stop loss. There does not seem to be a way to do this or am I missing a subtlety here?

    What I want to do is place a long order, that has a constant trailing value. e.g. Buy at $1.75 with $0.10 trailing loss. If the stock goes to say $2.15, the exit stop loss will update to exit at $2.05. If the stock drop to $2.05 or below, the trade automatically exists with a sell and I am notified.

    If it exists, could you point me to the doc?

    Thanks
    Des



  • @DesHartman I didn't find it as a built-in function, so it is one of my tasks. I plan to share my code as soon as it will be ready.

    Idea: send stop order after the main order is executed. Then cancel and send new stop order in the 'strategy.next ()'.



  • knowing that IB directly supports trailing order I am also interested in this.



  • @ab_trader That would be fantastic

    Most on-line brokers allow you to place a stop loss at the time of the order, so they execute it and notify you. I guess to simulate this, your approach would then work by updating the exit in the 'strategy.next()' method on each tick.

    Looking forward to seeing the code ;-)



  • @ab_trader any progress? :-)



  • I spent some time looking at this recently as well. I am wondering if we should not consider adopting the IB Python API code as an option in Backtrader to do this. I recognize that anyone using it would have to use Python3, which is fine by me but may not be for everyone. Therefore, would probably need to be an option.

    Just a thought



  • Here is the code that I came up with. I shown only pieces related to the stop loss and take profit orders processing. The code was developed for back testing only, I am not sure if it will be suitable for real-time trading.

    Potentially this code can be improved, any suggestions are welcomed!

    class MyStrategy(bt.Strategy):
    
        def __init__(self):
    
            # init stop loss and take profit order variables
            self.sl_order, self.tp_order = None, None
    
        def notify_trade(self, trade):
           
            if trade.isclosed:
                # clear stop loss and take profit order variables for no position state
                if self.sl_order:
                    self.broker.cancel(self.sl_order)
                    self.sl_order = None
    
                if self.tp_order:
                    self.broker.cancel(self.tp_order)
                    self.tp_order = None
    
        def next(self):
    			
            # process stop loss and take profit signals
            if self.position:
    
    	    # set stop loss and take profit prices
                # in case of trailing stops stop loss prices can be assigned based on current indicator value
                price_sl_long = self.position.price * 0.98
                price_sl_short = self.position.price * 1.02
                price_tp_long = self.position.price * 1.06
                price_tp_short = self.position.price * 0.94
    
                # cancel existing stop loss and take profit orders
                if self.sl_order:
                    self.broker.cancel(self.sl_order)
    
                if self.tp_order:
                    self.broker.cancel(self.tp_order)
    
                # check & update stop loss order
                sl_price = 0.0
                if self.position.size > 0 and price_sl_long !=0: sl_price = price_sl_long
                if self.position.size < 0 and price_sl_short !=0: sl_price = price_sl_short
    
                if sl_price != 0.0:
                    self.sl_order = self.order_target_value(target=0.0, exectype=bt.Order.Stop, price=sl_price)
                
                # check & update take profit order
                tp_price = 0.0
                if self.position.size > 0 and price_tp_long !=0: tp_price = price_tp_long
                if self.position.size < 0 and price_tp_short !=0: tp_price = price_tp_short
    
                if tp_price != 0.0:
                    self.tp_order = self.order_target_value(target=0.0, exectype=bt.Order.Limit, price=tp_price)
    


  • Code above can be slightly modified in order to plot stop loss and take profit levels on the price diagram. Stop loss and take profit prices need to be defined as class functions:
    sl_price is changed to self.sl_price
    tp_price is changed to self.tp_price

    Then the following observer can be added:

    class SLTPTracking(bt.Observer):
    
        lines = ('stop', 'take')
    
        plotinfo = dict(plot=True, subplot=False)
    
        plotlines = dict(stop=dict(ls=':', linewidth=1.5),
                         take=dict(ls=':', linewidth=1.5))
    
        def next(self):
    
            if self._owner.sl_price != 0.0:
                self.lines.stop[0] = self._owner.sl_price
    
            if self._owner.tp_price != 0.0:
                self.lines.take[0] = self._owner.tp_price
    

    And then final plot will show stop loss and take profit levels: Picture



  • Cool thanks @ab_trader. But you think this wouldn't work in reality? How come?

    Would be great to get a solution which works in RL.

    PS: do you have the full code for which you ran this on?

    Thanks!



  • I am not familiar with real trading approach using bt. @RandyT probably can tell more on this.

    Below is full code for sample trading system, implementing fixed stop loss and take profit orders.

    from __future__ import (absolute_import, division, print_function, unicode_literals)
    
    import datetime as dt
    import argparse
    
    import backtrader as bt
    
    def parse_args(pargs=None):
    
        parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
                                         description=('Strategy auto-generation framework'))
    
        parser.add_argument('--data', required=False, default='SPY',
                            metavar='TICKER', help='Yahoo ticker to download')
    
        parser.add_argument('--data_from', required=False, default='2015-01-01',
                            metavar='YYYY-MM-DD[THH:MM:SS]',
                            help='Date[time] in YYYY-MM-DD[THH:MM:SS] format')
    
        parser.add_argument('--data_to', required=False, default='2016-12-31',
                            metavar='YYYY-MM-DD[THH:MM:SS]',
                            help='Ending date[time]')
    
        parser.add_argument('--plot', required=False, default='',
                            nargs='?', const='volume=True',
                            help='kwargs in key=value format')
    
        return parser.parse_args(pargs)
    
    # script arguments parsing
    args = parse_args(None)
    
    # quotes feed for one ticker
    data = bt.feeds.YahooFinanceCSVData(dataname=args.data+'.csv',
                                        fromdate=dt.datetime.strptime(args.data_from, '%Y-%m-%d'),
                                        todate=dt.datetime.strptime(args.data_to, '%Y-%m-%d'),
                                        reverse=True, adjclose=False, plot=True)
    
    # observer showing stop loss and take profit levels
    class SLTPTracking(bt.Observer):
    
        lines = ('stop', 'take')
    
        plotinfo = dict(plot=True, subplot=False)
    
        plotlines = dict(stop=dict(ls=':', linewidth=1.5),
                         take=dict(ls=':', linewidth=1.5))
    
        def next(self):
    
            if self._owner.sl_price != 0.0:
                self.lines.stop[0] = self._owner.sl_price
    
            if self._owner.tp_price != 0.0:
                self.lines.take[0] = self._owner.tp_price
    
    # master strategy
    class MasterStrategy(bt.Strategy):
    
        params = (
        ('ema_period', 200),
        ('sl', 1.0), ('rr_ratio', 3), ('n', 5),
                 )
    
        def __init__(self):
    
            # init order variables
            self.order, self.sl_order, self.tp_order = None, None, None
            self.sl_price, self.tp_price = 0.0, 0.0
    
            self.ema = bt.indicators.ExponentialMovingAverage(period=int(self.p.ema_period),
                                                              subplot=False)
            
            # entry signals
            self.long = bt.indicators.CrossUp(self.data.close, self.ema)
            self.short = bt.indicators.CrossDown(self.data.close, self.ema)
    
        def notify_trade(self, trade):
    
            if trade.isclosed:
    
                # clear stop loss and take profit order variables for no position state
                if self.sl_order:
                    self.broker.cancel(self.sl_order)
                    self.sl_order = None
                    self.sl_price = 0.0
    
                if self.tp_order:
                    self.broker.cancel(self.tp_order)
                    self.tp_order = None
                    self.tp_price = 0.0
    
        def notify_order(self, order):
    
            if order.status in [order.Completed, order.Margin]:
                
                # clear order variable
                self.order = None
    
        def next(self):
    
            # process entries at bar open if not in position
            if not self.position:
                
               # check entry to long position
               if self.long[0] == True:
                    self.order = self.order_target_percent(target=1.0)
    
               # check entry to short position
               elif self.short[0] == True:
                    self.order = self.order_target_percent(target=-1.0)
                    
            # process exits and position support orders if in position
            if self.position:
    
                price_sl_long = self.position.price * (1 - self.p.sl / 100)
                price_sl_short = self.position.price * (1 + self.p.sl / 100)
    
                price_tp_long = self.position.price * (1 + self.p.sl / 100 * self.p.rr_ratio)
                price_tp_short = self.position.price * (1 - self.p.sl / 100 * self.p.rr_ratio)
                
                # cancel existing stop loss and take profit orders
                if self.sl_order:
                    self.broker.cancel(self.sl_order)
    
                if self.tp_order:
                    self.broker.cancel(self.tp_order)
    
                # check & update stop loss order
                self.sl_price = 0.0
                if self.position.size > 0 and price_sl_long !=0: self.sl_price = price_sl_long
                if self.position.size < 0 and price_sl_short !=0: self.sl_price = price_sl_short
    
                if self.sl_price != 0.0:
                    self.sl_order = self.order_target_value(target=0.0, exectype=bt.Order.Stop,
                                                            price=self.sl_price)
                # check & update take profit order
                self.tp_price = 0.0
                if self.position.size > 0 and price_tp_long !=0: self.tp_price = price_tp_long
                if self.position.size < 0 and price_tp_short !=0: self.tp_price = price_tp_short
    
                if self.tp_price != 0.0:
                    self.tp_order = self.order_target_value(target=0.0, exectype=bt.Order.Limit,
                                                            price=self.tp_price)
    
    # single backtest run
    def single_backtest():
    
        cerebro = bt.Cerebro()
        cerebro.addstrategy(MasterStrategy)
        cerebro.addobserver(SLTPTracking)
    
        cerebro.adddata(data)
        cerebro.broker.set_cash(1000000.0)
        cerebro.broker.set_coc(True)
        cerebro.run()
    
        print ('Broker value %0.2f' % cerebro.broker.getvalue())
    
        return cerebro
    
    if __name__ == '__main__':
    
        cerebro = single_backtest()
    
        if args.plot:
            cerebro.plot(style='bar', numfigs=1,
                            barup = 'black', bardown = 'black',
                            barupfill = True, bardownfill = False,
                            volup = 'green', voldown = 'red', voltrans = 50.0, voloverlay = False)
    

  • administrators

    The latest commit on the development branch contains support for StopTrail and StopTrailLimit orders.

    It is still untested. At least nothing seems to be broken. Usage pattern:

    # For a StopTrail going downwards
    self.buy(size=1, exectype=bt.Order.StopTrail, trailamount=0.25)  # last price will be used as reference
    # or
    self.buy(size=1, exectype=bt.Order.StopTrail, price=10.50, trailamount=0.25)
    
    # For a StopTrail going upwards
    self.sell(size=1, exectype=bt.Order.StopTrail, trailamount=0.25)  # last price will be used as reference
    # or
    self.sell(size=1, exectype=bt.Order.StopTrail, price=10.50, trailamount=0.25)
    

    One can also specify trailpercent instead of trailamount and the distance to the price will be calculated as a percentage of the price

    # For a StopTrail going downwards with 2% distance
    self.buy(size=1, exectype=bt.Order.StopTrail, trailpercent=0.02)  # last price will be used as reference
    # or
    self.buy(size=1, exectype=bt.Order.StopTrail, price=10.50, trailpercent=0.0.02)
    
    # For a StopTrail going upwards with 2% difference
    self.sell(size=1, exectype=bt.Order.StopTrail, trailpercent=0.02)  # last price will be used as reference
    # or
    self.sell(size=1, exectype=bt.Order.StopTrail, price=10.50, trailpercent=0.02)
    

    This behaves like a Stop order when the stop price is reached. The order is executed as if it were a Market order.

    The same concepts can be applied with StopTrailLimit as exectype. In this case one has to also specify plimit, just like with a StopLimit order. Once the stop price is reached, the execution takes place as a Limit order.

    Commit: https://github.com/mementum/backtrader/commit/cb2dcecf2b055f4669531a059d2dc7a7a93b8430


Log in to reply
 

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