Getting the current date in a strategy's next() method



  • I have a moving average crossover strategy that uses different sets of moving averages in different periods. Over one period of time one set of moving averages is used, then in another a different set of moving averages is used, and so on. The time frames are passed via parameters as a list; the same is true with the moving average windows. This strategy has to handle multiple stock symbols.

    (This is not a strategy for trading; this is intended to simulate the effect of optimization in an account, where each set of moving averages is the product of optimization over different periods.)

    Here is the code:

    class SMACWalkForward(bt.Strategy):
        """The SMAC strategy but in a walk-forward analysis context"""
        params = {"start_dates": None,    # Starting days for trading periods (a list)
                  "end_dates": None,      # Ending day for trading periods (a list)
                  "fast": None,           # List of fast moving average windows, corresponding to start dates (a list)
                  "slow": None}           # Like fast, but for slow moving average window (a list)
        
        def __init__(self):
            """Initialize the strategy"""
            
            self.fastma = dict()
            self.slowma = dict()
            self.regime = dict()
            
            self.date_combos = zip(self.p.start_dates, self.p.end_dates)
            
            # Error checking
            if type(self.p.start_dates) is not list or type(self.p.end_dates) is not list or \
               type(self.p.fast) is not list or type(self.p.slow) is not list:
                raise ValueError("Must past lists filled with numbers to params start_dates, end_dates, fast, slow.")
            elif len(self.p.start_dates) != len(self.p.end_dates) or \
                len(self.p.fast) != len(self.p.start_dates) or len(self.p.slow) != len(self.p.start_dates):
                raise ValueError("All lists passed to params must have same length.")
            
            for d in self.getdatanames():
                self.fastma[d] = dict()
                self.slowma[d] = dict()
                self.regime[d] = dict()
                
                # Additional indexing, allowing for differing start/end dates
                for sd, ed, f, s in zip(self.p.start_dates, self.p.end_dates, self.p.fast, self.p.slow):
                    # More error checking
                    if type(f) is not int or type(s) is not int:
                        raise ValueError("Must include only integers in fast, slow.")
                    elif f > s:
                        raise ValueError("Elements in fast cannot exceed elements in slow.")
                    elif f <= 0 or s <= 0:
                        raise ValueError("Moving average windows must be positive.")
    
                    if type(sd) is not dt.date or type(ed) is not dt.date:
                        raise ValueError("Only datetime dates allowed in start_dates, end_dates.")
                    elif ed - sd < dt.timedelta(0):
                        raise ValueError("Start dates must always be before end dates.")
    
                    # The moving averages
                    # Notice that different moving averages are obtained for different combinations of
                    # start/end dates
                    self.fastma[d][(sd, ed)] = btind.SimpleMovingAverage(self.getdatabyname(d),
                                                               period=f,
                                                               plot=False)
                    self.slowma[d][(sd, ed)] = btind.SimpleMovingAverage(self.getdatabyname(d),
                                                               period=s,
                                                               plot=False)
    
                    # Get the regime
                    self.regime[d][(sd, ed)] = self.fastma[d][(sd, ed)] - self.slowma[d][(sd, ed)]
                    
        def next(self):
            """Define what will be done in a single step, including creating and closing trades"""
            
            # Determine which set of moving averages to use
            curdate = self.datas[0].datetime.date(0)
            dtidx = None    # Will be index
            for sd, ed in self.date_combos:
                if sd < curdate and curdate <= ed:
                    dtidx = (sd, ed)
            for d in self.getdatanames():    # Looping through all symbols
                pos = self.getpositionbyname(d).size or 0
                if dtidx is None:    # Not in any window
                    break            # Don't engage in trades
                if pos == 0:    # Are we out of the market?
                    # Consider the possibility of entrance
                    # Notice the indexing; [0] always mens the present bar, and [-1] the bar immediately preceding
                    # Thus, the condition below translates to: "If today the regime is bullish (greater than
                    # 0) and yesterday the regime was not bullish"
                    if self.regime[d][dtidx][0] > 0 and self.regime[d][dtidx][-1] <= 0:    # A buy signal
                        self.buy(data=self.getdatabyname(d))
    
                else:    # We have an open position
                    if self.regime[d][dtidx][0] <= 0 and self.regime[d][dtidx][-1] > 0:    # A sell signal
                        self.sell(data=self.getdatabyname(d))
    

    Unfortunately, this strategy does not work because it never engages in a trade, and I don't know why. I passed the strategy pandas DataFrames. So while the strategy does run, it does not engage in any trades.

    This suggests the problem is in next(), likely around where I look up the current date of the bar being considered, then determining which period the strategy is currently in. So I believe that these lines are where the problem is:

    # Determine which set of moving averages to use
            curdate = self.datas[0].datetime.date(0)
            dtidx = None    # Will be index
            for sd, ed in self.date_combos:
                if sd < curdate and curdate <= ed:
                    dtidx = (sd, ed)
    

    Somehow I think this condition is not being triggered.

    With this in mind, I'd like to know if I am getting the time stamp for the current step in the backtest correctly. Is the line curdate = self.datas[0].datetime.date(0) how I am supposed to get the current date?


  • administrators

    Accounting of the current datetime is done by the only master object in the equation: the strategy itself.

    def next(self):
        curdt = self.datetime[0]  # float
        curdtime = self.datetime.datetime(ago=0)  # 0 is the default
        curdate = self.datetime.date(ago=0)  # 0 is the default
        curtime = self.datetime.time(ago=0)  # 0 is the default ago
    

    @Curtis-Miller said in Getting the current date in a strategy's next() method:

                if self.regime[d][dtidx][0] > 0 and self.regime[d][dtidx][-1] <= 0:    # A buy signal
                    self.buy(data=self.getdatabyname(d))
    
            else:    # We have an open position
                if self.regime[d][dtidx][0] <= 0 and self.regime[d][dtidx][-1] > 0:    # A sell signal
                    self.sell(data=self.getdatabyname(d))
    

    Isn't that simple crossover? This could be done like this in __init__

    bt.ind.CrossOver(d, fast, slow) 
    

    And later checked like this:

    if thecrossover > 0:
        buy
    
    If the crossover < 0:
        sell
    

    @Curtis-Miller said in Getting the current date in a strategy's next() method:

        for d in self.getdatanames():    # Looping through all symbols
    

    It may be you use a debugger and this suggestions is superfluous and seems primitive, but something like print('{}: the dtixdx is {}'.format(len(self), dtidx)) before that line could shed some light (the opinion here is that the more the printing, the easier is to debug)



  • @backtrader Thanks for the help! I was accessing dates wrong; that was one problem, so thanks for telling me the right way. Using your suggestion of printouts (I tried logging but it didn't seem to work in the Jupyter notebook, but I guess something else was weird; the printouts worked fine the last time) I also discovered that I was creating a generator with zip(), not a list, so the generator would run and then the loop was never seen again. The class now works, and the strategy does what it's expected to do.

    This was the last job in the blog post, so I will write it up and share when it's published Monday.

    I'll look into using CrossOver next time (the current code works, so I won't fix what isn't broken for now, lest it actually does break). Does that indicator plot both moving averages in the final visualization?


  • administrators

    There is a light python2-3 adaptation layer inside backtrader in backtrader.utils.py3, mostly to avoid importing six or similar packages. zip is including as a generator for Python2 (itertools.izip) to match the Python3 style.

    Inside the core, the adaptation layer is used and if a generator is not the needed result it will be simply wrapped in a list(generator). This makes things consistent and allows the package to work with Python2/3


Log in to reply
 

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