cloud The picture above shows bitcoin’s close price plotted with Ichimoku Cloud & William’s Fractals. 20 / 60 / 120 / 30 / 30 are the recommended settings for Ichimoku cloud when applied to Bitcoin.

After toying with PySide6 for a little while, I think it’s safe to say that Qt has become my favorite GUI toolkit over wxPython for programming graphical user interfaces in Python.

While matplotlib/mplfinance is superior for the visualization and analysis of financial data. I thought I’d try PyQTGraph out for this article to demonstrate the embedding of custom widgets in Qt designer.

GUI Programming With PySide6

Setup

The way I’ve set this up is in a virtual environment so open up the command line and change directory to your desktop and create the virtual environment in a folder called ‘virtual’ on your desktop. The next step is to change directory to the Scripts folder and activate the environment. Before we install any packages make sure pip is up to date and then install pyside6, pandas, and requests.

  1. cd Desktop && python -m venv virtual
  2. cd C:\Users\%USERNAME%\Desktop\virtual\Scripts && activate
  3. python.exe -m pip install --upgrade pip
  4. pip install pyside6 pandas requests pyqtgraph

Change directory back to the root of the virtual environment and create a folder called Projects, this is where we will create new projects in this environment. We can now create the root of our first directory by creating a folder called crypto within Projects.

  1. cd C:\Users\%USERNAME%\Desktop\virtual && md Projects
  2. cd Projects && md crypto && cd crypto

First Steps

QApplication QMainWindow QPushButton

Create a file called main.py and paste the following code within (I use VSCode & Notepad++ to edit code). You can then run the code in the command line console with the command python main.py. You should get a small window with a button you can click to get a “Clicked!” message. When the button is clicked, QPushButton sends a signal to our button_clicked slot and executes the code there.

import sys
#  QApplication is the application handler
#  QWidget is a simple and empty GUI widget
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton

def main():
    # Passing in sys.argv enables command line arguments 
    app = QApplication(sys.argv)

    # Create main window
    window = MainWindow()

    # Windows are hidden by default
    window.show()

    # Start event loop
    app.exec()

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Set a custom window title
        self.setWindowTitle("Hello")

        # Create button widget
        button = QPushButton("Click")

        # Connect QPushButton Signal to custom slot
        button.clicked.connect(self.button_clicked)

        # Set the central window
        self.setCentralWidget(button)

    # Create custom slot to accept QPushButton clicked signal
    def button_clicked(self):
        print("Clicked!")

if __name__ == "__main__":
    main()

Qt Designer

designer

Because programming GUIs by hand would be time consuming, monotonous, and partially masochistic. The Qt toolkit comes with an extremely helpful tool called Qt Designer. Let’s switch over to it by running the following command pyside6-designer and giving it a moment to load up. Once the program is running click the New icon in the upper left hand corner of the tool bar, select Main Window from templates/forms, and press Create.

mwindow

Once the main window has been created you can resize it if you want, I’ve resized mine to around 700x500. Now drag the Widget contain onto the main window and click the ‘Lay Out Vertically’ button so the Widget expands over the face of the main window. Rename the widget ‘graphWidget’.

promote

Right-click the Widget and select Promote to ... and fill in the promoted class name with ‘PlotWidget’ and header file with ‘pyqtgraph’, select the Promoted Class and click promote. Save the interface to \Desktop\virtual\Projects\crypto as crypto.ui by clicking on the save icon in the upper left hand side of the Qt designer. We can now generate python code with the command pyside6-uic crypto.ui -o ui_crypto.py and modify by inheritance and overriding.

default

We want to import the Ui_MainWindow class from ui_crypto.py file into our new main.py program. So replace the previous code in main.py with the following. Running the main program with python main.py gives the main window in the above picture.

import sys

from PySide6.QtWidgets import QApplication, QMainWindow, QDialog
from ui_crypto         import Ui_MainWindow

def main():
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    window = MainWindow()
    window.show()
    app.exec()

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)

if __name__ == "__main__":
    main()

line

At this point we just have a black screen and thats not very useful, so we need to decide what we actually want to appear here. Lets test out a line or two before attempting Ichimoku cloud with Bill William’s fractals.

import sys

from PySide6.QtWidgets import QApplication, QMainWindow, QDialog
from ui_crypto         import Ui_MainWindow
from PySide6.QtCore    import Qt

