4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-10-18

経緯

今はやってないですが、数年前は仮想通貨の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月号

4
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?