bitflyerのAPIを利用してPythonで自動売買プログラムつくってみた


## 経緯

今はやってないですが、数年前は仮想通貨のFX取引（レバレッジきかせて先物、空売りとかができるやつ）にハマっていた時期がありました。一時的に利益を上げたりしたものの、結局マイナスになりロスカットして退場。

そんな当時、以下のようなことを思っていました。

・感情に流されず売買（トレード）したい。
・短期売買をもっと頻繁かつ簡単にしたい。

じゃあプログラムで自動化してしまおう。そんな経緯で今回のプログラムを作りました。

## どうやってつくるか

各取引所で公開しているAPIを利用して、プログラムから注文を自動的に実行させようと考えました。今回は大手取引所の一つである、自分としてもよく利用するbitflyerでのAPIで売買プログラムを作ることにしました。

■自動売買プログラムに必要なプログラムは2つ。
①売り／買い注文、キャンセル等のAPIをメソッド化したモジュール
②①で作った自作モジュールから適宜メソッド（API）を呼び出して自動取引を行うプログラム

## コード
大前提、bitflyerの口座開設とPython3の実行環境は作っておきましょう。
まずは、①売り／買い注文、キャンセル等のAPIをメソッド化したモジュールを作成します。
```
import hashlib
import hmac
import requests
import datetime
import json
import urllib


class BFApi():
    CURRENCY_PAIR = ""
    API_KEY = ""
    API_SECRET = ""
    API_URL = "https://api.bitflyer.jp"
    nonce = int((datetime.datetime.today() - datetime.datetime(2017, 1, 1)).total_seconds()) * 10

    # コンストラクタ
    def __init__(self, key, secret, product_code="FX_BTC_JPY"):
        self.API_KEY = key
        self.API_SECRET = secret
        self.CURRENCY_PAIR = product_code

    # bitFlyerのプライベートAPIリクエストを送信する関数
    def _private_api(self, i_path, i_params=None):
        timestamp = str(datetime.datetime.today())
        headers = {"ACCESS-KEY": self.API_KEY, "ACCESS-TIMESTAMP": timestamp, "Content-Type": "application/json"}
        s = hmac.new(bytearray(self.API_SECRET.encode("utf-8")), digestmod=hashlib.sha256)
        b = None
        if i_params is None:
            w = timestamp + "GET" + i_path
            s.update(w.encode("utf-8"))
            headers["ACCESS-SIGN"] = s.hexdigest()
            b = requests.get(self.API_URL + i_path, headers=headers)
        else:
            body = json.dumps(i_params)
            w = timestamp + "POST" + i_path + body
            s.update(w.encode("utf-8"))
            headers["ACCESS-SIGN"] = s.hexdigest()
            b = requests.post(self.API_URL + i_path, data=body, headers=headers)

        if b.status_code != 200:
            raise Exception("HTTP ERROR status={0},{1}".format(b.status_code, b.json()))
        return b

    # 売買を行うAPIの共通部分
    def _trade_api(self, price, amount, side):
        j = self._private_api("/v1/me/sendchildorder",
                              {
                                  "product_code": self.CURRENCY_PAIR,
                                  "child_order_type": "LIMIT",
                                  "side": side,
                                  "price": price,
                                  "size": amount,
                                  "time_in_force": "GTC"
                              }).json()
        return j

    # 最終取引価格を得る関数
    def board(self):
        j = requests.get("https://api.bitflyer.jp/v1/board?product_code=FX_BTC_JPY").json()
        x = j["mid_price"]
        return x

    # 残高を得る関数
    def balance(self):
        j = self._private_api("/v1/me/getbalance").json()
        jpy = [i for i in j if i["currency_code"] == "JPY"][0]
        btc = [i for i in j if i["currency_code"] == "BTC"][0]
        eth = [i for i in j if i["currency_code"] == "ETH"][0]
        return {"btc": btc["available"], "jpy": jpy["available"], "eth": eth["available"]}

    # 証拠金残高を得る関数
    def collateral(self):
        j = self._private_api("/v1/me/getcollateral").json()
        return {"collateral": j["collateral"]}

    # 売り注文を実行する関数
    def sell(self, price, amount):
        j = self._trade_api(price, amount, "SELL")
        return j["child_order_acceptance_id"]

    # 買い注文を実行する関数
    def buy(self, price, amount):
        j = self._trade_api(price, amount, "BUY")
        return j["child_order_acceptance_id"]

    # 注文をキャンセルする関数
    def cancel(self, oid):
        j = self._private_api("/v1/me/cancelchildorder",
                              {
                                  "product_code": self.CURRENCY_PAIR,
                                  "child_order_acceptance_id": oid
                              })
        return

    # 注文状態を調べる関数
    def is_active_order(self, oid):
        j = self._private_api("/v1/me/getchildorders?" + urllib.parse.urlencode({
            "product_code": self.CURRENCY_PAIR})).json()
        t = [i for i in j if i["child_order_acceptance_id"] == oid]
        if t and t[0]["child_order_state"] == "ACTIVE":
            return True
        return False
```


