仮想通貨の自動取引入門 ~Backtestingによるバックテスト~

Yuya Sugano
Sep 1 · 22 min read

仮想通貨に限らず自動取引は取引時に人手を介さず、リスクをコントロールしつつコストの削減およびリターンを機械的に追求することであると考える。本稿ではAPIを通じて取得した分足や日足のOHLCデータからテクニカル指標を計算しグラフ描画する。またpythonのライブラリであるTa-LibとBacktestingを導入し、Ta-Libで記述したトレードロジックでバックテストを行う。最後にパラメータ最適化を行いより良い条件を選好する。

Image by TeroVesalainen from Pixabay

前回はPubnubを使用したマーケット・メイキング・アルゴリズムの実装を行ったが、在庫によるポジション損益を考慮していなかった。シンプルなマーケット・メイキング・アルゴリズムでは、スプレッド収益やメイカー・テイカー手数料によるリベート収益が発生していても、ボラティリティの高い場面においてポジション損益がコントロールできない。本稿では仮想通貨に限らず自動取引を行う上で必須となるバックテストのライブラリ紹介と実例を試してみる。


  • 自動取引とは(再掲)
  • OHLCの取得とデータ整形
  • Ta-Libのインストールと使用例
  • Backtestingによるバックテスト

自動取引とは

金融商品における自動取引とはシステムトレードの非裁量取引を、特にプログラムを用いて自動化しまた定例化することでリスクのコントロールもしくはリターンの追求を省力化した上で達成することであると考える。

システムトレードやアルゴリズム取引という語には使用する組織や文脈に応じて複数の意味があること、また意味の混ざることがあるためにここでは単純に自動取引という用語のみ使用する。[1]

アルゴリズムを使用しないものも含め、自動取引という用語の意味はより広義に捉えて頂いて問題ない。プログラムを介して、取引機会を発見し、それを自動化しているものを自動取引と呼びたいと思う。非裁量性や高頻度取引(High Frequency Trading)など様々な利点のある自動取引であるが、まず大分類としてコストの削減を目指すものとリターンの追求を実現するものの2種類がある。[2]


  • コストの削減
    ・取引コストの削減(執行系アルゴリズム、VAMPなどのベンチマーク系アルゴリズム)
    ・マーケット・メイクやバスケット取引などの定型的な業務の自動化による内部コストの削減
    ・マーケット・インパクトによるコストの削減
    ・最適な市場の選択をすることによる手数料の削減
  • リターンの追求
    ・収益機会の発見(裁定アルゴリズム、ディレクショナル・アルゴリズムなど)
    ・リベートの獲得(メイカー・テイカー手数料)
    ・スプレッド収益の発見(マーケット・メイキング・アルゴリズム)

個人投資家についてはよほど大口でない限り、リターン追求のために自動取引を導入すると考えられることから、ここでは収益機会の発見を達成する自動取引をプログラムでどのように記述していくかを考えていく。主に裁定アルゴリズム、ディレクショナルアルゴリズム、マーケット・メイキング・アルゴリズムを個人投資家は利用する。自動取引に限らないが以下のPDCAを回して行くことで(自動取引においては自動的に行うことが好ましい)、効率よく収益機会の発見および収益の発生を実現することが理想である。

1.情報の収集および整理
2.自動取引と約定の確認
3.バックテスト(ベンチマーク)


OHLCの取得とデータ整形

OHLCとは(Open/High/Low/Close)の省略表記で、ローソク足の価格データセット(始値・高値・安値・終値)である。ここに出来高(Volume)を加え、OHLCVで提供されることも多い。

OHLCのデータは引き続きbitbank.cc APIから取得し、pandasを使用してデータ整形を行うこととする。pandasのデータフレームはインデックス付きの行列のデータ形式で、描画や機械学習・ディープラーニングの処理へそのまま活用できるデファクトスタンダードのライブラリである。

bitbank.cc APIの『Candlestick』を呼び出すことでローソク足の情報を取得できることが確認できた。

bitbank.cc API Candlestick

通貨ペア、期間、日付(年数)をパラメータとして渡すことができる。時刻はUNIX時間であり、指定した日付の朝9:00からのデータが返却されることが確認できた(UTC 0:00からのデータ)。

Candlestick API parameters

以下、インタラクティブモードでの確認結果である。

