コアダンプの数だけ強くなれるよ

見習いエンジニアの備忘log

ZAIFのAPIで仮想通貨の積立スクリプトを作る(python)

pythonのお勉強の一環として、ちょこちょこ仮想通貨の自動取引の実装にトライしております。

今回はzaifでBTC/BCH/ETH/MONA/XEM(NEM)を500円分買い注文するプログラムを組んでみます。
作ったスクリプトとcronやsystemdのタイマーと組み合わせることで定期積立を実現できます。



はじめに


zaifを選択する理由は、国内の取引所の中ではAPIで操作できる通貨種類が多いからです。

現在日本円で手に入る主要な仮想通貨としては、

  • Bitcoin(BTC)
  • Bitcoin Cash(BCH)
  • Ethereum(ETH)
  • Ethereum classic(ETC)
  • XEM(NEM)
  • Monacoin(MONA)
  • Ripple(XRP)
  • Lisk(LSK)

このあたりでしょうか。

Zaifは大体カバーできてますね。

かつ販売ではなく取引ができるのでAPIのpairに指定できるというわけです。
(BitFlyerとCoinCheckはBitcoinしかpairに設定できないはず)

また、zaifには積立のサービスがありますが少額だと手数料の割合がそこそこ高いので自分で実装するとお得です。ただしセキュリティ面については自己責任で。


zaif.jp



実装方針


売りの実装は難しい(利益や損切りを考えると面倒)ので、まずは買いの実装だけやってみます。
バグって誤発注するのが怖いので予算は少額に設定しておきます。

  • 定期的に一定額(500円で買える分だけ)を購入する(積立)
  • 指値はpythonスクリプトを実行した時点の最終取引額
  • 最小注文単位の金額が500円を超える場合は注文しない
  • 約定判定はなし

実装にあたっては下記のAPIを使用します。

currency_pairs — Zaif api document v1.1.1 ドキュメント
last_price — Zaif api document v1.1.1 ドキュメント
trade — Zaif api document v1.1.1 ドキュメント


APIを利用するには事前にアカウント設定から取引用のキーを取得しておく必要があります。セキュリティ確保のため送信元IPアドレスの制限もしておくと良いでしょう。


ソースコード



zaif_fund.py

# -*- coding: utf-8 -*-
import sys
import json
import requests
import hmac
import hashlib
import time
from datetime import datetime
import urllib.parse
import math

class zaifApi:
    def __init__(self, key, key_secret, endpoint):
        self.key = key
        self.key_secret = key_secret
        self.endpoint = endpoint

    def get(self, path):
        nonce = int(datetime.now().strftime('%s'))
        text = nonce + self.endpoint + path

        signature = hmac.new(
                bytes(self.key_secret.encode('ascii')),
                bytes(text.encode('ascii')),
                hashlib.sha256).hexdigest()

        return requests.get(
                self.endpoint + path ,
                headers = self.__get_header(self.key, nonce, signature))

    def post(self, method, params):
        nonce = int(datetime.now().strftime('%s'))
        payload = {
           "method": method,
           "nonce": nonce,
        }
        payload.update(params)
        encoded_payload = urllib.parse.urlencode(payload)

        return requests.post(
                self.endpoint,
                data = payload,
                headers = self.__get_header(encoded_payload))

    def delete(self,path):
        nonce = str(int(time.time()))
        text = nonce + self.endpoint + path

        signature = hmac.new(
                bytes(self.key_secret.encode('ascii')),
                bytes(text.encode('ascii')),
                hashlib.sha256).hexdigest()

        return requests.delete(
                self.endpoint+path,
                headers = self.__get_header(self.key, nonce))

    def __get_header(self, params):
        signature = hmac.new(
                bytearray(self.key_secret.encode('utf-8')),
                digestmod=hashlib.sha512)
        signature.update(params.encode('utf-8'))

        return {
            'key': self.key,
            'sign': signature.hexdigest()
        }


def getLastPrice(pair):

    urlbase = 'https://api.zaif.jp/api/1/last_price/'

    response = requests.get(urlbase+pair)
    if response.status_code != 200:
        return None

    last_price = response.json()

    return last_price['last_price']

def getItemUnitMin(pair):

    urlbase = 'https://api.zaif.jp/api/1/currency_pairs/'

    response = requests.get(urlbase+pair)
    if response.status_code != 200:
        return None

    currency_pair = response.json()

    return currency_pair[0]['item_unit_min']

