order_target_percent calculation problems.



  • Hi All,

    I am writing a simple test code to try out the order_target functionality of backtrader as described here. For some reason the order_target_percent is not being placed/filled properly for backtesting purposes. I suspect the problem arises when market order size derived from the close of the day bar is larger than the open of the next trading day, causing the order to be placed at more than the portfolio liquidation value and thus not placed/filled.

    What happens in a portfolio asset allocation process is, at end of the rebalancing period, recalculate the statistics and the new weightings. The rebalancing orders will be placed at the open of the next rebalancing period and filled during that day.

    But instead, backtrader is placing the orders at the close of the day of a rebalance period but executing those orders on the next day. Those orders are not filled if the open of the next day is higher than the close of the previous day because the sizing is calculated off the wrong basis and triggers an order which buys more than the portfolio's cash.

    What can be done to alleviate this problem?

    Here's my code. Basically owns 100% S&P between October and April and hold only cash between May and September.

    class SellInMay(bt.Strategy):
        def next(self):
            dt = self.data.datetime.date()
            
            portfolio_value = self.broker.get_value()
            print('%04d - %s - Position Size:     %02d - Value %.2f' %
                  (len(self), dt.isoformat(), self.position.size, portfolio_value))
            
            data_value = self.broker.get_value([self.data])
            print('data is', self.data[0])
            
            port_perc = data_value / portfolio_value
            print('%04d - %s - data percent %.2f' %
                    (len(self), dt.isoformat(), port_perc))
    
            if dt.month >=10 or dt.month <=4:  
                percent = 1
                print('%04d - %s - Order Target Percent: %.2f' %
                        (len(self), dt.isoformat(), percent))
                self.order = self.order_target_percent(target=percent)
            else:
                percent = 0
                self.order = self.order_target_percent(target=percent)
                print('%04d - %s - Order Target Percent: %.2f' %
                        (len(self), dt.isoformat(), percent))
    

    And here is where I found the feature:

    Starting Portfolio Value: 10000.00
    0001 - 1950-01-03 - Position Size:     00 - Value 10000.00
    data is 16.66
    0001 - 1950-01-03 - data percent 0.00
    0001 - 1950-01-03 - Order Target Percent: 1.00                   <= Order placed
    0002 - 1950-01-04 - Position Size:     00 - Value 10000.00
    data is 16.85
    0002 - 1950-01-04 - data percent 0.00                            <= uptick, order_target_percent not filled.
    0002 - 1950-01-04 - Order Target Percent: 1.00                   <= Order placed
    0003 - 1950-01-05 - Position Size:     00 - Value 10000.00
    data is 16.93
    0003 - 1950-01-05 - data percent 0.00                            <= uptick, order_target_percent not filled.
    0003 - 1950-01-05 - Order Target Percent: 1.00                   <= Order placed
    0004 - 1950-01-06 - Position Size:     00 - Value 10000.00
    data is 16.98
    0004 - 1950-01-06 - data percent 0.00                            <= uptick, order_target_percent not filled.
    0004 - 1950-01-06 - Order Target Percent: 1.00                   <= Order placed
    0005 - 1950-01-09 - Position Size:     00 - Value 10000.00
    data is 17.08
    0005 - 1950-01-09 - data percent 0.00                            <= uptick, order_target_percent not filled.
    0005 - 1950-01-09 - Order Target Percent: 1.00                   <= Order placed
    0006 - 1950-01-10 - Position Size:     585 - Value 10000.00
    data is 17.030001000000002
    0006 - 1950-01-10 - data percent 1.00                            <= downtick, order_target_percent filled!!~
    0006 - 1950-01-10 - Order Target Percent: 1.00
    0007 - 1950-01-11 - Position Size:     587 - Value 10035.10
    


  • I've raised this as an issue on his github repo. Also had skipped trades due to price differences.



  • Thanks for the confirmation. I was reading through the documentations trying to figure out if I've placed the order wrong...

    Also, for chart outputs, anyway we can plot multiple lines of our choices? aka, plotting strategy values over time and a calculated benchmark over the same plot?


  • administrators

    The issue has been seen and was about to get the same answer. It's not an issue. It works exactly as designed.

    If no price is specified, then the close price is used for the calculations. If no order execution type is specified, then Market is used. An order for X shares with Market execution type. The matching price is the next incoming price.

    If the opening price is higher the order is as expected rejected because the account doesn't have enough cash. The platform will NOT look into the future (even if backtesting, the data is not always preloaded) to modify the calculation when the incoming price is there.

    Changing the execution type to Limit makes sure that the order is not rejected and waits for the price.



  • @backtrader

    Yes, its according to the design and outlined in your blog post. But the design is, in my honest opinion, flawed for asset allocation algorithm backtesting.

    As I've stated previously, in an asset allocation scenario, the weighting is determined after market close at the end of the day. The rebalancing orders are created next day at the open or sometimes during market, and filled by the end of the day.

    Using limit order will make sure the order is not rejected, but it will still cause rebalancing orders to be skipped for several days until we see a down-tick.

    Even if the backtrader uses open to calculate sizing, it still might not be filled if close is higher than open.

    Is there a way to create rebalancing orders to make sure that they get 100% filled during a daily bar?



  • Looks like we have two options:

    1. some look up into the future
    self.order_target_percent(target=1.0, exectype=bt.Order.Limit, price=self.data.open[1])
    
    1. split daily bar in two bars: OOOO only and OHLC. Then issue orders on the 1st new bar with set_coc parameter. I am not sure if this will work, but some similar things were shown here -
      https://www.backtrader.com/blog/posts/2016-07-29-pinkfish-challenge/pinkfish-challenge.html

  • administrators

    Splitting the bar is possible. The blog post was the first time it was done. Standard filters were added to the platform to do it.

    It has really not been tested in a while and some of the changes to the resampling/replaying may have had an impact.


  • administrators

    @ab_trader said in order_target_percent calculation problems.:

    1. some look up into the future

    self.order_target_percent(target=1.0, exectype=bt.Order.Limit, price=self.data.open[1])

    The existence of something at index [1] cannot be guaranteed. It will for sure be there if the data has been preloaded, but this guarantee is not possible.



  • @backtrader Thank you for direction. I'll check it.



  • @ab_trader Lookup into next day open gets the job done with a try except wraparound to catch IndexError but its a quick/dirty workaround with lookhead bias and won't be able to carry into live rebalancing.

    @backtrader Not sure how using DaySplitter_Close filter will carry the code to actual live rebalancing, any suggestions?

    Also, in testing DaySplitter_Close, I've added the DaySplitter_Close filter to my code, and it generates this error NameError: name 'datetime' is not defined when I tried either import datetimeorfrom datetime impiort datetime. I've also tried bothresults = cerebro.run()and justcerebro.run()` and neither method worked.

    I find it perplexing because in either import methods, feed.py always did import datetime so I am not sure what is wrong. Any help will be greatly appreciated. By the way pandas_datareader is required for future backtests that incorporate other macro data for portfolio rebalancing.

    Here's my code:

    import backtrader as bt
    import backtrader.filters as btfilters
    #from datetime import datetime
    import datetime
    
    class SellInMay(bt.Strategy):
        def next(self):
            dt = self.data.datetime.date()
            
            portfolio_value = self.broker.get_value()
            print('%04d - %s - Position Size:     %02d - Value %.2f' %
                  (len(self), dt.isoformat(), self.position.size, portfolio_value))
            
            data_value = self.broker.get_value([self.data])
            print('data is', self.data[0])
            
            port_perc = data_value / portfolio_value
            print('%04d - %s - data percent %.2f' %
                    (len(self), dt.isoformat(), port_perc))
    
            if dt.month >=10 or dt.month <=4:  
                percent = 1
                print('%04d - %s - Order Target Percent: %.2f' %
                        (len(self), dt.isoformat(), percent))
                self.order = self.order_target_percent(target=percent)                                                    
            else:
                percent = 0
                self.order = self.order_target_percent(target=percent)
                print('%04d - %s - Order Target Percent: %.2f' %
                        (len(self), dt.isoformat(), percent))
            
    def runstrat():
        start = datetime.datetime(1949,12,31)
        end = datetime.datetime(1951,1,3)
        spx = web.DataReader("^GSPC", 'yahoo', start, end)
        data = bt.feeds.PandasData(dataname=spx)
        data.addfilter(btfilters.DaySplitter_Close)        # <= added filter.
        
        cerebro = bt.Cerebro()
        cerebro.addstrategy(SellInMay)      
        cerebro.adddata(data)
        cerebro.broker.setcash(10000.0)
        
        print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
        results = cerebro.run()
        print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
            
        cerebro.plot()
        
    if __name__ == '__main__':
        runstrat()
    

    And here's the error:

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    <ipython-input-29-496d5974d836> in <module>()
         54 
         55 if __name__ == '__main__':
    ---> 56     runstrat()
    
    <ipython-input-29-496d5974d836> in runstrat()
         48 
         49     print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    ---> 50     results = cerebro.run()
         51     print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
         52 
    
    C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\cerebro.py in run(self, **kwargs)
        808             # let's skip process "spawning"
        809             for iterstrat in iterstrats:
    --> 810                 runstrat = self.runstrategies(iterstrat)
        811                 self.runstrats.append(runstrat)
        812         else:
    
    C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\cerebro.py in runstrategies(self, iterstrat, predata)
        871                 data._start()
        872                 if self._dopreload:
    --> 873                     data.preload()
        874 
        875         for stratcls, sargs, skwargs in iterstrat:
    
    C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\feed.py in preload(self)
        390 
        391     def preload(self):
    --> 392         while self.load():
        393             pass
        394 
    
    C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\feed.py in load(self)
        475                         retff = ff(self, *fargs, **fkwargs)
        476                 else:
    --> 477                     retff = ff(self, *fargs, **fkwargs)
        478 
        479                 if retff:  # bar removed from systemn
    
    C:\Anaconda3\envs\backtrader\lib\site-packages\backtrader\filters\bsplitter.py in __call__(self, data)
         87 
         88         # Adjust times
    ---> 89         dt = datetime.datetime.combine(datadt, data.p.sessionstart)
         90         ohlbar[data.DateTime] = data.date2num(dt)
         91 
    
    NameError: name 'datetime' is not defined
    

  • administrators

    @cnimativ said in order_target_percent calculation problems.:

    @backtrader Not sure how using DaySplitter_Close filter will carry the code to actual live rebalancing, any suggestions?

    Not sure because the recommendation was to use BarReplayer_Open and the filter is applied for backtesting. In live trading one would most probably issue the order during market open with the actual opening price. In any case the final order going to a broker must have a number of items to buy/sell and this has to be calculated using an actual price.

    Even in live circumstances and with the actual last price, the order could be rejected if the price moves against the order and the total cash is exceeded.

    Note:
    DaySplitter_Close was backported from a sample and unfortunately the datetime import was missed. Will be corrected.



  • @backtrader

    if I use BarReplayer_Open filter, then I'll need to have two feeds added:

    • one with the filter applied to simulate order processing, and
    • one for indicator calculations

    Am I correct?


  • administrators

    It is a replayer. The length of the stream will not be changed by the splitting of O and HLC. It simply gives you the chance to see O and act before you see the rest. The indicator will be calculated twice, but within the same length (like if you were receiving ticks in real-time for a daily bar)



  • @backtrader I've used Daysteps sample as a start point for BarReplayer_Open implementation. The sample itself works well, until I added orders (here is the updated next() from the sample):

        def next(self):
            self.callcounter += 1
    
            txtfields = list()
            txtfields.append('%04d' % self.callcounter)
            txtfields.append('%04d' % len(self))
            txtfields.append('%04d' % len(self.data0))
            txtfields.append(self.data.datetime.datetime(0).isoformat())
            txtfields.append('%.2f' % self.data0.open[0])
            txtfields.append('%.2f' % self.data0.high[0])
            txtfields.append('%.2f' % self.data0.low[0])
            txtfields.append('%.2f' % self.data0.close[0])
            txtfields.append('%.2f' % self.data0.volume[0])
            txtfields.append('%.2f' % self.data0.openinterest[0])
            print(','.join(txtfields))
    
            if len(self.data) > self.lcontrol:
                print('- I could issue a buy order during the Opening')
                self.order = self.buy(size=1)          #<------ this line was added
    
            self.lcontrol = len(self.data)
    

    This addition causes the following error message right on the 1st bar:

    Calls,Len Strat,Len Data,Datetime,Open,High,Low,Close,Volume,OpenInterest
    0001,0001,0001,2016-12-09T23:59:59.999989,225.41,225.41,225.41,225.41,0.00,0.00
    - I could issue a buy order during the Opening
    Traceback (most recent call last):
      File "bt_sample_daysteps.py", line 143, in <module>
        runstrat()
      File "bt_sample_daysteps.py", line 118, in runstrat
        cerebro.run(**(eval('dict(' + args.cerebro + ')')))
      File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 810, in run
        runstrat = self.runstrategies(iterstrat)
      File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 940, in runstrategies
        self._runnext(runstrats)
      File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 1250, in _runnext
        self._brokernotify()
      File "D:\Python27\lib\site-packages\backtrader\cerebro.py", line 1002, in _brokernotify
        self._broker.next()
      File "D:\Python27\lib\site-packages\backtrader\brokers\bbroker.py", line 863, in next
        self._try_exec(order)
      File "D:\Python27\lib\site-packages\backtrader\brokers\bbroker.py", line 809, in _try_exec
        popen = data.tick_open or data.open[0]
      File "D:\Python27\lib\site-packages\backtrader\lineseries.py", line 429, in __getattr__
        return getattr(self.lines, name)
    AttributeError: 'Lines_LineSeries_DataSeries_OHLC_OHLCDateTime_Abst' object has no attribute 'tick_open'
    

    Same error I obtained using BarReplayer_Open with little bit different definition of the Open virtual bar. Could you please advice me how to fix this?


  • administrators

    This is one of the many interactions of the many things introduced over time which have accumulated. Luckily it can be easily taken care of away from where the problem happens, by making it safe in the broker.

    This commit in the development branch should help: https://github.com/mementum/backtrader/commit/0c84b342a4155da27804c38f172955b733b0f581



  • Thank you!
    I'll wait for release.



  • @backtrader with new release this approach works great!. In order to have the order execution on the current bar Open, I set coc parameter to True. Otherwise execution happened on the next bar Open. Probably this replayer will increase run time, but now gaps can be also considered.

    Thank you!

    PS In the Daysteps sample it is a line cerebro._doreplay = True. This line doesn't change anything in the results. What is this line for?


  • administrators

    Something that later was detected automatically. To make some provisions internally like automatically disabling runonce=True (i.e.: set it to False)


  • administrators



  • @backtrader Very nice! One step closer to the quality of R's Quantstrat package. Will test it soon


Log in to reply
 

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