python-3.xmatplotlibvisualizationcandlestick-chartmplfinance

How to properly add a percentage change box inside a Japanese Candlestick chart using MatPlotLibFinance on Python3?


Context

I'm trying to figure out a good way to add percentage price change boxes inside a custom Japanese Candlestick chart that I have made using the MatPlotLibFinance library on Python3, these percentage price change boxes will help to visually appreciate how much the price increased or decreased from the open price of a particular candlestick.

Data

The following information is stored in a variable called df, it will be used to plot the candlestick chart

Index Start Date Open Price High Price Low Price Close Price Volume End Date Abs((CP-OP)/CP)*100 Low SMA 9 Close SMA 25 High SMA 99
12 2022-10-23 12:24:00 27.87 27.88 27.72 27.83 40623.0 2022-10-23 12:26:59.999 0.14 27.89888888888889 28.007600000000004 28.294343434343432
13 2022-10-23 12:27:00 27.83 27.91 27.83 27.91 17337.0 2022-10-23 12:29:59.999 0.29 27.887777777777778 27.997600000000002 28.289898989898994
14 2022-10-23 12:30:00 27.91 27.98 27.91 27.94 8235.0 2022-10-23 12:32:59.999 0.11 27.88222222222222 27.9908 28.286262626262626
15 2022-10-23 12:33:00 27.94 27.94 27.89 27.89 6809.0 2022-10-23 12:35:59.999 0.18 27.87333333333333 27.983599999999996 28.282121212121215
16 2022-10-23 12:36:00 27.89 27.9 27.85 27.88 4209.0 2022-10-23 12:38:59.999 0.04 27.863333333333333 27.973999999999997 28.277373737373736
17 2022-10-23 12:39:00 27.89 27.89 27.86 27.88 10082.0 2022-10-23 12:41:59.999 0.04 27.85666666666667 27.966400000000004 28.272121212121213
18 2022-10-23 12:42:00 27.88 27.89 27.83 27.88 13257.0 2022-10-23 12:44:59.999 0.0 27.846666666666668 27.957600000000003 28.26666666666667
19 2022-10-23 12:45:00 27.88 27.94 27.88 27.94 5462.0 2022-10-23 12:47:59.999 0.22 27.85 27.951999999999998 28.26131313131313
20 2022-10-23 12:48:00 27.93 28.03 27.93 28.03 10597.0 2022-10-23 12:50:59.999 0.36 27.855555555555554 27.9512 28.257070707070707
21 2022-10-23 12:51:00 28.03 28.06 27.98 28.05 10238.0 2022-10-23 12:53:59.999 0.07 27.884444444444444 27.951200000000004 28.253333333333334
22 2022-10-23 12:54:00 28.05 28.05 27.99 28.03 6352.0 2022-10-23 12:56:59.999 0.07 27.90222222222222 27.952800000000003 28.24959595959596
23 2022-10-23 12:57:00 28.02 28.04 28.0 28.04 3905.0 2022-10-23 12:59:59.999 0.07 27.91222222222222 27.9556 28.245656565656564
24 2022-10-23 13:00:00 28.03 28.05 28.02 28.03 4607.0 2022-10-23 13:02:59.999 0.0 27.926666666666666 27.9548 28.24222222222222
25 2022-10-23 13:03:00 28.04 28.04 28.0 28.03 4291.0 2022-10-23 13:05:59.999 0.04 27.94333333333333 27.956 28.23868686868687
26 2022-10-23 13:06:00 28.02 28.02 27.99 28.0 4856.0 2022-10-23 13:08:59.999 0.07 27.95777777777778 27.9568 28.234747474747476
27 2022-10-23 13:09:00 28.01 28.03 28.01 28.02 1343.0 2022-10-23 13:11:59.999 0.04 27.977777777777774 27.9584 28.230505050505048
28 2022-10-23 13:12:00 28.02 28.06 28.01 28.06 5932.0 2022-10-23 13:14:59.999 0.14 27.992222222222225 27.9624 28.226565656565658
29 2022-10-23 13:15:00 28.06 28.1 28.04 28.06 8292.0 2022-10-23 13:17:59.999 0.0 28.004444444444445 27.9656 28.223030303030303

When running df.dtypes, the following output is thrown:

Start Date             datetime64[ns]
Open Price                    float64
High Price                    float64
Low Price                     float64
Close Price                   float64
Volume                        float64
End Date               datetime64[ns]
Abs((CP-OP)/CP)*100           float64
Low SMA 9                     float64
Close SMA 25                  float64
High SMA 99                   float64
dtype: object

Also, another variable called df_trading_pair_date_time_index contains the same information as the previous variable with slight modifications, since it can only be used in this way in the script below:

import pandas as pd
import mplfinance as mpf
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

