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

Min Period for custom StochasticRSI indicator



  • I am trying to create a stochasticRSI indicator. I have set the self.addminperiod but the strategy next() gets called prior to this.

    Note1: This stochasticRSI indicator takes the calculated RSI value and calculates the stochastic oscillator value. A stochasticRSI with a period of 14, 14, 3, 3 would use an RSI with a period of 14, then after 14 RSI values it would calculate the Stochastic using the last 14 RSI values. It would then calculate a fastD and a slowK which are the simple moving average of the stochastic and the simple moving average of the %D respectively.

    Note2: In the below example I have minute data, but I am resampling it to 4H. The strategy is intended to run every minute but look at the 4H StochasticRSI value.

    class StochasticRSI(bt.Indicator):
        lines = ('stochastic', 'D', 'K')
    
        params = (('period', 14),('period_dfast', 3), ('period_kslow', 3))
        
        def __init__(self):
            self.addminperiod(self.p.period + self.p.period_dfast + self.p.period_kslow)
    
        def next(self):
            data = self.data.get(size=self.p.period)
            hh = max(data)
            ll = min(data)
            self.l.stochastic[0] = (self.data[0] - hh) / (hh - ll)
            self.l.D[0] = btind.SMA(self.l.stochastic, period=self.p.period_dfast)
            self.l.K[0] = btind.SMA(self.l.D, period=self.p.period_kslow)
    

    And I use it via

    class St(bt.Strategy):
        params = dict(multi=True)
    
        def __init__(self):
    
            self.rsi = rsi = btind.RSI(self.data1)
            self.stochRsi = StochasticRSI(rsi)
    
        def next(self):
            
            txt = ','.join(
                ['%04d' % len(self),
                 '%04d' % len(self.data0),
                 '%04d' % len(self.data1),
                 self.data.datetime.date(0).isoformat(),
                 '%.2f' % self.data0.close[0],
                 '%.2f' % self.rsi.rsi[0],
                 '%.2f' % self.stochRsi.stochastic[0],
                 '%.2f' % self.stochRsi.D[0]])
    
            print(txt)
    

    Here is the output and you can see that the output is starting at Hour 15 (3rd column) which shows that the RSI indicator is preventing the strategy from being called until there is a value RSI (after period=14 hours). I would expect nan values from Hour 15 to Hour 32 for StochasticRSI as the Stochastic RSI is waiting for 14 RSI values plus another 3 for the D moving average and another 3 for the K moving average.

    The StochasticRSI indicator next() only gets called after the RSI min period (14) + Stochastic RSI min period (14+3+3=20 Hours) but how come the strategy gets called after the first 14 hours and not after the first 32 hours?

    2882,2882,0015,2018-07-07,6607.49,52.47,nan,nan
    2883,2883,0015,2018-07-07,6609.01,52.47,nan,nan
    2884,2884,0015,2018-07-07,6609.01,52.47,nan,nan
    2885,2885,0015,2018-07-07,6609.01,52.47,nan,nan
    2886,2886,0015,2018-07-07,6609.00,52.47,nan,nan
    ...
    6958,6958,0033,2018-07-09,6712.15,57.46,nan,nan
    6959,6959,0033,2018-07-09,6712.16,57.46,nan,nan
    6960,6960,0033,2018-07-09,6712.15,57.46,nan,nan
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-219-444180449bef> in <module>()
          1 # Run over everything
    ----> 2 cerebro.run(runonce=False)
    ...
    <ipython-input-214-8cd4d0d08c3e> in next(self)
         22         self.l.stochastic[0] = (self.data[0] - hh) / (hh - ll)
         23         #print(self.l.stochastic[0])
    ---> 24         self.l.D[0] = btind.SMA(self.l.stochastic, period=self.p.period_dfast)
         25         self.l.K[0] = btind.SMA(self.l.D, period=self.p.period_kslow)
    
    ~/miniconda3/envs/pythontrading/lib/python3.5/site-packages/backtrader/linebuffer.py in __setitem__(self, ago, value)
        220             value (variable): value to be set
        221         '''
    --> 222         self.array[self.idx + ago] = value
        223         for binding in self.bindings:
        224             binding[ago] = value
    
    TypeError: a float is required
    

    And I'm also getting this error when trying to calculate the simple moving average of the stochastic value.



  • I'm adding here how I solved this problem. I'm not sure its the best way but it seems to work. This is also the code for a StochasticRsi indicator as seen in TradingView.com.

    Note 1: Its not possible to use the backtrader Stochastic Indicator as the built in one needs data.close whereas here the stochastic is calculated off the RSI value.

    Note 2: The is called the StochasticRsi, however its assuming that you are passing in the RSI as the data source. You could pass in any other data source and it would calculate the Stochastic of that also.

    In particular I'm not sure whether the two edge case values of 50 and 0 when the denominator of the stochastic is zero are the correct values to be defaulting to.

    class StochasticRSI(bt.Indicator):
        lines = ('D', 'K',)
        
        params = (('period', 14), ('period_dfast', 3), ('period_kslow', 3))
            
        plotinfo = dict(
            # Add extra margins above and below the 1s and -1s
            plotymargin=0.15,
    
            # Plot a reference horizontal line at 20 and 80
            plothlines=[20, 80],
    
            # Simplify the y scale to 100 and 0
            plotyticks=[100, 0])
    
        # Chose the colors of the D and K lines
        plotlines = dict(D=dict(color='g'), K=dict(color='b'))
        
        def _plotlabel(self):
            # This method returns a list of labels that will be displayed
            # behind the name of the indicator on the plot. e.g. StochasticRSI (14, 3, 3)
    
            # The period must always be there
            plabels = [self.p.period, self.p.period_dfast, self.p.period_kslow]
    
            return plabels
        
        def __init__(self):
            S = Stochastic(period=self.p.period)
            self.l.D = D = btind.SMA(S, period=self.p.period_dfast)
            self.l.K = btind.SMA(D, period=self.p.period_kslow)
        
        
    class Stochastic(bt.Indicator):
        lines = ('stochastic',)
    
        params = (('period', 14),)
        
        def __init__(self):
            self.addminperiod(self.p.period)
    
        def next(self):
            data = self.data.get(size=self.p.period)
            hh = max(data)
            ll = min(data)
            num = self.data[0] - ll
            den = hh - ll
            
            if (den == 0 and num == 0):
                self.l.stochastic[0] = 50
            elif (den == 0):
                self.l.stochastic[0] = 0
            else:
                self.l.stochastic[0] = 100 * ((self.data[0] - ll) / (hh - ll))
    

  • administrators

    Your major problems

    @sfkiwi said in Min Period for custom StochasticRSI indicator:

    def next(self):
    ....
    self.l.D[0] = btind.SMA(self.l.stochastic, period=self.p.period_dfast)
    self.l.K[0] = btind.SMA(self.l.D, period=self.p.period_kslow)

    This is wrong. Indicators, be it inside a Strategy or inside another indicators, are not instantiated during next. It wouldn't make any sense to repeat this operation for each and every bar.

       def __init__(self):
           self.addminperiod(self.p.period + self.p.period_dfast + self.p.period_kslow)
    
       def next(self):
           data = self.data.get(size=self.p.period)
           hh = max(data)
           ll = min(data)
    

    There is no need neither to use addminperiod nor to use self.data.get. For example:

    def __init__(self):
        hh = bt.ind.Highest(self.data, period=self.p.period)  # or the alias bt.MaxN(self.data, period=self.p.period)
        ll = bt.ind.Lowest(self.data, period=self.p.period)   # or the alias bt.MinN(self.data, period=self.p.period)
    
        self.l.stochastic = 100.0 * (self.data - ll) / (hh - ll)
    

    To handle the edge cases of 0/0 and 0/x, backtrader already provides a predefined bt.DivZeroByZero or bt.DivByZero (See the RSI and Stochastic code for the usage)

    Because both hh and ll already take into account the period, you don't have to deal with it. And there is no need to use data.get, because the calculation can be done directly on the spot.

    My suggestion is that you look at:

    And

    @sfkiwi said in Min Period for custom StochasticRSI indicator:

    Note 2: The is called the StochasticRsi, however its assuming that you are passing in the RSI as the data source. You could pass in any other data source and it would calculate the Stochastic of that also.

    This is NOT calculating the Stochastic, because it is based on a single point series. It is calculating a specific variant of the Stochastic, which needs series with at least 3 data points (High, Low and Last)



  • Thanks very helpful, here is what I ended up with:

    class StochasticRsi(bt.Indicator):
        lines = ('D', 'K',)
    
        params = (('period', 14), ('period_dfast', 3), ('period_kslow', 3),
                  ('movav', btind.MovAv.Simple),
                  ('upperband', 80.0), ('lowerband', 20.0),
                  ('safediv', True), ('safezero', 0))
        
        plotinfo = dict(
            # Add extra margins above and below the 1s and -1s
            plotymargin=0.15,
    
            # Plot a reference horizontal line at 1.0 and -1.0
            plothlines=[20, 80],
    
            # Simplify the y scale to 1.0 and -1.0
            plotyticks=[100, 0])
        
        plotlines = dict(D=dict(_name='%D', ls='--'),
                         K=dict(_name='%K'))
        
        def _plotinit(self):
            self.plotinfo.plotyhlines = [self.p.upperband, self.p.lowerband]
        
        def __init__(self):
            hh = bt.ind.Highest(self.data, period=self.p.period) 
            ll = bt.ind.Lowest(self.data, period=self.p.period) 
            num = self.data - ll
            den = hh - ll
            if self.p.safediv:
                stoch = 100.0 * DivByZero(num, den, zero=self.p.safezero)
            else:
                stoch = 100.0 * (num / den)
            self.l.D = D = self.p.movav(stoch, period=self.p.period_dfast)
            self.l.K = self.p.movav(D, period=self.p.period_kslow)
    

    @backtrader said in Min Period for custom StochasticRSI indicator:

    This is NOT calculating the Stochastic, because it is based on a single point series. It is calculating a specific variant of the Stochastic, which needs series with at least 3 data points (High, Low and Last)

    I won't argue with you on that, however this is how tradingview.com calculates their 'StochasticRsi' indicator.


  • administrators

    As some people would for sure describe it: "It looks very canonical"