import pyqtgraph as pg

def main():
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    window = MainWindow()
    window.show()
    app.exec()

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        
        # self.graphWidget = self.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        # Style
        self.graphWidget.setBackground('w')

        # Title
        self.graphWidget.setTitle("A Simple Line")

        # Legend
        self.graphWidget.addLegend()

        # Grid
        self.graphWidget.showGrid(x=True, y=True)
        
        # Plot Line
        pen  = pg.mkPen(color="r", width=3, style=Qt.SolidLine)
        self.graphWidget.plot(x=range(1,10+1), y=[1,20,25,30,5,10,15,35,40,45], name="Line", pen=pen)

if __name__ == "__main__":
    main()

bitcoin The picture above shows bitcoin’s close price plotted to the graph.

Lines are nice but not very useful unless they have some meaning that provides a reason as to why we are expending such great effort to plot them. We should go ahead and retrieve and plot the price action from Binance.us. Create the following directory structure.

Projects/crypto/
├─ binance/
│  ├─ __init__.py
│  ├─ api.py
│  ├─ binance.py
│  └─ market.py
├─ crypto.ui
├─ main.py 
└─ ui_crypto.py

Change main to the following code.

import sys

from PySide6.QtWidgets import QApplication, QMainWindow, QDialog
from ui_crypto         import Ui_MainWindow
from PySide6.QtCore    import Qt

from binance.binance   import Binance

import pyqtgraph as pg

def main():
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    window = MainWindow()
    window.show()
    app.exec()

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        
        # self.graphWidget = self.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        # Style
        self.graphWidget.setBackground('w')

        # Title
        self.graphWidget.setTitle("Bitcoin")

        # Legend
        self.graphWidget.addLegend()

        # Grid
        self.graphWidget.showGrid(x=True, y=True)
        
        # Plot Bitcoin
        bi = Binance()
        df = bi.candles(
            symbol   = "BTCUSDT",
            interval = "1d"
        )
        
        price = pg.mkPen(color="#3d3d3d", width=3, style=Qt.SolidLine)
        self.graphWidget.plot(df["close"], name="Price", pen=price)

if __name__ == "__main__":
    main()

Copy the following code into api.py and save the file.

from requests.exceptions import HTTPError
from functools           import wraps

class BinanceAPI:

    API_KEY    = "YOUR API KEY HERE"
    API_SECRET = "YOUR API SECRET HERE"
    HEADER     = {
        'get' : {"X-MBX-APIKEY": API_KEY},
    }

    # Base + Version
    __baseEndpoint = 'https://api.binance.us'
    ENDPOINT       = __baseEndpoint + '/api/v3/'
    
    @staticmethod
    def error_handler(func):
        @wraps(func)
        def logic(*args, **kwargs):
            resp = func(*args, **kwargs)
            
            if not resp.ok:
                raise HTTPError(f"[!] Error: {resp.status_code} {resp.reason} {resp.json()}")
                
            if resp.status_code != 200:
                print(f"[!] Warning: {resp.status_code} {resp.reason}")
            
            return resp
        return logic
    
    @staticmethod
    def deserialize(func):
        @wraps(func)
        def transform(*args, **kwargs):
            try:
                resp = func(*args, **kwargs)
            except HTTPError as error:
                print(error)
                return False

            return resp.json()
        return transform

Copy the following code into binance.py and save the file.

from binance.market import MarketData
from datetime       import datetime

import numpy  as np
import pandas as pd

import time

