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 toself.sl_price
tp_price
is changed toself.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)
-
The latest commit on the
development
branch contains support forStopTrail
andStopTrailLimit
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 oftrailamount
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 aMarket
order.The same concepts can be applied with
StopTrailLimit
asexectype
. In this case one has to also specifyplimit
, just like with aStopLimit
order. Once the stop price is reached, the execution takes place as aLimit
order.Commit: https://github.com/mementum/backtrader/commit/cb2dcecf2b055f4669531a059d2dc7a7a93b8430
-
@ab_trader Thankyou for sharing, your information and code is verry help me