全てのコードを掲載
ここでは、この記事で解説したプログラムのソースコードと、 ライブラリのソースコード、スタイルシートを掲載しています。
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; */
}
}