>>> import json
>>> import requests
>>> from datetime import datetime>>> headers = {'Content-Type': 'application/json'}
>>> api_url_base = 'https://public.bitbank.cc'
>>> pair = 'btc_jpy'
>>> period = '1min'
>>> today = "{0:%Y%m%d}".format(datetime.today())>>> api_url = '{0}/{1}/candlestick/{2}/{3}'.format(api_url_base, pair, period, today)
>>> response = requests.get(api_url, headers=headers)
>>> ohlcv = json.loads(response.content.decode('utf-8'))['data']['candlestick'][0]['ohlcv']
>>> print(json.dumps(ohlcv, indent=4, separators=(',', ': ')))
[
[
"1101000",
"1101001",
"1101000",
"1101001",
"0.3089",
1566000000000
],
...

前回指定した日付のOHLCデータを取得し、csvとして保存するコードをgithubへアップロードしていた。今回は日付ではなく期間に5minを渡し、5分足のデータを取得する。

https://github.com/yuyasugano/ohlc


period の設定の変更と日付の変更を行った。

headers = {'Content-Type': 'application/json'}
api_url_base = 'https://public.bitbank.cc'
pair = 'btc_jpy'
period = '5min'
today = datetime.today()
yesterday = today - timedelta(days=1)
today = "{0:%Y%m%d}".format(today)
yesterday = "{0:%Y%m%d}".format(yesterday)

timestamp にデータを取得したい日付を渡す。5分足で1日分のデータであれば基本的に288行、4列のpandasのデータ得られるはずである。またインデックスはDatetimeIndex型となるようにDataFrameを作成する。

def api_ohlcv(timestamp):
api_url = '{0}/{1}/candlestick/{2}/{3}'.format(api_url_base, pair, period, timestamp)
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
ohlcv = json.loads(response.content.decode('utf-8'))['data']['candlestick'][0]['ohlcv']
return ohlcv
else:
return None

pipenvプロジェクトでライブラリをインストールし、 pipenv run start コマンドでアプリケーションを動かすことができた。ここでは2019/8/30の5分足を対象としてデータを取得した。

$ pipenv install --dev
$ pipenv run start
open high low close
2019-08-30 09:00:00 1011306 1015973 1009764 1011000
2019-08-30 09:05:05 1010612 1010780 1010468 1010780
2019-08-30 09:10:10 1010748 1010932 1007830 1007830
2019-08-30 09:15:15 1007906 1009845 1007500 1009825
2019-08-30 09:20:20 1009840 1009840 1007768 1008132

次に、Ta-Libをインストールする。


Ta-Libのインストールと使用例

pipenv のプロジェクトにTa-Libを追加する方法を説明する。 pipでTa-Libをインストールする場合には、必要なCのライブラリ類を先にインストールしておく必要がある。[3]

Ta-Lib dependencies

Cのライブラリを整えてから pipでTa-Libをインストールする。プロジェクトへパッケージを追加できた(pipenvのプロジェクトを使用しているので pipenv コマンドを利用している)。

$ pip install Ta-Lib
$ pipenv install --dev Ta-Lib
Installing Ta-Lib…
Adding Ta-Lib to Pipfile's [dev-packages]…
? Installation Succeeded
Installing dependencies from Pipfile.lock (eba2e6)…
? ???????????????????????????????? 26/26 ― 00

Ta-Libの使用方法を確認する。Numpyのndarrayを読むと記載があるが、pandas.Seriesの型を利用できる。取得したOHLCのDataFrameであれば、単純移動平均値は以下のように計算できる。[4]

# df is DataFrame of ohlc data
talib.SMA(pd.Series(df['close']), timeperiod=5)
talib.SMA(pd.Series(df['close']), timeperiod=15)

テクニカル指標を計算および、matplotlibで前回グラフ描画したが、単純移動平均線の計算はTa-Libを使用して以下のように書き換えられる。

Matplotlib moving average sample with Ta-Lib

同時に描画部分をpyplotインターフェースを使用した書き方からオブジェクト指向インターフェースを使用しかた記載へ変えた。今後はオブジェクト指向インターフェースのパターンへ記述を変更する。[5]


Matplotlib objects figure, axes, axis

2019/8/30の5分足のOHLCデータからTa-Libを使用して作図した単純移動平均線のグラフは以下のようになる。

Simple moving average of btc/jpy on 30th Aug 2019

他のテクニカル指標の描画については割愛するが、Ta-Libを使用して同様に描画ができる。Ta-Libによる指標の計算は関数をパラメータ付きで呼ぶだけで非常に簡単に記述できる。OHLCデータの終値をpandas.Seriesへ変換して渡す。計算例を示す。

close = pd.Series(df['close'])# Simple Moving Average
sma = talib.SMA(close, timeperiod=5)
# Exponential Moving Average
ema = talib.EMA(close, timeperiod=5)
# Bollinger Bands
bbands = talib.BBANDS(close)
# Momentum
momentam = talib.MOM(close, timeperiod=10)
# MACD
macd = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)
# RSI
rsi = talib.RSI(close, timeperiod=9)
print(type(sma))
print(sma.shape)
print(sma.head(5))
...
<class 'pandas.core.series.Series'>
(288,)
2019-08-30 00:00:00 NaN
2019-08-30 00:05:05 NaN
2019-08-30 00:10:10 NaN
2019-08-30 00:15:15 NaN
2019-08-30 00:20:20 1009513.4
dtype: float64

