websocket深堀り4回シリーズ-1 そもそも websocketとは

websocket深堀り4回シリーズ

  1. そもそもwebsocketとは
  2. ビットコインのデータをwebsoketで受信 → クラス化
  3. websocket受信クラス → threadingでマルチタスク化
  4. websocketクラスをマルチタスクでビットフライヤーとバイナンスの同時受信

最小限websocketで繋いでデータをプリントする

まずは、PyscrypterにソースをコピペしてRUNする様子

遅いように見えますが、データが来ないのです。

ソースコード

そこら中に転がってる、ビットコインのデータをwebsocketで受信するコード

import websocket
import json
from pprint import pprint
URL = "wss://ws.lightstream.bitflyer.com/json-rpc"
CHANNEL = "lightning_executions_BTC_JPY"

def on_message(ws, message):
    message = json.loads(message)
    pprint(message)

def on_open(ws):
    ws.send(json.dumps({"method": "subscribe",
                        "params": {"channel": CHANNEL}}))

# -------- main --------
ws = websocket.WebSocketApp(URL,on_message=on_message, on_open=on_open)
ws.run_forever()

出力結果 pprintで整形出力してある

{'jsonrpc': '2.0',
 'method': 'channelMessage',
 'params': {'channel': 'lightning_executions_BTC_JPY',
            'message': [{'buy_child_order_acceptance_id': 'JRF20210516-210717-231265',
                         'exec_date': '2021-05-16T21:07:17.4793855Z',
                         'id': 2213681509,
                         'price': 4884761.0,
                         'sell_child_order_acceptance_id': 'JRF20210516-210717-314050',
                         'side': 'SELL',
                         'size': 0.0138},
                        {'buy_child_order_acceptance_id': 'JRF20210516-210702-341601',
                         'exec_date': '2021-05-16T21:07:17.4793855Z',
                         'id': 2213681510,
                         'price': 4884760.0,
                         'sell_child_order_acceptance_id': 'JRF20210516-210717-314050',
                         'side': 'SELL',
                         'size': 0.25145526}]}}

上記のコードを分解してみる

  1. URL = "wss://ws.lightstream.bitflyer.com/json-rpc"
  2. CHANNEL = "lightning_executions_BTC_JPY"

1のアドレスに接続して、2のデータを読み込む

ws = websocket.WebSocketApp(URL,on_message=on_message, on_open=on_open)

wsはwebsocketを略したのだろうが、他にもいっぱい出てくるので何だろな?ですね

実はただの変数名なので、何でも良いのです。後で”BFstream”とでも変えてみましょう

websocket.WebSocketApp(---------)

websocketというモジュールファイルの中のWebSoketAppというクラスを呼び出します。

   クラスは先頭が大文字で単語の接続に_を使わないというマナーなのでクラスとわかる。

   関数は全部小文字で単語は_でつなぐというマナーです。

問題はWebSoketAppの引数です

URLは上の変数で指定したwebのアドレス

on_message=on_message

on_message これはWebSoketAppクラスのキーワード引数名

on_message これはdef on_message(ws, message)の関数名

on_open=on_open 同じくローカルの関数名

関数名ならキーワード引数名と同じにする必要は?別の方が良いでしょ?

on_message=on_message on_open=on_open

=の左側のキーワード引数は、呼び出し側の関数やクラスが指定しているので変えられませんが、右側の関数名なら勝手な名前をつけられるのでは?後で関数名を変えてみましょう

データ処理関数 on_message

def on_message(ws, message):
message = json.loads(message)

3つの”message”が何だこりゃー?ですね

messageは新規の変数ですね messageはもらってきた引数を json.loadsに渡して処理をして貰う手続きです。これも名前は(青と青が同じであれば)自由のはずですね。何故同じにする?後で変えてみましょう。


接続時、何をするかの関数 on_open

def on_open(ws):
ws.send(json.dumps({"method": "subscribe",
"params": {"channel": CHANNEL}}))

サーバーに接続されたら、何をしたいかjson文字列で通知します。

def on_open(ws)

このはws何でしょうか?これも後で名前を変えてみましょう。

