python-3.xmatplotlibdrawingpanelmplfinance

How to draw a circle containing some values given a range of indices and patch it to a MatPlotLib panel properly? Python related


I have been struggling trying to make a program draw a circle containing the corresponding values of a specific range of indices in a MatplotLibAxes object, here's the data input stored in a variable called df:

Index Start Date Open Price High Price Low Price Close Price Volume End Date
0 2023-03-12 18:30:00 3.996 4.038 3.988 4.008 1216259.0 2023-03-12 18:44:59.999
1 2023-03-12 18:45:00 4.008 4.024 3.99 3.993 638860.0 2023-03-12 18:59:59.999
2 2023-03-12 19:00:00 3.993 4.024 3.992 4.019 297226.0 2023-03-12 19:14:59.999
3 2023-03-12 19:15:00 4.018 4.023 3.973 3.985 1101139.0 2023-03-12 19:29:59.999
4 2023-03-12 19:30:00 3.986 4.003 3.976 3.993 427351.0 2023-03-12 19:44:59.999
5 2023-03-12 19:45:00 3.993 4.01 3.965 3.975 750141.0 2023-03-12 19:59:59.999
6 2023-03-12 20:00:00 3.976 3.998 3.967 3.988 552681.0 2023-03-12 20:14:59.999
7 2023-03-12 20:15:00 3.989 4.009 3.983 4.004 322794.0 2023-03-12 20:29:59.999
8 2023-03-12 20:30:00 4.005 4.037 4.003 4.035 682787.0 2023-03-12 20:44:59.999
9 2023-03-12 20:45:00 4.035 4.12 4.035 4.091 2179361.0 2023-03-12 20:59:59.999
10 2023-03-12 21:00:00 4.091 4.096 4.063 4.084 474021.0 2023-03-12 21:14:59.999
11 2023-03-12 21:15:00 4.084 4.103 4.077 4.087 480628.0 2023-03-12 21:29:59.999
12 2023-03-12 21:30:00 4.086 4.107 4.076 4.086 212594.0 2023-03-12 21:44:59.999
13 2023-03-12 21:45:00 4.086 4.107 4.079 4.105 364555.0 2023-03-12 21:59:59.999
14 2023-03-12 22:00:00 4.104 4.108 4.06 4.072 474296.0 2023-03-12 22:14:59.999
15 2023-03-12 22:15:00 4.072 4.257 4.069 4.232 3230671.0 2023-03-12 22:29:59.999
16 2023-03-12 22:30:00 4.232 4.247 4.208 4.241 851126.0 2023-03-12 22:44:59.999
17 2023-03-12 22:45:00 4.241 4.276 4.218 4.254 1268534.0 2023-03-12 22:59:59.999
18 2023-03-12 23:00:00 4.255 4.315 4.253 4.312 1469747.0 2023-03-12 23:14:59.999
19 2023-03-12 23:15:00 4.313 4.354 4.295 4.343 1352840.0 2023-03-12 23:29:59.999
20 2023-03-12 23:30:00 4.344 4.479 4.336 4.464 1995492.0 2023-03-12 23:44:59.999
21 2023-03-12 23:45:00 4.463 4.532 4.412 4.517 2488653.0 2023-03-12 23:59:59.999
22 2023-03-13 00:00:00 4.517 4.592 4.482 4.58 2140025.0 2023-03-13 00:14:59.999
23 2023-03-13 00:15:00 4.58 4.695 4.552 4.625 1973254.0 2023-03-13 00:29:59.999
24 2023-03-13 00:30:00 4.626 4.7 4.577 4.677 2444439.0 2023-03-13 00:44:59.999
25 2023-03-13 00:45:00 4.677 4.678 4.584 4.595 1353901.0 2023-03-13 00:59:59.999
26 2023-03-13 01:00:00 4.594 4.601 4.528 4.528 1181759.0 2023-03-13 01:14:59.999
27 2023-03-13 01:15:00 4.528 4.546 4.489 4.499 785683.0 2023-03-13 01:29:59.999
28 2023-03-13 01:30:00 4.499 4.507 4.473 4.49 634040.0 2023-03-13 01:44:59.999
29 2023-03-13 01:45:00 4.49 4.5 4.473 4.475 361538.0 2023-03-13 01:59:59.999
30 2023-03-13 02:00:00 4.476 4.479 4.445 4.45 507443.0 2023-03-13 02:14:59.999
31 2023-03-13 02:15:00 4.451 4.457 4.422 4.43 514609.0 2023-03-13 02:29:59.999
32 2023-03-13 02:30:00 4.431 4.438 4.412 4.412 283667.0 2023-03-13 02:44:59.999
33 2023-03-13 02:45:00 4.413 4.437 4.401 4.435 443083.0 2023-03-13 02:59:59.999
34 2023-03-13 03:00:00 4.435 4.451 4.411 4.418 304109.0 2023-03-13 03:14:59.999
35 2023-03-13 03:15:00 4.418 4.435 4.393 4.432 354457.0 2023-03-13 03:29:59.999
36 2023-03-13 03:30:00 4.433 4.461 4.415 4.449 256813.0 2023-03-13 03:44:59.999
37 2023-03-13 03:45:00 4.45 4.462 4.435 4.439 226006.0 2023-03-13 03:59:59.999
38 2023-03-13 04:00:00 4.437 4.464 4.418 4.458 304705.0 2023-03-13 04:14:59.999
39 2023-03-13 04:15:00 4.459 4.465 4.436 4.439 288049.0 2023-03-13 04:29:59.999

