For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

Help with Stop-Loss



  • Hi there!

    I've been reading documentation and articles about a Stop-loss in Backtrader since the start of the week, and to be honest, it's far away from my mind.

    I've tried to follow by examples but something wrong happened and my strategy sells shares into the minus. Please help with understanding the Stop-loss realization in Backtrader.

    Here is my code:

    import pandas as pd
    import numpy as np
    
    import aiomoex
    import asyncio
    
    import plotly
    import matplotlib.pyplot as plt
    import matplotlib.pylab as pylab
    %matplotlib inline
    
    import backtrader as bt
    
    from datetime import datetime
    
    """
    Download data from MOEX.
    """
    async def load_candles(security_tiker, candle_interval, start_date, end_date):
        async with aiomoex.ISSClientSession():
            data = await aiomoex.get_market_candles(security=security_tiker, interval=candle_interval, start=start_date, end="2020-06-23", market='shares', engine='stock')
            security_df = pd.DataFrame(data)
        return security_df
    
    # set variables for downloading equities
    start_date = "2019-07-01"
    end_date = "2020-07-01"
    candle_interval = 1
        #1 - 1 minute
        #10 - 10 minutes
        #60 - 1 hour
        #24 - 1 day
        #7 - 1 week
        #31 - 1 month
        #4 - 1 quater
    
    benchmark = await load_candles("FXIT", candle_interval, start_date, end_date)
    test_security = await load_candles("FXIT", candle_interval, start_date, end_date)
    
    # do Backtrader-like data
    test_security_backtrader = test_security.filter(['end','open','high', 'low', 'close', 'value'])
    test_security_backtrader.columns = ['date','open','high', 'low', 'close', 'volume']
    test_security_backtrader['openInterest'] = 0
    
    test_security_backtrader['date'] = test_security_backtrader.astype('datetime64[ns]')
    
    
    '''
    Test trading strategy with Backtrader
    '''
    class MyStrategy(bt.Strategy):
        # list of parameters which are configurable for the strategy
        params = (('EMA_fast_period', 26),
                  ('EMA_slow_period', 287),
                  ('trail', 0.02)
                 )
    
        def __init__(self):
            # keep track of pending orders/buy price/buy commission
            self.order = None
            self.price = None
            self.comm = None
            
            # add an indicators
            self.EMA_fast = bt.indicators.EMA(self.data, period=self.p.EMA_fast_period)
            self.EMA_slow = bt.indicators.EMA(self.data, period=self.p.EMA_slow_period)
            self.Trend = bt.ind.CrossOver(self.EMA_fast, self.EMA_slow)  # crossover signal
        
        def log(self, txt):
            #Logging function
            dt = self.datas[0].datetime.datetime(0)
            print('%s, %s' % (dt.strftime('%Y-%m-%d %H:%M:%S'), txt))
        
        def notify_order(self, order):
            if order.status in [order.Submitted, order.Accepted]:
                # order already submitted/accepted - no action required
                return
    
            # report executed order
            if order.status in [order.Completed]:
                if order.exectype in [bt.Order.StopTrail]:
                    self.log(f'STOP-LOSS EXECUTED')
                else:
                    if order.isbuy():
                        self.log(f'BUY EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}')
                        self.price = order.executed.price
                        self.comm = order.executed.comm
                    else:
                        self.log(f'SELL EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}')
            
            # report failed order
            elif order.status in [order.Canceled, order.Margin, order.Rejected]:
                self.log('Order Failed')
    
            # set no pending order
            self.order = None    
        
        def next(self):
            if not self.position:  # not in the market
                if self.Trend > 0:  # if fast crosses slow to the upside it's UP trend
                    if self.order: # do nothing if an order is pending
                        return
                        
                    # calculate the max number of shares ('all-in')
                    size = int(self.broker.getcash() / self.data.open)
                    # buy order
                    self.order = self.buy(size=size) # enter long
                    #notificate about buy_order creation
                    self.log(f'BUY CREATED --- Size: {size}, Cash: {self.broker.getcash():.2f}, Open: {self.datas[0].open[0]}, Close: {self.datas[0].close[0]}')
               
                    '''
                    There is a STOP-LOSS
                    '''
                    self.stop_loss = self.sell(exectype=bt.Order.StopTrail, trailamount=self.p.trail)
                    
                    
            if self.position:  # in the market    
                # Selling by signals
                if self.Trend < 0: # cross to the downside it's DOWN trend
                    #sell order
                    self.order = self.sell(size=self.position.size)
                    #notificate about sell_order creation
                    self.log(f'SELL CREATED --- Size: {self.position.size}')
    
    
    # download data
    data = bt.feeds.PandasData(dataname=test_security_backtrader, 
                               datetime='date',
                               timeframe=bt.TimeFrame.Minutes, #set our timeframe of our data
                               compression=60) #compress timeframe to hours
    
    # create a Cerebro entity
    cerebro = bt.Cerebro(stdstats = True)
    
    # set up the backtest
    #cerebro.adddata(data)
    cerebro.resampledata(data,
                         timeframe=bt.TimeFrame.Minutes,
                         compression=60)
    cerebro.broker.setcash(600000.0)
    cerebro.addstrategy(MyStrategy)
    cerebro.addobserver(bt.observers.Value)
    
    # run backtest
    print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
    cerebro.run()
    print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')
    
    # plot results
    pylab.rcParams['figure.figsize'] = 12, 12  # that's image size for this interactive session
    pylab.rcParams['figure.facecolor'] = '#2D3642' 
    cerebro.plot(style='bars', iplot=True, volume=False)
    

    And as a result, we can see the strange thing with negative sell orders. What's wrong with Stop-loss?

    Starting Portfolio Value: 600000.00
    2019-08-13 17:00:00, BUY CREATED --- Size: 120, Cash: 600000.00, Open: 4994.0, Close: 5093.0
    2019-08-13 18:00:00, Order Failed
    2019-08-13 18:00:00, Order Failed
    2019-08-16 17:00:00, BUY CREATED --- Size: 119, Cash: 600000.00, Open: 5041.0, Close: 5055.0
    2019-08-16 18:00:00, Order Failed
    2019-08-16 18:00:00, STOP-LOSS EXECUTED
    2019-09-16 14:00:00, SELL CREATED --- Size: -1
    2019-09-16 15:00:00, SELL EXECUTED --- Price: 5048.00, Cost: -5048.00, Commission: 0.00
    2019-10-02 11:00:00, SELL CREATED --- Size: -2
    2019-10-02 12:00:00, SELL EXECUTED --- Price: 5058.00, Cost: -10116.00, Commission: 0.00
    2019-10-21 18:00:00, SELL CREATED --- Size: -4
    2019-10-21 23:59:59, SELL EXECUTED --- Price: 5060.00, Cost: -20240.00, Commission: 0.00
    2019-10-22 12:00:00, SELL CREATED --- Size: -8
    2019-10-22 13:00:00, SELL EXECUTED --- Price: 5068.00, Cost: -40544.00, Commission: 0.00
    2019-10-22 23:59:59, SELL CREATED --- Size: -16
    2019-10-23 11:00:00, SELL EXECUTED --- Price: 5029.00, Cost: -80464.00, Commission: 0.00
    2020-02-26 17:00:00, SELL CREATED --- Size: -32
    2020-02-26 18:00:00, SELL EXECUTED --- Price: 5873.00, Cost: -187936.00, Commission: 0.00
    Final Portfolio Value: 491674.98
    


  • From your log, the buy orders fail, the stop trail orders are being created. Without a size provided, it will use the set sizer. So it will set the size to 1. When size is 1, it will next time add the amount to the sell pos 1+1 = 2 + 2 = 4 + 4 = 8 ... and so on. To fix that you may consider using a buy_bracket or check if the buy order is created and then create a stop trail oder with the size of the buy order.



  • I think you might have a few issues at play:

    First, you are setting the buy size of your trade to be the maximum possilbe.

    size = int(self.broker.getcash() / self.data.open)
    

    This is problematic because in an upward trending market, the total cash you need will be greater than what you have, resulting in a 'margin' failure of the order. To set up your algorithm, use a trade value much smaller than the market value, say 50%. You can adjust the buffer later. Some people will use the volatility or trading range to assist in setting the buffer. I typically will just you 2.5% or so and see if I get stopped out.

    Second, you have two sell orders possible for every trade. So you need to have better verification if the trade should happen. When selling if there is a crossover down, you could use close instead of sell. This will only close an open position for the amount it is open. See here.

    if self.position:  # in the market    
        if self.Trend < 0:  # cross to the downside it's DOWN trend
            self.order = self.close()
    

    Next, your sell stop trail has a very small value (0.02) compared to the size of unit price (5000). This means you are definitly going to get stopped out right away just with regular volatility.

    The final issue is that you need to be able to kill the stop trail order if your sell due to crossdown occurs. As it stands, if you sell due to the cross down, your stop sell is still out there waiting to be triggered.

    You can solve this by cancelling the order when you sell by crossdown.

    if self.position:  # in the market    
        if self.Trend < 0:  # cross to the downside it's DOWN trend
            self.order = self.close()
            self.cancel(self.stop_loss)
    


  • @run-out thanks!

    I had heeded your advice. It taked me several days for studying documentation and at the end of the days, I've implemented sizer and bracket orders. Now it's better.

    Thank you for the bracket feature that is really powerful.


Log in to reply
 

});