Python {Article144}

ようこそ「Python」へ...

Python: Plotly DashでGMOコインの板情報をリアルタイムで取得してトレンドを予測するWebアプリを作る!【Dash実践】

ここでは、Plotly Dash(※)を使用してGMOコインの板情報をリアルタイムで取得してトレンドを予測するWebアプリを作る方法を解説します。 PlotlyとDashを使うことにより、GMOコインからリアルタイムで仮想通貨の板情報を取得してさまざまなグラフをWebページに表示させることができます。 Webアプリは、スマホ、タブレット、ディスクトップパソコンなどでさまざまなデバイスで利用できるメリットがあります。

Webページの右側には板情報の価格からトレンドを分析して、 4種類(価格の線グラフ、価格のパーセントの線グラフ、スプレッドの線グラフ、トレンドの線グラフ)のグラフを表示します。 Webページの左側には板情報の注文数量からトレンドを分析して、 4種類(数量の線グラフ、数量の累計の線グラフ、数量のパーセントの線グラフ、トレンドの線グラフ)のグラフを表示します。

Webページのドロップダウンからは、仮想通貨(BTC_JPY, ETH_JPY, LTC_JPY,...)のシンボルを選択して板情報を切り替えることができます。 さらに、Webページのスライダーをドラッグ(クリック)して、GMOコインから板情報を取得するインターバル(間隔)を変更することができます。 デフォルトでは「3秒」に設定されていますが、「3秒」刻みで「60秒」まで変更することができます。

ここで紹介するWebアプリは、仮想通貨の投資に活用するというよりは、 Plotly DashでWebアプリを作るときの参考にしていただくのが目的です。

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

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

説明文の左側に図の画像が表示されていますが縮小されています。 画像を拡大するにはマウスを画像上に移動してクリックします。 画像が拡大表示されます。拡大された画像を閉じるには右上の[X]をクリックします。 画像の任意の場所をクリックして閉じることもできます。
click image to zoom!
図A Desktop
click image to zoom!
図B Tablet
click image to zoom!
図C Mobile
click image to zoom!
図D ETH_JPY
click image to zoom!
図E Interval(10 secs)
click image to zoom!
図F Orders(10, 20)

