本記事はU-TOKYO AP Advent Calendar 2017の17日目です。

はじめに

年の瀬が近づき何かと出費がかさむ季節になりましたね。財布の中も真冬です。
実は2ヶ月ほど前から年越しに備えて仮想通貨で資産運用をしています。
他の資産運用と比べたときの仮想通貨取引のメリットは「少額でも大きな利益を得るチャンスがあること」と「24時間365日取引ができること」でしょうか。
ということで、その時に自動取引についていろいろと試行錯誤をしたので、勉強したことをまとめて記事にしたいと思います。
具体的には、PythonでbitFlyerのAPIを叩いてチャートを描画し、決められたアルゴリズムに従って自動でビットコインの売買をする、という一連の流れを紹介します。
ごく簡単な紹介にとどめるので、その先は各々で試して自分なりのやり方を考えてほしいです。
*僕は仮想通貨に関しても投資に関しても素人です。投資は自己責任で行ってください。しかし儲かった場合はその限りでありません。焼肉が食べたいです。
*リファクタリングなどが不十分で申し訳ありません。改善点や疑問などがございましたらコメントください。

準備

取引所はbitFlyerを使います。まだ登録していない方はこちらから。
他の取引所にも同様のAPIはあるはずなので、この記事と同じ方針でプログラムを書けば同じことができると思います。
言語はPython3を使います。出力結果を見て試行錯誤しながら進めるのがいいので、実行環境はJupyter Notebookを使います。

ビットコインについて

ビットコインという言葉が先行して盛り上がっていますが、そもそもビットコインを始めとする暗号通貨技術を理解していない人がとても多いように感じます。
もちろん技術を理解しなくても株と同じように取引はできますし儲けることもできますが、ブロックチェーンなどの周辺技術は「インターネット以来の大発明」とも言わはれていて新しいアイディアが目白押しなので、これを機に学んでおいて損はないでしょう。後で述べる「アルトコインの長期保有」をする際にはこの知識が役に立ちます。

ビットコインは、2009年にサトシ・ナカモトを名乗る謎の人物が暗号学のメーリングリストに公開した論文を元に作られました。
中央機関を置かず対等なユーザ同士で取引の正当性を担保するという全く新しい発想でネットワーク攻撃に強い堅牢なシステムを実現しているのですが、長くなるのでここでは立ち入らないことにします。
簡単な仕組みは公式サイトで解説されています。
元の論文はこちらで公開されています。たった9ページなので一度目を通してみるとおもしろいです。
やや雑な翻訳ですが、仮想通貨取引所のcoincheckが日本語版を公開しています。
個人的に読んでわかりやすかった本「Mastering Bitcoin」の日本語版がこちらで公開されています。
Amazonで購入する場合はこちらから。

チャートの描画

まずはbitFlyer APIを簡単に操作するためのライブラリpybitflyerをインストールします。

$ pip install pybitflyer

ライブラリをインポートします。


# グラフ描画に使う
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# bitFlyerにアクセスするのに使う
import pybitflyer
# 時間の管理に使う
import time
from datetime import datetime
# リアルタイムプロットに必要
from ipywidgets import FloatProgress
from IPython.display import display, clear_output
# Jupyter環境でグラフを表示するのに必要
%matplotlib inline

どんな情報が取り出せるのか見てみましょう。ちなみに注文を出さない場合はアカウント不要です。


api = pybitflyer.API()
ticker = api.ticker(product_code="BTC_JPY")
print(ticker)

こんな結果が返ってきます。


{'best_ask': 2030440.0,
 'best_ask_size': 1.39041687,
 'best_bid': 2030000.0,
 'best_bid_size': 0.1,
 'ltp': 2030732.0,
 'product_code': 'BTC_JPY',
 'tick_id': 6244412,
 'timestamp': '2017-12-15T13:44:16.227',
 'total_ask_depth': 1670.16083192,
 'total_bid_depth': 3345.18138717,
 'volume': 130289.3712451,
 'volume_by_product': 16077.77689361}

