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

Extra sell order



  • Hi there,

    I'm just playing with BT using the Clenows' relative momentum strategy, and I have noticed a strange behavior whereby BT sometimes generates an extra Sell order after a "close" order is placed. For example, s.close() will generate a Sell order that closes "s", and then an extra Sell order with a seemingly random size. The net result is now the system has a short position, which was never "asked" for.

    The code is below:

    from __future__ import (absolute_import, division, print_function,
                            unicode_literals)
    
    import datetime  # For datetime objects
    import os.path  # To manage paths
    import sys  # To find out the script name (in argv[0])
    from collections import defaultdict
    import numpy as np 
    from scipy.stats import linregress
    
    # Import the backtrader platform
    import backtrader as bt
    
    
    
    #Momentum indicator
    class MomentumIndicator(bt.ind.PeriodN):
        lines = ('trend', )
        params = dict(period = 90)
    
        def next(self):
            returns = np.log(self.data.get(size=self.p.period))       
            x = np.arange(len(returns))
            if (len(returns) >= self.p.period):
                slope, _, rvalue, _, _ = linregress(x, returns)
                annualized = (1 + slope) ** 252
            else:
                annualized = 0
                rvalue = 0
               # return
            if (len(self.lines.trend) >= self.p.period):
                self.lines.trend[0] = annualized * (rvalue ** 2)
    # Create a Momentum Strategy
    class MomentumStrategy(bt.Strategy):
        
        params = dict(
            momentum=MomentumIndicator,
            momentum_period=90,
    
            movav=bt.ind.SMA,
            idx_period=200,
            stock_period=100,
    
            volatr=bt.ind.ATR,
            vol_period=20, 
        )
        
        def __init__(self):
            
            self.d_with_len = []
            self.inds = defaultdict(dict)
            self.stocks = self.datas[1:]
            self.dicter = {}
            self.savedPositions = dict()
            
            # S&P500 MA
            self.idx_mav = self.p.movav(self.data0, period=self.p.idx_period)
            
            # Indicators
            for d in self.stocks:
                self.inds[d]['mom'] = self.p.momentum(d, period=self.p.momentum_period)
                self.inds[d]['mav'] = self.p.movav(d, period=self.p.stock_period)
                self.inds[d]['vol'] = self.p.volatr(d, period=self.p.vol_period)
    
        def notify_order(self, order):
            if order.status in [order.Completed]:
                self.dicter[order.ref] = None
                
                if order.isbuy():
                    if order.data._name not in self.savedPositions.keys():
                        self.savedPositions[order.data._name] = bt.num2date(order.executed.dt)
                    print('BUY EXECUTED on %s, Price: %.2f, Cost: %.2f, Comm %.2f, Datetime: %s, Size: %i' %
                            (order.data._name,
                             order.executed.price,
                             order.executed.value,
                             order.executed.comm,
                             bt.num2date(order.executed.dt),
                             order.executed.size))                    
                if order.issell():
                    if order.data._name in self.savedPositions.keys():
                        self.savedPositions.pop(order.data._name)
                    print('Sell EXECUTED on %s, Price: %.2f, Cost: %.2f, Comm %.2f, Datetime: %s, Size: %i' %
                            (order.data._name,
                             order.executed.price,
                             order.executed.value,
                             order.executed.comm,
                             bt.num2date(order.executed.dt),
                             order.executed.size))
                
        def prenext(self):
            # Populate d_with_len
            self.d_with_len = [d for d in self.datas if len(d)]
            # call next() even when data is not available for all tickers
            self.next()
    
        def nextstart(self):
            # This is called exactly ONCE, when next is 1st called and defaults to
            # call `next`
            self.d_with_len = self.datas  # all data sets fulfill the guarantees now
    
            self.next()  # delegate the work to next
    
        def next(self):
            
            # portfolio rebal every 5 days, positions rebal every 10 days
            l = len(self)
            if l % 5 == 0:
                self.rebalance_portfolio()
            if l % 10 == 0:
                self.rebalance_positions()        
           
    
        def rebalance_portfolio(self):
            # momentum ranking
            self.rankings = list(filter(lambda d: len(d) > self.p.idx_period, self.stocks))
            self.rankings.sort(key=lambda d: self.inds[d]["mom"][0])
            num_stocks = len(self.rankings)
    
            # sell stocks based on criteria
            for i, d in enumerate(self.rankings):
                #if self.getposition(d.data).size > 0:
                if self.getposition(d).size > 0:
                    if (i > num_stocks * 0.05 or d < self.inds[d]["mav"]) and self.savedPositions.get(d._name, None) != None:
                        print("try to close %s %s %i" % (d._name, datetime.datetime.strftime(self.datetime.date(ago=0) , '%Y-%m-%d'),self.getposition(d).size))
                        self.order = self.close(d)
                        
            if self.data0 < self.idx_mav:
                return
    
            # buy stocks with remaining cash
            for i, d in enumerate(self.rankings[:int(num_stocks * 0.05)]):
                cash = self.broker.get_cash()
                value = self.broker.get_value()
                if cash <= 0:
                    break            
                if not self.getposition(d).size :                
                    size = 1 / (len(self.stocks) * 0.05)                  
                    print("try to buy %s %s" % (d._name, datetime.datetime.strftime(self.datetime.date() , '%Y-%m-%d')))
                    self.order = self.order_target_percent(d, target=size, exectype=bt.Order.Close)               
                                    
        def rebalance_positions(self):
            num_stocks = len(self.rankings)
            
            if self.data0 < self.idx_mav:
                return
    
            # rebalance all stocks
            for i, d in enumerate(self.rankings[:int(num_stocks * 0.05)]):
                cash = self.broker.get_cash()
                value = self.broker.get_value()
                if cash <= 0:
                    break
                #size = value * 0.001 #/ self.inds[d]["vol"]
                size = 1 / (len(self.stocks) * 0.05)
                self.order = self.order_target_percent(d, target=size, exectype=bt.Order.Close)
    
    if __name__ == '__main__':
         # Create cerebro entity
        cerebro = bt.Cerebro()
        for symbol in ['$NDX','AAL','ADP','ALXN','BATRA','BATRK','BMRN','CRUS','CTSH','CTXS','DISCK','EXC','FISV','FLIR','FOX','GILD','HOLX','INCY','MAR','MAT','MSFT','MYL']: #,'NTAP','PRDO','ROST','SNPS','ULTA','URBN','WBA']: 
            data = bt.feeds.BacktraderCSVData(dataname="d:\\stage\\"+symbol+".csv", name=symbol)      
            cerebro.adddata(data,symbol)  
       
        # Set our desired starting cash
        cerebro.broker.set_cash(1000000)
        
        # Set the commission
        cerebro.broker.setcommission(commission=0.0001)
    
        # Add strategy
        cerebro.addstrategy(MomentumStrategy)   
        cerebro.addanalyzer(bt.analyzers.PositionsValue, headers=True, cash=True, _name='mypositions')
        
        # Print out the starting conditions
        print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    
        # Run over everything
        strats = cerebro.run()   
    
        print ("number of short positions: %i " %(len ([position for position in cerebro.broker.positions if cerebro.broker.getposition(position).size < 0]) ))
        print ("number of long positions: %i " %(len ([position for position in cerebro.broker.positions if cerebro.broker.getposition(position).size > 0]) ))
        
        #open_positions = ([position for position in cerebro.broker.positions if cerebro.broker.getposition(position).size > 0])
        for pos in cerebro.broker.positions:        
            if cerebro.broker.getposition(pos).size > 0:
                print("long %s" % (pos._name))     
                
            elif cerebro.broker.getposition(pos).size < 0 :
                print("short %s" % (pos._name) )        
                
    
        # Print out the final result
        print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
        print(datetime.datetime.strftime(datetime.datetime.today() , '%d/%m/%Y-%Hh/%Mm'))
       
    

    You will need the data, which can be downloaded here: data in CSV, and you will need to replace "D:\stage" in the code with the directory where you unzip the CSV.

    At the end of the run, there will be a short "GILD" position:

    ....
    number of short positions: 1 
    number of long positions: 1
    long FLIR
    short GILD
    ....
    

    In the debug print, at lines 302, you can see "Sell MAT" is executed twice:
    '''
    ...
    try to close MAT 2017-12-19 45683
    Sell EXECUTED on MAT, Price: 15.11, Cost: 712197.97, Comm 69.03, Datetime: 2017-12-20 23:59:59.999989, Size: -45683
    Sell EXECUTED on MAT, Price: 15.12, Cost: -37845.36, Comm 3.78, Datetime: 2017-12-20 23:59:59.999989, Size: -2503
    ...
    '''
    At you can see there is an extra order "Sell EXECUTED on MAT, Price: 15.12, Cost: -37845.36, Comm 3.78, Datetime: 2017-12-20 23:59:59.999989, Size: -2503", where does this come from? What is wrong with the code?

    Thanks,
    BrianH.



  • Nobody is curious if there is a bug with the framework or not? A "Sell" order was never issued (only "Close"), so how would the portfolio ends up with a Short position? It looks like some people actually use this in Production, wouldn't a wrong "broker" be concerning to you guys?



  • There is no evidence that this is a bug in the bt broker, but not in your script. I've used self.close() before and didn't get any excessive sell orders.



  • @ab_trader i agreed, that's why in my original post, i worded it as "what's wrong with my script". As you can see, there is no "Sell", only "Close"; but why would it generate a Short position? I realize it takes a bit of effort to download the data, and run the script. BTW, it's not the data; it's from Norgate Data (professional provider). And if i add more symbols (i.e. data feeds) to the script, BT will generate a different short - i.e a different symbol / data-feed will get shorted instead.



  • @Brian-Hoang said in Extra sell order:

    what's wrong with my script

    I believe that you should understand this by yourself, but not ask strangers in the internet. I'll give you a hint: your position management allows to open short positions in some cases, and it is not a bug of the self.close(). Actually you open short positions several times:
    2020-08-27_20-06-10.png



  • @ab_trader thanks - i'm using other frameworks (one C# and the other Python one) right now, and just evaluating BT, so learning mistakes happen.


Log in to reply
 

});