以上、Ta-Libを呼び出し取得したOHLCデータから各テクニカル指標が計算できることが確認できた。次はTa-Libを活用したバックテストに取り掛かる。


Backtestingによるバックテスト

バックテストとは、システムトレード(本稿では自動取引のプログラム)の有効性を検証する際に、過去のデータを用いて、一定期間にどの程度のパフォーマンスが得られたかをシミュレーションすること、である。バックテストは常に過去のデータの使用であり、モデルへの当てはまりの度合いを考慮しても場合によって有効と考えられないが、1つの指標として参考にできる。統計的に対象のバックテストにどの程度有意性があるかを計算しても良い。またバックテストに用いるデータは様々なパターンを用意することが望ましいと考えられる。バックテストの手法については既存のライブラリを利用する方法と自作する方法があるが、以下の記事を参考にBacktestingを試すこととした。[6]

  • 軽量で使いやすい、少数のサブモジュールで構成されている
  • pandas DataFrameをそのまま読み込んで使用できる(データはすべて自前で用意し整形する必要がある)
  • マルチフレームやパラメータヒートマップの可視化

ライブラリをインストールする。

$ pip install backtesting

本稿のプロジェクトではpipenvを使用しているためpipenvコマンドで導入した。Pipfileは以下のような状態となった。

$ pipenv install --dev backtesting
$ more Pipfile
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
python-bitbankcc = {git = "https://github.com/bitbankinc/python-bitbankcc.git"}
requests = "*"
numpy = "*"
pandas = "*"
ta-lib = "*"
backtesting = "*"
[packages][requires]
python_version = "3.7"
[scripts]
start = "python main.py"

ここからは『Backtesting.py Quick Start User Guide』に沿ってバックテストを実行する。[7]

DataFrameはインデックスがDatetimeIndexで各カラム名が指定されている(大文字: ”Open” “High” “Low” “Close” “Volume”)。インデックスは既にDatetime型なので、各カラム名を以下のように修正した。dfはbitbank.cc APIから取得した5分足のOHLCデータを引き続き使用している。

df_ = df.copy()
df_.columns = ['Close','Open','High','Low']
print(df_.columns)
Index(['Close', 'Open', 'High', 'Low'], dtype='object')

次にbacktestingに必要となるStrategyをコーディングする。backtesting.Strategyクラスを継承し、内部で init() メソッドと next() メソッドをオーバーライドする。 init() メソッドと next() メソッドについて簡単に説明する。

  • init()
    Stretegyが実行される前に呼ばれる関数で、Strategyで使用される指標やシグナルを計算しておく、本稿では単純移動平均値を計算する
  • next()
    backtestingインスタンスによって継続的に呼び出される関数で、各データポイント(ここではDataFrameの行)ごとにトレードの増減のシミュ―レーションを実行し計算する

単純移動平均値の計算にはTa-Libを使用する。


短期の期間を5(5分足では25分)、長期の期間を15(5分足では75分)としてゴールデンクロスで買い、デッドクロスで売るというStrategyをコーディングした(期間のパラメータは後ほど最適化する)。SMA_BacktestingはStrategyの init() で使用する関数で、 values をpandas.Seriesへ変換し期間 n で単純移動平均値を計算する。SmaCrossがStrategyクラスを継承している部分である。