def set_DateTimeIndex(df_trading_pair):
    df_trading_pair = df_trading_pair.set_index('Start Date', inplace=False)
    # Rename the column names for best practices
    df_trading_pair.rename(columns = { "Open Price" : 'Open',
                                       "High Price" : 'High',
                                       "Low Price" : 'Low',
                                       "Close Price" :'Close',
                              }, inplace = True)
            
    return df_trading_pair

 # Create another df just to properly plot the data
 df_trading_pair_date_time_index = set_DateTimeIndex(df)

Script

The following script will execute a function called mpl_plotting which takes as input the variables df, df_trading_pair_date_time_index will be used to plot Japanese Candlestick chart, while the last parameter of int type will be used to plot the price change boxes which will then be added to the Japanese candlestick Chart:

def mplf_plotting(df_trading_pair, df_trading_pair_date_time_index, entry_candlestick_index):
    
    entry_price = df_trading_pair['Open Price'].iat[entry_candlestick_index]
    
    maximum_price_reached = df_trading_pair['High Price'][entry_candlestick_index+1:].max()
    maximum_price_index = df_trading_pair['Low Price'][entry_candlestick_index+1:].idxmax()
    where_values_up = [entry_candlestick_index, maximum_price_index]
    
    minimum_price_reached = df_trading_pair['Low Price'][entry_candlestick_index+1:].min()
    minimum_price_index = df_trading_pair['Low Price'][entry_candlestick_index+1:].idxmin()
    where_values_down = [entry_candlestick_index, df_trading_pair['Start Date'][minimum_price_index]]

    # Plotting
    # Create my own `marketcolors` style:
    mc = mpf.make_marketcolors(up='#2fc71e',down='#ed2f1a',inherit=True)
    # Create my own `MatPlotFinance` style:
    s  = mpf.make_mpf_style(base_mpl_style=['bmh', 'dark_background'],marketcolors=mc, y_on_right=True)    

    # Plot it
    # First create a dictionary to store the plots to add
    subplots = {'Low SMA 9': mpf.make_addplot(df_trading_pair['Low SMA 9'], width=1, color='#F0FF42'),
                'Close SMA 25': mpf.make_addplot(df_trading_pair['Close SMA 25'], width=1.5, color='#EA047E'),
                'High SMA 99': mpf.make_addplot(df_trading_pair['High SMA 99'], width=2, color='#00FFD1')}

    pct_change_boxes ={'Percentage Change Up': mpf.make_addplot(df_trading_pair, fill_between=dict(y1=entry_price,y2=maximum_price_reached,where=where_values_up),alpha=0.5,color='g'),
                       'Percentage Change Down': mpf.make_addplot(df_trading_pair, fill_between=dict(y1=entry_price,y2=minimum_price_reached,where=where_values_down),alpha=0.5,color='g')}
    
    list_of_plots = list(subplots.values())
    #for i in list(pct_change_boxes.values()):
        #list_of_plots.append(i)
    
    trading_plot, axlist = mpf.plot(df_trading_pair_date_time_index,
                        figratio=(10, 6),
                        type="candle",
                        style=s,
                        tight_layout=True,
                        datetime_format = '%H:%M',
                        ylabel = "Precio ($)",
                        returnfig=True,
                        show_nontrading=True,
                        addplot=list_of_plots
                        )
    # Plotting
    
    # Add Title
    trading_pair = "SOLBUSD"
    symbol = trading_pair.replace("BUSD","")+"/"+"BUSD"
    axlist[0].set_title(f"{symbol} - 3m", fontsize=25, style='italic', fontfamily='fantasy')

    # Find which times should be shown every 6 minutes starting at the last row of the df
    x_axis_minutes = []
    for i in range (1,len(df_trading_pair_date_time_index),2):
        x_axis_minutes.append(df_trading_pair_date_time_index.index[-i].minute)

    # Set the main "ticks" to show at the x axis
    axlist[0].xaxis.set_major_locator(mdates.MinuteLocator(byminute=x_axis_minutes))

    # Set the x axis label
    axlist[0].set_xlabel('Zona Horaria UTC')
    # Set the y axis range 
    ymin_value = df_trading_pair[['Low Price','Low SMA 9','Close SMA 25', 'High SMA 99']].min(axis=1).min()
    ymax_value = df_trading_pair[['High Price','Low SMA 9','Close SMA 25', 'High SMA 99']].max(axis=1).max()
    axlist[0].set_ylim([ymin_value,ymax_value])

    # Set the SMA legends
    # First set the amount of legends to add to the legend box
    axlist[0].legend([None]*(len(subplots)+2)) 
    # Then Store the legend objects in a variable called "handles", based on this script, your objects to legend will appear from the third element in this list
    handles = axlist[0].get_legend().legendHandles

    # Finally set the corresponding names for the plotted SMA trends and place the legend box to the upper left corner in the bigger plot
    axlist[0].legend(handles=handles[2:],labels=list(subplots.keys()), loc = 'upper left', fontsize = 15)