askは売り注文、bidは買い注文のことです。ltpはLast Traded Price、つまり最終取引価格のことです。
デイトレードというとローソク足チャートを利用するのが一般的ですが、ここでは最終取引価格だけを見ることにします。
ローソク足チャートを描きたい場合は、最終取引価格を適当な数でまとめた上で始値・終値・安値・高値を選び、適当なライブラリを使って描画することになると思います。こちらが参考になります。

それでは、最終取引価格を1分ごとにリアルタイムでプロットしてみましょう。

# 最終取引価格を格納する配列
raws = []
# プロットの準備
fig = plt.figure(figsize=(16,10))
axe = fig.add_subplot(111)

while True:
    # 毎分00秒に稼働
    if datetime.now().strftime('%S') [0:2]== '00':
        clear_output(wait = True)
        # プロット用データの更新
        tick = api.ticker(product_code = "BTC_JPY")
        raws = np.append(raws, tick['ltp'])
        # プロット
        axe.plot(raws, "black", linewidth=2, label="Raw price")
        axe.set_title("Raw price")
        display(fig)
        # 次の00秒まで休憩
        time.sleep(57)
        axe.cla()

1.png
横軸は時間[分]、縦軸は価格[円]です。約7時間分の結果です。
このままではランダムなノイズが強く、大きな動きを読んで取引をするのが難しいです。
このようなノイズを緩和するために経済データの処理では単純移動平均(SMA; Simple Moving Average)という指標がよく用いられます。SMAは連続した数列において「前後数個」の単純平均をとったものを指すことが多いですが、経済データでは「現在より後」のデータが取得できないので「前数個」の単純平均を取ります。
すなわち、連続したk個の時系列データdtk+1,...,dtに対して、

SMAk(t)=1ki=tk+1tdi

と表されます。
移動平均線は、注目する期間(平均を取るデータの個数k)によって2つないし3つ描くことが多いです。これらの交点をゴールデンクロス/デッドクロスなどと分類して順張りの指標に使ったりします。短期線が長期線を上に貫く場合は買いサインの「ゴールデンクロス」で、下に貫く場合は売りサインの「デッドクロス」と呼びます。
ちなみに、より現在に近いデータに重みをつけた指数移動平均(EMA; Exponential Moving Average)という指標もあります。

せっかく平均を定義したので、標準偏差も見ることにしましょう。
移動平均線に標準偏差で幅を持たせたものをボリンジャーバンド(Bollinger band)と呼びます。
注目している区間のデータに正規分布を仮定すると、各データは95%の確率で2シグマ区間のボリンジャーバンドに収まるので、「価格がバンドより上にはみ出していたら次は下がるだろう」といった逆張りの指標として利用できます。
式で表すと次のようになります。