{method": "subscribe","params": {"channel":"lightning_executions_BTC_JPY"}}

上記の辞書データをstrに変換してサーバーに投げつけます。

紛らわしい関数名や変数名を変えてみる

import websocket
import json
from pprint import pprint

URL = "wss://ws.lightstream.bitflyer.com/json-rpc"
CHANNEL = "lightning_executions_BTC_JPY"

def get_data(a, b):
    c = json.loads(b) # json文字列(str)を辞書に変換する関数

def when_open(x):
    #   要求内容をjson文字列(str)に変換して渡す
    x.send(json.dumps({"method": "subscribe",
                        "params": {"channel": CHANNEL}}))
#main---------------------------
BFstream = websocket.WebSocketApp(URL,on_message=get_data, on_open=when_open)
BFstream.run_forever()

紛らわしい”ws”は一つもなくなりました。(ビットフライヤーのurl内は除く)

“message”もなくなりました。

URLとCHANNNELを外に出しても出さなくても元々数行ですから、意味がわかれば単純ですね。

ではどんなデータが取れるか、get_dataにprint文を追加してみる。

def get_data(a, b):
    c = json.loads(b)## json文字列(str)を辞書に変換する関数
    print('a',a) #不明、表向き使われていない?
    print('b',type(b),b) #サーバーから送られてきたjson文字列
    print('c',type(c),c) #json文字列を辞書に変換したもの
a <websocket._app.WebSocketApp object at 0x000002B3047A7A90>
b <class 'str'> {"jsonrpc":"2.0","method":"channelMessage","params":{"channel":"lightning_executions_BTC_JPY","message":[{"id":2214565246,"side":"SELL","price":4474821.0,"size":0.01,"exec_date":"2021-05-19T02:10:08.7844261Z","buy_child_order_acceptance_id":"JRF20210519-021008-296383","sell_child_order_acceptance_id":"JRF20210519-021008-228988"}]}}
c <class 'dict'> {'jsonrpc': '2.0', 'method': 'channelMessage', 'params': {'channel': 'lightning_executions_BTC_JPY', 'message': [{'id': 2214565246, 'side': 'SELL', 'price': 4474821.0, 'size': 0.01, 'exec_date': '2021-05-19T02:10:08.7844261Z', 'buy_child_order_acceptance_id': 'JRF20210519-021008-296383', 'sell_child_order_acceptance_id': 'JRF20210519-021008-228988'}]}}

bとcは見た目は同じ文字列で、データ形式が違うということですね。

bの「全体が文字列」では、欲しいデータは文字列内をfind(str)などで検索を繰り返さなくてはなりませんので、実用的でないです。

cの「辞書型データ」であれば、c[‘params’][‘message’][‘price’]として、データの位置を指定すれば欲しいデータを取れます。

もう一度ソースコードを見直す

データ受信関数

名前を変えたソースコード、何か安っぽいが(笑)、変数が3種類なのは解りやすい。

def get_data(a, b):
c = json.loads(b)

元のソースコード,messageだらけでどれがどうやら解らない。

def on_message(ws, message):
message = json.loads(message)

接続時関数

名前を変えたソースコード

def when_open(x):
# 要求内容をjson文字列(str)に変換して渡す
x.send(json.dumps({"method": "subscribe","params": {"channel": CHANNEL}}))

元のソースコード

def on_open(ws):
ws.send(json.dumps({"method": "subscribe","params": {"channel": CHANNEL}}))

殆ど変わってないが、wsという名前でなくても良いのは分かった。こうして見れば当たり前だけども(^_^;)

main部分

名前を変えたソースコード

BFstream = websocket.WebSocketApp(URL,on_message=get_data, on_open=when_open)
BFstream.run_forever()

元のソースコード

ws = websocket.WebSocketApp(URL,on_message=on_message, on_open=on_open)
ws.run_forever()

やはりon_message=on_messageでは解らない。

プログラムを最初に学び出した時、a=a+1をみて、何だこりゃ誤植か?と思ったのを彷彿とさせます。

on_message=get_dataはメッセージが来たらデータを取れよと解りやすい。(^_^;)

関数や変数の名前を変えただけのコードでビットコイン価格をwebsocketで受信し続ける

import websocket
import json
from pprint import pprint
#import pandas as pd

URL = "wss://ws.lightstream.bitflyer.com/json-rpc"
CHANNEL = "lightning_executions_BTC_JPY"

def get_data(a, b):
    c = json.loads(b)## json文字列(str)を辞書に変換する関数
    if c["method"] == "channelMessage":
        last1=c["params"]["message"][0]
        print(f'BF_BTC {last1["exec_date"][0:19]}  {last1["price"]:,.0f} {last1["side"]:4} {last1["size"]:.2f}')

def when_open(x):
    #   要求内容をjson文字列(str)に変換して渡す
    x.send(json.dumps({"method": "subscribe",
                        "params": {"channel": CHANNEL}}))
#main---------------------------
bFstream = websocket.WebSocketApp(URL,on_message=get_data, on_open=when_open)
bFstream.run_forever()

出力

BF_BTC 2021-05-19T03:03:00  4,385,539 BUY  0.13
BF_BTC 2021-05-19T03:03:00  4,387,689 BUY  0.00
BF_BTC 2021-05-19T03:03:00  4,385,475 SELL 0.19
BF_BTC 2021-05-19T03:03:00  4,385,475 SELL 0.04
BF_BTC 2021-05-19T03:03:01  4,385,475 SELL 0.04
BF_BTC 2021-05-19T03:03:01  4,385,475 SELL 0.13
BF_BTC 2021-05-19T03:03:01  4,385,475 SELL 0.19
BF_BTC 2021-05-19T03:03:01  4,385,000 SELL 0.00
BF_BTC 2021-05-19T03:03:01  4,383,565 SELL 0.01
BF_BTC 2021-05-19T03:03:02  4,385,439 BUY  0.00
BF_BTC 2021-05-19T03:03:02  4,385,439 BUY  0.01
BF_BTC 2021-05-19T03:03:04  4,386,138 BUY  0.01
BF_BTC 2021-05-19T03:03:05  4,387,686 BUY  0.01
BF_BTC 2021-05-19T03:03:05  4,387,687 BUY  0.55
BF_BTC 2021-05-19T03:03:05  4,389,092 BUY  0.01
BF_BTC 2021-05-19T03:03:06  4,387,690 SELL 0.07
BF_BTC 2021-05-19T03:03:06  4,390,000 BUY  0.00
BF_BTC 2021-05-19T03:03:06  4,390,000 BUY  0.02
BF_BTC 2021-05-19T03:03:06  4,387,693 SELL 0.04
>>> 

printする自データの取り出しは以下です

if c["method"] == "channelMessage": #メッセージがデータであれば ↓

last1=c["params"]["message"][0] # 価格等のデータまで掘り下げてから ↓

print(f'BF_BTC {last1["exec_date"][0:19]} {last1["price"]:,.0f} {last1["side"]:4} {last1["size"]:.2f}')

f リテラルを解説

f’ 文字列 {変数や式や関数など何でも取り込める} {}{}何個でも? ‘

fリテラル↑は、以前の.format↓よりはるかに便利で、分かりやすさも段違いに良い!

‘ 文字列 {} ‘.format(変数や式)#関数やクラスは不可

細かく説明

“last1″はデータを掘り下げる途中の階層に名前をつけて扱いやすくしたもの

last1["exec_date"][0:19]} #日付のデータを左から19文字取り出せ

“2021-05-19T02:10:09.9443526Z”→’2021-05-19T02:10:09′ #Tはそのまま

{last1["price"]:,.0f}

価格 4472414.0 を 小数点以下無しでカンマ区切りにする

{last1["side"]:4}

‘SELL’ か ‘BUY’を4文字幅で

{last1["size"]:.2f}

元データ 0.0646 を 小数点2位まで

結局websocket.WebSocketApp()が、「繋いでデータを受信する」を全部やってくれるのであった。

ただ、ボットに組み込むときは、接続確認や、切断時の処理などいろいろな手続をプログラムしなければいけない模様。

PS.色を付けたり分かりやすさを求めたが全然分かり易くない。

それは中身が難しいからだ┐(´д`)┌。

コメント

タイトルとURLをコピーしました