When running df.dtypes it throws

Start Date     datetime64[ns]
Open Price            float64
High Price            float64
Low Price             float64
Close Price           float64
Volume                float64
End Date       datetime64[ns]
dtype: object

The code to plot this data input is the following:

import pandas as pd
import matplotlib
import mplfinance as mpf
import matplotlib.pyplot as plt
import datetime

# Don't spend memory ram unnecesary pl0x
matplotlib.use("Agg")

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

def convert_to_unix_ms(string_date):
    date_format = "%d %b '%y %H:%M"
    dt = datetime.datetime.strptime(string_date, date_format)
    unix_timestamp_ms = int(dt.timestamp() * 1000)
    return unix_timestamp_ms

def plot_this(df):
    global trading_pair
    global start_date
    global end_date
    
    df_trading_pair_date_time_index = set_DateTimeIndex(df)
    
    # Define periods
    k_period = 14
    d_period = 1
    smooth_window = 3
       
    stochastic = pd.DataFrame()
    stochastic['%K'] = ((df['Close Price'] - df['Low Price'].rolling(k_period).min()) \
                        / (df['High Price'].rolling(k_period).max() - df['Low Price'].rolling(k_period).min())) * 100
    stochastic['%D'] = stochastic['%K'].rolling(d_period).mean()
    stochastic['%SD'] = stochastic['%D'].rolling(smooth_window).mean()
    stochastic['UL'] = 80
    stochastic['DL'] = 20
    
    # Get the index of the last nan value in the lower bound series    
    last_index_nan_value = len(stochastic['%D']) - pd.isna(stochastic['%D'])[::-1].argmax() - 1

    # Evaluate if there's a pattern
    previous_signal = "None"
    bearish_indices = []
    bearish_entries = []
    for i in range(last_index_nan_value+1,len(df)-3):
        slice_df = df.iloc[i:i+4]    
        if (slice_df["Volume"].max() > df["Volume"][slice_df.index[0]-8:slice_df.index[0]]).all():
            start = slice_df.index[0]
            end = slice_df.index[-1]
            # First BEARISH signal
            all_stochastics_up = stochastic.loc[start:end][["%D", "%SD"]].ge(stochastic.loc[start:end]["UL"], axis=0).all(axis=1)                
            all_stochastics_down = stochastic.loc[start:end][["%D", "%SD"]].le(stochastic.loc[start:end]["DL"], axis=0).all(axis=1)
            if all_stochastics_up.sum() >= 2 and previous_signal == "None":
        
                previous_signal = "bearish"
                bearish_indices.append(stochastic.loc[start:end].index.values.tolist())
        
        # SHORT ENTRY signal
        if previous_signal == "bearish":
            if df["Start Date"].loc[bearish_indices[-1][-1]] < slice_df["Start Date"].iat[0]:           
                if ((slice_df[:2]["Close Price"] > slice_df[:2]["Open Price"]).all() and (slice_df[-2:]["Close Price"] < slice_df[-2:]["Open Price"]).all()).all():
                    start = slice_df.index[0]
                    end = slice_df.index[-1]
                
                    previous_signal = "None"
                    bearish_entries.append(stochastic.loc[start:end].index.values.tolist())
    # Store the plots of the last 120 data rows of upper and lower bounds as well as the entry and exit points
    plots_to_add = {"Stochastics":mpf.make_addplot((stochastic[['%K', '%SD', 'UL', 'DL']]), ylim=[0, 100], panel=2, ylabel="Stochastics", y_on_right=False)}
    
    # Plotting
    # Create my own `marketcolors` style:
    mc = mpf.make_marketcolors(up='#0ECB81',down='#F64670',inherit=True)
    # Create my own `MatPlotFinance` style:
    s  = mpf.make_mpf_style(figcolor='#162125', facecolor= "#162125", marketcolors=mc, y_on_right=True, rc={'font.size':18, 'xtick.color': 'w'}, gridcolor='white', gridstyle='--', edgecolor='white')
    
    # Plot it
    candlestick_plot, axlist = mpf.plot(df_trading_pair_date_time_index,
                        figsize=(20,10),
                        figratio=(12, 6),
                        panel_ratios=(5,1,1),
                        type="candle",
                        volume=True,
                        style=s,
                        tight_layout=True,
                        datetime_format = '%b %d, %H:%M:%S',
                        ylabel = "Price ($)",
                        returnfig=True,
                        show_nontrading=True,
                        warn_too_much_data=870, # Silence the Too Much Data Plot Warning by setting a value greater than the amount of rows you want to be plotted
                        addplot = list(plots_to_add.values()) # Add the stochastic plot as well as the bullish entries to the main plot
                        )
    # Add Title
    axlist[0].set_title("APEUSDT - 15m", fontsize=60, style='italic', fontfamily='fantasy', color="white")
    
    # Set the color of the xticks, yticks and ylabel in every axes object
    ## Main Plot (Candlesticks)
    axlist[0].tick_params(axis='y', colors='white')
    axlist[0].yaxis.label.set_color('white')
    ## Volume Indicator
    axlist[2].tick_params(axis='y', colors='white')
    axlist[2].yaxis.label.set_color('white')
    ## Stochastics Indicator
    axlist[4].tick_params(axis='y', colors='white')
    axlist[4].yaxis.label.set_color('white')
    
    
    # Get the Volume indicator and modify its font size
    vol_ax = plt.gcf().axes[2]
    vol_ax.yaxis.label.set_size(15)
    
    # Set the x axis label
    axlist[0].set_xlabel('Timezone UTC')
    # Find the interval between the 7 custom x-tick marks
    time_delta = (df["Start Date"].iloc[-1]-df["Start Date"].iloc[last_index_nan_value+1])/6
    # Set the locations of the custom x-tick marks
    tick_locations = [df["Start Date"].iloc[last_index_nan_value+1] + i*time_delta for i in range(7)]
    # Set the labels of the custom x-tick marks
    tick_labels = [date.strftime("%b %d, %H:%M") for date in tick_locations]
    # Apply the custom x-tick marks and labels
    axlist[0].xaxis.set_ticks(tick_locations)
    axlist[0].xaxis.set_ticklabels(tick_labels)

    # Set the y axis range 
    ymin_value = pd.concat([df["Low Price"]], axis=0).min()
    ymax_value = pd.concat([df["High Price"]], axis=0).max()
    axlist[0].set_ylim([ymin_value,ymax_value])
    # Save the plot
    random_filename = "TEST_APEUSDT"+".png"
    candlestick_plot.savefig(random_filename,dpi=300, bbox_inches = "tight")
    
    #RELEASE THE MEMORY RAM
    plt.close('all')

