Python BITFINEX WebSocket取引① 詳細な解説

こちらのサイトでレクチャーをされています

Websocket Trading on Bitfinex Using Python: Part 1 - Create a Basic Authenticated Connection — Steemit
Repository With Sample Code Find the Python script on GitHub: bitfinex_websocket_basic.py My Profile On… by imwatsi


BITFINEX API は本元で確認

https://docs.bitfinex.com/docs/ws-general

環境の設定

websocket-clientモジュールをインストールするには、pipを使用します。

端末で次のコマンドのいずれかを入力します。

  • pip3 install websocket-client Linuxを使用している場合
  • pip install websocket-client Windows / MacOSを使用している場合

用語の定義

以下は、取引で使用される用語の定義であり、それらに慣れていない場合に役立つ可能性があります。

  • ティッカー:市場の状態に関する高レベルの概要。通常、現在のビッド/アスク価格、最後に取引された価格、24時間のボリューム、その日の高値/安値、および毎日のパーセント変化を示します。
  • 成行注文:注文帳に対して即座に実行される注文(最良の売値で買い、最良の買値で売る)。

WebSocket接続を使用して取引することの利点

リアルタイムデータストリーム

Websocket接続は、セッション全体を通じて存続するため、RESTを介してデータを要求する場合よりも高速にデータ更新を配信します。これにより、データをリアルタイムでコンピューターにプッシュできるため、メモリ内で最新の状態を維持できます

より迅速な注文

WebSocketを介して注文すると、RESTに比べて待ち時間が短くなります。これも、ライブ接続と送信されたリクエストのオーバーヘッドが小さいためです。

市場の変化への迅速な対応

取引戦略が市場の状況を迅速に更新することに依存している場合は、WebSocketが最適です。たとえば、オーダーブックの深さに基づく戦略(絶えず変化している)は、期待どおりに機能するためには、非常に低いレイテンシーを必要とします。

必要なモジュールをインポートする

以下のコードは、このスクリプトが依存するモジュールをインポートします。

import websocket
import hashlib
import hmac
import json
import time
import os
  • websocket WebSocket接続を開いて維持するために使用されます
  • hashlibそして、hmacの接続を認証するための暗号化関連の操作を処理します
  • json 着信JSONデータの解析と発信リクエストのフォーマットにも役立ちます
  • time 時間遅延を作成するために使用されます
  • os WebSocket接続が閉じたときに、プログラムを終了するトリガーとして使用されます
  • threading 同期操作用の新しいスレッドを作成するために使用されます

変数と定数を定義する

  • BitfinexアカウントのAPIキーとシークレットをそれぞれAPI_KEYAPI_SECRETに配置します。
  • チャンネルは、 WebSocketの接続で開かれたチャンネルを保存し、市場からティッカーデータを格納します

基本的なWebSocket関数を作成する

WebSocket接続を維持するには、さまざまな目的に役立ついくつかの機能が必要です。この基本的なフレームから始めることができます。

def on_message(ws, message):
    pass

def on_error(ws, error):
    pass

def on_close(ws):
    pass

def on_open(ws):
    pass

def connect_api():
    pass

connect_api() WebSocketインスタンスを作成し、接続を開始します
on_open() 接続が開いているときにタスクを実行する
on_message() サーバーから返送されたメッセージを処理する
on_error() エラーを処理する
on_close() 接続が閉じられたときに何が起こるかを判断する

connect_api()

関数connect_api()を使用して、wsと呼ばれるグローバルオブジェクトを作成します。このオブジェクトに、新しいWebSocketインスタンスを割り当て、そのパラメーターを設定し、他の関数とリンクして、正しく実行できるようにします。全体として、関数には以下に示すコードが含まれている必要があります。

WebSocket プロトコルを使用して通信するには、 WebSocket オブジェクトを作成する必要があります。これにより自動的にサーバーへの接続が開かれます。

