python-3.xpandasmatplotlibjalali-calendarmplfinance

How to show alternative calendar dates in mplfinance?


TL;DR - The issue

I have an mplfinance plot based on a pandas dataframe in which the indices are in Georgian calendar format and I need to have them displayed as Jalali format.

My data and code

My data looks like this:

            open    high    low     close
date                                       
2021-03-15  67330.0 69200.0 66870.0 68720.0
2021-03-16  69190.0 71980.0 69000.0 71620.0
2021-03-17  72450.0 73170.0 71700.0 71820.0
2021-03-27  71970.0 73580.0 70000.0 73330.0
2021-03-28  73330.0 73570.0 71300.0 71850.0
...         ...     ...     ...     ...

The first column is both a date and the index. This is required by mplfinance plot the data correctly; Which I can plot with something like this:

import mplfinance as mpf
mpf.plot(chart_data.tail(7), figratio=(16,9), type="candle", style='yahoo', ylabel='', tight_layout=True, xrotation=90)

Where chart_data is the data above and the rest are pretty much formatting stuff.

What I have now

My chart looks like this:

enter image description here

However, the I need the dates to look like this: 1400-01-12. Here's a table of equivalence to further demonstrate my case.

2021-03-15  1399-12-25
2021-03-16  1399-12-26
2021-03-17  1399-12-27
2021-03-27  1400-01-07
2021-03-28  1400-01-08

What I've tried

Setting Jdates as my indices:

chart_data.index = history.jdate
mpf.plot(chart_data_j)

Throws this exception:

TypeError('Expect data.index as DatetimeIndex')

So I tried converting the jdates into datetimes:

chart_data_j.index = pd.to_datetime(history.jdate)

Which threw an out of bounds exception:

OutOfBoundsDatetime: Out of bounds nanosecond timestamp: 1398-03-18 00:00:00

So I though maybe changing the timezone/locale would be an option, so I tried changing the timezones, following the official docs:

pd.to_datetime(history.date).tz_localize(tz='US/Eastern')

But I got this exception:

raise TypeError(f"{ax_name} is not a valid DatetimeIndex or PeriodIndex")

And finally I tried using libraries such as PersianTools and pandas_jalali to no avail.


Solution

  • You can get this to work by creating your own custom DateFormatter class, and using mpf.plot() kwarg returnfig=True to gain access to the Axes objects to be able to install your own custom DateFormatter.

    I have written a custom DateFormatter (see code below) that is aware of the special way that MPLfinance handles the x-axis when show_nontrading=False (i.e. the default value).

    import pandas as pd
    import mplfinance as mpf
    import jdatetime as jd
    import matplotlib.dates as mdates
    
    from matplotlib.ticker import Formatter
    class JalaliDateTimeFormatter(Formatter):
        """
        Formatter for JalaliDate in mplfinance.
        Handles both `show_nontrading=False` and `show_nontrading=True`.
        When show_nonntrading=False, then the x-axis is indexed by an
        integer representing the row number in the dataframe, thus:
        Formatter for axis that is indexed by integer, where the integers
        represent the index location of the datetime object that should be
        formatted at that lcoation.  This formatter is used typically when
        plotting datetime on an axis but the user does NOT want to see gaps
        where days (or times) are missing.  To use: plot the data against
        a range of integers equal in length to the array of datetimes that
        you would otherwise plot on that axis.  Construct this formatter
        by providing the arrange of datetimes (as matplotlib floats). When
        the formatter receives an integer in the range, it will look up the
        datetime and format it.
    
        """
        def __init__(self, dates=None, fmt='%b %d, %H:%M', show_nontrading=False):
            self.dates = dates
            self.len   = len(dates) if dates is not None else 0
            self.fmt   = fmt
            self.snt   = show_nontrading
    
        def __call__(self, x, pos=0):
            '''
            Return label for time x at position pos
            '''
            if self.snt:
                jdate = jd.date.fromgregorian(date=mdates.num2date(x))
                formatted_date = jdate.strftime(self.fmt)
                return formatted_date
    
            ix = int(round(x,0))
    
            if ix >= self.len or ix < 0:
                date = None
                formatted_date = ''
            else:
                date = self.dates[ix]
                jdate = jd.date.fromgregorian(date=mdates.num2date(date))
                formatted_date = jdate.strftime(self.fmt)
            return formatted_date
    
    #  ---------------------------------------------------
    
    df = pd.read_csv('so_67001540.csv',index_col=0,parse_dates=True)
    
    mpf.plot(df,figratio=(16,9),type="candle",style='yahoo',ylabel='',xrotation=90)
    
    dates     = [mdates.date2num(d) for d in df.index]
    formatter = JalaliDateTimeFormatter(dates=dates,fmt='%Y-%m-%d')
    
    fig, axlist = mpf.plot(df,figratio=(16,9), 
                           type="candle",style='yahoo',
                           ylabel='',xrotation=90,
                           returnfig=True)
    
    axlist[0].xaxis.set_major_formatter(formatter)
    mpf.show()
    
    date,open,high,low,close,alt_date
    2021-03-15,67330.0,69200.0,66870.0,68720.0,1399-12-25
    2021-03-16,69190.0,71980.0,69000.0,71620.0,1399-12-26
    2021-03-17,72450.0,73170.0,71700.0,71820.0,1399-12-27
    2021-03-27,71970.0,73580.0,70000.0,73330.0,1400-01-07
    2021-03-28,73330.0,73570.0,71300.0,71850.0,1400-01-08
    

    enter image description here

    enter image description here