The current output when running plot_this(df) is the following:

initial_output

The problem

Say I'm interested in drawing a purple circle that contains the data of this statement stochastic[["%K","%SD"]][15:19] and a red circle that contains the data of this statement stochastic[["%K","%SD"]][23:27].

The desired output should look something like this (I know these circles look like complete ovals, but they are indeed circles):

desired_output

What I have tried so far

I added the following code in the lines between # Get the Volume indicator and modify its font size and ## Stochastics Indicator, but it didn't make any difference nor threw any error:

# Define the ranges for the circles
purple_circle = [[15, 16, 17, 18, 19]]
red_circle = [[23, 24, 25, 26, 27]]

# Get the Stochastics subplot
stochastics_ax = axlist[4]

# Draw the purple circle
for range_ in purple_circle:
    for i in range(range_[0], range_[-1]+1):
        x, y = i, stochastics_ax.lines[0].get_ydata()[i]
        circle = Circle((x, y), radius=5, alpha=0.3, color='purple')
        stochastics_ax.add_patch(circle)

# Draw the red circle
for range_ in red_circle:
    for i in range(range_[0], range_[-1]+1):
        x, y = i, stochastics_ax.lines[0].get_ydata()[i]
        circle = Circle((x, y), radius=5, alpha=0.3, color='red')
        stochastics_ax.add_patch(circle)