class Binance(MarketData):
    """
    The purpose of this class is to make sense of the data provided by its inherited methods.
    """

    def test_connection(self):
        return self.ping()

    def candles(self, symbol="BTCUSDT", interval="1d", startTime=None, endTime=None, limit=500):
        """
        Returns kline data in a pandas DataFrame.

                                    open      high       low     close      volume
        date
        2022-01-07 23:59:59.999  41813.06  42003.24  41763.62  41877.37    3.722429
        2022-01-08 03:59:59.999  41953.26  42199.05  41775.61  41936.91   16.839122
        2022-01-08 07:59:59.999  41932.70  42044.44  41395.90  41610.62   34.417348
        2022-01-08 11:59:59.999  41616.50  41724.13  40502.75  40802.00   74.394267
        2022-01-08 15:59:59.999  40871.32  42318.07  40726.98  41691.66   37.362512
        ...                           ...       ...       ...       ...         ...
        2022-01-21 03:59:59.999  39120.92  39279.94  38682.95  38880.04   25.458106
        2022-01-21 07:59:59.999  38903.98  39030.43  37706.69  38849.32  121.282145
        2022-01-21 11:59:59.999  38866.64  39057.23  37857.95  37954.55  115.010300
        2022-01-21 15:59:59.999  37967.73  38508.49  35450.11  36474.82  350.844053
        2022-01-21 19:59:59.999  36445.31  36832.05  36157.48  36368.72   52.074380

        [84 rows x 5 columns]
        """

        # Open Time / Open / High / Low / Close / Volume / Close Time /
        # Quote Volume / Trades / Taker Buy Base / Taker Buy Quote / Ignore / 
        data    = self.klines( symbol, interval, startTime, endTime, limit )

        # Close Time / Open / High / Low / Close / Volume / 
        dateObj = lambda x: datetime.fromtimestamp(x/1000)

        df = pd.DataFrame(
            # Open / High / Low / Close / Volume / Datetime Object
            data    = [[dateObj(i[6])] + [np.double(j) for j in i[1:6]] for i in data],
            columns = ['date', 'open', 'high', 'low', 'close', 'volume'],
            # dtype   = np.double
        )
        df.set_index('date', inplace=True)

        return df

def say(string):
    t = time.strftime("%Y%m%d %I:%M:%S", time.localtime())
    print(f"[{t}] {string.title()} ...")
    # time.sleep(1)

Copy the following code into market.py and save the file.

from binance.api import BinanceAPI

import requests

class MarketData(BinanceAPI):
    
    @BinanceAPI.deserialize
    @BinanceAPI.error_handler
    def ping(self):
        """ 
        GET /api/v3/ping

        Test connectivity to the Rest API.

        Weight: 1

        Parameters: NONE

        Response: {}
        """  
        return requests.get(url = BinanceAPI.ENDPOINT + 'ping')
        
    @BinanceAPI.deserialize
    @BinanceAPI.error_handler
    def klines(self, symbol="BTCUSDT", interval="1d", startTime=None, endTime=None, limit=500):
        """ 
        GET /api/v3/klines

        Kline/candlestick bars for a symbol. 
        Klines are uniquely identified by their open time.

        Weight: 1

        | Name      | Type   | Mandatory | Description             |
        |-----------|--------|-----------|-------------------------|
        | symbol    | STRING | YES       |                         |
        | interval  | ENUM   | YES       |                         |
        | startTime | LONG   | NO        |                         |
        | endTime   | LONG   | NO        |                         |
        | limit     | INT    | NO        | Default: 500; max 1000. |

        If startTime and endTime are not sent, the most recent klines are returned.

        Response: 
        
        [
            [
                1499040000000, // Open time
                "0.00386200",  // Open
                "0.00386200",  // High
                "0.00386200",  // Low
                "0.00386200",  // Close
                "0.47000000",  // Volume
                1499644799999, // Close time
                "0.00181514",  // Quote asset volume
                1,             // Number of trades
                "0.47000000",  // Taker buy base asset volume
                "0.00181514",  // Taker buy quote asset volume
                "0" // Ignore.
            ]
        ]

        m -> minutes; h -> hours; d -> days; w -> weeks; M -> months
        """
        return requests.get(
            url     = BinanceAPI.ENDPOINT + 'klines',
            params  = {
                'symbol'    : symbol, 
                'interval'  : interval,
                'startTime' : startTime,
                'endTime'   : endTime,
                'limit'     : limit 
            }
        )

technical The picture above shows bitcoin’s close price plotted with Ichimoku Cloud & William’s Fractals. 20 / 60 / 120 / 30 / 30 are the recommended settings for Ichimoku cloud when applied to Bitcoin.

We would like to be able to apply technical analysis to the bitcoin price, and in order to do that we need to program the routines that would supply us with the information to plot. Luckily, in previous articles, namely Ichimoku Kinko Hyo (Ichimoku Cloud) and Bill William’s Alligator & Fractals I already did the work, so we can reuse some of the code. Create the additional analysis directory & file structure.

