エンジニアの頭の中

トレードbotエンジニアが書く技術と投資のブログ

PythonとBitMEX WebSocket API によるリアルタイムトレードbotの作り方

f:id:mitsu3204:20180819142757j:plain

仮想通貨の自動売買を高速で実現するためには、取引所のデータをリアルタイムに取得する必要がありwebsocketが欠かせません。

本記事では、PythonとBitMEXのWebSocket APIを使用して、仮想通貨のリアルタイムトレードを実現するbotの作り方を記載します。

PythonによるWebSocket APIの実装方法や、websocketを介して受け取るデータの内容に焦点を当てて行きます。

目次

BitMEXとは

BitMEXは自動売買に適した仮想通貨取引所

BitMEXは、ビットコインイーサリアムリップルなど様々な仮想通貨を取り扱う仮想通貨取引所です。

100倍という高レバレッジに対応しているのが特徴です。また、BitMEXはAPIとそのドキュメントが充実しており、自動売買を行うためのボットが作りやすいです。

裁量トレーダーもbotトレーダーも利用者は多いと思われます。本人確認が必要ないので、アカウントをサクッと作りやすいのも良いですね。

www.bitmex.com

BitMEXを使うなら手数料を理解しておくべき

BitMEXを利用するには、BitMEXの手数料体系を理解しておく必要があります。

BitMEXでは取引の際に注文の仕方がTakerかMakerかによって手数料が変わってきます。Takerは、0.075%の手数料が徴収されますが、Makerは-0.025%というマイナス手数料なので、手数料がもらえます。Maker手数料狙いのボットを作るのもアリかもしれないですね。

また、ファンディング手数料というのもあって、1日の中の決まった時間にポジションを保有していると手数料が発生します。ファンディング手数料は、ポジションがロングかショートかによって手数料を支払うのかもらえるのかが異なります。

BitMEXの手数料については、わかりやすく解説してくれている記事があるので、そちらを参考にすると良いと思います。

www.benyasu-btcblog.com

リアルタイム性の高いWebSocket APIがある

名前のとおり、WebSocketを使用したBitMEXのAPIのことです。REST APIを使用してクライアント側から要求を出す方法と異なり、取引に必要な各種情報をリアルタイムに入手することができます。

WebSocketを使用した事が無い人はイメージがつきにくいかもしれませんが、サーバ側の情報に更新があればサーバ側からクライアント側へ、即座にプッシュしてくれるということです。

例えば、発行していた注文に対して約定が発生した場合や、板の情報に変化が発生した場合などに、その変更内容をクライアント側から情報を取りにいかなくてもサーバ側から送ってくれます。

時間にシビアな高頻度の取引を行う場合は、REST APIよりもWebSocket APIのほうが適しているでしょう。逆に大きな時間間隔でトレードする場合は、REST APIのみでも問題無いと思います。

BitMEXでは、REST APIをあまり高頻度に実行していたり、エラーを頻発していると、一時的なアクセス制限を食らったり、該当アカウントによるAPIアクセスを全て遮断される場合があります。

後者の場合は、待っていてもアクセスできるようにはならず、サポートへ問い合わせる必要があるので気をつけてください。

ちなみに、私は証拠金不足エラーを連発した際に、BitMEXのAPIへのアクセスをブロックされたことがあります。ブロックを解除してもらおうとして、サポートに問い合わせたところ、修正したコードをチェックするから提出しろと言われました。

BitMEX公式のサンプルbot(market-maker)

BitMEXのgithubに、Pythonで書かれたmarket-makerのbotサンプルソースがあります。

github.com

このソースコード公式ドキュメントを読めば、大体WebSocket APIの使い方はわかるとは思います。

使用言語とライブラリ

Python 3.6系を使用しました。websocketは、websocket-clientを介して使用しました。 github.com

情報の取得にはWebSocket APIを使用しますが、注文の発行や取り消し等にはREST APIを使用します。

REST APIによる注文の発行や取り消し等にはccxtを使っています。ccxtの使い方については、過去記事をご覧ください。 www.hacky.xyz

WebSocket APIの使用方法

では、BitMEXのWebsocket APIを使用した実装方法を見ていきましょう。

サーバへ接続する

まずは、WebSocketに対応したリアルタイムAPIのURLであるwss://www.bitmex.com/realtimeに接続します。

import websocket

# サーバとのデータのやりとりを表示するため、Trueを指定する。(確認したくないのであればFalseで問題ないです)
websocket.enableTrace(True)

# 接続先URLと各コールバック関数を引数に指定して、WebSocketAppのインスタンスを作成
ws = websocket.WebSocketApp(url='wss://www.bitmex.com/realtime',
                  on_open=on_open,
                  on_message=on_message,
                  on_close=on_close,
                  on_error=on_error)