def connect_api():
    global ws
    websocket.enableTrace(False)
    ws = websocket.WebSocketApp('wss://api.bitfinex.com/ws/2',
                            on_message = on_message,
                            on_error = on_error,
                            on_close = on_close,
                            on_open = on_open)
    ws.run_forever()
  • この行websocket.enableTrace(False)は、デバッグ出力を無効にします。バグを修正しようとしたときに接続プロセスの出力を表示するには、これをTrueに設定します。
  • WebSocketインスタンスは(サーバーアドレスパラメーターを使用)ws = websocket.WebSocketApp("wss://api.bitfinex.com/ws/2",で作成され、他の関数は適切なパラメーター(on_message … etc)にリンクされます。
  • ws.run_forever() 接続を開始します

on_open()

接続が開かれると、関数on_openが呼び出されます。接続が正常に開かれたことを示すステートメントをprintします。また、認証とチャネルへのサブスクリプションを処理する場所でもあります(これらは、サーバーからデータを受信するための方法です)。

接続を開くと、サーバーにデータを送信することができます。 これを行うには、送信するメッセージごとに WebSocket オブジェクトの send() メソッドを呼び出します。

接続の確立は非同期であり、失敗しやすいため、 send() メソッドの呼び出しが WebSocket オブジェクトの作成直後に成功するという保証はありません。データの送信を試みるのは、少なくともいったん接続が確立してからでなければならないので、作業を行うための onopen イベントハンドラーを定義してその中で行います。

global API_KEY, API_SECRET
    def authenticate():
    # Authenticate connection
        nonce = str(int(time.time() * 10000000))
        auth_string = 'AUTH' + nonce
        auth_sig = hmac.new(API_SECRET.encode(), auth_string.encode(),
                            hashlib.sha384).hexdigest()

        payload = {'event': 'auth', 'apiKey': API_KEY, 'authSig': auth_sig,
                    'authPayload': auth_string, 'authNonce': nonce, 'dms': 4}
        ws.send(payload)
    print('API connected')
    authenticate()
    sub_ticker = {
        'event': 'subscribe',
        'channel': 'ticker',
        'symbol': "tBTCUSD"
    }
    ws.send(json.dumps(sub_ticker))
    # start printing the ticker
    Thread(target=print_ticker).start()

API_KEYAPI_SECRETのグローバル宣言があり、API認証の詳細を取得します。

そこにはネストされた関数がありauthenticate()ます。

APIキーを使用して署名されたメッセージを作成し、Bitfinexサーバーに送信して接続を認証し、アカウント機能へのアクセスを許可します。

その関数内のコードの説明は次のとおりです。

  • nonce = str(int(time.time() * 10000))リクエストを保護するために、ビットフィネックスはナンスとして「増え続ける数値文字列」を必要とします。このコードは、現在のタイムスタンプから1つを作成し、送信時にauth_stringと結合されます。

ノンス(英: nonce、ナンスとも)は、暗号通信で用いられる、使い捨てのランダムな値のことである[1]。ノンスはたいてい、認証の過程で使われ、リプレイ攻撃を行えないようにする働きを担っている。

  • 次のコードは署名を作成します。 auth_sig = hmac.new(API_SECRET.encode(), auth_string.encode(), hashlib.sha384).hexdigest()
  • 生成されたすべての入力を使用して、辞書を作成し、JSONとして送信します
payload = {'event': 'auth', 'apiKey': API_KEY, 'authSig': auth_sig,
                    'authPayload': auth_string, 'authNonce': nonce, 'dms': 4}
ws.send(json.dumps(payload))

メイン関数に戻ると、on_open()「API接続」を出力した後、authenticate()ネストされた関数を呼び出し、BTCUSDティッカーのサブスクリプション要求を含むメッセージを送信していることがわかります。

最後に、後で定義する関数を実行する新しいスレッドを開始しますprint_ticker。これにより、BTCUSDティッカーが端末にリアルタイムで出力されます。

error()で

この関数を使用して、WebSocketセッション中に発生するエラーを処理できます。簡単にするために、エラーを出力するprintステートメントを配置します。

def on_error(ws, error):
    print(error)

接続中にエラーが発生した場合、最初に error という名前の単純なイベントが WebSocket オブジェクトに送信され (その結果、その onerror ハンドラーが呼び出されます)、次に CloseEvent が WebSocket オブジェクトに送信され (onclose ハンドラーが呼び出されます) 接続の終了の理由を示します。

on_close()

def on_close(ws):
    print("### API connection closed ###")
    os._exit(0)

この関数を使用して、接続が閉じたときに何が起こるかを判別します。ここでは、簡単にするために、printステートメントでユーザーに通知してから、スクリプトを終了します。

on_message()

WebSocket 接続を介して受信されるテキストは、 UTF-8 形式です

WebSocket接続が開かれるとすぐに、メッセージを送受信できます。このスクリプトでは、最初に受信する必要のあるメッセージは、サーバーに送信した認証要求から、認証ステータス(成功または失敗)を通知するメッセージになります。

これを処理し、成功したかどうかを判断するためのコードが必要です。この関数は、サブスクライブするデータチャネルの詳細、およびそれらのチャネルが提供するデータを含むメッセージも受信します。すべてがこの関数によって処理されます。関数全体のコードを以下に示します。ここでは、そのコンポーネントを分解します。

WebSockets はイベント駆動型 API です。メッセージを受信すると、 message イベント WebSocket オブジェクトに送信されます。これを処理するには、 message イベントのイベントリスナーを追加するか、 onmessage イベントハンドラーを使用するかします。

    global channels, tickers
    data = json.loads(message)
    # Handle events
    if 'event' in data:
        if data['event'] == 'info':
            pass # ignore info messages
        elif data['event'] == 'auth':
            if data['status'] == 'OK':
                print('API authentication successful')
            else:
                print(data['status'])
        # Capture all subscribed channels
        elif data['event'] == 'subscribed':
            if data['channel'] == 'ticker':
                channels[data['chanId']] = [data['channel'], data['pair']]

上記のコードは、上記で定義したグローバル変数チャネルティッカーを宣言しています。ifステートメントの組み合わせにより成功した認証シナリオが除外され、正常にサブスクライブされたチャネルがキャプチャされ、チャネル変数に保存されます。

次のコードブロックは、オープンチャネルから受信するデータを処理します。

    # Handle channel data
    else:
        chan_id = data[0]
        if chan_id in channels:
            if 'ticker' in channels[chan_id]: # if channel is for ticker
                if data[1] == 'hb':
                    pass
                else:
                    # parse ticker and save to memory
                    sym = channels[chan_id][1]
                    ticker_raw = data[1]
                    ticker_parsed = {
                        'bid': ticker_raw[0],
                        'ask': ticker_raw[2],
                        'last_price': ticker_raw[6],
                        'volume': ticker_raw[7],
                    }
                    tickers[sym] = ticker_parsed

チャネルはチャネルID番号で識別され、これらはチャネルディクショナリにエントリを格納するために使用されるキーです。上記のコードは、メッセージからチャネルIDを抽出し、保存されているチャネルで一致するものを探します。

見つかった場合は、チャネルが線のあるティッカー用であることを確認するために、さらにチェックが行われます。

if ‘ticker’ in channels[chan_id]:

これにより、たとえばオーダーブックやキャンドルデータチャネルがある場合に、他のチャネルタイプが除外されます。

Bitfinexは、チャネルを開いたままにするために定期的にハートビートメッセージを送信するため、次のように無視します。

if data[1] == 'hb':
    pass

コードの残りの部分は、ティッカー情報を解析し、それをティッカー変数に保存します。Bitfinexはこれらの更新をリストのセットとして送信するため、ビッド、アスクなどにインデックスが付けられます。

ticker_raw[0]参照。

これらのメッセージは頻繁に届き、複数のティッカー(たとえば、100の市場シンボル)をサブスクライブしている場合、関数on_messageは毎回使用されます。これは、1秒あたり複数回の場合があります。この場合、これは遅延のボトルネックになる可能性があり、他の関数に延期して新しいスレッドを開くことにより、この関数で実行される計算の量を減らすための対策を講じる必要があります。私の次のチュートリアルはこのテーマになります。

on_message関数をまとめると、全体が次のようになります。

def on_message(ws, message):
    global channels, balances, tickers
    data = json.loads(message)
    # Handle events
    if 'event' in message:
        if data['event'] == 'info':
            pass # ignore info messages
        elif data['event'] == 'auth':
            if data['status'] == 'OK':
                print('API authentication successful')
            else:
                print(data['status'])
        # Capture all subscribed channels
        elif data['event'] == 'subscribed':
            if data['channel'] == 'ticker':
                channels[data['chanId']] = [data['channel'], data['pair']]
    # Handle channel data
    else:
        chan_id = data[0]
        if chan_id in channels:
            if 'ticker' in channels[chan_id]: # if channel is for ticker
                if data[1] == 'hb':
                    pass
                else:
                    # parse ticker and save to memory
                    sym = channels[chan_id][1]
                    ticker_raw = data[1]
                    ticker_parsed = {
                        'bid': ticker_raw[0],
                        'ask': ticker_raw[2],
                        'last_price': ticker_raw[6],
                        'volume': ticker_raw[7],
                    }
                    tickers[sym] = ticker_parsed

new_order_market()

次に、新しい成行注文を出すことができる新しい関数を定義します。wsまず、グローバル変数(WebSocketインスタンス)を宣言します。次に、タイムスタンプを使用して、新しい注文の「クライアント注文ID」を生成します。

その後、注文の詳細(クライアント注文ID、取引記号(BTCUSDなど)、取引金額)を含む辞書を作成します。この辞書は、ビットフィネックスが必要とする特別なメッセージ形式で含まれています。

以下に示すように、メッセージは、注文の詳細をペイロードとして、0(ゼロ)チャネルにリストとして送信する必要があります。これがコンパイルされると、ws.send()関数を介してJSONとして送信されます。

def new_order_market(symbol, amount):
    global ws
    cid = int(round(time.time() * 1000))
    order_details = {
        'cid': cid,
        'type': 'EXCHANGE MARKET',
        'symbol': 't' + symbol,
        'amount': str(amount)
    }
    msg = [
            0,
            'on',
            None,
            order_details
        ]
    ws.send(json.dumps(msg))

この関数を使用して注文するには、new_order_market("BTCUSD", 0.01)たとえば、0.01BTCを市場価格で購入するように呼び出すことができます。売りの場合はマイナスの値を入れます。

また、以下のような関数を追加し、スクリプトのinitで新しいスレッドを開くことにより、ユーザー入力から呼び出すこともできます。ユーザーコマンドをリッスンし、コード化されたとおりに命令を実行するループを実行します。

def user_input():
    while True:
        command = input()
        if command == 'Buy':
            new_order_market("BTCUSD", 0.01)

print_ticker()

最後に作成する関数は、現在のティッカーデータをメモリから取得して端末に出力するために使用されるループです。

end="\r"値を更新している間、同じ行の印刷を維持するためにprintステートメントを使用するため、ティッカーの変化をリアルタイムで確認できます。

def print_ticker():
    global ticker
    symbol = 'BTCUSD'
    while len(tickers) == 0:
        # wait for tickers to populate
        time.sleep(1)
    while True:
        # print BTCUSD ticker every second
        details = tickers[symbol]
        print('%s:  Bid: %s, Ask: %s, Last Price: %s, Volume: %s'\
            %(symbol, details['bid'], details['ask'],\
            details['last_price'], details['volume']), end="\r", flush=True)
        time.sleep(1)

これでスクリプトが完了しました

デバッグ出力

以下は、設定からの出力のスクリーンショットですwebsocket.enableTrace(True)。最初のスクリーンショットには、接続が確立されているときに送信したリクエストと受信したレスポンスのヘッダー情報が含まれています。

次のスクリーンショットは、印刷物のステートメントと、ぎこちないように見えるものが混在していることを示しています。そこには2つの送信ステートメントがあります。1つは接続の認証用で、もう1つはBTCUSDティッカーチャネルへのサブスクライブ用です。どちらも暗号化されているため、判読できない形式です。最後の行は、スクリプトがリアルタイムのティッカーを出力する場所であり、コードで述べたように毎秒更新されます。

スクリプトを正常に実行すると、次の端末出力が得られるはずです。

MULTI versionへ続く

 

コメント

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