BLGk+(t)=SMAk(t)+2(1ki=tk+1t(diSMAk(t))2BLGk(t)=SMAk(t)2(1ki=tk+1t(diSMAk(t))2

ややこしい式になってしまいましたが、要は平均に標準偏差2つ分を足し引きしているだけです。
実際のグラフを見てみましょう。
2.png
実線が移動平均線、破線がボリンジャーバンドを表しています(線を太くするべきでした…)。赤が15分の短期、緑が60分の長期です。
赤の短期移動平均線は黒の元データに比べてやや遅れて、そしてより滑らかに推移しています。緑の長期移動平均線はさらに遅れてかつ滑らかに推移しています。
ボリンジャーバンドは相場が動く時に広がり、安定しているときに縮んでいますね。

チャートを描画するコードはこちらです。

# 最終取引価格、移動平均、標準偏差を格納する配列
raws = []
sma1, sma2 = [], []
sgm1, sgm2 = [], []

# 移動平均を取る幅
itr1 = 15 # 15 mins
itr2 = 60  # 60 mins

# 60分間の最終取引価格の配列
current_price = api.ticker(product_code = "BTC_JPY")['ltp']
ltps2 = current_price*np.ones(itr2) 

plt.ion()
fig = plt.figure(figsize=(16,5))
axe = fig.add_subplot(111)

while True:
    # 60秒ごとに稼働
    if datetime.now().strftime('%S') [0:2]== '00':
        clear_output(wait = True)
        tick = api.ticker(product_code = "BTC_JPY")
        # 最終取引価格の更新
        ltps2 = np.hstack((ltps2[1:itr2], tick['ltp']))
        ltps1 = ltps2[itr2-itr1:itr2]
        # プロット用データの更新
        raws = np.append(raws, [ltps1[itr1-1]])
        sma1 = np.append(sma1, [ltps1.mean()])
        sma2.append(ltps2.mean())
        sgm1 = np.append(sgm1, [ltps1.std()])
        sgm2 = np.append(sgm2, [ltps2.std()])
        # プロット
        axe.plot(raws, "black", linewidth=2, label="Raw price")
        axe.plot(sma1, "r", linewidth=1, label="15min SMA")
        axe.plot(sma2, "g", linewidth=1, label="60min SMA")
        axe.plot(sma1+2*sgm1, "r", linewidth=1, linestyle="dashed", label="15min 2sigma")
        axe.plot(sma1-2*sgm1, "r", linewidth=1, linestyle="dashed")
        axe.plot(sma2+2*sgm2, "g", linewidth=1, linestyle="dashed", label="60min 2sigma")
        axe.plot(sma2-2*sgm2, "g", linewidth=1, linestyle="dashed")
        axe.legend(loc='upper left')
        axe.set_title("SMA and Bollinger band")
        display(fig)
        # 次の00秒まで休憩
        time.sleep(57)
        axe.cla()

チャートをさらに作り込みたい方は、Cryptowatchというサイトが参考になります。いろいろな取引所のいろいろな仮想通貨に対応しているのでトレーディングにも使えます。

実際に取引をする

さて、いよいよ自動取引の説明に移ります。
まずはbitFlyer Lightningにアクセスして、左のタブの「API」からAPI KeyとAPI Secretを取得してください。

key = "ここにAPI Keyを入れる"
secret = "ここにAPI Secretを入れる"
api_endpoint = 'https://api.bitflyer.jp'
api = pybitflyer.API(api_key=key, api_secret=secret)

このKeyとSecretは外に漏れないように厳重に管理してください。
うっかりGitHubなどに上げて悪意のある人に見られたら死にます。
KeyとSecretはgitignoreされた別ファイルに書くなどするのが推奨のやり方です。
また、自動取引をやめるときはbitFlyer LightningのサイトからAPIを無効化しておきましょう。

次に、売買の注文です。これは何度も使うので関数にまとめておきます。

# 成行買い注文
def buy_btc_mkt(amt):
    amt = int(amt*100000000)/100000000
    buy = api.sendchildorder(product_code="BTC_JPY", child_order_type="MARKET", side="BUY", size=amt, minute_to_expire=10, time_in_force="GTC")
    print("BUY ", amt, "BTC")
    print(buy)

# 成行売り注文
def sell_btc_mkt(amt):
    amt = int(amt*100000000)/100000000
    sell = api.sendchildorder(product_code="BTC_JPY", child_order_type="MARKET", side="SELL", size=amt, minute_to_expire=10, time_in_force="GTC")
    print("SELL ", amt, "BTC")
    print(sell)

# 指値買い注文
def buy_btc_lmt(amt, prc):
    amt = int(amt*100000000)/100000000
    buy = api.sendchildorder(product_code="BTC_JPY", child_order_type="LIMIT", price=prc, side="BUY", size=amt, minute_to_expire=10, time_in_force="GTC")
    print("BUY ", amt, "BTC")
    print(buy)

# 指値売り注文
def sell_btc_lmt(amt, prc):
    amt = int(amt*100000000)/100000000
    sell = api.sendchildorder(product_code="BTC_JPY", child_order_type="LIMIT", price=prc, side="SELL", size=amt, minute_to_expire=10, time_in_force="GTC")
    print("SELL ", amt, "BTC")
    print(sell)

引数のうちamtが取引量、prcが取引価格です。
注文を受け付けられた時点の価格で売り買いするのを成行注文(market order)、自分の指定した価格になったら売り買いするのを指値注文(limit order)と言います。
10分経っても取引が行われない場合は注文をキャンセルするようにしています。
また、注文を出したら「どれだけの量を売り買いするか」と「注文が受け付けられたか」をprintで出力しています。
あと、各関数のはじめに100,000,000を掛けたり割ったりしているのは、注文の最小単位が1 satoshi (=1/100,000,000 BTC)だからです。僕ははじめ、これより小さい単位での注文を送っていたため全く取引が成立しないという問題で数日間悩んでいました。

次は口座にある資産の情報を取り出したいと思います。

balance = api.getbalance()
print(balance)

各通貨の保有額、使用可能額、通貨コードが配列として返ってきます。

[{'amount': **, 'available': **, 'currency_code': 'JPY'},
 {'amount': **, 'available': **, 'currency_code': 'BTC'},
 {'amount': **, 'available': **, 'currency_code': 'BCH'},
 {'amount': **, 'available': **, 'currency_code': 'ETH'},
 {'amount': **, 'available': **, 'currency_code': 'ETC'},
 {'amount': **, 'available': **, 'currency_code': 'LTC'},
 {'amount': **, 'available': **, 'currency_code': 'MONA'}]

今回はJPYとBTCの取引なので、このうち0番目と1番目の要素を使います。
例えばJPYの使用可能金額はこうやって取り出します。

jpy = balance[0]['available']

以上で必要な情報は揃いました。

さて、肝心のアルゴリズムですが、簡単に「上がり始めたら買う、下がり始めたら売る」という後追いの方針でいきたいと思います。
BTCが上がっているときはBTCで、下がっているときはJPYで、どちらか判断しにくいときは半々で持つというイメージです。
売り買いの量はそのときの資産と移動平均の傾き(変化率)から計算することにしましょう。
横軸の変化率が0に近いときは売り買いをしない(手数料がかかるので)で、ある閾値th1を超えたら変化率に応じて売り買いをします。変化率の絶対値が大きいときは取引額の割合(縦軸)を増やします。ある閾値th2を超えたら全額を動かします。
3.png
グラフのような特性を実現する関数がこちらです。th2=1/800,th1=0.7th2としています。

def compute(x, th1, th2):
    if 0 < x and x < th1:
        out = 0
    elif th1 <= x and x <= th2:
        out = -1/(th1-th2)**2*(x-th2)**2+1
    elif th2 < x:
        out = 1
    else:
        out = 0
    return out  

以上の関数を1分ごとに実行するラッパーがこちらです。

def main(th1, th2):

    # 移動平均を取る幅[分]
    itr = 20 
    # 最終取引価格と移動平均を一時的に保存する配列を用意
    current_price = api.ticker(product_code = "BTC_JPY")['ltp']
    ltps = current_price*np.ones(itr)
    smas = current_price*np.ones(2)
    # 最小取引額[BTC]
    min_btc = 0.001
    # 資産と時間を格納する配列。あとで確認するときに使える
    jpys = []
    btcs = []
    tms = []
        # 最終取引価格と移動平均を格納する配列。あとで確認するときに使える
        raw = []
    smoothed = [] 

    while True:
        # 00秒に稼働
        if datetime.now().strftime('%S')0:2] == '00':
            # 資産を取得し格納
            balance = api.getbalance()
       jpy = balance[0]['available']
            jpys.append(jpy)
            btc = balance[1]['available']
            btcs.append(btc)
       # 時間を文字列で取得し格納
            tick = api.ticker(product_code = "BTC_JPY")
            tm = tick['timestamp']
            tm = str((int(tm[11:13])+9)%24) + tm[13:19]
            tms.append(tm)
            # 最終取引価格と移動平均の更新
            ltps = np.hstack((ltps[1:itr], tick['ltp']))
            smas = np.hstack((smas[1], ltps.mean()))
            # 確認用データの更新
            raw.append(ltps[itr-1])
            smoothed.append(smas[1])

            # 移動平均の分率利率
            r = (smas[1]-smas[0])/smas[0]
            print(tm, r)

            # 利率が正の時はBTC買い
            if r > 0:
                # JPY資産のうちどれだけBTCに変えるかを計算
                amt_jpy = compute(r, th1, th2)*jpy
                amt_btc = amt_jpy/ltps[itr-1]
                # 購入量が最小取引額を超えていれば指値買い
                if amt_btc > min_btc:
                    buy_btc_lmt(amt_btc, ltps[itr-1])

            # 利率が負の時はBTC売り
            if r < 0:
         # BTC資産のうちどれだけJPYに変えるかを計算
                amt_btc = compute(-r, th1, th2)*btc
                # 売却量が最小取引額を超えていれば指値売り
                if amt_btc > min_btc:
                    sell_btc_mkt(amt_btc)

            # 次の00秒まで休憩
            time.sleep(57)

指値は少し待てば成行よりもいい条件で取引できることが多いので、ここでは指値取引だけを使いました。
なお、bitFlyer FXだと執筆時点で手数料無料ですが、強制ロスカットなど不測の事態が起こるので今回の自動取引では扱いませんでした。

所感と今後の展望

お察しの通り、このアルゴリズムが成功するかどうかは閾値のth1th2に懸かっています。
僕は10秒ごとの実行にして、なんどもシミュレーションをした上で最適な閾値を決めました。
そしてドキドキしながらプログラムを実行し、コンピュータを家に置いて外出したのですが、損こそしなかったものの期待したほどの利益が出ませんでした。
この最適閾値はそのときの値動きのパターンに依存するので、一定の値でやってもうまくいきません。
パフォーマンスからフィードバックを与えて閾値を動的に変更するような仕組みにすればもっとうまくいく気がします。
ARMAモデルなども試してみたかったのですが時間がありませんでした。
シミュレーションをしたい方は、Poloniexというアメリカの取引所のAPIを使うと過去のデータを取得することができます。

あと、pybitflyerはエラー処理が未完成なので、しばしば通信エラーなどでプログラムが止まってしまいます。
取引所のサーバが落ちることもありますし、本気で儲けを出すためにはこの辺りのフォローもしっかりしておく必要があります。

最後に身もふたもない話をすると、ビットコインで取引をするよりも、将来性の高いアルトコイン(XRPとか)を青田買いして長期保有する方が簡単に儲けが出せると思います。
僕はそれに気づいてデイトレードはやめてしまいました…。でもこの勉強は、いつか株を始めるときなどにきっと役立つはずです。
アルトコインを買う場合は上述のPoloniexがおすすめです。基軸通貨のBTCをどこかで手に入れる必要があるのが難点ですが、とにかく取扱通貨が豊富です。
日本で人気のcoincheckは、スプレッドでがっつり持っていかれるのでおすすめできません。

仮想通貨豆知識

忘年会、新年会などでの話のネタにどうぞ。

・ビットコインの最小単位は1億分の1で、これをsatoshiと呼びます。つまり、1億 satoshiで1 BTCです。SI単位系では10の3乗ごとに一区切りなので普通は10億か100万にするところを、あえて1億にしているのには何かこだわりを感じますね。

・ビットコインのブロックチェーンの最初のブロック(ジェネシスブロック)には"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"と(ハッシュ値で)書かれています。これはリーマンショック後に掲載されたタイムズ紙の見出しで、ジェネシスブロックが2009年1月3日以降に作られたものであることを証明するとともに、既存の金融システムの脆弱性をナカモトが皮肉ったものと考えられています。

・ビットコインが初めて実際の商品の購入に使用されたのは2010年5月22日です。アメリカで初期からのビットコインファンであるLaszlo Hanyeczがピザ2枚を10,000 BTCで購入しました。これは現在のレートで計算すると約200億円。歴史上最も高価なピザと言えますね。それ以来、5月22日はビットコイナーの間でBitcoin Pizza Dayとして毎年お祝いされているそうです。

・本記事の執筆時点(2017年12月17日)で世界には1300以上の仮想通貨がありますが、中には野獣先輩を模したYAJU、スーパーマンを模したSUPERMAN、猫を模したNYANといったネタコインもあります。ちなみにYAJUはアゼルバイジャンのエンジニアが開発したそうです。

参考文献

本編で紹介できなかったリンクをまとめました。

Bitcoinを技術的に理解する

bitFlyer LightningのAPIをPythonから使えるパッケージ「pybitflyer」を作りました

仮想通貨取引所のPoloniexからAPI経由でデータ取得し、ディープラーニング(Chainer)で翌日の価格予測をしよう

Jupyter notebookにMatplotlibでリアルタイムにチャートを書く

テクニカル分析ABC |ガイド・投資講座 |セミナー・マーケット情報|株のことならネット証券会社【カブドットコム】