# Execute the function to plot
mplf_plotting(df, df_trading_pair_date_time_index, 14)

The Problem

After running the script above, the following output is thrown:

Traceback (most recent call last):

  File "C:\Users\ResetStoreX\AppData\Local\Programs\Python\Python39\lib\site-packages\spyder_kernels\py3compat.py", line 356, in compat_exec
    exec(code, globals, locals)

  File "c:\users\resetstorex\downloads\binance futures data\binance api key + binance wrapper\bollinger bands\timeframe - 30 minutes\binance_futures_busd-backtesting-of-moving-averages.py", line 224, in <module>
    mplf_plotting(df_trading_pair[dict_index[i]:dict_index[i]+20], df_trading_pair_date_time_index, dict_index[i]+2)

  File "c:\users\resetstorex\downloads\binance futures data\binance api key + binance wrapper\bollinger bands\timeframe - 30 minutes\binance_futures_busd-backtesting-of-moving-averages.py", line 136, in mplf_plotting
    trading_plot, axlist = mpf.plot(df_trading_pair_date_time_index,

  File "C:\Users\ResetStoreX\AppData\Local\Programs\Python\Python39\lib\site-packages\mplfinance\plotting.py", line 720, in plot
    ax = _addplot_columns(panid,panels,ydata,apdict,xdates,config)

  File "C:\Users\ResetStoreX\AppData\Local\Programs\Python\Python39\lib\site-packages\mplfinance\plotting.py", line 1014, in _addplot_columns
    yd = [y for y in ydata if not math.isnan(y)]

  File "C:\Users\ResetStoreX\AppData\Local\Programs\Python\Python39\lib\site-packages\mplfinance\plotting.py", line 1014, in <listcomp>
    yd = [y for y in ydata if not math.isnan(y)]

TypeError: must be real number, not Timestamp

output image

If I decided to remove the following lines from the function:

for i in list(pct_change_boxes.values()):
    list_of_plots.append(i)

The following output is thrown:

second output

Desired output

I was expecting my script to print a image like the one down below, it essentially shows how much the price increased or decreased in percentage values based on the 3rd parameter passed to the mplf_plotting function:

desired output

The Question

How could I fix my function to throw an output like the desired one?


Solution

  • The closest thing I have come up with was to make use of the following original MatPlotLib functions: matplotlib.pyplot.text, matplotlib.axes.Axes.vlines, matplotlib.axes.Axes.hlines and also fixed some minor issues in the mplf_plotting function in order to avoid SettingWithCopyWarning from Pandas as well as other errors I can't remember atm.

    The following improvements were implemented:

    1. The mplf_plotting will now take just a deepcopy of the df variable, and the regular df_trading_pair_date_time_index

    2. The first thing this function will do is to reset the index of the information passed to the temporal df_trading_pair variable for then deleting the old index column left.

    3. The value assigned to the entry_candlestick_index will now be the the first valid index of the df_trading_pair plus the necessary amount of candlesticks to get the actual location of the desired candlestick (as it was previously set as 14, the amount to add must be 2).

    4. The values stored in the lists of where_values_up and where_values_down will now be actual Timestamp dates rather than its corresponding index values.

    5. The percentage change values to show in the final output will be calculated in the pct_change_up and pct_change_down variables

    Note: The following solution doesn't end up adding an actual percentage change box to the japanese candlestick chart, but hey, it works for me atm ¯\(ツ)/¯.

    Alternative Script

    from datetime import timedelta
    
    def mplf_plotting(df_trading_pair, df_trading_pair_date_time_index):
        
        # Reset the index
        df_trading_pair.reset_index(inplace=True)
        df_trading_pair.drop('index', inplace=True, axis=1)
        
        entry_candlestick_index = df_trading_pair.first_valid_index()+2
        
        entry_price = df_trading_pair['Open Price'].iat[entry_candlestick_index]
        
        maximum_price_reached = df_trading_pair['High Price'][entry_candlestick_index+1:].max()
        maximum_price_index = df_trading_pair['Low Price'][entry_candlestick_index+1:].idxmax()
        where_values_up = [df_trading_pair['Start Date'].iat[entry_candlestick_index], df_trading_pair['Start Date'].iat[maximum_price_index]]
        pct_change_up = round((maximum_price_reached-entry_price)/entry_price*100,2)
        
        minimum_price_reached = df_trading_pair['Low Price'][entry_candlestick_index+1:].min()
        minimum_price_index = df_trading_pair['Low Price'][entry_candlestick_index+1:].idxmin()
        where_values_down = [df_trading_pair['Start Date'].iat[entry_candlestick_index], df_trading_pair['Start Date'][minimum_price_index]]
        pct_change_down = round((minimum_price_reached-entry_price)/entry_price*100,2)
    
        # Plotting
        # Create my own `marketcolors` style:
        mc = mpf.make_marketcolors(up='#2fc71e',down='#ed2f1a',inherit=True)
        # Create my own `MatPlotFinance` style:
        s  = mpf.make_mpf_style(base_mpl_style=['bmh', 'dark_background'],marketcolors=mc, y_on_right=True)    
    
        # Plot it
        # First create a dictionary to store the plots to add
        subplots = {'Low SMA 9': mpf.make_addplot(df_trading_pair['Low SMA 9'], width=1, color='#F0FF42'),
                    'Close SMA 25': mpf.make_addplot(df_trading_pair['Close SMA 25'], width=1.5, color='#EA047E'),
                    'High SMA 99': mpf.make_addplot(df_trading_pair['High SMA 99'], width=2, color='#00FFD1')}
        
        list_of_plots = list(subplots.values())
        
        trading_plot, axlist = mpf.plot(df_trading_pair_date_time_index,
                            figratio=(10, 6),
                            type="candle",
                            style=s,
                            tight_layout=True,
                            datetime_format = '%H:%M',
                            ylabel = "Precio ($)",
                            returnfig=True,
                            show_nontrading=True,
                            addplot=list_of_plots
                            )    
        # Add Title
        symbol = trading_pair.replace("BUSD","")+"/"+"BUSD"
        axlist[0].set_title(f"{symbol} - 3m", fontsize=25, style='italic', fontfamily='fantasy')
    
        # Find which times should be shown every 6 minutes starting at the last row of the df
        x_axis_minutes = []
        for i in range (1,len(df_trading_pair_date_time_index),2):
            x_axis_minutes.append(df_trading_pair_date_time_index.index[-i].minute)
        
        # Plot and label the Maximum Positive ROI
        axlist[0].hlines(maximum_price_reached, xmin=where_values_up[0], xmax=where_values_up[1],
                         color="#06FF44", linestyle="solid", linewidth=1.5, alpha=0.7)
        axlist[0].vlines(where_values_up[0], ymin=entry_price, ymax=maximum_price_reached,
                         color="#06FF44", linestyle="solid", linewidth=1.5, alpha=0.7)
        axlist[0].text(x=where_values_up[0]+timedelta(minutes=1), y=maximum_price_reached-0.009, s=f"ROI: +{pct_change_up}%", 
                       ha="left", va="top", fontsize="14", color="#06FF44", backgroundcolor='#000000')
        
        # Plot and label the Maximum Negative ROI
        axlist[0].hlines(minimum_price_reached, xmin=where_values_down[0], xmax=where_values_down[1],
                         color="#F51912", linestyle="solid", linewidth=1.5, alpha=0.7)
        axlist[0].vlines(where_values_down[0], ymin=entry_price, ymax=minimum_price_reached,
                         color="#F51912", linestyle="solid", linewidth=1.5, alpha=0.7)
        axlist[0].text(x=where_values_up[0]+timedelta(minutes=1), y=minimum_price_reached+0.009, s=f"ROI: {pct_change_down}%", 
                       ha="left", va="bottom", fontsize="14", color="#F51912", backgroundcolor='#000000') 
        
        # Set the main "ticks" to show at the x axis
        axlist[0].xaxis.set_major_locator(mdates.MinuteLocator(byminute=x_axis_minutes))
    
        # Set the x axis label
        axlist[0].set_xlabel('Zona Horaria UTC')
        # Set the y axis range 
        ymin_value = df_trading_pair[['Low Price','Low SMA 9','Close SMA 25', 'High SMA 99']].min(axis=1).min()
        ymax_value = df_trading_pair[['High Price','Low SMA 9','Close SMA 25', 'High SMA 99']].max(axis=1).max()
        axlist[0].set_ylim([ymin_value,ymax_value])
    
        # Set the SMA legends
        # First set the amount of legends to add to the legend box
        axlist[0].legend([None]*(len(subplots)+2)) 
        # Then Store the legend objects in a variable called "handles", based on this script, your objects to legend will appear from the third element in this list
        handles = axlist[0].get_legend().legendHandles
    
        # Finally set the corresponding names for the plotted SMA trends and place the legend box to the upper left corner in the bigger plot
        axlist[0].legend(handles=handles[2:],labels=list(subplots.keys()), loc = 'upper left', fontsize = 15)
    
    # Execute the function to plot
    mplf_plotting(df.copy(deep=True), df_trading_pair_date_time_index)
    

    Output

    solved