Ntale Geofrey · Follow
11 min read · Mar 21, 2024
Renko charts have been in existence for a considerable time. The chart is believed to have acquired its name long ago, deriving from the Japanese term “renga,” which translates to “brick.” Traders use “Renko charts” to focus on price movement, providing a clearer visual representation of the trends and reversals.
Why would you want to consider the Renko Charts for your Trading Strategy?
To break it down, simply imagine that you are building a house with some special bricks called Renko, or better still “Renga” — as they are called in Japenese. Each brick represents a fixed price movement rather than a fixed timeframe it takes to lay. With Renko bricks what matters is the stacking size rather than the time it would take to stake them. So in 2 hours, you can stake one brick or 5 bricks or any number of bricks — what matters is the size, not the duration. This is what makes Renko so interesting, you don’t have to watch every tick of the clock as most charting tools do.
So now you might be wondering, what is the staking criteria if the duration it takes to lay doesn't matter?
To put it simply, whenever the price say; moves up by $20, you lay a brick upwards and when the price goes down by -$20 you lay the brick downwards. Pretty much a very straightforward concept right? Refer to the illustration in the figure above.
If we compare this to the traditional candlesticks, a new candle would be formed only when a specified duration has elapsed.
This means that you have candles chronologically stacked every say, 2 hours. In the candlestick charting style, both the price and time play an equal role in forming the chart. This results in a lot more market noise representation than the Renko chart.
Now let's get right into the code;
Step 1. Import the dependencies.
import ccxt
from datetime import datetime
import pandas as pd
import scipy.optimize as opt
import numpy as np
import pandas_ta as ta
from stocktrends import Renko
import mplfinance as mpf
import matplotlib.pyplot as plt
The first thing you want to do is to prepare your coding environment and then import the dependencies. As you can see we are using the “ccxt” package for collecting the data. It provides data from various crypto exchanges. In this example, we use it to get the the data from binance. Other important libraries for executing the strategy include the good old pandas, numpy, and Scipy for statistical computations. The “pandas_ta” library is for accessing the technical indicators — it's built on top of “TA-Lib” a popular Python technical analysis library. The “stocktrends ” library is used to calculate the Renko bricks and generate a Renko dataframe. And finally, we will use the “mplfinance” and the “matplotlib ”libraries for data visualization.
Step 2: Fetch the asset data from ccxt
def fetch_asset_data(symbol, start_date, interval, exchange):
# Convert start_date to milliseconds timestamp
start_date_ms = exchange.parse8601(start_date) ohlcv = exchange.fetch_ohlcv(symbol, interval, since=start_date_ms)
header = ["Date", "Open", "High", "Low", "Close", "Volume"]
df = pd.DataFrame(ohlcv, columns=header)
df['Date'] = pd.to_datetime(df['Date'], unit='ms')
df.set_index("Date", inplace=True)
return df
The function “fetch_asset_data” returns the data as pandas dataframe by passing in the symbol, start_date, interval, and exchange arguments. The symbol is the asset pair (such as ETH/USDT), the start date is the beginning data collection date, the interval is the time it takes to generate a single candle (which forms a row in the dataframe), and the exchange is the broker (e.g., Binance).
Step 3: Generate the Renko bricks as a dataframe
def renko_data(data): # Get the Renko data
# For a stable backtest we need to drop tha last row of the dataframe
# This is because you want to backtest with any live candles
# In this case the CCXT's last data points (last row) is live so that's why we need to drop it
# In other words you want completed candles
data.drop(data.index[-1], inplace=True) data['ATR'] = ta.atr(high=data['High'], low=data['Low'], close=data['Close'], length=14)
data.dropna(inplace=True)
def evaluate_brick_size_atr(brick_size, atr_values):
# Calculate number of bricks based on ATR and brick size
num_bricks = atr_values // brick_size
return np.sum(num_bricks)
# Get optimised brick size
brick = opt.fminbound(lambda x: -evaluate_brick_size_atr(x, data['ATR']), np.min(
data['ATR']), np.max(data['ATR']), disp=0)
def custom_round(number):
# List of available rounding values
rounding_values = [0.001, 0.005, 0.01, 0.05,
0.1, 0.5, 1] + list(range(5, 100, 5))
rounding_values += list(range(100, 1000, 50)) + \
list(range(1000, 10000, 100))
# Finding the closest rounding value
closest_value = min(rounding_values, key=lambda x: abs(x - number))
return closest_value
brick_size = custom_round(brick)
print(f'brick size: {brick_size}')
data.reset_index(inplace=True)
data.columns = [i.lower() for i in data.columns]
df = Renko(data)
df.brick_size = brick_size
renko_df = df.get_ohlc_data()
# Capitalize the Column names for ohlc
renko_df.rename(columns={'date': 'Date', 'open': 'Open', 'high': 'High', 'low': 'Low', 'close': 'Close'}, inplace=True)
# Return the ohlc colums to floats
renko_df['Open'] = renko_df['Open'].astype(float)
renko_df['High'] = renko_df['High'].astype(float)
renko_df['Low'] = renko_df['Low'].astype(float)
renko_df['Close'] = renko_df['Close'].astype(float)
return renko_df
The function renko_data calculates the Renko data for the asset pair “ohlc ” dataframe as an input. The Average True Range is calculated and used to determine the optimal brick size for the Renko chart. The brick size is rounded off for consistency. The function returns the Renko data as a pandas dataframe with columns Date, Open, High, Low, and Close where each value is converted into a float data type.
It’s important to ensure solid and dependable backtested results. Thus, we drop the dataframe’s last row, which contains live data. Refreshing the strategy system with live data introduces inconsistencies. As a result, it is critical to only rely on previous data represented by completed candles. This is achieved with this line “data.drop(data.index[-1], inplace=True)”.
This function keeps the Renko brick size dynamically stable. Some of the drawbacks of some renko systems include the use of a relatively small brick size, which results in a large number of bricks forming in the same time period, especially when the price changes rapidly. You need a consistent brick size that adapts automatically based on market volatility. This is what the function accomplishes.
Step 4: Generate the Signals and Positions
def generate_positions(renko_df):
# Rename the index of the renko data to brick
renko_df.index.name = "brick" # Initialize signals list with 0 (no signal) for the first brick
signals = []
for i in range(0, len(renko_df)):
# Get the current and previous brick colors
is_current_green = renko_df['Close'].iloc[i] > renko_df['Open'].iloc[i]
is_prev_green = renko_df['Close'].iloc[i -
1] > renko_df['Open'].iloc[i - 1]
if is_current_green and not is_prev_green:
signals.append(1) # Buy signal when the brick changes to green
elif is_current_green and is_prev_green:
signals.append(1) # Hold signal when the brick remains green
elif not is_current_green and is_prev_green:
signals.append(-1) # Sell signal when the brick changes to red
elif not is_current_green and not is_prev_green:
signals.append(-1) # Hold signal when the brick remains red
# Add the 'signals' column to the DataFrame
renko_df['signals'] = signals
renko_df['signals'] = renko_df["signals"].shift(1) #Remove look ahead bias
renko_df.fillna(0.0, inplace=True)
renko_df.set_index("Date", inplace=True)
# Create the Positions
# Initialize positions with nan
renko_df['buy_positions'] = np.nan
renko_df['sell_positions'] = np.nan
renko_df.index.freq = pd.infer_freq(renko_df.index)
# Update the buy_positions with the close price where the signal is 1 and the previous signal is not equal to the current signal
buy_signal_indices = renko_df[(renko_df['signals'] == 1) & (renko_df['signals'] != renko_df['signals'].shift(1))].index
renko_df.loc[buy_signal_indices, 'buy_positions'] = renko_df.loc[buy_signal_indices, 'Close']
# Update the sell_positions with close price where the signal is -1 and the previous signal is not equal to the current signal
sell_signal_indices = renko_df[(renko_df['signals'] == -1) & (renko_df['signals'] != renko_df['signals'].shift(1))].index
renko_df.loc[sell_signal_indices, 'sell_positions'] = renko_df.loc[sell_signal_indices, 'Close']
# Reset duplicate dates in the positions to nan, i.e where the previous date is equal to the current date
renko_df.loc[renko_df.index == pd.Series(renko_df.index).shift(1), ['buy_positions', 'sell_positions']] = np.nan
return renko_df
The function “generate_positions” processes the renko dataframe (renko_df) to generate signals followed by buy and sell trading positions. For reliable backtested results, we remove look ahead bias using this “renko_df[‘signals’] = renko_df[“signals”].shift(1)”; otherwise, the result will be ambiguously inflated and inaccurate.
Step 5: Calculate the performance
def calculate_strategy_performance(strategy_df, capital=100, leverage=1):
# Initialize the performance variables
cumulative_balance = capital
investment = capital
pl = 0
max_drawdown = 0
max_drawdown_percentage = 0 # Lists to store intermediate values for calculating metrics
balance_list = [capital]
pnl_list = [0]
investment_list = [capital]
peak_balance = capital
# Loop from the second row (index 1) of the DataFrame
for index in range(1, len(strategy_df)):
row = strategy_df.iloc[index]
# Calculate P/L for each trade signal
if row['signals'] == 1:
pl = ((row['Close'] - row['Open']) / row['Open']) * \
investment * leverage
elif row['signals'] == -1:
pl = ((row['Open'] - row['Close']) / row['Close']) * \
investment * leverage
else:
pl = 0
# Update the investment if there is a signal reversal
if row['signals'] != strategy_df.iloc[index - 1]['signals']:
investment = cumulative_balance
# Calculate the new balance based on P/L and leverage
cumulative_balance += pl
# Update the investment list
investment_list.append(investment)
# Calculate the cumulative balance and add it to the DataFrame
balance_list.append(cumulative_balance)
# Calculate the overall P/L and add it to the DataFrame
pnl_list.append(pl)
# Calculate max drawdown
drawdown = cumulative_balance - peak_balance
if drawdown < max_drawdown:
max_drawdown = drawdown
max_drawdown_percentage = (max_drawdown / peak_balance) * 100
# Update the peak balance
if cumulative_balance > peak_balance:
peak_balance = cumulative_balance
# Add new columns to the DataFrame
strategy_df['investment'] = investment_list
strategy_df['cumulative_balance'] = balance_list
strategy_df['pl'] = pnl_list
strategy_df['cumPL'] = strategy_df['pl'].c*msum()
# Calculate other performance metrics (replace with your calculations)
overall_pl_percentage = (
strategy_df['cumulative_balance'].iloc[-1] - capital) * 100 / capital
overall_pl = strategy_df['cumulative_balance'].iloc[-1] - capital
min_balance = min(strategy_df['cumulative_balance'])
max_balance = max(strategy_df['cumulative_balance'])
# Print the performance metrics
print("Overall P/L: {:.2f}%".format(overall_pl_percentage))
print("Overall P/L: {:.2f}".format(overall_pl))
print("Min balance: {:.2f}".format(min_balance))
print("Max balance: {:.2f}".format(max_balance))
print("Maximum Drawdown: {:.2f}".format(max_drawdown))
print("Maximum Drawdown %: {:.2f}%".format(max_drawdown_percentage))
# Return the Strategy DataFrame
return strategy_df
You want to know how your strategy is performing by checking on metrics like the profit and loss(p/l), the account growth(balance), and the maximum drawdown. The function “calculate_strategy_performance” takes the arguments “strategy_df” — which is returned from the “generate_positions” function, the “capital ” which represents the initial investment amount, and then the “leverage” which is the borrowing multiplier from the broker. The default value for ‘leverage’ is “x1” indicating no leverage is taken by default. However, feel free to experiment with your desired leverage amount.
It’s important to exercise caution when using leverage, especially in live trading, as over-leveraging can swiftly deplete your account if the trade position goes unfavorably. Please be cautious about this — and of course, nothing in this article is investment advice.
The function returns a dataframe “strategy_df” that contains the performance metrics and prints out some other metrics like the overall profit/loss, the balance, and the maximum drawdown.
Maximizing returns by compounding
This function adopts a compounding strategy, where the investment for each new position is based on the available balance, including past gains from closed positions. A key code snippet from the function is “cumulative_balance += pl”.
Step 6: Calculate the performance
# Plot the Candlestick data, the buy and sell signal markers
def plot_candlestick(df):
# Plot the candlestick chart
mpf.plot(df, type='candle', style='charles', datetime_format='%Y-%m-%d', xrotation=20,
title=str(symbol + ' Candlestick Chart'), ylabel='Price', xlabel='Date', scale_width_adjustment=dict(candle=2))# Plot the Renko Data.
def plot_renko(renko_df):
# Plot the Renko Chart
adp = [mpf.make_addplot(renko_df['buy_positions'], type='scatter', marker='^', label= "Buy", markersize=80, color='#2cf651'),
mpf.make_addplot(renko_df['sell_positions'], type='scatter', marker='v', label= "Sell", markersize=80, color='#f50100')
]
mpf.plot(renko_df, addplot=adp, type='candle', style='charles', datetime_format='%Y-%m-%d', xrotation=20,
title=str(symbol + ' Renko Chart'), ylabel='Price', xlabel='Date', scale_width_adjustment=dict(candle=2))
# Plot the performance curve
def plot_performance_curve(strategy_df):
# Plot the performance curve
plt.plot(strategy_df['cumulative_balance'])
plt.title('Performance Curve')
plt.xlabel('Date')
plt.ylabel('Balance')
plt.xticks(rotation=70)
plt.show()
Now let's make some plots to visualize the data. For this three functions are created:
- The “plot_candlestick” function for plotting the original collected ohlc data from binance
- The “plot_renko” function for plotting the Renko bricks and the positions with buy and sell markers.
- And finally the “plot_performance_curve” for visualizing the growth curve of the balance.
Step 7: Run the strategy
if __name__ == "__main__":
# Define the symbol, start date, interval and exchange
symbol = "ETH/USDT"
start_date = "2022-12-1"
interval = '4h'
exchange = ccxt.binance() # Fetch the historical data and Convert the data to a Pandas dataframe
data = fetch_asset_data(symbol=symbol, start_date=start_date, interval=interval, exchange=exchange)
# Print the asset dataframe
print(data)
# Plot the Symbol data candlestick chart
plot_candlestick(df=data)
# Get the Renko Bricks
renko_df = renko_data(data)
print(renko_df)
# Generate Strategy Signals
positions_df = generate_positions(renko_df)
print(positions_df)
# Plot the Renko Bricks and Positions
plot_renko(renko_df)
# Calculate Strategy Performance
strategy_df = calculate_strategy_performance(positions_df)
print(strategy_df)
# Plot the performance curve
plot_performance_curve(strategy_df)
We have come quite a long way; now we need to run the strategy. We setup the variables and call the functions under the ‘if __name__ == “__main__”:’ constructor that will run the script when called in the Python interpreter (terminal).
For this tutorial, we illustrate with the ETH/USDT pair. Feel free to experiment with other assets. If you need to experiment with non-crypto assets like forex or stocks you may need to use another data provider such as Yahoo Finance (yfinance python library)
Step 8: Let’s take a look at the results
If you are working with a Python interpreter like I did in vscode, open up the terminal, and point it to the file directory. Of course, you need to have already installed a python environment with all the dependencies installed. In this case, my file is called “dynamicRenko.py”.
Run the script:
At the time of this tutorial, this is the resulting dataframe for ETH/USDT
And here is the candlestick plot for the asset data:
The following was the resulting dataframe (renko_df) after converting the candlestick data to renko bricks. In addition to that, the signals and positions columns were added.
And this was the resulting plot for “plot_renko(renko_df)”. As you can see the chart shows clear trends and clear buy and sell signals.
The calculated brick size was 25. This is also displayed on the terminal:
The last part is to determine the performance of the strategy. This was the resulting dataframe for the strategy(strategy_df):
And these were the performance metrics. This performance was limited to profit/loss, the ultimate balance, and the maximum drawdown. Feel free to determine other metrics like Sortino ratio, alpha etc.
As you can see for roughly a year, the strategy made a 309.7% return on investment. This is quite decent, right? — but remember past performance doesn't guarantee future performance.
And the this is the performance curve:
Now the Dynamic Renko Strategy must surely be interesting. Renko charts offer a unique perspective on price movements, emphasizing significant price changes rather than time intervals. Thus, traders identify trends and reversals more clearly. And this may lead to more profitability.
Here is the full code on my GitHub.