# BitMEXのサーバへ接続する
ws.run_forever()
コールバック関数を指定する

WebSocketAppを呼び出す際に、urlの他に以下の四つの引数を指定しています。

これらの引数には、関数でサーバからデータを受信した際のコールバック関数を指定します。 わかりやすいように、引数と同名の関数を定義します。

def on_open(ws):
    # サーバとの接続時に実行する処理

def on_message(ws, message):
    # サーバからのメッセージを受信した場合に実行する処理

def on_close(ws):
    # サーバとの切断時に実行する処理

def on_error(ws, error):
    # エラー発生時に実行する処理

各関数が、引数に受け取っているwsというのは、WebSocketAppクラスのインスタンスです。また、on_messageの第二引数である、messageは、受信したメッセージが文字列(str)として渡されます。

接続成功時

サーバへの接続に成功すると、サーバからメッセージを受信し、on_messageが呼び出されます。 on_messageで受け取るメッセージの内容は以下のような、ウェルカムメッセージ的なもので、トレードに必要な情報は入っていないため、無視しても問題ないです。

{
  "info": "Welcome to the BitMEX Realtime API.",
  "version": "2018-08-17T17:20:19.000Z",
  "timestamp": "2018-08-18T03:53:16.727Z",
  "docs": "https://testnet.bitmex.com/app/wsAPI",
  "limit": {
    "remaining": 39
  }
}
  • info: Welcome to the BitMEX Realtime APIというウェルカムメッセージです。
  • version: 2018-08-04T20:30:14.000Zのように日時の情報が入っています。バージョンとのことなので、恐らくサーバ側のアプリケーションの最終更新日時かと思われます。
  • timestamp: 接続時の日時(UTC)です。
  • docs: APIのドキュメントのURLです。
  • limit
    • remaining: 一定時間の間の接続可能な数や購読可能な数には限りがあります。その残りの利用可能な回数を示しています。相当頻繁に接続を繰り返さない限りは、枯渇することはないので、心配はしなくて良いと思います。

トピックを購読する

WebSocket APIで、サーバから情報を受け取るには、受け取りたい情報が配信されるトピックを指定して、購読の要求を行う必要があります。

認証無しで購読可能なトピックには、以下のものがあります。

"announcement",// サイトのお知らせ
"chat",        // Trollbox チャット
"connected",   // 接続ユーザー/ボットの統計
"funding",     // 最新のスワップ資金調達率。 資金調達間隔ごとに送信 (通常8時間)
"instrument",  // 取引高とビッド/アスクなどの最新商品情報
"insurance",   // デイリーの保険基金に関する最新情報
"liquidation", // ブックに記入される清算注文
"orderBookL2", // フルレベル 2 のオーダーブック
"orderBook10", // 従来のフルブックプッシュを使用する上位 10 レベル
"publicNotifications", // システム全体への通知 (短期公開メッセージ用)
"quote",       // ブックの上位レベル
"quoteBin1m",  // 1 分足クォートビン
"quoteBin5m",  // 5 分足クォートビン
"quoteBin1h",  // 1 時間足クォートビン
"quoteBin1d",  // 1 日足クォートビン
"settlement",  // 決済
"trade",       // ライブ取引
"tradeBin1m",  // 1 分足取引ビン
"tradeBin5m",  // 5 分足取引ビン
"tradeBin1h",  // 1 時間足取引ビン
"tradeBin1d",  // 1 日足取引ビン
トピックの購読要求を行う

例えば、5分足のローソク足データを受信したい場合は、以下のように、tradeBin5mのトピックに対する購読の要求を行います。

# 1分足のデータを要求するために送信するデータ
channels = {
    'op': 'subscribe',
    'args': [
        'tradeBin5m'
    ]
}

# サーバにトピックの購読要求を送信する
ws.send(json.dumps(channels))

購読要求を送信するタイミングは、サーバへの接続が完了しているのであれば、いつでも良いですが、起動時に常に必要になるトピックについては、on_openの処理の中で購読要求を送って良いと思います。

購読要求に対する応答を受け取る

トピックの購読要求を送信すると、購読に成功すればサーバから以下のレスポンスが返されます。

{
  "success":true,
  "subscribe":"tradeBin1m",
  "request":{
    "op":"subscribe",
    "args":[
      "tradeBin5m"
    ]
  }
}

※実際の受信データでは改行は入っていません。読みやすさのために改行しています。

このレスポンスを受け取った際は、先ほど出てきたon_messageが呼び出されます。上記の応答内容は、on_messageの第二引数であるmessagejson形式の文字列として格納されています。

受信したメッセージを処理するon_messageの実装は、以下のようなイメージになるかと思います。

