Python3 暗号通貨アービトラージBOTでスリッページ(約定時の価格滑り)を回避する方法

こんにちは!magito(@magimagi1223)です。

今回はアービトラージ(裁定取引/アビトラ)BOTを開発・運用する際に役立つ(かもしれない)小ネタを紹介したいと思います!

ご紹介するのは、「約定時のスリッページ(価格滑り)」を回避する方法です。

--------------------------------------------------------

目次

1.  はじめに

2. スリッページを回避する方法

3. サンプルコードの紹介

--------------------------------------------------------


<用語の定義>

アービトラージ(裁定取引):
同一価値の商品に一時的な価格差が生じた際、割安な方を買い、割高な方で売ることにより利益を得ること。
例:ビットコインについて、取引所Aの買値(ask)よりも取引所Bの売値(bid)の方が高い場合、取引所Aで買って取引所Bで売ることでその価格差だけ利益が得られる。

スリッページ:
成行注文を執行する際、想定よりも悪い価格で(買い注文ならより高い価格で、売り注文ならより安い価格で)約定してしまうこと。


1.  はじめに

アービトラージはその性質上、損失リスクのほとんどない、極めて手堅い手法といえますが、一回の取引で得られる利益が非常に少ないのが難点です。そのため、取引時の些細な損失が、大きなリスクに繋がる恐れがあります。

「スリッページ」はそのリスクの一つであり、約定価格が悪い方向に滑ることにより、一回の取引での損益が期待に反しマイナスとなってしまうことがあります。


2. スリッページを防ぐ方法

アービトラージを自動売買(BOT)で行う場合、価格取得をどのように行なっていますか?多くの場合は、ティッカーに含まれる、最良のASK/BID値を参照しているのではないかと思います。

しかし、これでは参照したASK/BIDで約定しない可能性大です。なぜなら、そのASK/BIDの指値に存在する"数量"を考慮していないからです。

もし、自分が1BTCを成行買いするとき、最良ASKが100万円だとしましょう。このとき、1BTCを100万円で買おうと思っても、この指値に0.1BTCしか存在しなければ、1BTCを100万円で約定させることは不可能です。仮に板がスカスカで、(かなり極端ですが)次の最良指値が110万だとしたら、そこで全て約定した場合の合計額は109万となり、9万円分も想定外のコストが発生してしまいます。


では、このようなスリッページを防ぐにはどうすればいいか。僕が行なっていたのは、「スリッページが起きそうな場合は注文しない(裁定機会とみなさない)」という方法です。非常にシンプル。

具体的には、以下のステップで取引を行います。

1.  APIで価格取得する際、「ティッカー」ではなく「板情報」をリクエストする。
2. 取得した板情報(価格+数量)の最良BID/ASKから順に数量の蓄積値を計算していき、自分の注文したい量を上回った時点での指値の価格を"実効的な"ASK/BIDの値とする。
3. 計算した"実効的な"ASK/BIDの値を裁定機会の判定に使用する。

こうすることで、最良BID/ASKの周辺がスカスカな場合でも、見かけ上の裁定機会をフィルタリングし、「ほぼ確実に想定価格で約定する」注文を執行することができます。


3. サンプルコード

サンプルコードを見た方が分かりやすいと思います。今回は国内5取引所の"実効的な"ASK/BIDを計算する場合のコードを紹介します。コードも非常にシンプルです。

import ccxt
exchange_list = ['bitflyer', 'coincheck', 'quoinex', 'zaif', 'bitbank']

def get_eff_tick(n, size): # n:価格取得する取引所のINDEX, size:成行注文する数量

    # 板情報を取得
    while True:
        try:
            exchange = eval('ccxt.' + exchange_list[n] + '()')
            book = exchange.fetch_order_book('BTC/JPY')
            print(book)
            break
        except Exception as e:
            print(e)
            sleep(1)

    # 実効ASK計算
    i = 0
    s = 0
    while s <= size:
        s += book['asks'][i][1]
        i += 1

    # 実効BID計算
    j = 0
    t = 0
    while t <= size:
        t += book['bids'][j][1]
        j += 1

    return {'ask': book['asks'][j-1][0], 'bid': book['bids'][i-1][0]}

例えば、bitFlyer(現物)で1 BTCを買いたい場合は、n = 0, size = 1 とすればOKです。

print(get_eff_tick(0, 1))

# 出力
=> {'ask': 923999.0, 'bid': 923514.0}

このとき、取得した板情報は↓のようになっており、get_eff_tick()が、1 BTCがしっかり全量約定するbid値を返してくれていることが分かります。

{'bids': [[923801.0, 0.10000514], [923610.0, 0.004], [923534.0, 0.3706], 
[923532.0, 0.47730441], [923514.0, 0.1724], [923501.0, 0.2], ......
                                  #↑ここで蓄積値が合計1BTCを超える


もちろん、この手法を使ったとしても、自分の約定前に板状況が変化し約定価格も変わってしまう可能性はありますが、見かけ上の裁定機会は全てフィルタリングすることができるため効果は大きいと思います。


<改良案>

今回のコードでは、約定が完了する時点の指値を"実効的な"ASK/BIDとしていますが、実際はそこに到達するまでに注文量の一部は約定するので、平均約定価格はそれよりも良い価格(買い注文ならより安い価格、売り注文ならより高い価格)になります。したがって、平均約定価格を"実効的な"ASK/BIDとする方が厳密な値となり、裁定機会をより正確に判定することができるようになります。(今回の方法でも、計算上は損することはないので、それほど問題にはならないかもですが。)


以上になります!

最後まで読んでいただき、ありがとうございました!