Search code examples
python-3.xmatplotlibplotvisualizationcandlestick-chart

How to properly plot a candlestick chart alongside trendlines but considering a y axis with percentage change values using MatPlotLib and Python?


When I was trying to compare 3 financial assets based exclusively on its price series, I realized that to do so all of those price series had to be converted to price change series, so that way, one could properly see which one of them was performing better during a period of time.

Later on, I set a custom plot on tradingview that allowed me to visualize exactly what I wanted to do on my end, here:

Desired output

This plot basically shows:

  • The price change series of 3 financial assets, these price change series are calculated as follows: (Close Price - Very first Open Price)/(Very first Open Price). (Keep also in mind these series are updated automatically based on the the data that it's being considered so if you zoom-in or zoom-out the main plot the price change series wil change too).

  • The candlestick chart of one of the assets, in this case it's BNBUSDT, since my interest is to find a candlestick pattern in that asset.

  • The period of time at which these data were recorded, which start date was 14th of September 2022 01:00:00 UTC and finish date was 14th of September 2022 23:00:00 UTC, furthermore each candlestick represent 1 hour of data and all of these information was requested from Binance Exchange.

So, I put my brain to work, and managed to get this output:

current output

Using this info:

  • df_chg_btc
Start Date % Chg
2410 2022-09-14 01:00:00 0.51
2411 2022-09-14 02:00:00 1.22
2412 2022-09-14 03:00:00 0.62
2413 2022-09-14 04:00:00 0.95
2414 2022-09-14 05:00:00 0.58
2415 2022-09-14 06:00:00 0.2
2416 2022-09-14 07:00:00 0.22
2417 2022-09-14 08:00:00 0.92
2418 2022-09-14 09:00:00 0.74
2419 2022-09-14 10:00:00 0.7
2420 2022-09-14 11:00:00 0.13
2421 2022-09-14 12:00:00 0.55
2422 2022-09-14 13:00:00 0.56
2423 2022-09-14 14:00:00 0.47
2424 2022-09-14 15:00:00 -0.1
2425 2022-09-14 16:00:00 -0.28
2426 2022-09-14 17:00:00 -0.46
2427 2022-09-14 18:00:00 -2.08
2428 2022-09-14 19:00:00 -1.22
2429 2022-09-14 20:00:00 -1.25
2430 2022-09-14 21:00:00 0.02
2431 2022-09-14 22:00:00 0.48
2432 2022-09-14 23:00:00 0.14
  • df_chg_eth
Start Date % Chg
2410 2022-09-14 01:00:00 0.59
2411 2022-09-14 02:00:00 1.75
2412 2022-09-14 03:00:00 1.87
2413 2022-09-14 04:00:00 3.04
2414 2022-09-14 05:00:00 2.53
2415 2022-09-14 06:00:00 2.18
2416 2022-09-14 07:00:00 2.45
2417 2022-09-14 08:00:00 1.88
2418 2022-09-14 09:00:00 2.0
2419 2022-09-14 10:00:00 1.79
2420 2022-09-14 11:00:00 1.38
2421 2022-09-14 12:00:00 2.02
2422 2022-09-14 13:00:00 2.01
2423 2022-09-14 14:00:00 1.82
2424 2022-09-14 15:00:00 1.36
2425 2022-09-14 16:00:00 0.75
2426 2022-09-14 17:00:00 1.0
2427 2022-09-14 18:00:00 0.35
2428 2022-09-14 19:00:00 1.67
2429 2022-09-14 20:00:00 1.84
2430 2022-09-14 21:00:00 2.98
2431 2022-09-14 22:00:00 4.15
2432 2022-09-14 23:00:00 4.12
  • df_chg
Start Date % Chg
2410 2022-09-14 01:00:00 -0.14
2411 2022-09-14 02:00:00 0.54
2412 2022-09-14 03:00:00 0.72
2413 2022-09-14 04:00:00 1.26
2414 2022-09-14 05:00:00 0.83
2415 2022-09-14 06:00:00 0.22
2416 2022-09-14 07:00:00 0.4
2417 2022-09-14 08:00:00 0.76
2418 2022-09-14 09:00:00 0.68
2419 2022-09-14 10:00:00 0.58
2420 2022-09-14 11:00:00 0.58
2421 2022-09-14 12:00:00 0.5
2422 2022-09-14 13:00:00 1.22
2423 2022-09-14 14:00:00 0.79
2424 2022-09-14 15:00:00 0.54
2425 2022-09-14 16:00:00 0.07
2426 2022-09-14 17:00:00 -0.07
2427 2022-09-14 18:00:00 -0.83
2428 2022-09-14 19:00:00 -0.18
2429 2022-09-14 20:00:00 -0.04
2430 2022-09-14 21:00:00 0.5
2431 2022-09-14 22:00:00 0.61
2432 2022-09-14 23:00:00 0.47
  • df_ohlc
Start Date Open Price High Price Low Price Close Price Volume End Date
2410 2022-09-14 01:00:00 278.0 278.1 276.7 277.6 16139.244 2022-09-14 01:59:59.999
2411 2022-09-14 02:00:00 277.5 280.1 277.4 279.5 13583.474 2022-09-14 02:59:59.999
2412 2022-09-14 03:00:00 279.6 280.7 278.9 280.0 13142.362 2022-09-14 03:59:59.999
2413 2022-09-14 04:00:00 280.1 282.0 279.9 281.5 13518.034 2022-09-14 04:59:59.999
2414 2022-09-14 05:00:00 281.4 281.8 280.0 280.3 9906.908 2022-09-14 05:59:59.999
2415 2022-09-14 06:00:00 280.3 281.2 278.4 278.6 14126.511 2022-09-14 06:59:59.999
2416 2022-09-14 07:00:00 278.6 279.7 277.1 279.1 23193.697 2022-09-14 07:59:59.999
2417 2022-09-14 08:00:00 279.1 280.2 277.2 280.1 25811.769 2022-09-14 08:59:59.999
2418 2022-09-14 09:00:00 280.1 280.7 279.1 279.9 11960.898 2022-09-14 09:59:59.999
2419 2022-09-14 10:00:00 279.8 280.3 279.1 279.6 11943.115 2022-09-14 10:59:59.999
2420 2022-09-14 11:00:00 279.6 280.5 278.0 279.6 17152.818 2022-09-14 11:59:59.999
2421 2022-09-14 12:00:00 279.6 280.7 277.5 279.4 28562.346 2022-09-14 12:59:59.999
2422 2022-09-14 13:00:00 279.4 282.8 278.6 281.4 32421.324 2022-09-14 13:59:59.999
2423 2022-09-14 14:00:00 281.3 281.4 278.6 280.2 25790.991 2022-09-14 14:59:59.999
2424 2022-09-14 15:00:00 280.3 281.1 279.4 279.5 12341.206 2022-09-14 15:59:59.999
2425 2022-09-14 16:00:00 279.5 279.7 278.0 278.2 18802.725 2022-09-14 16:59:59.999
2426 2022-09-14 17:00:00 278.1 279.5 277.7 277.8 13507.618 2022-09-14 17:59:59.999
2427 2022-09-14 18:00:00 277.7 278.2 273.3 275.7 35438.286 2022-09-14 18:59:59.999
2428 2022-09-14 19:00:00 275.8 277.7 275.3 277.5 13976.371 2022-09-14 19:59:59.999
2429 2022-09-14 20:00:00 277.5 278.4 277.3 277.9 9690.736 2022-09-14 20:59:59.999
2430 2022-09-14 21:00:00 277.9 279.4 276.8 279.4 11821.016 2022-09-14 21:59:59.999
2431 2022-09-14 22:00:00 279.4 280.4 278.8 279.7 17948.597 2022-09-14 22:59:59.999
2432 2022-09-14 23:00:00 279.8 279.9 278.7 279.3 13572.694 2022-09-14 23:59:59.999

And this python code:

import pandas as pd
from pandas import Timedelta
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

## Assume here were set the variables of df_chg_btc, df_chg_eth, df_chg

df_chg_btc = df_chg_btc.set_index('Start Date', inplace=False)
df_chg_eth = df_chg_eth.set_index('Start Date', inplace=False)
df_chg = df_chg.set_index('Start Date', inplace=False)

trading_pair = 'BNB/USDT'
start_index = '14 September 2022'

## Plotting using matplotlib and saving the plot in axlist
# Initialize the plot with #162125 as outer color and a figsize of (12, 6)
fig, ax = plt.subplots(figsize=(12, 6), facecolor='#162125')

# Place the indices and values of df_chg,df_chg_btc and df_chg_eth in the x and y axis correspondingly
symbol = trading_pair.replace("USDT", "")+"/"+"USDT"
ax.plot(df_chg.index, df_chg['% Chg'], marker='o', linestyle='-', color='#39c9bb', label=f'{symbol}')  
ax.plot(df_chg_btc.index, df_chg_btc['% Chg'], marker='o', linestyle='-', color='#ff9900', label='BTC/USDT')
ax.plot(df_chg_eth.index, df_chg_eth['% Chg'], marker='o', linestyle='-', color='#8893B1', label='ETH/USDT')

# Set inner color of the plot
ax.set_facecolor("#162125")

# Show every 5th tick to avoid overcrowding
ax.set_xticks(ax.get_xticks()[::5])
# Set the color of x ticks, rotation and the font weight
ax.tick_params(axis='x', colors='white')
plt.xticks(rotation=45)
ax.set_xticklabels(ax.get_xticks(), weight='bold', size=10)
ax.set_xlabel('Time UTC', color='white', fontsize=14, fontweight='bold')
# Format the date on the x-axis
if df_chg.index[1]-df_chg.index[0] == Timedelta(hours=1):
    
    time_format = mdates.DateFormatter('%H:%M:%S')
    ax.xaxis.set_major_formatter(time_format)
    ax.xaxis.set_major_locator(mdates.HourLocator(interval=4))

# Set the color of y ticks and the font weight
ax.set_yticks(ax.get_yticks(), size=10)
ax.set_yticklabels(['{:,.2f}%'.format(y) for y in ax.get_yticks()], weight='bold')
ax.tick_params(axis='y', colors='white')
ax.set_ylabel('% Change', color='white', fontsize=14, fontweight='bold')    

# Set the title
if df_chg.index[1]-df_chg.index[0] == Timedelta(hours=1):
    ax.set_title(f'Price Change Comparison - {start_index}', color='white', fontsize=16, fontweight='bold')

# Set grid linestyle, width and color
ax.grid(color='white', linestyle='--', linewidth=1)

# Create the legend with customizations
legend = ax.legend(prop={'size': 12, 'weight': 'bold'})

# Customize the legend text color
legend.get_texts()[0].set_color('#162125')
# Set the opacity (alpha) of the legend box
legend.get_frame().set_alpha(1)

plt.tight_layout()

# Update all the spines' outercolors to the same color as facecolor
for spine in ax.spines.values():
    spine.set_edgecolor('#FFFFFF')

# Save the plot    
filename = start_index+".png"
fig.savefig(+trading_pair+'//'+filename,dpi=300, bbox_inches = "tight")

#RELEASE THE MEMORY RAM
plt.close('all')

But I have no idea how to properly plot the df_ohlc data since every time I try to come up with a solution it ends up messed up terribly (i.e. insanely big green and red rectangles that supposedly represent the data, wrong location of the candlesticks, an extra y axis representing the price of the asset...). I also tried to find for similar approaches to my case but couldn't find anything on StackOverflow nor the first 4 pages of Google using words like

"candlestick plot with price change values matplotlib"

It would be great if anybody here could help me out, any approach using matplotlib is welcomed.


Solution

  • Solved, I got illuminated:

    import pandas as pd
    from pandas import Timedelta
    import matplotlib.pyplot as plt
    import matplotlib.dates as mdates
    
    def convert_ohlc_prices_to_percentage_values(df):
        # Create a new DataFrame to store the results
        df_result = pd.DataFrame()
    
        # Add the 'Start Date' column from the original DataFrame to the new DataFrame
        df_result['Start Date'] = df['Start Date']
        # Reset the index accordingly
        df_result.set_index(df.index, inplace=True)
        for col in df.columns:
            if 'Price' in col:
                # Calculate the result using the formula
                result = round((df[col] - df['Open Price'].iat[0]) / df['Open Price'].iat[0] * 100, 2)
    
                # Replace values equal to 'Open Price'.iat[0]' with 0
                result = result.where(result != 0, 0)
    
                # Add the result to the new DataFrame
                df_result[col] = result
            
        # Add the 'Volume' and 'End Date' column from the original DataFrame to the new DataFrame
        df_result['Volume'] = df['Volume']
        df_result['End Date'] = df['End Date']    
    
        return df_result
    
    ### Assume here were set the variables of df_ohlc, df_chg_btc, df_chg_eth, df_chg
    
    df_chg_btc = df_chg_btc.set_index('Start Date', inplace=False)
    df_chg_eth = df_chg_eth.set_index('Start Date', inplace=False)
    df_chg = df_chg.set_index('Start Date', inplace=False)
    df_ohlc_percentages = convert_ohlc_prices_to_percentage_values(df_ohlc)
    
    trading_pair = 'BNB/USDT'
    start_index = '14 September 2022'
    
    ## Plotting using matplotlib and saving the plot in axlist
    # Initialize the plot with #162125 as outer color and a figsize of (12, 6)
    fig, ax = plt.subplots(figsize=(12, 6), facecolor='#162125')
    
    # Place the indices and values of df_chg,df_chg_btc and df_chg_eth in the x and y axis correspondingly
    symbol = trading_pair.replace("USDT", "")+"/"+"USDT"
    ax.plot(df_chg.index, df_chg['% Chg'], marker='o', linestyle='-', color='#39c9bb', label=f'{symbol}')  
    ax.plot(df_chg_btc.index, df_chg_btc['% Chg'], marker='o', linestyle='-', color='#ff9900', label='BTC/USDT')
    ax.plot(df_chg_eth.index, df_chg_eth['% Chg'], marker='o', linestyle='-', color='#8893B1', label='ETH/USDT')
    
    # Creación del gráfico de velas
    # Calcula la anchura según el intervalo de tiempo (15 minutos en este caso)
    time_diff = df_ohlc_percentages["Start Date"].diff().mean()
    width = 0.6 * time_diff
    for index, row in df_ohlc_percentages.iterrows():
        open_price = row["Open Price"]
        close_price = row["Close Price"]
        high_price = row["High Price"]
        low_price = row["Low Price"]
        date = row["Start Date"]
    
        color = '#027F7F' if close_price >= open_price else '#FB0000'
        height = abs(close_price - open_price)
        bottom = min(open_price, close_price)
    
        ax.bar(date, height, width=width, bottom=bottom, align='center', color=color)
        ax.vlines(x=date, ymin=low_price, ymax=high_price, color=color, linewidth=1)
    
    # Set inner color of the plot
    ax.set_facecolor("#162125")
    
    # Show every 5th tick to avoid overcrowding
    ax.set_xticks(ax.get_xticks()[::5])
    # Set the color of x ticks, rotation and the font weight
    ax.tick_params(axis='x', colors='white')
    plt.xticks(rotation=45)
    ax.set_xticklabels(ax.get_xticks(), weight='bold', size=10)
    ax.set_xlabel('Time UTC', color='white', fontsize=14, fontweight='bold')
    # Format the date on the x-axis
    if df_chg.index[1]-df_chg.index[0] == Timedelta(hours=1):
        
        time_format = mdates.DateFormatter('%H:%M:%S')
        ax.xaxis.set_major_formatter(time_format)
        ax.xaxis.set_major_locator(mdates.HourLocator(interval=4))
    
    # Set the color of y ticks and the font weight
    ax.set_yticks(ax.get_yticks(), size=10)
    ax.set_yticklabels(['{:,.2f}%'.format(y) for y in ax.get_yticks()], weight='bold')
    ax.tick_params(axis='y', colors='white')
    ax.set_ylabel('% Change', color='white', fontsize=14, fontweight='bold')    
    
    # Set the title
    if df_chg.index[1]-df_chg.index[0] == Timedelta(hours=1):
        ax.set_title(f'Price Change Comparison - {start_index}', color='white', fontsize=16, fontweight='bold')
    
    # Set grid linestyle, width and color
    ax.grid(color='white', linestyle='--', linewidth=1)
    
    # Create the legend with customizations
    legend = ax.legend(prop={'size': 12, 'weight': 'bold'})
    
    # Customize the legend text color
    legend.get_texts()[0].set_color('#162125')
    # Set the opacity (alpha) of the legend box
    legend.get_frame().set_alpha(1)
    
    plt.tight_layout()
    
    # Update all the spines' outercolors to the same color as facecolor
    for spine in ax.spines.values():
        spine.set_edgecolor('#FFFFFF')
    
    # Save the plot    
    filename = start_index+".png"
    fig.savefig(+trading_pair+'//'+filename,dpi=300, bbox_inches = "tight")
    
    #RELEASE THE MEMORY RAM
    plt.close('all')
    

    Báasically, what I did was:

    1. Transform df_ohlc prices into percent values considering the change against the first value in the ["Open Price"] column. That's what the convert_ohlc_prices_to_percentage_values(df) function is for.
    2. Then before proceeding to place the candles, I had to define the width that these would have because otherwise they would "mess up the graph" (again), for this I had to take into account the average difference between the values of ["Start Date" ] and multiply them by a constant (0.6), then I use 2 for loops to make and place each candle, using the .bar() and .vlines() methods, and voila.

    Output:

    Desired output

    If someone knows another approach, feel free to share it.