def on_message(self, ws: WebSocketApp, message: str):

    logger.debug('Received message -> ' + message)

    # JSON形式の文字列から辞書型に変換
    message = json.loads(message)  # type: dict

    # successのキーが存在する、かつsuccessのキーに紐づく値がTrueであれば、購読成功
    if 'success' in message and message['success'] and message['request']['op'] == 'subscribe':

        # トピックの購読に成功した時の処理
        ....

successのキーをinで確認しているのは、受信したメッセージが、クライアントからの要求に対する応答メッセージであることの確認のためです。

購読に成功したトピックのデータを受信する

購読に成功すると、それ以降は、該当のトピックに更新があれば、都度サーバからデータが送られてきます。 例えば、tradeBin5mのトピックを購読した場合、以下のようなデータが送られてきます。

{
  "table": "tradeBin5m",
  "action": "insert",
  "data": [
    {
      "timestamp": "2018-08-12T13:10:00.000Z",
      "symbol": "XBTUSD",
      "open": 6240,
      "high": 6250,
      "low": 6237,
      "close": 6245,
      "trades": 2134,
      "volume": 12536109,
      "vwap": 6242.5869,
      "lastSize": 17111,
      "turnover": 200821656480,
      "homeNotional": 2008.2165648000002,
      "foreignNotional": 12536109
    }
  ]
}

symbolに通貨の情報が入っています。上記の場合、XBTとUSDのペアとなります。open, high, low, close が、ローソク足四本値を表しています。

購読対象の通貨を限定する

tradeBin5mのようにトピック名だけを指定して、購読要求を送信すると、BitMEXが取り扱う全ての通貨ペアの情報が送られてきます。 情報を受け取る通貨を限定したい場合は、トピック名の後ろに:<シンボル名>の文字列を付与したうえで、購読要求を送信します。

例えば、XBTUSDのみの5分足データを受け取りたいのであれば、tradeBin5m:XBTUSDと指定します。通貨ペアの指定方法は、他のトピックの場合も同様です。

認証が必要なトピックを購読する

先に示したトピックの一覧は、全て公開トピックであり、認証無しで購読可能なトピックです。ユーザーに紐づくデータを受信するには、認証が必要です。認証が必要となるトピックは以下です。

"affiliate",   // アフィリエイトの状況 (合計紹介ユーザーや支払い率等)
"execution",   // 個別執行 (注文ごとに複数の可能性)
"order",       // リアルタイムでの注文の最新状況
"margin",      // 最新アカウント残高と証拠金必要額に関する最新情報
"position",    // ポジションに関する最新情報
"privateNotifications", // 個人的通知 - 現在未使用
"transact"     // 入金/出金に関する最新情報
"wallet"       // ビットコインアドレス残高データ (合計入金・出金額等)

認証から購読までの流れとしては、まず、サーバへの接続が完了後、クライアント側からサーバに対して、必要なデータを送りつけて認証要求を行います。

すぐにサーバから認証の結果が返って来るので、認証に成功すれば、購読したいトピックに対して、購読要求を行います。その後は、購読したトピックのデータを受信できるようになります。

認証を行う

認証を行うには、APIキーや署名など、認証に必要なデータをサーバへ送信する必要があります。 タイミングとしては、サーバへの接続完了後である必要があるので、コールバックであるon_openが実行された際に、認証要求を行えば良いでしょう。

コード例

import urllib
import hmac
import hashlib

def signature(self, api_secret: str, verb: str, url: str, nonce: int) -> str:
    data = ''
    parsed_url = urllib.parse.urlparse(url)
    path = parsed_url.path
    if parsed_url.query:
        path = path + '?' + parsed_url.query
    message = (verb + path + str(nonce) + data).encode('utf-8')
    sign = hmac.new(api_secret.encode('utf-8'), message, digestmod=hashlib.sha256).hexdigest()
    return sign

def on_open(ws):

    # セッションの有効期限を5秒に指定(例なので短くしている)
    expires = int(time.time()) + 5

    # 署名を作成
    signature = signature(api_secret=<APIシークレット>, verb='GET', url='/realtime', nonce=expires)

    # 認証用の送信データを作成
    auth = {
        'op': 'authKeyExpires',
         'args': [<APIキー>, expires, signature]
    }

    # サーバへ送信
    ws.send(json.dumps(auth))

関数signatureで署名を作成しています。その後、APIキー、セッションの有効期限、署名をauthに詰め込んで、サーバへ送信しています。

認証結果の受け取り

認証要求を送信した後は、on_messageで、以下のような認証結果を受信します。

{
  "success":true,
  "request":{
    "op":"authKeyExpires",
    "args":[
      "<APIキー>",
      "<セッション有効期限>",
      "<署名>"
    ]
  }
}

ポイントは、successの値がtrueであることです。trueであれば、認証に成功しています。argsの中身は、こちらが送信したデータの内容が詰まっているだけです。 認証に成功したら、認証が必要なトピックに対して、購読要求を送信すれば、サーバから購読成功のレスポンスと、トピックの更新情報が送られて来るようになります。

