PythonでWebsocketのクライアント実装(bitbankのリアルタイムデータ配信APIがPubNubからSocket.IOに急変したのでその対応)

#背景
暗号資産(仮想通貨)の自動売買取引のボットをbitbank上で動かしていたんだけど、リアルタイムの価格情報やローソクチャートを取得するAPIの技術仕様がPubNubからWebsocket(Socket.IO)に変更された

* [API関連アップデートのお知らせ / API Update Notice](https://blog.bitbank.cc/20191007/)

今月に入ってアナウンスがあってPubNubは非推奨となり、将来的に廃止されるとのことだったが、先日のbitbankのメンテナンスで事前の告知なくPubNubが急遽廃止されて、アタフタ...

おそらく、メンテナンスに絡む技術的な問題か何かで急遽廃止せざるを得なくなったのだろう(知らんけどw)(つか、少なくとも事前に廃止告知と、告知後の並行稼働期間は設けてくれよっ！とは思ったわ)

ということで、ビット子ボットちゃんの処理を、早急にWebsocket対応にしたいわけだけど、Websocket扱ったことがないので四苦八苦中、というか、まだ事前の調査段階

#下調べ
公式のドキュメントは、Websocketを知っている人前提の非常に簡単な内容

* [リアルタイムデータ配信API (2019-09-27) · bitbankinc/bitbank-api-docs · GitHub](https://github.com/bitbankinc/bitbank-api-docs/blob/master/public-stream_JP.md)

Pythonでボットは開発しているので、Pythonでの実装方法を知りたいの、ということで色々検索してみたけど、クライアントとしての実装の情報がなかなか見つからない

ドンピシャなコードサンプルはあったけど...

* [bitbankのリアルタイムデータ配信APIに接続するPythonコード - arms22](https://scrapbox.io/arms22/bitbank%E3%81%AE%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%87%E3%83%BC%E3%82%BF%E9%85%8D%E4%BF%A1API%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8BPython%E3%82%B3%E3%83%BC%E3%83%89)

上記は、 `import socketio` とあるので、おそらく、以下の python-socketio というライブラリを利用していると思われる

* [python-socketio · PyPI](https://pypi.org/project/python-socketio/)
* [python-socketio — python-socketio documentation](https://python-socketio.readthedocs.io/en/latest/index.html)
    * [Getting Started > Client Examples](https://python-socketio.readthedocs.io/en/latest/intro.html#client-examples)
    * [The Socket.IO Client](https://python-socketio.readthedocs.io/en/latest/client.html)

他に、以下のあたりをザザッと見たけど、情報が古かったり、求めている情報ではなさそう(検索の仕方が悪いのかな... 適当な情報が見つからない...)

* [PythonでWebSocketのお勉強 | MUDAなことをしよう。](https://make-muda.net/2017/10/5645/)
* [Pythonでsocket.io 2.0のクライアントはsocketIO-client-nexusに限定されているかもしれない｜コンピュータ｜ブログ｜Take3's web](https://www.valuestar.work/news/archives/63)
* [websocket を用いて bitFlyer の Ticker 情報からOHLCを表示する - Qiita](https://qiita.com/aisurta/items/c21e4f65a904e8df347a)

そんな中、以下の結構クリティカルじゃないかと思われるbitbankのこのAPIに関するissueを見つけてしまった...

* [【GitHub】bitbankの300秒でWSの接続が切れる問題は運営にどうにかしてほしい。 | ツイレポ](https://twirepo.com/detail/3733060/)
* [Constantly getting disconnected from websocket after a few minutes · Issue #4 · bitbankinc/bitbank-api-docs · GitHub](https://github.com/bitbankinc/bitbank-api-docs/issues/4)

技術仕様とか、これを使う仕組みを考えたら「5分でセッションを強制的に切っちゃう」とかあり得ないですよ？という2人のユーザー(エンジニア？)からのツッコミに対して、運営(bitbank)の中の人っ？が「"malicious connections" (悪意ある接続) 対応のため」という理由付けで説明をしていることに対して、そういうのは別の対策をすべきでしょっ？というやりとりになっていて... 今後も継続して使うことに不安も覚えるが... とりあえず、まだ、5分で切れちゃう問題は解消されていないので、その前提で実装をする必要がある

ライブラリ側で再接続の考慮がされている場合もあるけど、それがない場合は、プログラム内で考慮してあげるという感じかな... めんどっ...

運営さんからもライブラリに再接続のオプションがあるから、それを使うか自前で実装して的なフォローが書かれていた

で、利用することになりそうな python-socketio のドキュメントを読むと...

> * [API Reference — python-socketio documentation](https://python-socketio.readthedocs.io/en/latest/api.html)
>
> reconnection – True if the client should automatically attempt to reconnect to the server after an interruption, or False to not reconnect. The default is True.
>
> reconnection_attempts – How many reconnection attempts to issue before giving up, or 0 for infinity attempts. The default is 0.
>
> reconnection_delay – How long to wait in seconds before the first reconnection attempt. Each successive attempt doubles this delay.
>
> reconnection_delay_max – The maximum delay between reconnection attempts.

ベースになるクラスのインスタンスを作成する時の再接続のオプションがいくつかあるので対応は問題なくできそう

(>>> 追記 2019-11-02)
どうやらちょっとだけ改善されて、300secの5分ではなく、1時間に変更された模様
でも、問題指摘した人は、せめて24時間にしてよって投稿してる
> [Constantly getting disconnected from websocket after a few minutes · Issue #4](https://github.com/bitbankinc/bitbank-api-docs/issues/4#issuecomment-546161979)
> Upon further testing, I can see now the disconnect interval is exactly 3600 sec (1 hour). Can we not extend this to a 24 hour interval? For example, binance forces a server disconnect only once every 24 hours.

そんなアップデートを見つつ、別のissueを見ていたらローソクチャートがリアルタイムAPIから除外されてた... orz それ、私のビット子ボットちゃんとっては大変困るんですが...

* [Websocket API missing candlesticks stream endpoints · Issue #3](https://github.com/bitbankinc/bitbank-api-docs/issues/3)

Public API でも取得できるんだけど、日付指定でその日のチャート全部だから最新のものだけを参照しないといけないし... 別の改修が必要になるじゃないノォ...
(<<< 追記 2019-11-02)

# 実装

コード書きながら、どんな感じの実装になるのか具体化するので、その工程を書き残しておく

コードの前にまずはライブラリのインストールで、公式ドキュメントには、インストールするのに、 `pip` コマンドの例があるけど、asyncio を利用した非同期用のライブラリと、標準的なライブラリでインストールの時の引数の指定が異なるっぽい

* [The Socket.IO Client — python-socketio documentation](https://python-socketio.readthedocs.io/en/latest/client.html)

非同期処理は、利用しているフレームワーク responder の方でゴニョゴニョするので良いはずだから(フレームワーク側のシンプルな実装で非同期に簡単にできるから)、とりあえず、標準のライブラリを入れれば良さそう

構築した環境は、pipenv管理なので、コマンドは `pip` ではなく、`pipenv` 

```
pipenv install python-socketio
```

公式ドキュメントのコードをベースに、巷の実装例を参考に内容を精査していく

```python
import socketio

sio = socketio.Client(
    reconnection=True, 
    reconnection_attempts=0, 
    reconnection_delay=1,
    reconnection_delay_max=30
)
```
SocketIOのクライアント・インスタンス作成時のパラメータは以下のような意味
(だと思うw)

* 再接続を有効にして
* attempts=0で無限で再接続を試みる
* 再接続の遅延間隔は1秒(再接続に失敗するごとに、この間隔は倍化)
* 再接続の遅延間隔の最大値は、30秒

この後の接続やデータ取得、切断の処理あたりについて色々な実装例が見られるので、少々混乱w

公式ドキュメントと、巷のどなたかの実装例を比較してみると、デコレーターで実装している部分と、メソッドで実装している部分があって、どちらのコードでも良さそうだけど、微妙にそれぞれ差異があって... 何が正しいのか、どれが標準的な実装方法なのかわかりづらい

というか、単に、Pythonの実装方法やデコレーターを深く理解していないからという初歩的？構文理解の話のように話でもあるような気はするが...

全体のボットの実装方法が手続き的に書いているので、デコレーターではなくメソッドを利用した実装方法の方が扱いやすい気がする...

で、改めて、以下のサンプルコードを見ると、アァーなるほどという感じ

* [bitbankのリアルタイムデータ配信APIに接続するPythonコード - arms22](https://scrapbox.io/arms22/bitbank%E3%81%AE%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%87%E3%83%BC%E3%82%BF%E9%85%8D%E4%BF%A1API%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8BPython%E3%82%B3%E3%83%BC%E3%83%89)

```python
import socketio
 
 def on_connect():
 	print('connect')
 	sio.on('message',on_data)
 	sio.emit('join-room','ticker_btc_jpy')
 	sio.emit('join-room','transactions_btc_jpy')
 	sio.emit('join-room','depth_diff_btc_jpy')
 	sio.emit('join-room','depth_whole_btc_jpy')
 
 def on_data(data):
 	print(data)
 
 sio = socketio.Client()
 sio.on('connect', on_connect)
 sio.connect('wss://stream.bitbank.cc', transports = ['websocket'])
 sio.wait()
```

インスタンスを生成 `sio = socketio.Client()` した後、クラスの `on` メソッドで接続時のハンドラーメソッドを設定 `sio.on('connect', on_connect)` してから `connect` メソッドで接続

`on_connect` のハンドラーメソッド内で、実際に接続するときに、データ取得(message を受け取る)時のハンドラーメソッドを定義している
 
`on` メソッドで `disconnect` のハンドラーも設定しておくと良さそう

で、`connect` と `disconnect` には print による出力ではなく、ロギングの処理を書いておくのも忘れずに

`message` のハンドラー設定は、`on_connect` のハンドラーの中じゃなくても良さそうな気がする...

なので、クライアント・インスタンスを作成してからの処理は以下のようにしたらどうだろうか

```python

sio = socketio.Client()
sio.on('connect', on_connect)
sio.on('message', on_message)
sio.on('disconnect', on_disconnect)
sio.connect('wss://stream.bitbank.cc', transports = ['websocket'])
sio.wait()
```

あとは、ビット子ボットちゃんのファイル構成とかが関係してくるので、机上のコーディングはこの程度にしておき、ここまでで得た情報で実コーディングを進めてみる

# 追記(顛末)
前述の内容を踏まえて、実際にコーディングを進めてテスト実行をしてみたけれど、動作しない... エラーは出ない... テストコードを実行すると `sio.wait()` の部分で少し待たされて... 何も標準出力に出てこない

print文だとダサいのと、python-socketio が logger に標準対応しているので logger を仕込んだコードを書いてみたが... ログレベルを DEBUG にしているけど、何も出力されない...

実際のコードが以下のような感じ

```python
import socketio
import json
import logging

logging.basicConfig(
    level=logging.ERROR,
    filename='./try.log',
    format='%(levelname)s : %(asctime)s : %(message)s'
)
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

sio = socketio.Client(
    reconnection=True, 
    reconnection_attempts=0, 
    reconnection_delay=1, 
    reconnection_delay_max=30, 
    logger=True
)
logger.info('Created socketio client')

@sio.event
def connect():
    logger.info('connected to server')
    sio.emit('join-room','transactions_btc_jpy')

@sio.event
def message(data):
    logger.info('print message')
    print(json.dumps(data, indent=2))

@sio.event
def disconnect():
    logger.info('disconnected from server')

sio.connect('wss://stream.bitbank.cc', transports=['websocket'])
sio.wait()
``` 

エラーなり、ログなりが出力されれば、調べることもできるけど、何も出力されないんじゃぁ...

どうにも手立てが思いつかないので人に頼ろうと思い、Discord の Python JP コミュニティがあるので、そこにでも投稿しようと思って、メッセージもしたためて、送信押す直前で、再度メッセージに掲載した上記実行コードを見直していたら...

気づきました

```python
logging.basicConfig(
    level=logging.ERROR,
```

ココ、読み込んでいるモジュール側のログレベルが ERROR になっているっwwwwww
ということで、この ERROR のところを...

```python
logging.basicConfig(
    level=logging.DEBUG,
```

DEBUG にしたところ！ログに以下が出力されました

```
WARNING : 2019-10-30 21:15:18,357 : websocket-client package not installed, only polling transport is available
```

依存ライブラリがあるんかーい！
まとめてインストールされないんかーい！
ドキュメントに書かんかーい！

`pipenv install ライブラリ名` で依存関係のあるライブラリも一緒にインストールされると思うじゃない？ `Pipfile.lock` 見ると関連するライブラリもインストールされているっぽいけど... なぜ？

一番関連ありそうな、最重要そうな `websocket-client` がインストールされないんだと...

結果

```
pipenv install websocket-client
```

したら全て解決！
これでようやくやりたいことができる