Python matplotlib subplots x-axis not aligned

  matplotlib, python-3.x

Python matplotlib subplots x-axis not aligned
[![enter image description here][1]][1]
[1]: https://i.stack.imgur.com/fdjVd.png

Here’s the code behind this plot: Stock prices on top (From MEXC and gateio) on CROUSDT. Then trading volume in bottom. Notice the two volumes bar charts in the bottom, their x-axis align. But price charts x-axis not aligned with volumes bar charts in the bottom: But they used same x-axis source: pd_gateio_grouped[‘time_axis_label’] and pd_mexc_grouped[‘time_axis_label’]
I tried quite a few things but is running out of tricks.

import sys
import traceback
from datetime import datetime, timedelta, date
from typing import final
import pytz
import arrow
import pandas as pd
import json
import requests
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import asyncio
import nest_asyncio

from ccxt.gateio import gateio
from ccxt.mexc import mexc
from ccxt import Exchange

async def run():
    tday = datetime.combine(date.today(), datetime.min.time())
    tm1 = tday - timedelta(days=1)
    MAX_NUM_TRADES : int = 1000 # ccxt exchange.fetch_trades max return 1000 rows. You need implement pagination if you want more.
    msg : str = None

    gateio_trades = None
    mexc_trades = None
    formatted_trades_gateio = None
    formatted_trades_mexc = None
    pd_gateio_grouped = None
    pd_mexc_grouped = None

    def _log(msg : str):
        print(f"{datetime.utcnow()} {msg}")

    def _bucket_datetime(dt : datetime):
        minute : int = dt.minute
        if minute>=0 and minute<=10:
            return datetime(dt.year, dt.month, dt.day, dt.hour, 0)
        elif minute>10 and minute<=20:
            return datetime(dt.year, dt.month, dt.day, dt.hour, 10)
        elif minute>20 and minute<=30:
            return datetime(dt.year, dt.month, dt.day, dt.hour, 20)
        elif minute>30 and minute<=40:
            return datetime(dt.year, dt.month, dt.day, dt.hour, 30)
        elif minute>40 and minute<=50:
            return datetime(dt.year, dt.month, dt.day, dt.hour, 40)
        elif minute>50:
            return datetime(dt.year, dt.month, dt.day, dt.hour, 50)
    
    def _minute_buckets(dt : datetime):
        bucketed_dt = _bucket_datetime(dt)
        return bucketed_dt.strftime("%H:%M")

    def _fetch_from_ccxt_exchange(exchange : Exchange, symbol : str, since, limit, params):
        return exchange.fetch_trades(symbol=denormalized_ticker_cex, since=since, limit=limit, params={})

    DATE_FORMAT = '%H:%M'
    SMALL_SIZE = 7
    MEDIUM_SIZE = 10
    BIGGER_SIZE = 12
    colors = {'red': '#ff207c', 'grey': '#42535b', 'blue': '#207cff', 'orange': '#ffa320', 'green': '#00ec8b'}
    plt.rc('figure', figsize=(13, 7))
    plt.ion()

    plt.rc('font', size=SMALL_SIZE)          # controls default text sizes
    plt.rc('axes', titlesize=SMALL_SIZE)     # fontsize of the axes title
    plt.rc('axes', labelsize=SMALL_SIZE)    # fontsize of the x and y labels
    plt.rc('xtick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
    plt.rc('ytick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
    plt.rc('legend', fontsize=SMALL_SIZE)    # legend fontsize
    plt.rc('figure', titlesize=SMALL_SIZE)  # fontsize of the figure title
    fig, axes = plt.subplots(4, 1, gridspec_kw={'height_ratios': [4, 1, 1, 1]})
    fig.tight_layout(pad=3)

    for ax in axes:
        ax.grid(True)
    
    one_loop = asyncio.get_event_loop()
    nest_asyncio.apply()

    i = 0
    while True:
        try:
            # STEP 1. Fetches
            denormalized_ticker_cex : str = 'CRO/USDT'
            fetches = [
                    one_loop.run_in_executor(None, _fetch_from_ccxt_exchange, gateio(), denormalized_ticker_cex, datetime.timestamp(tm1), MAX_NUM_TRADES, {}),
                    one_loop.run_in_executor(None, _fetch_from_ccxt_exchange, mexc(), denormalized_ticker_cex, datetime.timestamp(tm1), MAX_NUM_TRADES, {}),
                ]
            results = await asyncio.gather(*fetches)
            gateio_trades = results[0]
            mexc_trades = results[1]

            pd_gateio_grouped = None
            pd_mexc_grouped = None

            # STEP 2. Format CEX trades
            dt_min_gateio = arrow.get(gateio_trades[0]['datetime']).datetime
            dt_max_gateio = arrow.get(gateio_trades[len(gateio_trades)-1]['datetime']).datetime
            dt_min_mexc = arrow.get(mexc_trades[0]['datetime']).datetime
            dt_max_mexc = arrow.get(mexc_trades[len(mexc_trades)-1]['datetime']).datetime

            dt_min = dt_min_gateio if dt_min_gateio>dt_min_mexc else dt_min_mexc
            dt_max = dt_max_gateio if dt_max_gateio<dt_max_mexc else dt_max_mexc
            _log(f"dt_min: {dt_min}, dt_max: {dt_max}")

            formatted_trades_gateio = [ {
                                        'exchange' : 'gateio',
                                        'datetime' : arrow.get(t['datetime']).datetime,
                                        'time_axis_label' : _bucket_datetime(arrow.get(t['datetime']).datetime),
                                        'minute_bucket' : _minute_buckets(arrow.get(t['datetime']).datetime),
                                        'date' : arrow.get(t['datetime']).datetime.date(),
                                        'hh' : arrow.get(t['datetime']).datetime.hour,
                                        'mm' : arrow.get(t['datetime']).datetime.minute,
                                        # price in quote ccy. amount in base ccy.
                                        'price' : float(t['price']),
                                        'signed_amount' : float(t['amount']) if t['side']=='buy' else (-1 * float(t['amount']))
                                    } for t in gateio_trades 
                                        if arrow.get(t['datetime']).datetime>=dt_min
                                    ]
            pd_gateio = pd.DataFrame.from_dict(formatted_trades_gateio)
            pd_gateio['trade_consideration'] = pd_gateio['price'] * pd_gateio['signed_amount']
            pd_gateio.sort_values(by=['datetime'], ascending=[True], inplace=True)
            pd_gateio_grouped = pd_gateio.groupby(["exchange", "time_axis_label", "minute_bucket"]).agg({ "trade_consideration": sum, 'signed_amount' : sum })
            pd_gateio_grouped['volume_weighted_price'] = pd_gateio_grouped['trade_consideration'] / pd_gateio_grouped['signed_amount']
            pd_gateio_grouped.reset_index(inplace=True)
            pd_gateio_grouped.sort_values(by=['time_axis_label'], ascending=[True], inplace=True)

            formatted_trades_mexc = [ {
                                        'exchange' : 'mexc',
                                        'datetime' : arrow.get(t['datetime']).datetime,
                                        'time_axis_label' : _bucket_datetime(arrow.get(t['datetime']).datetime),
                                        'minute_bucket' : _minute_buckets(arrow.get(t['datetime']).datetime),
                                        'date' : arrow.get(t['datetime']).datetime.date(),
                                        'hh' : arrow.get(t['datetime']).datetime.hour,
                                        'mm' : arrow.get(t['datetime']).datetime.minute,
                                        'price' : float(t['price']),
                                        'signed_amount' : float(t['amount']) if t['side']=='buy' else (-1 * float(t['amount']))
                                    } for t in mexc_trades 
                                        if arrow.get(t['datetime']).datetime>=dt_min
                                    ]
            pd_mexc = pd.DataFrame.from_dict(formatted_trades_mexc)
            pd_mexc['trade_consideration'] = pd_mexc['price'] * pd_mexc['signed_amount']
            pd_mexc_grouped = pd_mexc.groupby(["exchange", "time_axis_label", "minute_bucket"]).agg({ "trade_consideration": sum, 'signed_amount' : sum })
            pd_mexc_grouped['volume_weighted_price'] = pd_mexc_grouped['trade_consideration'] / pd_mexc_grouped['signed_amount']
            pd_mexc_grouped.reset_index(inplace=True)
            pd_mexc_grouped.sort_values(by=['time_axis_label'], ascending=[True], inplace=True)

            msg = f"gateio #trades: {len(formatted_trades_gateio)}, UTC createtime: 1st trade: {formatted_trades_gateio[0]['datetime']}, most recent: {formatted_trades_gateio[len(formatted_trades_gateio)-1]['datetime']}"
            _log(msg)
            msg = f"mexc #trades: {len(formatted_trades_mexc)}, UTC createtime: 1st trade: {mexc_trades[0]['datetime']}, most recent: {mexc_trades[len(mexc_trades)-1]['datetime']}"
            _log(msg)

            # STEP 2. Charting https://matplotlib.org/3.1.1/api/pyplot_summary.html
            plot_price = axes[0]
            plot_price.xaxis.set_major_formatter(mdates.DateFormatter(DATE_FORMAT))
            plot_price.tick_params(axis="x", labelbottom=True, rotation=0)
            line3 = None
            if i==0:    
                plot_price.set_xlabel('time (UTC)')
                plot_price.set_ylabel('price (Volume weighted)')
                line1 = plot_price.plot(pd_gateio_grouped['time_axis_label'], pd_gateio_grouped['volume_weighted_price'], color=colors['blue'], linewidth=2, label='gateio')
                line2 = plot_price.plot(pd_mexc_grouped['time_axis_label'], pd_mexc_grouped['volume_weighted_price'], color=colors['green'], linewidth=2, label='mexc')
            else:
                line1[0].set_xdata(pd_gateio_grouped['time_axis_label'])
                line1[0].set_ydata(pd_gateio_grouped['volume_weighted_price'])
                line2[0].set_xdata(pd_mexc_grouped['time_axis_label'])
                line2[0].set_ydata(pd_mexc_grouped['volume_weighted_price'])
                try:
                    fig.canvas.draw()
                    fig.canvas.flush_events()
                except:
                    msg = f"Unhandled exception on fig.canvas.draw: {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
                    _log(f"{msg}")

            fig.suptitle(f"MEXC vs Gateio {denormalized_ticker_cex} last_updated_utc: {datetime.utcnow()}", fontsize=14, fontweight='bold')
            plot_price.legend(loc='upper left')

            plot_vol_gateio = axes[1]
            plot_vol_gateio.bar(pd_gateio_grouped['minute_bucket'], pd_gateio_grouped['trade_consideration'], color='blue', label='gateio')
            if i==0:
                plot_vol_gateio.tick_params(axis="x", labelbottom=True, rotation=0)
                plot_vol_gateio.set_xlabel('time (UTC)')
                plot_vol_gateio.set_ylabel('volume (Base ccy)')
                plot_vol_gateio.legend(loc='upper left')

            plot_vol_mexc = axes[2]
            plot_vol_mexc.bar(pd_mexc_grouped['minute_bucket'], pd_mexc_grouped['trade_consideration'], color='green', label='mexc')
            if i==0:
                plot_vol_mexc.tick_params(axis="x", labelbottom=True, rotation=0)
                plot_vol_mexc.set_xlabel('time (UTC)')
                # plot_vol_mexc.set_ylabel('volume (Base ccy)')
                plot_vol_mexc.legend(loc='upper left')
            
            if i==0:
                plt.tight_layout()
                plt.show(block=False)

            plt.pause(0.01)
            
        except:
            msg = f"Unhandled exception: {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
            _log(f"{msg}")
        finally:        
            i = i+1

            if gateio_trades:
                gateio_trades.clear()
                gateio_trades = None
            if mexc_trades:
                mexc_trades.clear()
                mexc_trades = None
            
            if formatted_trades_gateio:
                formatted_trades_gateio.clear()
                formatted_trades_gateio = None
            if formatted_trades_mexc:
                formatted_trades_mexc.clear()
                formatted_trades_mexc = None
            
            try:
                pd_gateio_grouped.drop(pd_gateio_grouped.index, inplace=True)
            except:
                pass
            try:
                pd_mexc_grouped.drop(pd_mexc_grouped.index, inplace=True)
            except:
                pass
            

if __name__ == "__main__":
    asyncio.run(run())

Source: Python-3x Questions

LEAVE A COMMENT