if __name__ == '__main__':

    argv = sys.argv
    argc = len(argv)

    if (argc != 3):
        print('usage: python %s <key> <secret key>' % argv[0])
        quit()

    key = argv[1]
    key_secret = argv[2]

    endpoint = 'https://api.zaif.jp/tapi'

    # 予算
    budget_jpy = 500

    # 取引対象の通貨
    pairs = [
        'btc_jpy',
        'bch_jpy',
        'eth_jpy',
        'xem_jpy',
        'mona_jpy',
    ]

    # Instance
    zaif = zaifApi(key,key_secret,endpoint)

    for p in pairs:
        # 認証に使うnonceのための遅延処理
        time.sleep(1.1)

        # 取引通貨の通貨情報を取得
        item_unit_min = getItemUnitMin(p)
        if (item_unit_min == None):
            # 失敗した場合は中断
            continue

        # 最終取引価格を取得
        last_price = getLastPrice(p)
        if (last_price == None):
            # 失敗した場合は中断
            continue

        # 注文価格が予算を超えた場合は中断
        if ((item_unit_min * last_price) > budget_jpy):
            continue

        # 注文量の計算
        # buget_jpy / last_price = 123.456789, item_unit_min = 0.01の場合, amount = 123.45になるような計算を行う
        # floadの場合, 最小桁付近で計算誤差がでるためroundで桁を調整する
        amount = round((math.floor((budget_jpy / last_price) / item_unit_min) * item_unit_min), int(math.log10(1/item_unit_min)))

        # 小数点を使わない場合は整数に変換する
        if (amount.is_integer()):
            amount = int(amount)

        if (last_price.is_integer()):
            last_price = int(last_price)

        # リクエストのパラメータ設定
        method = 'trade'
        params = {
            'currency_pair': p,
            'action' : 'bid',
            'price' : last_price,
            'amount': amount,
        }

        print(params)
        print('total  = {0:.2f} yen' .format(amount*last_price))

        # 注文の送信
        response = zaif.post(method, params)
        if response.status_code != 200:
            continue

        print(response.json())
        print('')


実行結果


$ /usr/local/bin/python3 zaif_fund.py 
usage: python zaif_fund.py <key> <secret key>


$ /usr/local/bin/python3 zaif_fund.py  'あなたのAPIキー' 'あなたのプライベートキー'
{'amount': 0.0005, 'currency_pair': 'btc_jpy', 'action': 'bid', 'price': 936820}
total  = 468.41 yen
{'return': {'order_id': 0, 'remains': 0.0, 'received': 0.0005, 'funds': {'保有資産の一覧'}}, 'success': 1}

{'amount': 0.0031, 'currency_pair': 'bch_jpy', 'action': 'bid', 'price': 159200}
total  = 493.52 yen
{'return': {'order_id': 0, 'remains': 0.0, 'received': 0.0030907, 'funds': {'保有資産の一覧'6}}, 'success': 1}

{'amount': 0.0065, 'currency_pair': 'eth_jpy', 'action': 'bid', 'price': 76800}
total  = 499.20 yen
{'return': {'order_id': xxxxxxxxx, 'remains': 0.0065, 'received': 0.0, 'funds': {'保有資産の一覧'}}, 'success': 1}

{'amount': 13.6, 'currency_pair': 'xem_jpy', 'action': 'bid', 'price': 36.55}
total  = 497.08 yen
{'return': {'order_id': xxxxxxxxx, 'remains': 13.6, 'received': 0.0, 'funds': {'保有資産の一覧'}}, 'success': 1}

{'amount': 1, 'currency_pair': 'mona_jpy', 'action': 'bid', 'price': 442.5}
total  = 442.50 yen
{'return': {'order_id': xxxxxxxxx, 'remains': 1.0, 'received': 0.0, 'funds': {'保有資産の一覧'}}, 'success': 1}



応答のremainsが0.0以外のものは、板に注文が残っていて、0.0の場合は既に取引が完了しています。

約定の判定処理はありませんが、上げトレンドが続くようなことでもない限り大体翌日ぐらいまでには約定する気がします。

今のところ2ヶ月ほど週1回の頻度で実行してますが、注文が未約定のまま残ったことは無いです。


仮想通貨を取引所に置きっぱなしが怖い人は送金するAPIの組み合わせることで別walletへの退避も自動で出来ますね。