I'm open to learn the necessary to fix my code.


Solution

  • The short answer is that a circle can be drawn using a matplotlib Circle Patch (similar to what you have tried already), or by using a circle shaped marker on a scatter plot.

    There are problems with the Circle Patch. For example, it requires direct access to the Axes object. Furthermore it may require a coordinates transformation so that it actually appears round.

    Using a scatter plot with a circle 'o' scatter marker is a much simpler approach, and can be done without accessing the Axes objects by using mplfinance's mpf.make_addplot(data,type='scatter',...). Also, we won't need any coordinate transformations to ensure nice round circles.

    Before I show a specific example, since you said "I'm open to learn ... to fix my code," let me offer some constructive criticism, and I hope it will be accepted in the helpful spirit intended. As a rule, when there is more than one way to code something, simpler is better. First allow me to commend you on the excellent quality of you question overall, clearing describing what you are trying to accomplish, and showing what you have coded so far. That said, some of the code is more complicated than it needs to be. For example:

    1. There is no need to definte a separate function to set the DatetimeIndex, nor even to call set_index() itself, since Pandas has a couple of convenient ways to do that in a single line of code. For example, using read_csv() or using the Pandas.DatetimeIndex() constructor:
    # This tells read_csv which column to use for the index, 
    # AND it tells it to parse the text into Pandas Timestamp objects:
    df = pd.read_csv(filename,index_col='Start Date',parse_dates=True)
    
    # Alternatively, if the dataframe is already in memory, with a 'Start Date' column, then:
    df.index = pd.DatetimeIndex(df['Start Date'])
    # The above will leave the 'Start Date' column inplace while at the same time using
    # it to set the index.  The column can then be ignored or deleted.  
    
    1. The code creates it's own mplfinance style, but then uses Axes object methods to modify style items. Instead of Axes.tick_params(axis='y', colors='white') and yaxis.label.set_color('white'), specify these items using rc= when calling mpf.make_mpf_style()
    s = mpf.make_mpf_style(figcolor='#162125', facecolor= "#162125", marketcolors=mc, 
                           y_on_right=True,gridcolor='white',gridstyle='--',edgecolor='white'
                           rc={'font.size':18, 'xtick.color': 'w', 
                               'axes.labelsize': 12, 'ytick.color': 'w',          # NEW
                               'ytick.labelcolor': 'w', 'axes.labelcolor':'w'},)  # NEW
    
    1. Simlarly the figure can be saved using kwarg savefig and the x-axis label can be set with kwarg xlabel.
    2. Also, specifying both figsize and figratio does not make sense. Note that figsize includes both figscale and figratio. So either specify figsize alone, or specify figscale and figratio.

    In summary, if there is a simpler way to do something, choose that way. And along these lines, when using mplfinance if directly accessing the Axes object can be avoided, then it should be avoided. It will keep the code simpler and easier to maintain. From what I can tell the only thing you are trying to do that (presently) requires direct access to the Axes objects is setting the tick locations. That will be available in a future version of mplfinance. Here is a re-written, simplified version of your code that I believe does everything you want except for specifically defining where the xticks will be located. Please note that in the example below, to keep the example simple I hard-coded the location of the circles; however that portion of the code is isolated to into functions so that it can be filled in and made dynamic later. I hope this helps:


    Here is the data that you provide, but in csv format:

    Index,Start Date,Open Price,High Price,Low Price,Close Price,Volume,End Date
    0,2023-03-12 18:30:00,3.996,4.038,3.988,4.008,1216259.0,2023-03-12 18:44:59.999
    1,2023-03-12 18:45:00,4.008,4.024,3.99,3.993,638860.0,2023-03-12 18:59:59.999
    2,2023-03-12 19:00:00,3.993,4.024,3.992,4.019,297226.0,2023-03-12 19:14:59.999
    3,2023-03-12 19:15:00,4.018,4.023,3.973,3.985,1101139.0,2023-03-12 19:29:59.999
    4,2023-03-12 19:30:00,3.986,4.003,3.976,3.993,427351.0,2023-03-12 19:44:59.999
    5,2023-03-12 19:45:00,3.993,4.01,3.965,3.975,750141.0,2023-03-12 19:59:59.999
    6,2023-03-12 20:00:00,3.976,3.998,3.967,3.988,552681.0,2023-03-12 20:14:59.999
    7,2023-03-12 20:15:00,3.989,4.009,3.983,4.004,322794.0,2023-03-12 20:29:59.999
    8,2023-03-12 20:30:00,4.005,4.037,4.003,4.035,682787.0,2023-03-12 20:44:59.999
    9,2023-03-12 20:45:00,4.035,4.12,4.035,4.091,2179361.0,2023-03-12 20:59:59.999
    10,2023-03-12 21:00:00,4.091,4.096,4.063,4.084,474021.0,2023-03-12 21:14:59.999
    11,2023-03-12 21:15:00,4.084,4.103,4.077,4.087,480628.0,2023-03-12 21:29:59.999
    12,2023-03-12 21:30:00,4.086,4.107,4.076,4.086,212594.0,2023-03-12 21:44:59.999
    13,2023-03-12 21:45:00,4.086,4.107,4.079,4.105,364555.0,2023-03-12 21:59:59.999
    14,2023-03-12 22:00:00,4.104,4.108,4.06,4.072,474296.0,2023-03-12 22:14:59.999
    15,2023-03-12 22:15:00,4.072,4.257,4.069,4.232,3230671.0,2023-03-12 22:29:59.999
    16,2023-03-12 22:30:00,4.232,4.247,4.208,4.241,851126.0,2023-03-12 22:44:59.999
    17,2023-03-12 22:45:00,4.241,4.276,4.218,4.254,1268534.0,2023-03-12 22:59:59.999
    18,2023-03-12 23:00:00,4.255,4.315,4.253,4.312,1469747.0,2023-03-12 23:14:59.999
    19,2023-03-12 23:15:00,4.313,4.354,4.295,4.343,1352840.0,2023-03-12 23:29:59.999
    20,2023-03-12 23:30:00,4.344,4.479,4.336,4.464,1995492.0,2023-03-12 23:44:59.999
    21,2023-03-12 23:45:00,4.463,4.532,4.412,4.517,2488653.0,2023-03-12 23:59:59.999
    22,2023-03-13 00:00:00,4.517,4.592,4.482,4.58,2140025.0,2023-03-13 00:14:59.999
    23,2023-03-13 00:15:00,4.58,4.695,4.552,4.625,1973254.0,2023-03-13 00:29:59.999
    24,2023-03-13 00:30:00,4.626,4.7,4.577,4.677,2444439.0,2023-03-13 00:44:59.999
    25,2023-03-13 00:45:00,4.677,4.678,4.584,4.595,1353901.0,2023-03-13 00:59:59.999
    26,2023-03-13 01:00:00,4.594,4.601,4.528,4.528,1181759.0,2023-03-13 01:14:59.999
    27,2023-03-13 01:15:00,4.528,4.546,4.489,4.499,785683.0,2023-03-13 01:29:59.999
    28,2023-03-13 01:30:00,4.499,4.507,4.473,4.49,634040.0,2023-03-13 01:44:59.999
    29,2023-03-13 01:45:00,4.49,4.5,4.473,4.475,361538.0,2023-03-13 01:59:59.999
    30,2023-03-13 02:00:00,4.476,4.479,4.445,4.45,507443.0,2023-03-13 02:14:59.999
    31,2023-03-13 02:15:00,4.451,4.457,4.422,4.43,514609.0,2023-03-13 02:29:59.999
    32,2023-03-13 02:30:00,4.431,4.438,4.412,4.412,283667.0,2023-03-13 02:44:59.999
    33,2023-03-13 02:45:00,4.413,4.437,4.401,4.435,443083.0,2023-03-13 02:59:59.999
    34,2023-03-13 03:00:00,4.435,4.451,4.411,4.418,304109.0,2023-03-13 03:14:59.999
    35,2023-03-13 03:15:00,4.418,4.435,4.393,4.432,354457.0,2023-03-13 03:29:59.999
    36,2023-03-13 03:30:00,4.433,4.461,4.415,4.449,256813.0,2023-03-13 03:44:59.999
    37,2023-03-13 03:45:00,4.45,4.462,4.435,4.439,226006.0,2023-03-13 03:59:59.999
    38,2023-03-13 04:00:00,4.437,4.464,4.418,4.458,304705.0,2023-03-13 04:14:59.999
    39,2023-03-13 04:15:00,4.459,4.465,4.436,4.439,288049.0,2023-03-13 04:29:59.999
    

    And here is the code:

    #!/usr/bin/env python
    # coding: utf-8
    
    # ---
    #
    # # Draw circle on plot with mplfinance
    #
    # ## https://stackoverflow.com/questions/75737197/
    #
    # ---
    
    # NOTICE we keep it simple:
    # These are the only imports we need:
    import pandas as pd
    import mplfinance as mpf
    
    # The data is stored in 'so75737197_data.csv'
    # Only read the columns we need, specify which column will be the index,
    # and `parse_dates=True` so that the index will be a DatetimeIndex:
    columns_we_need = ['Start Date','Open Price','High Price','Low Price','Close Price','Volume']
    
    # Use the first line down below if you just copiedpasted the data into a txt file and saved it as 'test_ape.txt'
    #df = pd.read_csv('test_ape.txt',usecols=columns_we_need,index_col='Start Date',parse_dates=True)
    df = pd.read_csv('so75737197_data.csv',
                      usecols=columns_we_need,index_col='Start Date',parse_dates=True)
    
    # Calculate the stochastics:
    k_period = 14
    d_period = 1
    smooth_window = 3
    
    stochastic = pd.DataFrame()
    stochastic['%K'] = ((df['Close Price'] - df['Low Price'].rolling(k_period).min()) \
                        / (df['High Price'].rolling(k_period).max() - df['Low Price'].rolling(k_period).min())) * 100
    stochastic['%D'] = stochastic['%K'].rolling(d_period).mean()
    stochastic['%SD'] = stochastic['%D'].rolling(smooth_window).mean()
    stochastic['UL'] = 80
    stochastic['DL'] = 20
    
    
    # Create my own `marketcolors` style:
    mc = mpf.make_marketcolors(up='#0ECB81',down='#F64670',inherit=True)
    
    # Create my own `MatPlotFinance` style:
    s  = mpf.make_mpf_style(figcolor='#162125', facecolor= "#162125", marketcolors=mc, y_on_right=True,
                            gridcolor='white', gridstyle='--', edgecolor='white',
                            rc={'font.size':18,'xtick.color': 'w','axes.labelsize': 12,
                                'ytick.color': 'w', 'ytick.labelcolor': 'w', 'axes.labelcolor':'w'})
    
    def get_stochastic_indicator_purple(df, stochastic):
        # Add logic here to determine where the purple circles should be.
    
        # For now, for demonstration purposes,
        # we are just hard-coding the 17th position:
    
        # We are going to display the circles using mpf.make_addplot(type='scatter')
        # With `mpf.make_addplot()` scatter plots, we want data to exist where we want
        # the markers (circles) to appear, and NAN values to exist in all other locations:
        circles = [float('nan')]*len(df)
        circles[17] = stochastic.iloc[17]['%K']
        return circles
    
    def get_stochastic_indicator_red(df, stochastic):
        # Add logic here to determine where the red circles should be.
    
        # For now, for demonstration purposes,
        # we are just hard-coding the 25th position:
    
        # We are going to display the circles using mpf.make_addplot(type='scatter')
        # With `mpf.make_addplot()` scatter plots, we want data to exist where we want
        # the markers (circles) to appear, and NAN values to exist in all other locations:
        circles = [float('nan')]*len(df)
        circles[25] = stochastic.iloc[25]['%K']
        return circles
    
    purple_circles = get_stochastic_indicator_purple(df, stochastic)
    red_circles    = get_stochastic_indicator_red(df, stochastic)
    
    # The purpose of these `range_markers` was to make it easier to know
    # how big to make the `markersize` so that the circle would cover
    # these ranges as you had requested.  Once the correct circle size
    # is determined, then the range_markers are no longer needed:
    # range_markers = [float('nan')]*len(df)
    # range_markers[15] = 50
    # range_markers[19] = 50
    # range_markers[23] = 50
    # range_markers[27] = 50
    
    # Set ylim on the stochastic to make room for the circles and
    # also to see the stochastic a little better:
    ylimits = (-10,140)
    
    # To make cicles, choose 'o' as the marker for the scatter plot.  Normally 'o' will
    # create a solid circle for a scatter plot, but by setting `color='None'` and
    # `edgecolors=<color>` we can make hollow circles:
    plots_to_add = {"Stochastics":  mpf.make_addplot(stochastic[['%K', '%SD', 'UL', 'DL']],
                                                     ylim=ylimits, panel=2, ylabel="Stochastics",y_on_right=False),
                    "Purple_Cicles":mpf.make_addplot(purple_circles,type='scatter',markersize=9600,color='None',
                                                     edgecolors='purple',marker='o',linewidths=10,panel=2,ylim=ylimits),
                    "Red_Cicles":   mpf.make_addplot(red_circles,type='scatter',markersize=9600,color='None',
                                                     edgecolors='red'   ,marker='o',linewidths=10,panel=2,ylim=ylimits),
                   #"RangeMarkers": mpf.make_addplot(range_markers,type='scatter',markersize=250,color="w",
                   #                                 edgecolors='yellow',marker='^',linewidths=3,panel=2,ylim=(0,140))
                   }
    
    filename = 'test_apeusdt.png'
    mpf.plot(df,columns=columns_we_need[1:],figsize=(18,10),panel_ratios=(5,1.25,2),type="candle",
             volume=True,style=s,tight_layout=True,datetime_format='%b %d, %H:%M',ylabel="Price ($)",
             show_nontrading=True,warn_too_much_data=870, # Silence the Too Much Data Warning
             addplot = list(plots_to_add.values()),       # Add the stochastic plot as well as the bullish entries to the main plot
             title=dict(title="APEUSDT - 15m", fontsize=40, style='italic', weight='bold', color="white"),
             xlabel='Timezone UTC',
             savefig=dict(fname=filename,dpi=300,bbox_inches="tight")
            )
    
    print('Done: Result is in file "'+filename+'"')
    

    And the result that I get:

    enter image description here