Projects/crypto/
├─ analysis/
│  ├─ __init__.py
│  ├─ ichimoku.py
│  └─ williams.py
├─ binance/
│  ├─ __init__.py
│  ├─ api.py
│  ├─ binance.py
│  └─ market.py
├─ crypto.ui
├─ main.py 
└─ ui_crypto.py

Change main to the following code.

import sys

from PySide6.QtWidgets import QApplication, QMainWindow, QDialog
from ui_crypto         import Ui_MainWindow
from PySide6.QtCore    import Qt

from binance.binance   import Binance
from analysis.williams import Fractals
from analysis.ichimoku import IchimokuKinkoHyo

import pyqtgraph as pg

def main():
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    window = MainWindow()
    window.show()
    app.exec()

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        
        # self.graphWidget = self.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        # Style
        self.graphWidget.setBackground('w')

        # Title
        self.graphWidget.setTitle("Bitcoin")

        # Legend
        self.graphWidget.addLegend()

        # Grid
        self.graphWidget.showGrid(x=True, y=True)
        
        # Plot Bitcoin
        bi = Binance()
        df = bi.candles(
            symbol   = "BTCUSDT",
            interval = "1d"
        )
        
        dates = df.index.to_pydatetime()
        dates = [i.timestamp() for i in dates]
        
        price = pg.mkPen(color="#3d3d3d", width=3, style=Qt.SolidLine)
        self.graphWidget.plot(dates, df["close"], name="Price", pen=price)
        
        # Plot Technicals
        IchimokuKinkoHyo().plot_qt(df, self.graphWidget, pg, Qt)
        Fractals().plot_qt_f(df, self.graphWidget, pg, Qt)

if __name__ == "__main__":
    main()

Copy the following code into ichimoku.py and save the file.

from datetime import timedelta
from numpy    import nan

class IchimokuKinkoHyo:

    def __tenkan_sen(self, df, period=20):
        """
        Tenkan-sen (Conversion Line): (20-period high + 20-period low)/2))
            The default setting is 9 periods and can be adjusted. On a daily chart, 
            this line is the midpoint of the 9-day high-low range, which is almost two weeks.  
        """
        high = df['high'].rolling(period).max()
        low  = df['low'].rolling(period).min()
        return (high + low)/2

    def __kijun_sen(self, df, period=60):
        """
        Kijun-sen (Base Line): (60-period high + 60-period low)/2))
            The default setting is 26 periods and can be adjusted. On a daily chart, 
            this line is the midpoint of the 26-day high-low range, which is almost one month).  
        """
        high = df['high'].rolling(period).max()
        low  = df['low'].rolling(period).min()
        return (high + low)/2

    def __senkou_span_a(self, df, future=30):
        """
        Senkou Span A (Leading Span A): (Conversion Line + Base Line)/2))
            This is the midpoint between the Conversion Line and the Base Line. The Leading Span A forms one of the two cloud boundaries. 
            It is referred to as "Leading" because it is plotted 26 periods in the future and forms the faster cloud boundary.
        """
        return ((self.__tenkan_sen(df) + self.__kijun_sen(df))/2).shift(future)

    def __senkou_span_b(self, df, period=120, future=30):
        """
        Senkou Span B (Leading Span B): (120-period high + 120-period low)/2))
            On the daily chart, this line is the midpoint of the 52-day high-low range, which is a little less than 3 months. 
            The default calculation setting is 52 periods, but can be adjusted. This value is plotted 26 periods in the future
            and forms the slower cloud boundary.
        """
        high = df['high'].rolling(period).max()
        low  = df['low'].rolling(period).min()
        # return high.add(low).divide(2).shift(future)
        return ((high + low)/2).shift(future)

    def __kumo(self, df):
        """
        Senkou Span A & Senkou Span B
        """
        return self.__senkou_span_a(df), self.__senkou_span_b(df)

    def __chikou_span(self, df, offset=30):
        """
        Chikou Span (Lagging Span): Close plotted 30 days in the past
            The default setting is 26 periods, but can be adjusted. 
        """
        return df['close'].shift(-30)

    def _ichimoku_cloud(self, df, t=20, k=60, b=120, c=30, future=30):
        """
        Settings: 20/60/120/30
        """
        # lastDate = datetime.strptime(lastDate, "%Y-%m-%d %H:%M:%S.%f")
        lastDate = df.index[-1].to_pydatetime()
        
        for i in range(1, future+1):
            df.loc[lastDate+timedelta(days=i)] = [nan]*5

        return self.__tenkan_sen(df, t),            \
               self.__kijun_sen(df, k),             \
               self.__senkou_span_a(df, future),    \
               self.__senkou_span_b(df, b, future), \
               self.__chikou_span(df, c)

    def plot_qt(self, df, graphWidget, pg, Qt):
        tenkan,kijun,senkouA,senkouB,chikou = self._ichimoku_cloud(df)

        dates = df.index.to_pydatetime()
        dates = [i.timestamp() for i in dates]

        penTenkan  = pg.mkPen(color="r",          width=3, style=Qt.SolidLine)
        penKijun   = pg.mkPen(color="b",          width=3, style=Qt.SolidLine)
        penSenkouA = pg.mkPen(color="dark green", width=3, style=Qt.DashDotDotLine)
        penSenkouB = pg.mkPen(color="tomato",     width=3, style=Qt.DashDotDotLine)
        penChikou  = pg.mkPen(color="light grey", width=3, style=Qt.DashLine)

        # plotTenkan = self.graphWidget.plot(x, y, name="Tenkan Sen", pen=penTenkan, symbol='+', symbolSize=15, symbolBrush=('b'))
        plotTenkan  = graphWidget.plot(dates, tenkan,  name="Tenkan Sen",    pen=penTenkan)
        plotKijun   = graphWidget.plot(dates, kijun,   name="Kijun Sen",     pen=penKijun)
        plotSenkouA = graphWidget.plot(dates, senkouA, name="Senkou Span A", pen=penSenkouA)
        plotSenkouB = graphWidget.plot(dates, senkouB, name="Senkou Span B", pen=penSenkouB)
        plotChikou  = graphWidget.plot(dates, chikou,  name="Chikou Span",   pen=penChikou)

        fillAB      = pg.FillBetweenItem(plotSenkouA, plotSenkouB, 'light grey')
        graphWidget.addItem(fillAB)

