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

Renko Indicator to address existing issues



  • I'm creating a new Renko Indicator to address the issues (i.e. not based on the actual price movement, atr) ...
    https://community.backtrader.com/topic/511/renko-bricks/17
    https://community.backtrader.com/topic/1694/filter-on-replaydata/4.

    class Renko(bt.Indicator):
        '''
        During a trend continuation, a new brick of the same color is added to the chart when the Renko Brick Size value,
        plus one tick is met. A reversal in the Renko trend and the formation of a different color brick is formed
        when price surpasses the Renko Brick Size parameter by twice the size plus one tick.
    
        Formula:
            atr  = SmoothedMovingAverage(TrueRange, period)
            open = open
            high = (close + size)
            low  = (close - size)
            close = close
            direction = Sum(up) - Sum(down) Notes: Neutral = 0, Up = 1, Down = -1
    
        See also:
            https://school.stockcharts.com/doku.php?id=chart_analysis:renko
        '''
    
        lines = ('open', 'high', 'low', 'close', 'volume', 'direction')
    
        '''
            Fixed = Fixed Brick Size, False, means ATR
        '''
        params = (('fixed', True), ('brick_size', 0.0001), ('period', 14), ('field', 'close'), ('movav', MovAv.Smoothed))
    
        plotinfo = dict(subplot=False)
    
        def __init__(self):
            o = self.data.open
            h = self.data.high
            l = self.data.low
            c = self.data.close
            v = self.data.volume
    
            up_count = {}
            down_count = {}
           
            # Due we need to keep track of all Bricks with OHLCV to push to Strategy after time resolution?
            total_bricks = {}
    
            (brick_lower_limit, brick_upper_limit) = (c - self.p.brick_size, c + self.p.brick_size)
    
            self.lines.atr = ATR(self.data, period=self.p.period, movav=self.p.movav)
            self.lines.close = c
            self.lines.open = o
            self.lines.high = c + self.p.brick_size
            self.lines.low = c - self.p.brick_size
            self.lines.volume = v
            self.lines.direction = sum(self.up_count) - sum(self.down_count)
    
            super(Renko, self).__init__()
    
        def prenext(self):
            print('prenext:: current period:', len(self))
    
        def nextstart(self):
            print('nextstart:: current period:', len(self))
            # emulate default behavior ... call next
            self.next()
    
        def next(self):
            print('next:: current period:', len(self))
    
        def _plotlabel(self):
            plabels = [self.p.period]
            plabels += [self.p.movav] * self.p.notdefault('movav')
            return plabels
    

    Test Cases

    1. 1-minute resolution, fixed size = 1, direction=1 (upward), price movement= 3 => Expectation = 3 green (up) bricks
    2. 1-minute resolution, fixed size = 1, direction=1, price movement = -2 => Expectation = 0 bricks; not enough price movement when changing direction
    3. 1-minute resolution, fixed size = 1, direction=1, price movement = -2.1 => Expectation = 2 red (down) bricks

    I need the Strategy to expose all three bricks in the Next(self) would need access to all three bricks given that the resolution is 1 minute.

    Is this the correct implementation strategy? What will be the issues around time resolutions?

    I had thought about just sampling at a lower resolution, which would capture the price movements and push out the bars. However, we would still need a custom Renko indicator to address the up/down logic.



  • A new Renko class was created and I stopped using the RenkoFilter. The new Renko class didn't inherit from bt.Indicator as this open up a bunch of other issues. If you need it please fill free to ping me. I'll open it up once I get a few more unit tests around it.



  • Hey Bo, Im very curious about your Renko implementation. Im just starting to work with backtrader. So, cant provide much prior knowledge. But since this Indicator is on our bucketlist, Ill implement it myself. Id be super happy to see your new version
    Best regards



  • Here's the hack I implemented for now. It's based on Pyrenko code I found on Github. In my next(), after I pass the data, I access the brick to generate my signal.

    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib.patches as patches
    
    '''
            Formation of New Renko Bricks
            During a trend continuation, a new brick of the same color is added to the chart when the Renko Box Size value, 
            plus one tick, is met. A reversal in the Renko trend and the formation of a different color brick is formed 
            when price surpasses the Renko Box Size parameter by twice the size plus one tick.
            '''
    
    
    class Renko:
        def __init__(self, HLC_history=None, auto=False, brick_size=1.0):
            self.source_prices = []
            self.renko_prices = []
            self.renko_directions = []
    
            if auto:
                self.brick_size = self.__get_optimal_brick_size(HLC_history.iloc[:, [0, 1, 2]])
            else:
                self.brick_size = brick_size
    
        def __renko_rule(self, close):
            # Get the gap between two prices
            if self.brick_size == 0:
                gap_div = 0
            else:
                gap_div = int(float(close - self.renko_prices[-1]) / self.brick_size)
    
            is_new_brick = False
            start_brick = 0
            num_new_bars = 0
    
            # When we have some gap in prices
            if gap_div != 0:
                # Forward any direction (up or down)
                if (gap_div > 0 and (self.renko_directions[-1] > 0 or self.renko_directions[-1] == 0)) or (gap_div < 0 and (self.renko_directions[-1] < 0 or self.renko_directions[-1] == 0)):
                    num_new_bars = gap_div
                    is_new_brick = True
                    start_brick = 0
                # Backward direction (up -> down or down -> up)
                elif np.abs(gap_div) >= 2:  # Should be double gap at least
                    num_new_bars = gap_div
                    num_new_bars -= np.sign(gap_div)
                    start_brick = 2
                    is_new_brick = True
                    self.renko_prices.append(self.renko_prices[-1] + 2 * self.brick_size * np.sign(gap_div))
                    self.renko_directions.append(np.sign(gap_div))
                #else:
                #num_new_bars = 0
    
                if is_new_brick:
                    # Add each brick
                    for d in range(start_brick, np.abs(gap_div)):
                        self.renko_prices.append(self.renko_prices[-1] + self.brick_size * np.sign(gap_div))
                        self.renko_directions.append(np.sign(gap_div))
    
            return num_new_bars
    
        # Getting renko on history
        def build_history(self, prices):
            if len(prices) > 0:
                # Init by start values
                self.source_prices = prices
                self.renko_prices.append(prices.iloc[0])
                self.renko_directions.append(0)
    
                # For each price in history
                for p in self.source_prices[1:]:
                    self.__renko_rule(p)
    
            return len(self.renko_prices)
    
        # Getting next renko value for last price
        def do_next(self, close):
            if len(self.renko_prices) == 0:
                self.source_prices.append(close)
                self.renko_prices.append(close)
                self.renko_directions.append(0)
                return 1
            else:
                self.source_prices.append(close)
                return self.__renko_rule(close)
    
        # Simple method to get optimal brick size based on ATR
        @staticmethod
        def __get_optimal_brick_size(HLC_history, atr_timeperiod=14):
            brick_size = 0.0
    
            # If we have enough of data
            if HLC_history.shape[0] > atr_timeperiod:
                brick_size = np.median(talib.ATR(high=np.double(HLC_history.iloc[:, 0]),
                                                 low=np.double(HLC_history.iloc[:, 1]),
                                                 close=np.double(HLC_history.iloc[:, 2]),
                                                 timeperiod=atr_timeperiod)[atr_timeperiod:])
    
            return brick_size
    
        def evaluate(self, method='simple'):
            balance = 0
            sign_changes = 0
            price_ratio = len(self.source_prices) / len(self.renko_prices)
    
            if method == 'simple':
                for i in range(2, len(self.renko_directions)):
                    if self.renko_directions[i] == self.renko_directions[i - 1]:
                        balance = balance + 1
                    else:
                        balance = balance - 2
                        sign_changes = sign_changes + 1
    
                if sign_changes == 0:
                    sign_changes = 1
    
                score = balance / sign_changes
                if score >= 0 and price_ratio >= 1:
                    score = np.log(score + 1) * np.log(price_ratio)
                else:
                    score = -1.0
    
                return {'balance': balance, 'sign_changes:': sign_changes,
                        'price_ratio': price_ratio, 'score': score}
    
        def get_renko_prices(self):
            return self.renko_prices
    
        def get_renko_directions(self):
            return self.renko_directions
    
        def plot_renko(self, col_up='g', col_down='r'):
            fig, ax = plt.subplots(1, figsize=(20, 10))
            ax.set_title('Renko chart')
            ax.set_xlabel('Renko bars')
            ax.set_ylabel('Price')
    
            # Calculate the limits of axes
            ax.set_xlim(0.0,
                        len(self.renko_prices) + 1.0)
            ax.set_ylim(np.min(self.renko_prices) - 3.0 * self.brick_size,
                        np.max(self.renko_prices) + 3.0 * self.brick_size)
    
            # Plot each renko bar
            for i in range(1, len(self.renko_prices)):
                # Set basic params for patch rectangle
                col = col_up if self.renko_directions[i] == 1 else col_down
                x = i
                y = self.renko_prices[i] - self.brick_size if self.renko_directions[i] == 1 else self.renko_prices[i]
                height = self.brick_size
    
                # Draw bar with params
                ax.add_patch(
                    patches.Rectangle(
                        (x, y),   # (x,y)
                        1.0,     # width
                        self.brick_size,  # height
                        facecolor=col
                    )
                )
    
            plt.show()
    


  • Bo, fantastic work!

    I am trying to reproduce the basic pyrenko example (https://github.com/quantroom-pro/pyrenko/blob/master/pyrenko_tutorial.ipynb) inside backtrader, but unfortunately I am pretty new in backtrader and can't adapt your code. Main idea is only to compute the output inside backtrader and maybe plot some basic Renko's.

    I also try to use your class in most basic quick start guide: https://www.backtrader.com/docu/quickstart/quickstart/ but obtain some errors:

    AttributeError                            Traceback (most recent call last)
    <ipython-input-140-d76f523e9db2> in <module>
          1 optimal_brick = Renko(auto = False)
          2 renko_obj_atr = Renko(data)
    ----> 3 renko_obj_atr.build_history(data)
          4 renko_obj_atr.plot_renko()
    
    <ipython-input-135-0c0ccf80bef5> in build_history(self, prices)
         64             # Init by start values
         65             self.source_prices = prices
    ---> 66             self.renko_prices.append(prices.iloc[0])
         67             self.renko_directions.append(0)
         68 
    
    ~/opt/anaconda3/lib/python3.8/site-packages/backtrader/lineseries.py in __getattr__(self, name)
        459         # in this object if we set an attribute in this object it will be
        460         # found before we end up here
    --> 461         return getattr(self.lines, name)
        462 
        463     def __len__(self):
    
    AttributeError: 'Lines_LineSeries_DataSeries_OHLC_OHLCDateTime_Abst' object has no attribute 'iloc'
    

    Should be possible that you put here some psudo-code to get started with pyrenko inside backtrader? If so I would be more than grateful and could help with documentation and ongoing work if you need it.

    Many thanks!



  • Let us know if you achieve some progress with Renko.


Log in to reply
 

});