お次は、②①で作った自作モジュールから適宜メソッド（API）を呼び出して自動取引を行うプログラムを作成します。

```
import time
from bf_module import BFApi

# API_KEY API_SECRET のキーを入力する。取扱注意！ 
API_KEY = ""
API_SECRET = ""

# bitflyer取引所のパラメータ
order_min_size = 0.01  # BTCの数量最小値
order_digit = 3  # BTC数量の桁数
fee_rate = 0.0  # 取引手数料のレート(%)

# 1回の取引量
buy_unit = 0.01  # 購入価格
profit = 200  # 利益

api = BFApi(API_KEY, API_SECRET)

while True:
    print(" ### bitflyer ### ")
    print("BTC/JPYの自動売買取引を開始します")
    time.sleep(5)

    # 最終取引価格の取得
    print("最終取引価格を取得しています")
    buy_price = api.board()
    print("購入予定価格は {0} 円です".format(int(buy_price)))

    # BTCの購入数量の決定
    buy_amount = round(buy_unit, order_digit)
    print("BTCの購入数量は {0} です".format(buy_amount))

    if buy_amount > 0:
        # BTC残高が不十分なら終了
        buy_amount = max(order_min_size, buy_amount)

        # 残高を計算
        balance = api.balance()

        # JPY残高の確認
        if balance["jpy"] < buy_amount * buy_price:
            print("JPY残高が不足しています。")
            continue
        print("現在のBTC残高は {0} です".format(balance["btc"]))

        # 買い注文の開始
        print("買い注文を開始します")
        print("購入価格 x 購入数量={0} x {1}".format(int(buy_price), buy_amount))
        oid = api.buy(int(buy_price), buy_amount)
        print("注文ID={0}".format(oid))

        # 買い注文がサーバで処理されるまで少し待つ
        print("サーバ処理中(3秒間処理待ち)")
        time.sleep(3)

        # さらに最大30秒間買い注文が約定するのを待つ
        for i in range(0, 10):
            if api.is_active_order(oid) == False:
                oid = None
                break
            print("まだ約定に時間がかかりそうです({0}回目/10回中)".format(i))
            print("少々お待ちを")
            time.sleep(3)

        # 注文が残っていたらキャンセルする
        if oid != None:
            api.cancel(oid)
            print("約定できなかったため注文をキャンセルしています")
            print("注文ID={0}".format(oid))
            time.sleep(5)

            # 購入予定価格の決定(bidの先頭)
            sell_price = buy_price + profit  # 購入価格に利益を加算して売り価格を算出

            # 再売り注文
            print("再売り注文をしています")
            oid02 = api.sell(int(sell_price), buy_amount)
            print("売却価格 x 売却数量={0} x {1}".format(int(sell_price), buy_amount))
            print("注文ID={0}".format(oid02))
            time.sleep(5)

            # 注文が成立するまで永遠に待つ
            while api.is_active_order(oid02):
                print("少々お待ちを")
                time.sleep(3)

            print("売り注文が約定されました。")
            print("注文ID={0}".format(oid02))
        else:
            print("売り注文が約定されました。引き続き買い注文へ移ります")

```

## 補足

プログラムの流れは下記の流れの繰り返しです。買い→売り注文が前提になります。

残高確認 → 買い注文 → 約定待ち → 約定完了 → 売り注文 → 約定待ち→ 約定完了 → 残高確認 ・・・


## 今後
他の言語（rubyとか）でも同様にプログラムを作っていきたい。あと、今回は「買う → 売る」の単純なプログラムだったので、「売る → 買う」バージョンも実装させたいす・・・いつか。



## 免責事項
利用・改善大歓迎ですが、損しても知りません。

## 参考情報
日経ソフトウエア 2017年9月号,2017年11月号