Copy the following code into williams.py and save the file.

from pandas   import concat
from datetime import timedelta
from numpy    import nan

class Fractals:

    def __williams_bearish_fractal(self, df):
        """
        GREEN or UP = BEARISH

        The Formulas for Fractals Are:

        Bearish Fractal = High(N) > High(N−2) and
                          High(N) > High(N−1) and
                          High(N) > High(N+1) and
                          High(N) > High(N+2)

        where:
            N   = High of the current price bar
            N−2 = High of price bar two periods to the left of N
            N−1 = High of price bar one period to the left of N
            N+1 = High of price bar one period to the right of N
            N+2 = High of price bar two periods to the right of N
        """
        
        def bear_fractals(iterable):
            for segment in ( iterable[i:i+5] for i in range(len(iterable)-(5-1)) ):
                if all([ segment[2] > segment[i] for i in [0, 1, 3, 4] ]):
                    yield segment[2:3]

        series = concat(bear_fractals(df['high']), axis=0)
        series.name = 'up'
        return series

    def __williams_bullish_fractal(self, df):
        """
        RED or DOWN = BULLISH

        Bullish Fractal = Low(N) < Low(N−2) and
                          Low(N) < Low(N−1) and
                          Low(N) < Low(N+1) and
                          Low(N) < Low(N+2)

        where:
            N   = Low of the current price bar
            N−2 = Low of price bar two periods to the left of N
            N−1 = Low of price bar one period to the left of N
            N+1 = Low of price bar one period to the right of N
            N+2 = Low of price bar two periods to the right of N
        """
        def bull_fractals(iterable):
            for segment in ( iterable[i:i+5] for i in range(len(iterable)-(5-1)) ):
                if all([ segment[2] < segment[i] for i in [0, 1, 3, 4] ]):
                    yield segment[2:3]

        series = concat(bull_fractals(df['low']), axis=0)
        series.name = 'down'
        return series

    def _williams_fractal(self, df):
        """
        1. Isolate a high/low (N) point on the chart.
        2. If there are two lower highs to the left of the high or 
           two higher lows to the left of the low (N-2 and N-1), 
           there is a possible pattern. The pattern still needs 
           two more bars on the right to confirm.
        3. If two lower highs occur after the high then a bearish 
           fractal is complete (N+1 and N+2). If two higher lows 
           occur after the low a bullish fractal is complete.

        https://www.investopedia.com/terms/f/fractal.asp
        """
        
        s1 = self.__williams_bearish_fractal(df)
        s2 = self.__williams_bullish_fractal(df)

        df = df.merge(s1, how='outer', on='date')
        df = df.merge(s2, how='outer', on='date')

        return df
    
    def plot_qt_f(self, df, graphWidget, pg, Qt):
        df = self._williams_fractal(df)

        dates = df.index.to_pydatetime()
        dates = [i.timestamp() for i in dates]

        graphWidget.plot(dates, df["up"],   symbolPen='w', symbol='t1', symbolSize=15, symbolBrush=("teal"))
        graphWidget.plot(dates, df["down"], symbolPen='w', symbol='t',  symbolSize=15, symbolBrush=("tomato"))