Plotly DashでGMOコインの板情報をリアルタイムで取得してトレンドを予測するWebアプリを作る!【Dash実践】

  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のファイルには「リスト8-1」のコードをコピペします。 次に、新規サブフォルダ「lib」を作成したしたら、新規のPythonファイル「gmo_api.py, plotly_lib.py」を作成して、 「リスト8-2, リスト8-3」をコピペします。 最後に、新規サブフォルダ「assets」を作成したら、新規のCSSファイル「article144.css」を作成して、 「リスト8-4」をコピペします。

    click image to zoom!
    図2
    図2は、VS Codeの画面です。 プロジェクトフォルダ「dash」直下にサブフォルダ「assets, lib」が表示されています。 「assets」フォルダには「article144.css」ファイルが表示されています。 「lib」フォルダには「gmo_api.py, plotly_lib.py」ファイルが表示されています。
  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には、ブラウザ(MS Edge)にアプリのWebページが表示されています。
  4. Webページをディスクトップパソコン、タブレット、スマホに表示して見る

    ここでは、前出のWebアプリをディスクトップ、タブレット、スマホで表示してレイアウトの確認をします。 デバイスごとにレイアウトを変えるには、 Bootstrap5のグリッドシステム(Grid System)を使うと簡単に実装することができます。 グリッドシステムを使用すると画面が12等分されます。 この場合、1行(Row)に1個のカラム(1段組)を表示したいときは「size=12」、 1行(Row)に2個のカラム(2段組)を表示したいときは「size=6」を指定します。

    Bootstrap5のグリッドシステムには、4種類のクラス「xs, sm, md, lg」が用意されています。
    • xs (phones) screen size < 768px:
      スクリーンサイズが768px以下のデバイスに適用
    • sm (tablets) screen size >= 768px :
      スクリーンサイズが768pxと等しいかそれ以上のデバイスに適用
    • md (small laptops) screen size >= 992px :
      スクリーンサイズが992pxと等しいかそれ以上のデバイスに適用
    • lg (laptops & desktops) screen size >= 1200px :
      スクリーンサイズが1200pxと等しいかそれ以上のデバイスに適用

    ここでは、「xs」と「sm」を使用してスマホ、タブレット、ディスクトップのレイアウトを変えています。

    タブレットとディスクトップに表示するとき2段組みにするには、 CSSのflexboxレイアウトを使用します。 ここではDashの行「dbc.Row()」にCSSのクラス「flex-container」を追加してflexboxレイアウトを適用させています。 なお、ここで追加するCSSは、「@media screen and (min-width: 768px)」内に記述しているので、 タブレットとディスクトップのときのみ適用されます。

    「display: flex」は、要素(dbc.Row)をフレキシブルコンテナに変換し、flexboxレイアウトを有効にしています。 「flex-wrap: wrap」は、フレキシブルアイテム(top, middle, bottom)がコンテナの幅に収まらない場合に、 複数の行または列にわたってアイテムを折り返します。 「flex-direction: column」は、フレキシブルコンテナ内のアイテム(bottom)を列方向に配置することを指定します。 これで2段組みで表示されるようになります。

      mobile
      +--------+  
      |  top   |
      |--------|
      |        |
      | middle |
      |        |
      |--------|
      | bottom |
      +--------+
    
      tablet/desktop
      +--------+ +--------+  
      |  top   | |        |
      |--------| | middle |
      | bottom | |        |
      +--------+ +--------+
    
    @media screen and (min-width: 768px) {  
        .flex-container {
            display: flex;
            flex-wrap: wrap;
            flex-direction: column;
            height: 45em;
        }
    }
    
    ### Generate Container
    app.layout = dbc.Container([
        dbc.Row([
            dbc.Col(
                top,
                xs=dict(order=1, size=12),  # mobile
                sm=dict(order=1, size=6)    # tablet, desktop
            ),
            dbc.Col(
                middle,
                xs=dict(order=2, size=12),  # mobile
                sm=dict(order=3, size=6)    # tablet, desktop
            ),
            dbc.Col(
                bottom,
                xs=dict(order=3, size=12),  # mobile
                sm=dict(order=2, size=6)    # tablet, desktop
            )                  
        ], className='flex-container'),       
    ], fluid=True) 
    click image to zoom!
    図4-1
    図4-1では、Webページをディスクトップパソコンに表示しています。 Webページには2段組みで表示されています。 左側には板情報の数量によるトレンド、 右側には板情報の価格によるトレンドが表示されています。

    click image to zoom!
    図4-2
    図4-2では、Webページをタブレット(iPad Air)に表示しています。 タブレットの場合、ディスクトップパソコンと同様2段組みで表示されます。

    click image to zoom!
    図4-3
    図4-3では、Webページをスマホ(iPhone SE)に表示しています。 スマホの場合、1段組みで「top, middle, bottom」の順に表示されます。 ここでは、「top, middle」が表示されています。

    click image to zoom!
    図4-4
    図4-4では、Webページをスマホ(iPhone SE)に表示しています。 ここでは、「bottom」が表示されています。
  5. Webページのドロップダウンから仮想通貨を切り替えて見る

    ここでは、Webページのドロップダウンから仮想通貨のシンボルを選択してグラフを「BTC_JPY」から「ETH_JPY」に切り替えています。 Webページのドロップダウンから仮想通貨のシンボルを選択したとき、 JavaScriptのイベントで選択したシンボル(ETH_JPY)を取得してブラウザ経由でWebサーバーに送信(ポストバック)します。 Webサーバーでは、ポストバックされたWebページのデータ(仮想通貨のシンボル)を取得すると、 Dashのコールバック関数「update_figures()」にコントロールを渡します。

    コールバック関数では、引数「symbol」に格納されている仮想通貨のシンボル(ETH_JPY)を指定して、 関数「get_orderbooks_try(), get_orderbook_price_fig(), get_orderbook_qty_fig()」を呼び出します。 最後に、関数の戻り値として取得した板情報の価格と数量のグラフをブラウザに送信して表示させます。 これで、ブラウザには「ETH_JPY」のグラフが表示されます。

    from lib.gmo_api import get_orderbooks_try
    from lib.plotly_lib import get_orderbook_price_fig, get_orderbook_qty_fig
    
    qty_precision_dict = {'BTC_JPY': 100, 'ETH_JPY': 10, 'LTC_JPY': 10, 'BCH_JPY': 10, 'XRP_JPY': 1}
    
    ### Lay out the top of the web page
    top = html.Div(
        [
            html.H5([
                '🎉GMO Coin Trend Tracker',            
                html.Span(' (トレンド分析アプリ)', style={'font-size': 'small'}),
                ]),
            html.Hr(),
            dcc.Dropdown(
                    ['BTC_JPY', 'ETH_JPY', 'LTC_JPY', 'XRP_JPY', 'BCH_JPY'], 
                    'LTC_JPY',
                    placeholder='Select a coin',
                    id='coin-dd',                                   # Input(value)         
                ),                  
        ],
        id='top')  
    
    ##################################################################
    # Dash Callback for Timer, Dropdown(symbol), Sliders(order book)
    ##################################################################
    @app.callback(
        Output('price-fig', 'figure'),      # 1: id=price-fig       return(price_fig)
        Output('qty-fig', 'figure'),        # 2: id=qty-fig         return(qty_fig)
        Input('coin-dd', 'value'),          # 1: id=coin-dd         update_figures(symbol)    
        Input('price-slider', 'value'),     # 2: id=price-slider    update_figures(price_rows)   
        Input('qty-slider', 'value'),       # 3: id=qty-slider      update_figures(qty_rows)      
        Input('timer', 'n_intervals'),      # 4: id=timer           update_figures(n_intervals)   
    )
    ###########################################################################################
    def update_figures(symbol: str, price_rows: int, qty_rows: int, n_intervals: int) -> tuple:
        print(f"update_figures({symbol=}, {price_rows=}, {qty_rows=}, {n_intervals=}, {dt.datetime.now()})")
    
        if symbol is None:
            return go.Figure(), go.Figure()        
    
        precision = qty_precision_dict[symbol]
        
        (sell_df, buy_df) = get_orderbooks_try(symbol)
        
        price_fig = get_orderbook_price_fig(sell_df, buy_df, symbol, price_rows)
        qty_fig = get_orderbook_qty_fig(sell_df, buy_df, symbol, qty_rows, precision)
    
        return (price_fig, qty_fig) 
    click image to zoom!
    図5-1
    図5-1では、ドロップダウンから「ETH_JPY」を選択しています。 ドロップダウンから「ETH_JPY」を選択すると、 JavaScripのイベントで選択したシンボルを取得してブラウザ経由でWebサーバーに送信(ポストバック)します。

    click image to zoom!
    図5-2
    図5-2には、「ETH_JPY」のグラフが表示されています。 Webサーバーは、ブラウザ経由で送信されたデータ(仮想通貨のシンボル)を取得すると、 Dashのコールバック関数にコントロールを渡します。 コントロール関数では、「ETH_JPY」のグラフを作成してブラウザに送信します。 これで、ブラウザには「ETH_JPY」のグラフが表示されます。

    click image to zoom!
    図5-3
    図5-3では、ドロップダウンから「LTC_JPY」を選択して、 Webページに表示されるグラフを切り替えています。
  6. Webページのスライダーをドラッグして各種グラフを更新する間隔を変更して見る

    ここでは、スライダーから「10」をクリックしてWebページを更新するインターバルを「10秒」に切り替えています。 ブラウザのスライダーから「10」を選択すると、 JavaScriptのイベントで選択した値(10)を取得してブラウザ経由でWebサーバーに送信(ポストバック)します。

    Webサーバーは、ブラウザから送信された値(10)を取得すると、コールバック関数「update_timer_interval()」にコントロールを渡します。 コールバック関数では、Dashの「dcc.Interval()」の「interval」プロパティを「10 * 1000」に更新するように指示します。 ちなみに、「interval」プロパティにはミリセカンド(ms)の単位で指定するので「10秒」をミリセカンドに変換しています。

    Webサーバーは、ブラウザ経由で「dcc.Interval()」の「interval」を「10 * 1000」に更新するように指示します。 これで、コールバック関数「update_figures()」には「10秒」間隔でコントロールが渡るようになります。 つまり、Webページのグラフが10秒間隔で更新されるようになります。

    ### Lay out the top of the web page
    top = html.Div(
        [
            html.H5([
                '🎉GMO Coin Trend Tracker',            
                html.Span(' (トレンド分析アプリ)', style={'font-size': 'small'}),
                ]),
            html.Hr(),
            dcc.Dropdown(
                    ['BTC_JPY', 'ETH_JPY', 'LTC_JPY', 'XRP_JPY', 'BCH_JPY'], 
                    'LTC_JPY',
                    placeholder='Select a coin',
                    id='coin-dd',                                   # Input(value)         
                ),          
            html.Br(),        
            dcc.Slider(3, 60, 10, value=3, id='interval-slider'),   # Input(value)    
            html.Div('📍Update interval for the order book (in seconds)', style={'text-align': 'center'}),        
        ],
        id='top')  
    
    
    ### Generate Container
    app.layout = dbc.Container([
        dbc.Row([
            dbc.Col(
                top,
                xs=dict(order=1, size=12),  # mobile
                sm=dict(order=1, size=6)    # tablet, desktop
            ),
            dbc.Col(
                middle,
                xs=dict(order=2, size=12),  # mobile
                sm=dict(order=3, size=6)    # tablet, desktop
            ),
            dbc.Col(
                bottom,
                xs=dict(order=3, size=12),  # mobile
                sm=dict(order=2, size=6)    # tablet, desktop
            )                  
        ], className='flex-container'),
        dcc.Interval(id='timer', interval=1000*3, n_intervals=0) # 3000 ms Input(n_intervals) Output(interval)        
    ], fluid=True) 
    
    
    ###################################################
    # Dash Callback for Order Book Intervals Dropdown
    ###################################################
    @app.callback(
        Output('timer', 'interval'),        # id=timer, property=interval         
        Input('interval-slider', 'value')   # id=interval-slider, property=value       
    )
    ######################################################
    def update_timer_interval(orderbook_intervals): # order book intervals(sec)
        print(f"update_timer_interval({orderbook_intervals=})")
        return orderbook_intervals * 1000   # sec * 1000 => ms  
    click image to zoom!
    図6-1
    図6-1には、Webページから「3秒」を選択したときの画面が表示されています。 VS Codeの「Terminal」ウィンドウには、コールバック関数「update_fugures()」で「print」した時刻が表示されています。 時刻が「3秒」間隔になっているのが確認できます。 これは、Webページのグラフが3秒間隔で更新されていることを意味します。

    click image to zoom!
    図6-2
    図6-2には、スライダーから「10秒」を選択したときの画面が表示されています。 VS Codeの「Terminal」ウィンドウには、時刻が「10秒」間隔で表示されています。 これは、Webページのグラフが10秒間隔で更新されていることを意味します。
  7. Webページのスライダーをドラッグして板情報から取得する注文件数を変更して見る

    ここでは、GMOコインから取得した板情報の注文件数を変更しています。 デフォルトの状態では、GMOコインから取得した板情報は「10件」表示されます。 ここでは、「5件」から「10件」と「5件」から「20件」に注文件数を変更しています。

    ブラウザのスライダーから板情報の注文件数を選択すると、 JavaScriptのイベントでその値を取得してブラウザ経由でWebサーバーに送信(ポストバック)します。

    Webサーバーは、値を取得したらコールバック関数「update_figures」にコントロールを渡します。 コールバック関数では、引数「price_rows, qty_rows」に格納されている注文件数を使用して 関数「get_orderbook_price_fig(), get_orderbook_qty_fig()」を呼びます。 そして、作成したグラフをブラウザに送信するように指示します。 これで、Webページのグラフはスライダーから選択した注文件数になります。

    from lib.gmo_api import get_orderbooks_try
    from lib.plotly_lib import get_orderbook_price_fig, get_orderbook_qty_fig
    
    ### Lay out the middle of the web page
    middle = html.Div(
        [
            dcc.Graph(id='price-fig'),                              # Output(figure) 
            html.Br(),      
            dcc.Slider(5, 50, 10, value=10, id='price-slider'),     # Input(value)   
            html.Div('📍Number of orders for the order book', style={'text-align': 'center'}),     
        ],    
        id='middle') 
    
    ### Lay out the bottom of the web page
    bottom = html.Div(
        [
            dcc.Graph(id='qty-fig'),                                # Output(figure) 
            html.Br(),
            dcc.Slider(5, 50, 10, value=10, id='qty-slider'),       # Input(value)  
            html.Div('📍Number of orders for the order book', style={'text-align': 'center'}),       
        ],    
        id='bottom')
    
    ##################################################################
    # Dash Callback for Timer, Dropdown(symbol), Sliders(order book)
    ##################################################################
    @app.callback(
        Output('price-fig', 'figure'),      # 1: id=price-fig       return(price_fig)
        Output('qty-fig', 'figure'),        # 2: id=qty-fig         return(qty_fig)
        Input('coin-dd', 'value'),          # 1: id=coin-dd         update_figures(symbol)    
        Input('price-slider', 'value'),     # 2: id=price-slider    update_figures(price_rows)   
        Input('qty-slider', 'value'),       # 3: id=qty-slider      update_figures(qty_rows)      
        Input('timer', 'n_intervals'),      # 4: id=timer           update_figures(n_intervals)   
    )
    ###########################################################################################
    def update_figures(symbol: str, price_rows: int, qty_rows: int, n_intervals: int) -> tuple:
        print(f"update_figures({symbol=}, {price_rows=}, {qty_rows=}, {n_intervals=}, {dt.datetime.now()})")
    
        if symbol is None:
            return go.Figure(), go.Figure()        
    
        precision = qty_precision_dict[symbol]
        
        (sell_df, buy_df) = get_orderbooks_try(symbol)
        
        price_fig = get_orderbook_price_fig(sell_df, buy_df, symbol, price_rows)
        qty_fig = get_orderbook_qty_fig(sell_df, buy_df, symbol, qty_rows, precision)
    
        return (price_fig, qty_fig)     
    click image to zoom!
    図7-1
    図7-1では、「価格のトレンド」と「数量のトレンド」とも「5件」の注文データを使用してグラフを作成しています。

    click image to zoom!
    図7-2
    図7-2では、板情報の注文件数を変更しています。 左側の「数量のトレンド」では「10件」の注文データを使用してグラフを作成しています。 右側の「価格のトレンド」では「20件」の注文データを使用してグラフを作成しています。
  8. 全てのコードを掲載

    ここでは、この記事で解説したプログラムのソースコードと、 ライブラリのソースコード、スタイルシートを掲載しています。 CSSファイル(article144.css)は「assets」フォルダに格納しておくと自動的に取り込まれます。

    リスト8-1: article144.py
    # Article144 - GMO Coin TrendTracker: Analyzing Order Book Trends by Price and Quantity
    
    '''
      mobile
      +--------+  
      |  top   |
      |--------|
      |        |
      | middle |
      |        |
      |--------|
      | bottom |
      +--------+
    
      tablet/desktop
      +--------+ +--------+  
      |  top   | |        |
      |--------| | middle |
      | bottom | |        |
      +--------+ +--------+
    
    '''
    
    # Import python libraries
    from time import sleep    
    import datetime as dt
    from datetime import timedelta
    import math
    import numpy as np
    import pandas as pd
    
    from dash import Dash, html, dcc, dash_table, ctx, State, Input, Output  # pip install dash
    import dash_bootstrap_components as dbc # pip install dash-bootstrap-components
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    
    from lib.gmo_api import get_orderbooks_try
    from lib.plotly_lib import get_orderbook_price_fig, get_orderbook_qty_fig
    
    ### Lay out the top of the web page
    top = html.Div(
        [
            html.H5([
                '🎉GMO Coin Trend Tracker',            
                html.Span(' (トレンド分析アプリ)', style={'font-size': 'small'}),
                ]),
            html.Hr(),
            # html.Label('Coin Symbol:', style={'text-align': 'left'}),
            dcc.Dropdown(
                    ['BTC_JPY', 'ETH_JPY', 'LTC_JPY', 'XRP_JPY', 'BCH_JPY'], 
                    'BTC_JPY',
                    placeholder='Select a coin',
                    id='coin-dd',                                   # Input(value)         
                ),          
            html.Br(),        
            dcc.Slider(3, 60, 10, value=3, id='interval-slider'),   # Input(value)    
            html.Div('📍Update interval for the order book (in seconds)', style={'text-align': 'center'}),
            # html.Br(),          
        ],
        id='top', style={'height': '13em'})  
    
    ### Lay out the middle of the web page
    middle = html.Div(
        [
            dcc.Graph(id='price-fig'),                              # Output(figure) 
            html.Br(),      
            dcc.Slider(5, 50, 10, value=10, id='price-slider'),     # Input(value)   
            html.Div('📍Number of orders for the order book', style={'text-align': 'center'}),     
        ],    
        id='middle', style={'height': '34em'}) 
    
    ### Lay out the bottom of the web page
    bottom = html.Div(
        [
            dcc.Graph(id='qty-fig'),                                # Output(figure) 
            html.Br(),
            dcc.Slider(5, 50, 10, value=10, id='qty-slider'),       # Input(value)  
            html.Div('📍Number of orders for the order book', style={'text-align': 'center'}),       
        ],    
        id='bottom', style={'height': '30em'})
    
    ### Instantiate Dash 
    app = Dash(__name__, external_stylesheets=[dbc.themes.CYBORG])
    app.title = 'GMO Coin TrendTracker: Analyzing Order Book Trends by Price and Quantity'  
    
    ### Define price, qty precisions
    price_precision_dict = {'BTC_JPY': 0, 'ETH_JPY': 0, 'LTC_JPY': 0, 'BCH_JPY': 0, 'XRP_JPY': 3}
    qty_precision_dict = {'BTC_JPY': 100, 'ETH_JPY': 10, 'LTC_JPY': 10, 'BCH_JPY': 10, 'XRP_JPY': 1}
    
    ### Generate Container
    app.layout = dbc.Container([
        dbc.Row([
            dbc.Col(
                top,
                xs=dict(order=1, size=12),  # mobile
                sm=dict(order=1, size=6)    # tablet, desktop
            ),
            dbc.Col(
                middle,
                xs=dict(order=2, size=12),  # mobile
                sm=dict(order=3, size=6)    # tablet, desktop
            ),
            dbc.Col(
                bottom,
                xs=dict(order=3, size=12),  # mobile
                sm=dict(order=2, size=6)    # tablet, desktop
            )                  
        ], className='flex-container'),
        dcc.Interval(id='timer', interval=1000*3, n_intervals=0) # 3000 ms Input(n_intervals) Output(interval)        
    ], fluid=True) 
    
    
    ###################################################
    # Dash Callback for Order Book Intervals Dropdown
    ###################################################
    @app.callback(
        Output('timer', 'interval'),        # id=timer, property=interval         
        Input('interval-slider', 'value')   # id=interval-slider, property=value       
    )
    def update_timer_interval(orderbook_intervals): # order book intervals(sec)
        # print(f"update_timer_interval({orderbook_intervals=})")
        return orderbook_intervals * 1000   # sec * 1000 => ms
    
    
    ##################################################################
    # Dash Callback for Timer, Dropdown(symbol), Sliders(order book)
    ##################################################################
    @app.callback(
        Output('price-fig', 'figure'),      # 1: id=price-fig       return(price_fig)
        Output('qty-fig', 'figure'),        # 2: id=qty-fig         return(qty_fig)
        Input('coin-dd', 'value'),          # 1: id=coin-dd         update_figures(symbol)    
        Input('price-slider', 'value'),     # 2: id=price-slider    update_figures(price_rows)   
        Input('qty-slider', 'value'),       # 3: id=qty-slider      update_figures(qty_rows)      
        Input('timer', 'n_intervals'),      # 4: id=timer           update_figures(n_intervals)   
    )
    ###########################################################################################
    def update_figures(symbol: str, price_rows: int, qty_rows: int, n_intervals: int) -> tuple:
        # print(f"update_figures({symbol=}, {price_rows=}, {qty_rows=}, {n_intervals=}, {dt.datetime.now()})")
    
        if symbol is None:
            return go.Figure(), go.Figure()        
    
        precision = qty_precision_dict[symbol]
        
        (sell_df, buy_df) = get_orderbooks_try(symbol)
        
        price_fig = get_orderbook_price_fig(sell_df, buy_df, symbol, price_rows)
        qty_fig = get_orderbook_qty_fig(sell_df, buy_df, symbol, qty_rows, precision)
    
        return (price_fig, qty_fig)
    
    ### Run the server
    if __name__ == '__main__':
        app.run_server(debug=True)  

    GMOコインのAPIの使い方については、 「記事(Article116)」で詳しく解説していますので、 ここでは解説を省略します。

    リスト8-2: 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_orderbooks_try(symbol: str) -> tuple:  # return (sell_df, buy_df)   
        # print(f"get_orderbooks_try({symbol=})")
        retry_count = 0
        api_retry_limit_count = 5
        # retry N times if invalid or connection or time out errors
        while True:
            status, sell_df, buy_df = get_orderbooks(symbol)  # => return (status, sell_df, buy_df)           
            # 4:invalid request (requests are too many), 7:connection error, 8:time out error ?
            if status == 4 or status == 8:                
                if retry_count < api_retry_limit_count:
                    retry_count += 1                    
                    sec = np.random.randint(low=3, high=61, size=(1))[0]  # get random number between 3~60 seconds                       
                    print(f"Due to get_orderbooks({status=}) error sleeping {sec} seconds ▶ {retry_count=} ") 
                    sleep(sec)    # sleep 3~60 seconds                                                    
                    continue
            elif status == 7:   # connection error 
                retry_count += 1                    
                sec = np.random.randint(low=3, high=61, size=(1))[0]  # get random number between 3~60 seconds                       
                print(f"Due to get_orderbooks({status=}) error sleeping {sec} seconds ▶ {retry_count=} ") 
                sleep(sec)    # sleep 3~60 seconds                                                    
                continue                 
            break   # exit while loop                        
        # end of while True:          
        # print(f"get_orderbooks_try({symbol=}) ▶ {sell_df.shape=}, {buy_df.shape=}")       
    
        return (sell_df, buy_df)    # ask, bid
    
    ######################################### 
    def get_orderbooks(symbol: str) -> tuple:  # return (status, sell_df, buy_df)  
        # symbol = self.coin.symbol
    
        # print(f"get_orderbooks({symbol=})")
    
        endPoint = 'https://api.coin.z.com/public'
        path = f'/v1/orderbooks?symbol={symbol}'
        url = endPoint + path
    
        status = 999    # misc error
    
        while True:            
            try:
                res_dict = requests.get(url) # 3 seconds  # timeout=0.001  sec
                dict = res_dict.json()      # requests.exceptions.JSONDecodeError:             
            
            except requests.exceptions.HTTPError as errh:
                print(f"HTTP ERROR: get_orderbooks() ▶ request.get() {errh}")                        
                status = 999                # misc error
                sell_df = pd.DataFrame()    # return empty dataframe  
                buy_df = pd.DataFrame()     # return empty dataframe                           
                break   # return with empty dataframe
    
            except requests.exceptions.ConnectionError as errc:                      
                print(f"CONNECTION ERROR: get_orderbooks() ▶ request.get() {errc}")
                status = 7                  # retry error
                sell_df = pd.DataFrame()    # return empty dataframe  
                buy_df = pd.DataFrame()     # return empty dataframe                           
                break   # return with empty dataframe                   
    
            except requests.exceptions.Timeout as errt:       
                print(f"TIMEOUT ERROR: get_orderbooks() ▶ request.get() {errt}")                
                status = 8                  # retry error
                sell_df = pd.DataFrame()    # return empty dataframe  
                buy_df = pd.DataFrame()     # return empty dataframe                           
                break   # return with empty dataframe                                    
    
            except requests.exceptions.JSONDecodeError as errd:    
                print(f"JSON DECODE ERROR: get_orderbooks() ▶ res_dict.json() {errd}" )                                        
                df = pd.DataFrame()     # return empty dataframe 
                status = 999            # json decode error
                break   # error return   
    
            except requests.exceptions.RequestException as err:
                print(f"EXCEPTION ERROR: get_orderbooks() ▶ request.get() {err}" )
                status = 999                # misc error
                sell_df = pd.DataFrame()    # return empty dataframe  
                buy_df = pd.DataFrame()     # return empty dataframe                           
                break   # return with empty dataframe                                                       
                                    
            except:     #  block lets you handle the error
                print(f"MISC EXCEPTION ERROR: get_orderbooks() ▶ return...")
                status = 999                # misc error    
                sell_df = pd.DataFrame()    # return empty dataframe  
                buy_df = pd.DataFrame()     # return empty dataframe                           
                break   # return with empty dataframe                 
    
            # end of try except else finally        
    
            # # dict = res_dict.json()  # ★ RequestJSONDecodeError() => moved to try...except
    
            # # {'status': 0,
            # #  'data': {
            # #   'asks': [{'price': '4042000', 'size': '0.0005'},
            # #    {'price': '4045000', 'size': '0.0028'},
            # #    {'price': '4045018', 'size': '0.0001'},
            # #    {'price': '4049000', 'size': '0.001'},
            # #    {'price': '4049310', 'size': '0.0329'},
            # #    {'price': '4049520', 'size': '0.0125'},
            # #    {'price': 5381822.0, 'size': '0.001'}],
            # #   'bids': [{'price': 5013050.0, 'size': '0.0749'},
            # #    {'price': 5013000.0, 'size': '0.0015'},
            # #    {'price': 5012425.0, 'size': '0.05'},
            # #    {'price': 5012420.0, 'size': '0.05'},
            # #    {'price': 4740000.0, 'size': '0.2135'}],
            # #   'symbol': 'BTC'},
    
            ### Normal Case      
    
            ####################################################### debug info
            # print('-'*100, 'dump response from get_orderbooks()')
            # print(dict) 
            # print('-'*100, 'dump response from get_orderbooks()')    
            ####################################################### debug info               
    
            status = dict.get('status')
    
            if status == 0:
                data_dict = dict.get('data')
    
                asks = data_dict.get('asks') # sell order info (ascending order) 
                bids = data_dict.get('bids') # buy order info (descending order)       
    
                sell_df = pd.DataFrame(asks)      
                buy_df = pd.DataFrame(bids)
    
                # {'asks': [{'price': '4064331', 'size': '0.0001'},
                #   {'price': '4069888', 'size': '0.0309'},
                #   {'price': '4070000', 'size': '0.0073'},
                #   {'price': '4070620', 'size': '0.001'},
                #   {'price': 5381822.0, 'size': '0.001'}],
    
                #  'bids': [{'price': 5013050.0, 'size': '0.0749'},
                #    {'price': 5013000.0, 'size': '0.0015'},
                #    {'price': 5012425.0, 'size': '0.05'},
                #    {'price': 5012420.0, 'size': '0.05'},
                #    {'price': 4740000.0, 'size': '0.2135'}],            
    
                # Data columns (total 2 columns):
                #  #   Column    Non-Null Count  Dtype
                # ---  ------    --------------  -----
                #  0   price     1000 non-null   float64
                #  1   quantity  1000 non-null   float64
                # dtypes: float64(2)
    
    
                if sell_df.shape[0] > 0:
                    # convert data types
                    sell_df = sell_df.astype({'price': 'float', 'size': 'float'})  
                    # rename column name
                    sell_df.rename(columns={'size': 'quantity'}, inplace=True) 
                    
                if buy_df.shape[0] > 0:
                    # convert data types
                    buy_df = buy_df.astype({'price': 'float', 'size': 'float'}) 
                    # rename column name
                    buy_df.rename(columns={'size': 'quantity'}, inplace=True)                                   
            
            else:   # error return
                msg_list = dict.get('messages')
                msg_code = msg_list[0].get('message_code')
                msg_str = msg_list[0].get('message_string')	            
    
                if status == 5:   # Maintenance
                    print(f"GMO Maintenance: get_orderbooks({symbol}) ▶ {status=}, {msg_code} - {msg_str} quit python...")
                    quit()
    
                else:   # status == ?
                    print(f"Invalid Request: get_orderbooks({symbol}) ▶ {status=}, {msg_code} - {msg_str} error return with empty dataframe...") 
                    ###################################################### debug info
                    print('-'*100, 'dump response from get_orderbooks()')
                    print(dict) 
                    print('-'*100, 'dump response from get_orderbooks()')    
                    ##################################################### debug info            
                    sell_df = pd.DataFrame()    # return empty dataframe  
                    buy_df = pd.DataFrame()     # return empty dataframe                
            
            # end if if status == 0:        
            break
        # end of while true:
    
        # print(f"get_orderbooks({symbol=}) ▶ {status=}, {sell_df.shape=}, {buy_df.shape=}")
     
        return (status, sell_df, buy_df)    # status, sell_df(ascending order), buy_df(descending order)    

    Plotlyの使い方については、後日「Plotly超入門」シリーズで解説します。

    リスト8-3: lib/plotly_lib.py
    # Plotly Dash Libraries
    
    # Import python libraries
    from time import sleep    
    import datetime as dt
    from datetime import timedelta
    import math
    import numpy as np
    import pandas as pd
    
    from dash import Dash, html, dcc, dash_table, ctx, State, Input, Output  # pip install dash
    import dash_bootstrap_components as dbc # pip install dash-bootstrap-components
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    
    #################################################################################################################
    def get_orderbook_price_fig(sell_df: pd.DataFrame, buy_df: pd.DataFrame, symbol='BTC_JPY', rows=10) -> go.Figure:
        sell_df = sell_df.head(rows).copy()
        buy_df = buy_df.head(rows).copy()
    
        ### Calculate price change percent
        sell_df['price_pct'] = sell_df['price'].pct_change(fill_method='ffill')
        buy_df['price_pct'] = abs(buy_df['price'].pct_change(fill_method='ffill'))
    
        ### Add a trend status column (1: up, -1: down, 0: no change )
        all_df = sell_df.copy()
        all_df['sell_price'] = sell_df['price']
        all_df['buy_price'] = buy_df['price']
        all_df['sell_price_pct'] = sell_df['price_pct']
        all_df['buy_price_pct'] = buy_df['price_pct']
     
        conditions = np.sign(all_df['sell_price_pct'] - all_df['buy_price_pct'])
        trends = [-1, 1, 0]
        all_df['trend'] = np.select([conditions > 0, conditions < 0, conditions == 0], trends)
    
        filter_up = all_df['trend'] == 1
        up_df = all_df[filter_up]
    
        filter_down = all_df['trend'] == -1
        down_df = all_df[filter_down]
    
        ### Add a Bid-Ask Spread column
        all_df['bid_ask_spread'] = all_df['sell_price'] - all_df['buy_price']
    
        ### Plot using Plotly Graph Objects (GO)
        fig = make_subplots(rows=4, cols=1,
                            shared_xaxes=True,
                            subplot_titles=(
                                    'Price (Yen)', 
                                    'Price (%)', 
                                    'Bid-Ask Spread',                             
                                    f'Trend up({up_df.shape[0]}), down({down_df.shape[0]})'
                                )
                            )
    
        # price (yen)
        trace1 = go.Scatter(x=sell_df.index, y=sell_df['price'],
                            mode='lines',
                            line=dict(color='red'),
                            name='ask'
                           )
    
        trace2 = go.Scatter(x=buy_df.index, y=buy_df['price'],
                            mode='lines',
                            line=dict(color='green'),
                            name='bid'
                            )   
    
        # price (%)
        trace3 = go.Scatter(x=sell_df.index, y=sell_df['price_pct'],
                            mode='lines',
                            line=dict(color='red'),
                            showlegend=False
                            )
    
        trace4 = go.Scatter(x=buy_df.index, y=buy_df['price_pct'],
                            mode='lines',
                            line=dict(color='green'),
                            showlegend=False
                            )   
    
        # bid-ask spread
        trace5 = go.Scatter(x=all_df.index, y=all_df['bid_ask_spread'],
                            mode='lines',
                            line=dict(color='tomato'),
                            name='spread'
                            )
    
        # trend (up, down)
        trace6 = go.Scatter(x=all_df.index, y=all_df['trend'],
                            line_shape='hv',   # vhv, hvh, vh, hv, 
                            line_color = 'orange',  # tomato, orange                 
                            name='trend'
                            )   
    
        fig.add_trace(trace1, row=1, col=1)
        fig.add_trace(trace2, row=1, col=1)
    
        fig.add_trace(trace3, row=2, col=1)
        fig.add_trace(trace4, row=2, col=1)
    
        fig.add_trace(trace5, row=3, col=1)
    
        fig.add_trace(trace6, row=4, col=1)
    
        fig.update_layout(
            template='plotly_dark',    
            title='🌐Order Book Trends by Price',    
            legend=dict(
                x=0.55,            
                y=-0.1,        
                xanchor='left',
                yanchor='top',            
                orientation='h',
            ),
            hovermode='x'
        )
       
        return fig
    
    ##############################################################################################################################
    def get_orderbook_qty_fig(sell_df: pd.DataFrame, buy_df: pd.DataFrame, symbol='BTC_JPY', rows=10, precision=100) -> go.Figure:  
    
        sell_df = sell_df.head(rows).copy()
        buy_df = buy_df.head(rows).copy()
    
        ### Update Qty 0.01BTC => 1BTC
        sell_df['quantity'] = sell_df['quantity'] * precision
        buy_df['quantity_qty'] = buy_df['quantity'] * precision
    
        ### Calculat Cumulative Sum of the Quantity
        sell_df['sum_qty'] = sell_df['quantity'].cumsum()
        sell_df.fillna(0, inplace=True)
        buy_df['sum_qty'] = buy_df['quantity'].cumsum()
        buy_df.fillna(0, inplace=True)
    
        ### Calculate quantity change percent
        sell_df['qty_pct'] = sell_df['sum_qty'].pct_change(fill_method='ffill')
        buy_df['qty_pct'] = abs(buy_df['sum_qty'].pct_change(fill_method='ffill'))
    
        ### Get Status (1: up, -1: down, 0: no change )
        all_df = pd.DataFrame({'sell_qty_pct': sell_df['qty_pct'], 'buy_qty_pct': buy_df['qty_pct']})
        all_df['sell_qty_pct'] = sell_df['qty_pct']
        all_df['buy_qty_pct'] = buy_df['qty_pct']
    
        conditions = np.sign(all_df['sell_qty_pct'] - all_df['buy_qty_pct'])
        trends = [-1, 1, 0]
        all_df['trend'] = np.select([conditions > 0, conditions < 0, conditions == 0], trends)
        filter_up = all_df['trend'] == 1
        up_df = all_df[filter_up]
    
        filter_down = all_df['trend'] == -1
        down_df = all_df[filter_down]
    
        ### Plot using Plotly Graph Objects (GO)
        fig = make_subplots(rows=4, cols=1,
                            shared_xaxes=True,
                            subplot_titles=(
                                    'Quantity (size)', 
                                    'Quantity (sum)', 
                                    'Quantity (%)', 
                                    f'Trend up({up_df.shape[0]}), down({down_df.shape[0]})'
                                )
                            )
    
        # quantity (size) 
        trace1 = go.Scatter(x=sell_df.index, y=sell_df['quantity'],
                            mode='lines',
                            line=dict(color='red'),
                            name='ask'
                        )
    
        trace2 = go.Scatter(x=buy_df.index, y=buy_df['quantity'],
                            mode='lines',
                            line=dict(color='green'),
                            name='bid'
                        )   
        # quantity (sum)
        trace3 = go.Scatter(x=sell_df.index, y=sell_df['sum_qty'],
                            mode='lines',
                            line=dict(color='red'),
                            showlegend=False
                            # name='ask'
                        )
    
        trace4 = go.Scatter(x=buy_df.index, y=buy_df['sum_qty'],
                            mode='lines',
                            line=dict(color='green'),
                            showlegend=False
                            # name='bid'
                        )   
    
        # quantity (%)
        trace5 = go.Scatter(x=sell_df.index, y=sell_df['qty_pct'],
                            mode='lines',
                            line=dict(color='red'),
                            showlegend=False
                        )
    
        trace6 = go.Scatter(x=buy_df.index, y=buy_df['qty_pct'],
                            mode='lines',
                            line=dict(color='green'),
                            showlegend=False
                        )   
    
        # trend (+, -)
        trace7 = go.Scatter(x=all_df.index, y=all_df['trend'],
                            line_shape='hv',   # vhv, hvh, vh, hv, 
                            line_color = 'orange',  # tomato, orange                 
                            name='trend'
                        )   
    
        fig.add_trace(trace1, row=1, col=1)
        fig.add_trace(trace2, row=1, col=1)
    
        fig.add_trace(trace3, row=2, col=1)
        fig.add_trace(trace4, row=2, col=1)
    
        fig.add_trace(trace5, row=3, col=1)
        fig.add_trace(trace6, row=3, col=1)
    
        fig.add_trace(trace7, row=4, col=1)
    
        fig.update_layout(
            template='plotly_dark',    
            title='🌐Order Book Trends by Quantity',    
            legend=dict(
                x=0.75,            
                y=-0.1,        
                xanchor='left',
                yanchor='top',            
                orientation='h',
            ),
            hovermode='x'
        )
        
        return fig  

    CSSのクラス「Select-control, Select-value-label, Select-menu-outer」は、 ドロップダウンをダークモードで表示するときに必要になります。 ダークモード以外で使用するときは、これらのスタイルシートをコメントにしてください。

    「@media screen and (min-width: 768px)」は、 レスポンシブウェブデザイン対応にするときに必要になります。

    リスト8-4: assets/artice144.css
    #coin-dd .Select-control {
        background-color: rgb(25, 25, 25) !important;
        color:white;
      } 
    
    #coin-dd .Select-value-label {
        color: white !important;
    }    
      
    #coin-dd .Select-menu-outer {
        background-color: rgb(25, 25, 25);
        color: white;
    } 
    
    @media screen and (min-width: 768px) {
        div#middle {
            margin-top: 13em;
        }    
        .flex-container {
            display: flex;
            flex-wrap: wrap;
            flex-direction: column;
            height: 45em;
            /* height: 350px;  */
        }
    }