from backtesting import Strategy
from backtesting.lib import crossover
def SMA_Backtesting(values, n):
"""
Return simple moving average of `values`, at
each step taking into account `n` previous values.
"""
close = pd.Series(values)
return talib.SMA(close, timeperiod=n)
class SmaCross(Strategy):

# Define the two MA lags as *class variables*
# for later optimization
n1 = 5
n2 = 15

def init(self):
# Precompute two moving averages
self.sma1 = self.I(SMA_Backtesting, self.data['Close'], self.n1)
self.sma2 = self.I(SMA_Backtesting, self.data['Close'], self.n2)

def next(self):
# If sma1 crosses above sma2, buy the asset
if crossover(self.sma1, self.sma2):
self.buy()
# Else, if sma1 crosses below sma2, sell it
elif crossover(self.sma2, self.sma1):
self.sell()

Crossoverというライブラリを使用することでゴールデンクロス・デッドクロスの条件式を簡略化できる。複雑で分かりづらい条件を記述する必要がない。 crossover を使用しないゴールデンクロス・デッドクロスの例を記載しておく。

# sample next function for golden-cross and dead-cross
def next(self):
if (self.sma1[-2] < self.sma2[-2] and self.sma1[-1] > self.sma2[-1]):
self.buy()
elif (self.sma1[-2] > self.sma2[-2] and self.sma1[-1] < self.sma2[-1]):
self.sell()

データおよびStrategyを継承したSmaCrossクラスが完成した。 Backtest を呼び出してバックテストを実施することができる。ここでは 1 BTC から開始し、ブローカー手数料を 0.2% とした(テイカー手数料と考えてよい)。

from backtesting import Backtestbt = Backtest(df_, SmaCross, cash=1, commission=.002)
bt.run()

出力を記載する。

  • Equity Final [$] — 取引期間後の資産高
  • Return [%] — 取引期間におけるリータン率
  • # Trades — 取引期間内での取引回数
  • Win Rate [%] — トレードにおける勝率
Start                     2019-08-30 00:00:00
End 2019-08-30 23:55:55
Duration 0 days 23:55:55
Exposure [%] 80.4538
Equity Final [$] 0.964456
Equity Peak [$] 1
Return [%] -3.55443
Buy & Hold Return [%] 0.760798
Max. Drawdown [%] -4.74665
Avg. Drawdown [%] NaN
Max. Drawdown Duration NaN
Avg. Drawdown Duration NaN
# Trades 19
Win Rate [%] 21.0526
Best Trade [%] 0.658739
Worst Trade [%] -0.684338
Avg. Trade [%] -0.204906
Max. Trade Duration 0 days 02:30:30
Avg. Trade Duration 0 days 01:00:49
Expectancy [%] 0.328925
SQN -2.78069
Sharpe Ratio -0.629509
Sortino Ratio -1.2004
Calmar Ratio -0.0431686
_strategy SmaCross
dtype: object

5と15と設定した単純移動平均の期間のパラメータをBacktest.optimize()関数で最適化できる。資産高『Equity Final [$]』を最大化するパラメータを複数のn1/n2のパターン内から見つける最適化指示を行える。 constraint には制約を設定する。ここではn2の値が常にn1より大きい条件の下、n1が5/10/15、n2が15/20/25/30/35/40/45/50を取るパターンを試した。

stats = bt.optimize(n1=range(5, 15, 5),
n2=range(15, 50, 5),
maximize='Equity Final [$]',
constraint=lambda p: p.n1 < p.n2)
print(stats)

図の右側がパラメータ最適化を行った結果で、n1=10/n2=25のときに最大資産高である BTC 0.970733 が得られることが分かる。

Left: n1=5, n2=15, Right: Optimized

以上から、対象日(2019/8/30)の5分足を基礎とした単純移動平均によるトレードは収益を上げられないことが判明した。異なる期間やトレーディングロジックを変えることでバックテストから収益性の高い取引手法を選好できる。本稿で使用したpipenvプロジェクトおよびJupyter Notebookを以下リポジトリへアップロードした。

https://github.com/yuyasugano/sma-backtesting

以上、次回以降は機械学習やディープラーニングを活用した手法を探ってみたい。


Yuya Sugano

Written by

@HashHub_Tokyo — 機械学習講座メンター、ブロックチェーン・セキュリティ関連の記事を執筆。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。Vinyl DJ, Backpacker, Technology Orchestrator. https://twitter.com/SuganoYuya

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade