【脳死BOT】一年越しくらいに雑な感じでBitMEXの自動取引BOT作ってみた

作ろう作ろうと思って半年以上が経過してました。
最初に作ろうと思ってからは1年以上経過してますね。

社畜とか言い訳は事欠かないのですが無駄に腰が重かった……。
お金を転がして遊ぶということの心理的な抵抗は思いの外大きいのかもしれません。

BOT作成時のメモ

いろいろ忘れてるので自分の記事を見返しつつ必要なものとか足りなかったものをメモしていきます。

念の為注釈しておくと、この方法以外にもやりかたはいくらでもあります。

基本形

import ccxt
import datetime
import json
from pprint import pprint
import time

### 準備
# ログイン周りの処理
bm_keys_json = open('config/mexkeys.json', 'r')
bm_key = json.load(bm_keys_json)
bitmex = ccxt.bitmex(bm_key)

# パラメータ設定
symbol = 'BTC/USD'
type = 'StopLimit'
side = 'buy'
amount = 1.0
price = 6000.0
params = { 'stopPx': 5999.0, }

diff_triger = 5
loop_second = 60

# 処理開始
while True:
    # ポジションの取得
    # ポジションが存在する場合はそっとしておく

    # 注文の取得
    # 前回の注文が残っており、値段が乖離していた場合はキャンセルする

    # 価格の確認
    # 前回価格と現在価格を比較する
    # 上がっていたら売りモード、下がっていたら買いモードとなる
    # 前回価格が存在しない場合、現在価格を前回価格に設定し、購入処理を飛ばす

    # 購入処理
    # 設定に応じた注文を出す
    # 注文ごとにtry-catchを行い、通るまでループする

    # ループ終了
    time.sleep(loop_second)

ccxt関連

最新の価格を取得

bitmex.fetch_ticker(symbol)['last']

注文を取得

bitmex.fetch_orders()

全部取得できる。
条件によって絞るのは以下のリスト内包表記とやらを利用する。
例えばオープン状態、要するにまだ成立してなくてキャンセルもされてない注文を取得するのはこんな感じ。

orders2 = [d for d in bitmex.fetch_orders() if d['status'] == 'open']

最初見たときは意味がわからなかったんですけど。
やってることを理解した上で冷静に眺めるとなんかわかる気がしてきますね。
for文とif文をこうやって並べると、要素の一つ一つに対して条件を判定してくれるんだなー、みたいな。

ちなみにopen状態の注文を取得するだけならfetch_open_orders()というのがあるのでそっちでよさそうです。

ポジション所持通貨の取得

>>> bitmex.fetch_balance()['BTC']['used']
6.949999999999925e-06

ポジションって書いていたのですが所持している通貨の数量を取得するが正しいです。

ポジションは以下。

ポジションの取得

>>> bitmex.private_get_position()[0]['currentQty']
1

で、ポジションの取得がこっちですね。

配列で返ってくるのがよくわかりませんが、ともあれこれで現在のポジションがプラマイ付きで取得できるため、ストップの向きの判定などに使えます。

注文を作成

成行

symbol = 'BTC/USD'
type = 'market'
side = 'buy'
amount = 1.0
price = None
params = {}

order = bitmex.create_order(symbol, type, side, amount, price, params)

現在の価格で買う。

指値

symbol = 'BTC/USD'
type = 'limit'
side = 'buy'
amount = 1.0
price = bitmex.fetch_ticker(symbol)['last'] - 10
params = {}

order = bitmex.create_order(symbol, type, side, amount, price, params)

10ドル下に指値を出す。

トレールストップ

symbol = 'BTC/USD'
type = 'stop'
side = 'sell'
amount = 1.0
price = None
offset = -10
stopPx = bitmex.fetch_ticker(symbol)['last'] + offset
params = {
    'stopPx': stopPx,
    'pegPriceType': 'TrailingStopPeg',
    'pegOffsetValue': offset,
}

order = bitmex.create_order(symbol, type, side, amount, price, params)

条件を考える

材料は揃ったので、実際の条件を考えてみます。

細かい条件は考え始めるとキリがなさそうなので、まずはシンプルに考えます。

ぱっと考えるとポジションの有無・注文の有無で2x2の4パターン。

ポジションも注文も正負が存在する……と考えると、3x3で9パターンですね。
だるい。

表にするとこんな感じになるでしょうか。

注文 ポジションなし あり(+) あり(ー)
なし 注文を出す ストップを出す ストップを出す
買い 成立待ち エラー 成立待ち
売り 成立待ち 成立待ち エラー

まあ、面倒は面倒ですが正負に関しては逆になっただけなのでそこまで細かく考えることもなさそうです。

一応個別の状態を考えてみます。

注文がない場合

ポジションがなければ、ごくごく自然な初期状態です。
ストップ注文が成立したあとかもしれませんが、とにかく注文を新たに立てるだけです。

ポジションがあれば、それを解消する方向にストップを立てるだけですね。
即座にストップを立てるような仕組みにしている場合は、「そのストップが何らかの理由で通っていない」という異常ケースかもしれません。

注文がある場合

ポジションがない場合は、その注文が成立するのを待っている状態ですね。
基本的には問題ないはずなのですが、その注文がストップだったり、あまりにも現在価格と乖離していたり、注文を出してから時間が経ちすぎている場合は注文を出し直すことが必要かもしれません。

ポジションがある場合は、ポジションを減らす注文……というかストップならば正常です。
ポジションを増やす注文に関しては異常のはずなので、取り消して再度注文を出し直すことになります。

実際の分岐はこんな感じ

というわけで、条件分岐としてはこんな感じ?

# 処理開始
while True:
    try:
        # アクティブな注文の取得
        orders = bitmex.fetch_open_orders()
        # 現在の所持数を取得
        balance = bitmex.fetch_balance()['BTC']['total']
        balance_used = bitmex.fetch_balance()['BTC']['used']

        ## 注文がない場合
        if len(orders) == 0:
            # 注文数を算出
            order_amount = balance * size_percent / 100.0
            order_amount = order_amount if order_amount >= min_size else min_size

            # 買いか売りかを決定
            side = 'buy' if random() > 0.5 else 'sell'

            ## ポジションがない場合は注文とトレールストップを出す
            if balance_used == 0:
                make_position(side, order_amount)
                make_trailstop(side, order_amount)

            ## ポジションだけがある場合、ストップを出す
            else:
                side = 'buy' if balance_used > 0 else 'sell'
                make_trailstop(side, order_amount)

        ## 注文がある場合
        else:
            ## ポジションがない(成立しなかった)場合、キャンセルする
            if balance_used == 0:
                cancel_orders(orders)

            ## ポジションがある場合
            else:
                # ストップなら何もしない
                if orders[0]['type'] == 'stop':
                    # 適当にログとか出す
                    pprint('hoge')
                else:
                    # それ以外ならキャンセル
                    cancel_orders(orders)

    except Exception as x:
        # ログとか出す
        pprint('error')

    finally:
        time.sleep(loop_second)

実際に組んで動かしてたらあっさり完成まで持っていけました。
やはり動かすのが一番ですね。

ログに関してはqiitaに書いた記事が間に合わせで作るには有用だと信じています。

脳死BOT

とか呼んで作ってました。

定期的にトレーリングストップで注文を出すだけ。

買いか売りかは完全にランダム。

という雑にも程があるBOTです。

株における「猿がダーツして決めた銘柄を買うほうが成績が良い」という話を参考にしています。

どうなるかなー。

B! LINE