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-minute resolution, fixed size = 1, direction=1 (upward), price movement= 3 => Expectation = 3 green (up) bricks
- 1-minute resolution, fixed size = 1, direction=1, price movement = -2 => Expectation = 0 bricks; not enough price movement when changing direction
- 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.