Python {Article139}

ようこそ「Python」へ...

Python: DashのDataTableを使って仮想通貨の収益率(累積リターン)を表示して並べ替えるWebアプリを作る!【Dash実践】

ここでは、Dash(※)のDataTableを使用してGMO Coinの仮想通貨の収益率(累積リターン)を表示するWebアプリを作る方法を解説します。 Webページには、GMO Coinの全ての仮想通貨のシンボル、累積リターンを表形式で表示します。 Webページからは、表の見出しに表示されている「並べ替えの矢印ボタン」をクリックしてカラム(列)を上昇・降順に並べ替えることができます。 つまり、収益率が高い順に仮想通貨を表示させたり、収益率が低い順に表示させることが可能です。 さらに、表から特定のカラム(列)を削除することも可能です。 また、表のヘッダーにマウスを移動すると「ツールチップ」を表示させるといったこともできます。

※ Dashは、PythonでWebアプリケーションを作成するためのフレームワークです。 Plotlyのグラフ描画ライブラリを利用しており、データを視覚化するために最適化されています。 Dashは、PythonのWebフレームワークであるFlaskをベースにしており、 Pythonの標準ライブラリを使用しているので、 パフォーマンスが高く、実行速度が速いといった特徴があります。

Dashは、Pythonを使用したデータ分析やデータ視覚化のためのツールとして広く使用されており、 ビジネスインテリジェンス、科学、エンジニアリング、金融、医療、教育などのさまざまな分野で使用されています。 Dashを使用することで、カスタマイズされたWebアプリケーションを比較的簡単に作成できます。 Dashは、PythonによるWebアプリケーション開発において選択肢にいれておきたい開発ツールのひとつです。

説明文の左側に図の画像が表示されていますが縮小されています。 画像を拡大するにはマウスを画像上に移動してクリックします。 画像が拡大表示されます。拡大された画像を閉じるには右上の[X]をクリックします。 画像の任意の場所をクリックして閉じることもできます。
click image to zoom!
図A DataTable[1]
click image to zoom!
図B DataTable[2]
click image to zoom!
図C DataTable[3]
click image to zoom!
図D DataTable[4]
click image to zoom!
図E DataTable[5]
click image to zoom!
図F DataTable[6]
click image to zoom!
図G DataTable[7]

DashのDataTableを使って仮想通貨の累積リターンを表示して並べ替えるWebアプリを作る

  1. まずは、Pythonの開発環境を準備する

    まずは、 「記事(Article137)」を参照して、 Pythonの開発環境を準備してください。 ここでは、Pythonのプロジェクトフォルダとして「Dash」を使用しています。

    click image to zoom!
    図1
    図1は、Visual Studio Code(VS Code)の「Terminal」メニューから「New Terminal」を選択して、 「Terminal」ウィンドウを開いたときの画面です。 緑色の「(venv)」が表示されていれば、 Pythonの仮想環境が正常に作成されていることになります。
  2. Visual Studio Codeを起動してプログラムファイルを作成する

    Pythonの開発環境の準備が完了したら、 VS Codeを起動して新規のPythonファイル(*.py)を作成します。 ここで作成したPythonのファイルには「リスト1」のコードをコピペします。

    リスト1:Article139.py
    # Article139.py
    
    ### Import the libraries
    import os
    import math
    import numpy as np
    import pandas as pd
    import requests
    
    import datetime as dt
    from datetime import timedelta
    from time import sleep
    
    from dash import Dash, dash_table, html, Output, Input, State, dcc, ctx
    from dash.dash_table import DataTable, FormatTemplate
    from dash.dash_table.Format import Format, Scheme, Align, Trim
    import dash_bootstrap_components as dbc
    
    from lib.com_lib import get_data, calculate_cum_log_return
    from lib.gmo_api import get_crypto, get_crypto_symbols
    
    import warnings
    warnings.simplefilter('ignore')
    
    #################################
    # Main
    #################################
    
    ### Load the data from gmo coin
    
    # get all symbols from gmo coin 
    df = get_crypto_symbols()
    symbols = df['symbol'].values.tolist()
    
    interval = '1day' 
    date_list = ['2020','2021','2022','2023'] 
    
    symbol_list = []
    cum_log_return_list = []
    cum_log_return_pct_list = []
    for symbol in symbols:
        csv_file = f"datasets/csv/gmo_crypto_2020_2023({symbol})_{interval}.csv"  
        isFile = os.path.isfile(csv_file)
        if not isFile: 
            for date in date_list:  # '2020','2021','2022','2023'
                df = get_crypto(symbol, interval, date)    # get n rows from starting date
                if not df.empty: 
                    df.to_csv(csv_file, index=True)
        # end of if not isFile:
        isFile = os.path.isfile(csv_file)
        if isFile:
            df = get_data(csv_file)
            if not df.empty:
                df = calculate_cum_log_return(df)
                df.replace([np.inf, -np.inf], np.nan).dropna(axis=1, inplace=True)
                cum_log_return = df.iloc[-1]['cum_log_return']
                cum_log_return_pct = df.iloc[-1]['cum_log_return_pct']
                symbol_list.append(symbol)
                cum_log_return_list.append(cum_log_return)
                cum_log_return_pct_list.append(cum_log_return_pct)
    # end of for symbol in symbols:
    
    ### Create DataFrame from dict
    data = {
        'symbol': symbol_list,
        'cum_log_return': cum_log_return_list,
        'cum_log_return_pct': cum_log_return_pct_list
    }
    
    raw_df = pd.DataFrame(data)
    if raw_df.empty:
        print(f"Quit the program due to raw_df is empty: {raw_df.empty=}")
        quit()
    
    ### Replace np.inf or -np.inf (positive or negative infinity) with np.nan(Not A Number)
    df = raw_df.replace([np.inf, -np.inf], np.nan)
    df.isnull().sum() 
    
    ### Drop rows if np.nan (Not A Number)
    df.dropna(axis=0, inplace=True)
    df.isnull().sum() 
    
    ### Define Dash DataTable info
    dt_dict = df.to_dict('records')
    dt_cols = [{"name": i, "id": i} for i in df.columns]
    
    ### Instantiate Dash Class
    app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) 
    
    ### Layout the Web Page
    app.layout = dbc.Container(
        [ 
            dbc.Row(
                html.H3('GMO Coin: Cryptocurrencies by Cumulative Log Return'),             
            ),
            dbc.Row(html.Br()),
            
            dbc.Row(
                dash_table.DataTable(
                    id='dt',
                    data=dt_dict,
                    columns=dt_cols,                                     
                ),
            ),
            dbc.Row(html.Br()),
          
        ], fluid=False,
    )  
    
    ### Run the server
    if __name__ == '__main__':
        app.run_server(debug=True)  
    click image to zoom!
    図2
    図2は、VS Codeの編集画面にプログラムのソースコードが表示されている画面です。
  3. プログラムを起動してブラウザにアプリを表示する

    VS Codeの右上から「▶」ボタンをクリックしてアプリを起動します。 以下、手順は図の説明で解説します。

    click image to zoom!
    図3-1
    アプリが起動すると「Dash is running on http://....」のメッセージが表示されます。 [Ctrl]を押しながらマウスで「URL」のリンクをクリックします。 しばらくすると、デフォルトのブラウザにアプリのWebページが表示されます。

    click image to zoom!
    図3-2
    図3-2には、ブラウザ(Google Chrome)にアプリのWebページが表示されています。 Webページには、GMOコインの全ての仮想通貨のシンボルと累積リターン(収益率)が表示されています。 仮想通貨のシンボルが右詰めで表示されているので後述するステップで左詰にします。 また、後述するステップでは、累積リターンの小数点以下の桁数を揃えるようにフォーマットします。
  4. DashのDataTableにフォーマット機能を追加して累積リターンの桁数を揃える

    ここでは、DataTableのフォーマット機能を使用して、 Webページに表示されている累積リターンの小数点以下の桁数を揃えるようにします。 また、累積リターンをパーセントで表示するときは「%」も追加します。

    click image to zoom!
    図4
    図4には、累積リターンの小数点以下の桁数が「6桁」に固定されています。 パーセント(%)で表示するときは、小数点以下の桁数が「2桁」に固定されています。 さらに、「%」の記号も追加されています。
  5. DashのDataTableに条件付きスタイルシートを追加して仮想通貨のシンボルを左詰めにする

    ここでは、DataTableに条件付きのスタイルシートを適用して、 仮想通貨のシンボルを左詰めにします。

    click image to zoom!
    図5
    図5には、仮想通貨のシンボルが左詰めで表示されています。 一般に、テキスト文字は左詰め、数値は右詰めにすると見やすくなります。
  6. DashのDataTableに各種スタイルシートを追加して表を見やすくする

    ここでは、DataTableに各種のスタイルシートを追加して見やすくしています。 表のヘッダーには、スタイルシートの「fontweight: bold」を適用して太字にしています。 表の明細行には、条件付きのスタイルシートを適用してシマウマ模様(ストライプ柄)で表示されるようにしています。

    click image to zoom!
    図6-1
    図6-1には、表のヘッダーが「太字」で表示されています。 また、表の明細行は「ストライプ柄」で表示されています。


    click image to zoom!
    図6-2
    図6-2には、表をリスト形式で表示しています。 つまり、表のカラムの境界線をなくしています。
  7. DashのDataTableにツールチップを追加する

    ここでは、DataTableのヘッダーにツールチップを表示しています。 マウスを表のヘッダーに移動すると「ツールチップ」が表示されます。

    click image to zoom!
    図7
    図7では、表のヘッダー「cum_log_return_pct」にマウスを移動して「ツールチップ」を表示しています。 表のヘッダーを「略称」で表示するときは、ツールチップを表示するとわかりやすい表になります。
  8. DashのDataTableのカラム(列)の並べ替え機能を追加する

    ここでは、DataTableに並べ替え機能を追加しています。 表のヘッダーにマウスを移動すると「▼」と「▲」が表示されます。 記号ボタンをクリックするとカラムを昇順・降順に並べ替えします。 この機能を使うと、仮想通貨の収益率の高い順、または低い順に並べ替えて表示させることができます。

    click image to zoom!
    図8
    図8では、表のヘッダーにマウスを移動して並べ替えの矢印ボタンを表示させています。 矢印ボタンをクリックすると、カラムを昇順・降順に並べ替えします。 ちなみに、並べ替え処理はJavaScriptで処理しているのでポストバック処理が発生しません。 つまり、すべてクライアント側のブラウザで処理されるので瞬時に並べ替え処理が完了します。
  9. DashのDataTableからカラム(列)を削除する機能を追加する

    ここでは、DataTableから特定のカラム(列)を削除する機能を追加しています。 Webページをスマホ、タブレットなどで閲覧するときは、 不要なカラムを削除することにより表が見やすくなります。

    click image to zoom!
    図9-1
    図9-1は、マウスを表のヘッダーに移動したときの画像です。 ヘッダーに削除のアイコンが表示されています。 この削除アイコンをクリックするとカラムが表から削除されます。


    click image to zoom!
    図9-2
    図9-2は、表から「cum_log_return」のカラムを削除したときの画面です。 Webページの幅を狭めても見やすい状態で表示されます。

    click image to zoom!
    図9-3
    図9-3は、Webページを「iPhone SE」に表示したときの画面です。

    click image to zoom!
    図9-4
    図9-4は、Webページを「Samsung Galaxy」に表示したときの画面です。 ちなみに、削除したカラムを復活させるには、ブラウザの「リロード」ボタンをクリックします。 ここでは、「リロード」ボタンをクリックして削除したカラムを復活させています。
  10. 本記事で解説している全てのコードを掲載

    ここでは、本記事で解説している全てのコードを掲載しています。 ここに掲載している「Article139.py」は最終版ですから、全ての機能が含まれています。 なお、Article139.pyでインポートしている「gmo_api.py, com_lib.py」のファイルは、「lib」フォルダに格納してください。

    リスト2: Article139.py
    # Article.py 
    
    ### Import the libraries
    import os
    import math
    import numpy as np
    import pandas as pd
    import requests
    
    import datetime as dt
    from datetime import timedelta
    from time import sleep
    
    from dash import Dash, dash_table, html, Output, Input, State, dcc, ctx
    from dash.dash_table import DataTable, FormatTemplate
    from dash.dash_table.Format import Format, Scheme, Align, Trim
    import dash_bootstrap_components as dbc
    
    from lib.com_lib import get_data, calculate_cum_log_return
    from lib.gmo_api import get_crypto, get_crypto_symbols
    
    import warnings
    warnings.simplefilter('ignore')
    
    #################################
    # Main
    #################################
    
    ### Load the data from gmo coin
    
    # get all symbols from gmo coin 
    df = get_crypto_symbols()
    symbols = df['symbol'].values.tolist()
    
    interval = '1day' 
    date_list = ['2020','2021','2022','2023'] 
    
    symbol_list = []
    cum_log_return_list = []
    cum_log_return_pct_list = []
    for symbol in symbols:
        csv_file = f"datasets/csv/gmo_crypto_2020_2023({symbol})_{interval}.csv"  
        isFile = os.path.isfile(csv_file)
        if not isFile: 
            for date in date_list:  # '2020','2021','2022','2023'
                df = get_crypto(symbol, interval, date)    # get n rows from starting date
                if not df.empty: 
                    df.to_csv(csv_file, index=True)
        # end of if not isFile:
        isFile = os.path.isfile(csv_file)
        if isFile:
            df = get_data(csv_file)
            if not df.empty:
                df = calculate_cum_log_return(df)
                df.replace([np.inf, -np.inf], np.nan).dropna(axis=1, inplace=True)
                cum_log_return = df.iloc[-1]['cum_log_return']
                cum_log_return_pct = df.iloc[-1]['cum_log_return_pct']
                symbol_list.append(symbol)
                cum_log_return_list.append(cum_log_return)
                cum_log_return_pct_list.append(cum_log_return_pct)
    # end of for symbol in symbols:
    
    ### Create DataFrame from dict
    data = {
        'symbol': symbol_list,
        'cum_log_return': cum_log_return_list,
        'cum_log_return_pct': cum_log_return_pct_list
    }
    
    raw_df = pd.DataFrame(data)
    if raw_df.empty:
        print(f"Quit the program due to raw_df is empty: {raw_df.empty=}")
        quit()
    
    ### Replace np.inf or -np.inf (positive or negative infinity) with np.nan(Not A Number)
    df = raw_df.replace([np.inf, -np.inf], np.nan)
    df.isnull().sum() 
    
    ### Drop rows if np.nan (Not A Number)
    df.dropna(axis=0, inplace=True)
    df.isnull().sum() 
    
    ### Define Dash DataTable info
    df['cum_log_return_pct'] = df['cum_log_return']
    dt_dict = df.to_dict('records')
    # dt_cols = [{"name": i, "id": i} for i in df.columns]
    
    log_return = FormatTemplate.Format(precision=6, scheme=Scheme.fixed)
    log_return_pct = FormatTemplate.percentage(2)
    
    dt_cols = [
        dict(id='symbol', name='symbol', type='text'),
        dict(id='cum_log_return', name='cum_log_return', type='numeric', deletable=True, format=log_return),
        dict(id='cum_log_return_pct', name='cum_log_return_pct', type='numeric', format=log_return_pct)
    ]
    
    tip_header_cols = {
        'symbol': 'crypto symbol',
        'cum_log_return': 'cumulative log return',
        'cum_log_return_pct': 'cumulative log return percentage (%)',
    }
    
    style_header = {
        'backgroundColor': 'rgb(210, 210, 210)',
        'color': 'black',
        'fontWeight': 'bold'
    }
    
    style_cell_conditional= [{
            'if': {'column_id': 'symbol'},
            'textAlign': 'left'
    }]
    
    style_data_conditional = [{
            'if': {'row_index': 'odd'},
            'backgroundColor': 'rgb(220, 220, 220)',
    }]  
    
    ### Instantiate Dash Class
    app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) 
    
    ### Layout the Web Page
    app.layout = dbc.Container(
        [ 
            dbc.Row(
                html.H3('GMO Coin: Cryptocurrencies by Cumulative Log Return'),   
            ),
            dbc.Row(html.Br()),
            
            dbc.Row(
                dash_table.DataTable(
                    id='dt',
                    data=dt_dict,
                    columns=dt_cols,        
                    tooltip_header=tip_header_cols, 
                    tooltip_delay=0,
                    tooltip_duration=None,                 
                    sort_action='native',
                    sort_mode='multi',
                    style_header=style_header,                             
                    style_cell_conditional=style_cell_conditional,  
                    style_data_conditional=style_data_conditional,                              
                ),
            ),
            dbc.Row(html.Br()),
          
        ], fluid=False,
    )  
    
    ### Run the server
    if __name__ == '__main__':
        app.run_server(debug=True)  


    リスト3: /lib/gmo_api.py
    # gmo_api.py
    
    import math
    import numpy as np
    import pandas as pd
    import requests
    
    import datetime as dt
    from datetime import timedelta
    from time import sleep
    
    import warnings
    warnings.simplefilter('ignore')
    
    ####################################################
    def get_crypto_symbols(symbol=None) -> pd.DataFrame:
        # Define GMO Coin API endpoint
        endpoint = 'https://api.coin.z.com/public/v1/ticker'    
        params = {}    
        if symbol is not None:    
            # add a new element(symbol) to the params dictionary
            params['symbol'] = symbol
            
        res_dict  = requests.get(endpoint, params=params) 
        dict = res_dict.json()
        status = dict.get('status')
        # no error ?
        if status == 0:
            data_list = dict.get('data') 
            df = pd.DataFrame(data_list)    # ask, bid, high, last, low, symbol, timestamp, volume       
            return df
        else:
            print(f"get_crypto_symbols() error => {status=}")     
            return pd.DataFrame()
    
    ############################################################ Load data from GMO Coin
    def get_crypto(symbol='BTC', interval='1day', start='2018'):
        '''
        interval=1min 5min 10min 15min 30min 1hour for YYYYMMDD
        interval=4hour 8hour 12hour 1day 1week 1month  YYYY             
        start='2018'       
        '''      
        url = 'https://api.coin.z.com/public/v1/klines'       
       
        params = {
            'symbol': symbol,
            'interval': interval,    
            'date': start
        }        
        
        try:
            res_dict  = requests.get(url, params=params)    
            dict = res_dict.json()
            status = dict.get('status')
            # no error ?
            if status == 0:
                data_list = dict.get('data') 
                df = pd.DataFrame(data_list)            
                df.columns = ['date', 'open', 'high', 'low', 'close', 'volume']
                df['date'] = pd.to_datetime(df['date'], unit='ms')
                df.set_index('date', inplace=True)
                df = df.astype(float)
                df['symbol'] = symbol
                # df.reset_index(inplace=True)
                # print(f"get_crypto({symbol=}, {interval=}, {start=}) => {df.shape[0]=}")        
                return df
            else:
                print(f"get_crypto({symbol=}, {interval=}, {start=}) error => {status=}")     
                return pd.DataFrame()
        except requests.exceptions.HTTPError as e:
            print(f"get_crypto({symbol=}, {interval=}, {start=}) HTTP error: {e}") 
        except Exception as e:   
            print(f"get_crypto({symbol=}, {interval=}, {start=}) exception error: {e}")
        
        return pd.DataFrame()   


    リスト4: /lib/com_lib.py
    # com_lib.py
    
    import math
    import numpy as np
    import pandas as pd
    import requests
    
    import datetime as dt
    from datetime import timedelta
    from time import sleep
    
    import warnings
    warnings.simplefilter('ignore')
    
    ############################################
    def get_data(csv_file: str) -> pd.DataFrame:
        # print(f"Loading data: {csv_file} ")
        df = pd.read_csv(csv_file)       
        # date,open,high,low,close,adj close,volume,symbol
        df['date'] = pd.to_datetime(df['date'])            
        df.set_index(['date'], inplace=True)
        return df
    
    ###############################################################
    def calculate_cum_log_return(df: pd.DataFrame) -> pd.DataFrame:
        # Calculate log return
        df['log_return'] = np.log(df['close'] / df['close'].shift(1))  
     
        # Calculate cumulative log return
        df['cum_log_return'] = np.exp(df['log_return'].cumsum()) - 1
        df['cum_log_return_pct'] = df['cum_log_return'] * 100
    
        # Preview the resulting dataframe 
        # print(f"Cumulative Log Return for {df.iloc[-1]['symbol']} = {df.iloc[-1]['cum_log_return_pct']:.2%}")     
        return df