dateaxis You can see the dates have been fixed in the picture above.

In order to fix how the dates are displayed at the bottom of the chart we need to add one more file into the program. Name this file dateaxis.py and create it next to main.py in the root directory of the crypto directory.

Projects/crypto/
├─ analysis/
│  ├─ __init__.py
│  ├─ ichimoku.py
│  └─ williams.py
├─ binance/
│  ├─ __init__.py
│  ├─ api.py
│  ├─ binance.py
│  └─ market.py
├─ crypto.ui
├─ dateaxis.py
├─ main.py 
└─ ui_crypto.py

Change the code in main to the following

import sys

from PySide6.QtWidgets import QApplication, QMainWindow, QDialog
from ui_crypto         import Ui_MainWindow
from PySide6.QtCore    import Qt

from binance.binance   import Binance
from analysis.williams import Fractals
from analysis.ichimoku import IchimokuKinkoHyo

from dateaxis          import DateAxisItem

import pyqtgraph as pg

def main():
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    window = MainWindow()
    window.show()
    app.exec()

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        
        # Repair Dates
        date_axis        = DateAxisItem(orientation='bottom')
        self.graphWidget = pg.PlotWidget(axisItems = {'bottom': date_axis})
        self.setCentralWidget(self.graphWidget)

        # Style
        self.graphWidget.setBackground('w')

        # Title
        self.graphWidget.setTitle("Bitcoin")

        # Legend
        self.graphWidget.addLegend()

        # Grid
        self.graphWidget.showGrid(x=True, y=True)
        
        # Plot Bitcoin
        bi = Binance()
        df = bi.candles(
            symbol   = "BTCUSDT",
            interval = "1d"
        )
        
        dates = df.index.to_pydatetime()
        dates = [i.timestamp() for i in dates]
        
        price = pg.mkPen(color="#3d3d3d", width=3, style=Qt.SolidLine)
        self.graphWidget.plot(dates, df["close"], name="Price", pen=price)
        
        # Plot Technicals
        IchimokuKinkoHyo().plot_qt(df, self.graphWidget, pg, Qt)
        Fractals().plot_qt_f(df, self.graphWidget, pg, Qt)

if __name__ == "__main__":
    main()

Copy the following code into the new dateaxis.py file.

from pyqtgraph import AxisItem
from datetime  import datetime

class DateAxisItem(AxisItem):

    def tickStrings(self, values, scale, spacing):
        ret = []
        if not values:
            return []

        if spacing >= 31622400:
            fmt = "%Y"

        elif spacing >= 2678400: 
            fmt = "%m/%d/%Y"

        elif spacing >= 86400:  
            fmt = "%b/%d"

        elif spacing >= 3600: 
            fmt = "%b/%d-%Hh"

        elif spacing >= 60: 
            fmt = "%H:%M"

        elif spacing >= 1: 
            fmt = "%H:%M:%S"

        else:
            fmt = '[+%fms]'  

        for x in values:
            try:
                t = datetime.fromtimestamp(x)
                ret.append(t.strftime(fmt))
            except ValueError:
                ret.append('')

        return ret

Conclusion & Attributions

The Bitcoin & Dogecoin icons used in the original picture at the top of the article were created by Freepik and downloaded from flaticon.com, the Ethereum icon was created by tulpahn and the candlestick icon was created by Zulkifly Suradin.