受信したデータのフォーマット

各トピックで更新が発生するたびに送られてくるメッセージには、いくつかのフォーマットがあります。

  1. 接続成功時のメッセージ
  2. クライアントからの処理要求に対する応答
  3. Pong
  4. トピックのデータ
1. 接続成功時のメッセージ

前述したサーバ接続直後に受信するウェルカムメッセージ的なものがこれにあたります。

{
  "info": "Welcome to the BitMEX Realtime API.",
  "version": "2018-08-17T17:20:19.000Z",
  ... 省略 ...
}

私が使っている限りでは、接続成功時以外で同様のフォーマットのメッセージを受信したことはまだありません。

2. クライアントからの処理要求に対する応答

クライアント側からトピックの購読や認証の要求を送信した際には、以下のフォーマットのレスポンスを受信します。

{
    "success": <処理結果>
    "request": {
        "op": "<要求した操作>"
        "args": [
            "引数1",
            "引数2",
            ...
        ]
    }
}

「要求した操作」の部分は、トピックの購読の場合であれば、subscribe、認証の場合であれば、authKeyExpiresといった具合です。

argsの配列ですが、クライアント側から送信したメッセージのパラメータが格納されています。

argsを見たいことはあまり無いような気がします。opでリクエストの種別を判定して、successで成功しているかどうかを見るというくらいでしょう。

3. pong

クライアントとサーバ間の接続が切断されていないことの確認のために、クライアント側からpingを投げる場合があります。それに対する応答メッセージです。

pong

上記のとおり、特に構造は持っておらず、pongという文字列のみが送られてきます。

4. トピックの購読データ

各トピックを購読することによって受信するデータです。

ローソク足や板の情報、保有ポジション、残高など、全てこれに該当します。

どのトピックのメッセージでも、必ずtableactiondataという3つのキーが含まれており、これらの内容を見て情報の種別と更新内容を把握します。


{
  "table": "execution",
  "action": "insert",
  "data": [
    {
      "execID": "xxxxxxxx",
      "orderID": "xxxxxxxx",
      ....
    }
  ]
}
table

tableは、トピック名が格納されています。orderのトピックのデータであれば、orderpositionのトピックのデータであれば、positionといった具合です。tableの値を見ることによって、受信したメッセージのトピックを判断します。

action

actionは、トピックで発生したイベントを表しています。actionの値には、以下の4種類があります。

  • partial・・・ある時点でのトピックのデータの塊がこのpartialというactionで送られてきます。例えば、最初のサーバへの接続時に有効な注文が複数存在する状態であれば、それらの注文に関する情報がまとめてこのactionで送られてきます。partialを受信した後は、差分のみが送られてきます。

    また、partialには、トピックのデータのキー情報の定義も格納されています。このキー情報は、データの更新時などに、対象を特定するために必要なものです。

    注文情報を例とすると、{"table": "order", "action": "partial", "keys": ["orderID"], "types": {...のように、keysフィールドに注文IDであるorderIDが指定されています。

    キー情報は一つとは限りません。複数のフィールドによって構成される複合キーの場合もあります。例えば、tablepositionのメッセージであれば、データのキー情報は"keys": ["account", "symbol", "currency"],となっています。

  • insert・・・トピックのデータに追加があったことを表しています。ローソク足の追加や、注文の追加などが該当します。

  • update ・・・トピックのデータに追加があったことを表しています。ポジションの価格の変更や、注文の約定などが該当します。

  • delete・・・トピックのデータに追加があったことを表しています。

data

データ部になります。追加、更新、削除された内容そのものや、削除された情報のIDなどが格納されています。

actionが、insertであれば、データの全てのフィールドが格納されていますが、updateの場合は、データを特定するためのキー情報(注文IDなど)と更新されたフィールドのみがdataに格納されています。

通信エラー発生時の再接続

websocketによるクライアントとサーバ間の接続が、何らかの通信トラブルによって、切断されてしまう場合があります。

いつのまにか通信が切断されてボットが停止したままとなっては、困ってしまいます。通信に問題が発生した場合は、それを検知して、再度サーバとの接続処理を行う必要があります。

サーバへの再接続

通信に問題が発生した場合はon_errorが呼び出されるので、この関数でエラー処理を行った後、再接続処理を行います。

厳密には、再接続というよりプログラム自体の再実行となります。

import os
import sys

os.execv(sys.executable, [sys.executable] + sys.argv)

osモジュールとsysモジュールを使用して、実行中のPythonプログラムに実行時と同等の引数を指定して、再度実行します。

こうすることによって、プログラムが最初から処理をやり直すため、再度サーバへの接続処理が行われます。

以上になります。また追記していこうと思います。