仮想通貨の自動取引入門 ~機械学習を用いたOHLCデータの3値分類問題化~

Yuya Sugano
27 min readSep 15, 2019

--

仮想通貨に限らず自動取引は取引時に人手を介さず、リスクをコントロールしつつコストの削減およびリターンを機械的に追求することであると考える。以前までの記事でクラシカルなテクニカル指標が仮想通貨の価格変動には適用しづらいことを見てきた(時すでに遅し?)。本稿ではAPIを通じて取得した分足のOHLCデータから正解データをラベリングし、機械学習の分類問題として入力データに対する正解ラベルを予測することを考える。

Pairplot by Seaborn for revised OHLC data

前回までで検討したマーケット・メイキング・アルゴリズムや各テクニカル指標に基づく自動売買から一旦離れ、本稿ではより洗練された手法として機械学習を用いてOHLCデータに対する正解ラベルを与え、教師あり学習の3種分類問題として自動取引することを考える。

本稿で取り扱わないことを先に記載する。機械学習における説明変数の特徴量エンジニアリング変数間の交互作用の追加、機械学習アルゴリズムのパラメータ調整グリッドサーチによる最適なパラメータの探索、は行わない。

  • 自動取引とは(再掲)
  • OHLCの取得とデータ整形
  • 正解ラベルの定義とデータ修正
  • 教師あり機械学習によるモデリング
  • 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へアップロードしていた。今回は分足を使用するため期間を1minに設定する。

https://github.com/yuyasugano/ohlc

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

headers = {'Content-Type': 'application/json'}
api_url_base = 'https://public.bitbank.cc'
pair = 'btc_jpy'
period = '1min'

timestamp にデータを取得したい日付を渡す。分足で1日分のデータであれば基本的に1440行、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/9/1の分足を対象としてデータを取得した。今回は volume も計算へ加え、1440行、5列とした。以下のようにして volume をDataFrameへ加えることができる。

df = pd.DataFrame({'open': open, 'high': high, 'low': low, 'close': close, 'volume': volume}, index=date_time_index)

変更を加えたコードを実行すると以下の結果が得られる。

$ pipenv install --dev
$ pipenv run start
open high low close volume
2019-09-01 00:00:00 1020201 1020201 1020201 1020201 0.0001
2019-09-01 00:01:01 1020201 1020201 1020201 1020201 0.0000
2019-09-01 00:02:02 1020201 1020201 1020201 1020201 0.0000
2019-09-01 00:03:03 1020201 1020201 1020201 1020201 0.0004
2019-09-01 00:04:04 1020200 1020478 1020200 1020478 1.0640

新規にpipenvプロジェクトを始める場合は以下のライブラリを導入する。

pipenv --python 3.7.3
pipenv install --dev requests numpy pandas
pipenv install --dev Ta-Lib backtesting scikit-learn
pipenv install --dev git+https://github.com/bitbankinc/python-bitbankcc.git#egg=python_bitbankcc

次に正解ラベルを定義し、DataFrameの各行における正解をラベリングしていく。

正解ラベルの定義とデータ修正

OHLCから計算した複数テクニカル指標の中で正解ラベルへ相関の強いものを説明変数として選択し特徴量エンジニアリングを行いたいが、まずは正解ラベルを定義し、OHLCデータから直接特徴量となる指標を計算してみる(例えば移動平均線やモメンタムを特徴量として使用しない)。また特徴量エンジニアリングや変数間の交互作用の追加はここでは検討しない。

外観を掴むために2019/9/1のデータを描画する。matplotlibのオブジェクト指向パターンで書きだした。

def draw(df):
df1 = df.copy()
fig = plt.figure(figsize=(15,5))
ax = fig.add_subplot(1, 1, 1)
ax.set_title('btc/jpy by bitbank.cc API')
ax.plot(df1.index, df1.close,label="original")
ax.set_xlim(df1.index[0], df1.index[-1])
locator = mdates.AutoDateLocator()
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(mdates.AutoDateFormatter(locator))
ax.xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax.grid()

draw(df)
2019/9/1 分足データ bitbank.cc API

本稿の解析にあたって以下のような検討を行っている。

  • 1日分の分足(1440行)から現在の次点のcloseが上昇するか下降するかを予測する
  • 1分後の分足のcloseの値からの変化率が特定のある定数を上回っている場合に『1』、下回っている場合に『-1』、そうでない場合に『0』というラベルを与える(3値の分類問題とする)
  • 変化率は正規分布に従うものと仮定する(詳細は後述)

変化率の定数は大きな変化率を選びすぎて、ラベル『0』が多くならないように、また小さすぎる変化率によって予測に意味をなさない『1』と『-1』のラベルを増やさないようにする。仮にこのような変化率を標準化することで、その分布が標準正規分布に従う場合には、標準偏差-1以下と1以上の変化率データを使用する。この変化率が統計的に比較的高い上昇、もしくは下降を表していると考えられるからである。

仮にいま1 BTCを自動取引に使えるとする。1 BTC = 1,000,000 JPYのときに変化率が1%(10,000 JPYの上昇もしくは下降)の分足を1440行のうち半数である720行について80%の確率で正答できるモデルを作成できたとして、そのときの利回りを計算してみる(ここで残りの半数である720行はラベル『0』の変化率が1%未満であったと仮定する)。1日における各分足から上昇もしくは下降を計576回正答することができるので、5,760,000 JPYの利益となり、このとき日次収益率は576%である。ボラティリティの高いビットコインであっても分足で変化率が1%を超えることはほぼないため、576%は全く現実的でない。妥当な数値は 0.03%(300 JPYの上昇もしくは下降)程度で、日次収益率は約17.3%となる。またテイカー手数料は考慮していない。

dfはbitbank.cc APIから取得した分足のOHLCデータを引き続き使用している。df_へ学習データと計算した教師データが保存されている。 pct_change(1) によって間隔1(1分の行)の差分を計算できるが、対象の行に格納される計算はその行と1行前の行との差であるため shift(-1) を使用して行を1段引き上げている(つまり現在時点の行における1分後のcloseからの変化率を知りたいため、1行ずれていると考えた)。

lambda関数でラベルの付与の計算を指定しているが、ここでは変化率が0.01%(0.0001)以上である場合に『1』、-0.01%(-0.0001)以下の場合に『-1』、その間であるときに『0』というラベルを与えている。yに変化率を、y_にラベル付けしたものを格納する。

Labelling and the histgram for the distribution

このコードを実行するとX/yのshape情報と終値の変化率を標準化したグラフが出力される。まず0.01%の変化という低い率であるにもかかわらず、ラベル『1』『-1』は双方170程度しかない。0.01%は1 BTC = 1,000,000 JPYであるときに100 JPYである。

--------------------------------------------------------------------
X shape: (1440,5)
y shape: (1440,1)
--------------------------------------------------------------------
y
-1 172
0 1092
1 176
dtype: int64
y=1 up, y=0 stay, y=-1 down
--------------------------------------------------------------------

y_std = (y-y.mean())/y.std() で標準化したデータから表示された分布である。鐘形に見えるが標準正規分布に従うデータではないようである。

pct_change/freq distribution for 2019/9/1

特徴量として終値であるcloseと変化率を標準化したデータおよび取引ボリュームを残す。Open/High/Low/Closeはそのままで説明変数としては適当でないと考えたため削除し、追加で以下の2種類を用いることとした。

  • CloseからOpenを除算した値
  • HighからLowを除算した値
X['diff1'] = X.close - X.open
X['diff2'] = X.high - X.low
X_ = X.drop(['open', 'high', 'low'], axis=1)
X_.join(y_).head(200)

X_を機械学習の学習データとして、y_を正解データとして使用する。

Revised DataFrame for Machine learning

Seabornのpairplotを利用して各説明変数の相関を確認できる。顕著な特徴は見られないがラベル『-1』については、取引量が多く、値動きの激しいときには下落している可能性が高いことが分かる。それほど強い相関は確認できない。

# seaborn plot
import seaborn as sns
df_ml = X_.join(y_)
print(df_ml.dtypes)
print(df_ml['y'].value_counts())
sns.pairplot(df_ml, hue='y', vars=['close', 'volume', 'diff1', 'diff2'])
Pairplot by Seaborn for revised OHLC data

Seabornの使い方は以下のブログを参考にした。[3]

教師あり機械学習によるモデリング

OHLCデータから正解データのラベリングおよび特徴量の計算および選択を行ってきた(この特徴量は今後見直すこととする)。ここからは機械学習のライブラリであるscikit-learnを使用して説明変数X_のデータと被説明変数y_のデータを分類のアルゴリズム(Classifier)を用いて学習させていく。

scikit-learnからライブラリとして使用できる4つのアルゴリズムを選択した。またパラメータ調整やグリッドサーチによる最適なパラメータの探索は行わない。評価は混合行列による精度スコアとf1スコアとする。

  • k近傍法
  • ロジスティック回帰
  • ランダムフォレスト
  • グラジアントブースト

X_とy_に格納されているデータを訓練用(モデル学習用)とテスト用(モデル評価用)へと分割し、各アルゴリズムを用いて学習させた結果が以下である。ここでTrainと表記のあるスコアについては訓練したモデルに対してX_trainとy_trainから計算したスコア、Testと表記のあるものについては同じモデルに対してX_testとy_testから計算したスコアを表している。テストデータでの結果をみるとグラジアントブーストのモデルが7割程度の正答率を期待できそうである。ランダムフォレストは訓練データへの当てはまりは最も良いが汎化能力がない。

多値分類問題(ここでは3値分類問題)におけるf1スコアは各ラベルごとのf1スコアを求めてから、f1スコアの平均を計算するマクロ平均と、厳密な計算に基づくマイクロ平均がある。さらにマクロ平均には平均計算を行う際に正解ラベルの比率に基づいた重み付き平均をとる方法がある。[4]

KNN Train Accuracy: 0.817
KNN Test Accuracy: 0.689
KNN Train F1 Score: 0.796
KNN Test F1 Score: 0.645
Logistic Train Accuracy: 0.769
Logistic Test Accuracy: 0.750
Logistic Train F1 Score: 0.698
Logistic Test F1 Score: 0.664
RandomForest Train Accuracy: 0.974
RandomForest Test Accuracy: 0.693
RandomForest Train F1 Score: 0.973
RandomForest Test F1 Score: 0.652
GradientBoosting Train Accuracy: 0.882
GradientBoosting Test Accuracy: 0.748
GradientBoosting Train F1 Score: 0.867
GradientBoosting Test F1 Score: 0.695

コードは以下の通りである。

Machine Learning for OHLC data

注意が必要な点として、この結果は3値分類問題への正答率であるということである。ラベル『0』とした変化率への正答率も含まれており、このラベルがデータに占める割合が最も多い。実際にこのモデルを使用した場合にどのような資産高を描くかバックテストによって確認を行うこととする。

Backtestingによるバックテスト

今回のモデルは分足の終値の変化率に応じて3種の正解ラベルを付与したデータを、3値分類問題として複数の機械学習アルゴリズムを適用し作成したものであった。このモデルは訓練データから構築したものであるが、同日(2019/9/1)の分足データをそのまま使用してバックテストを行う。バックテストのデータには訓練データに使用したデータおよびテストデータも含まれていることに注意が必要である。Backtestingの導入方法については前回の記事で記載した。[5]

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

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

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

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

比較のためランダムフォレストおよびグラジアントブーストのモデルを適用する。 self.label へラベルを格納する。ラベルが『1』のときに self.buy() をラベルが『-1』のときに self.sell() を呼ぶ。

from backtesting import Strategy
from backtesting.lib import crossover
def RF_Backtesting(df):
"""
Return the label from calculating df(close, volume, diff1, diff2), at
each step taking into account `n` previous values.
"""
return pipe_rf.predict(df)
def GB_Backtesting(df):
"""
Return the label from calculating df(close, volume, diff1, diff2), at
each step taking into account `n` previous values.
"""
return pipe_gb.predict(df)
class MachineLearning(Strategy):

def init(self):
# Precompute a label for data
close = pd.DataFrame({'Close': self.data.Close})
open = pd.DataFrame({'Open': self.data.Open})
high = pd.DataFrame({'High': self.data.High})
low = pd.DataFrame({'Low': self.data.Low})
volume = pd.DataFrame({'Volume': self.data.Volume})
self.df = close.join(open).join(high).join(low).join(volume)
self.df['diff1'] = self.df['Close'] - self.df['Open']
self.df['diff2'] = self.df['High'] - self.df['Low']
self.label = self.I(GB_Backtesting, self.df.drop(['Open', 'High', 'Low'], axis=1))
# self.label = self.I(RF_Backtesting, self.df.drop(['Open', 'High', 'Low'], axis=1))
def next(self):
if self.label == 1:
self.buy()
elif self.label == -1:
self.sell()

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

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

出力を記載する。

  • Equity Final [$] — 取引期間後の資産高
  • Return [%] — 取引期間におけるリータン率
  • # Trades — 取引期間内での取引回数
  • Win Rate [%] — トレードにおける勝率
Start                     2019-09-01 00:00:00
End 2019-09-01 23:59:59
Duration 0 days 23:59:59
Exposure [%] 98.3761
Equity Final [$] 0.873191
Equity Peak [$] 1
Return [%] -12.6809
Buy & Hold Return [%] 1.64673
Max. Drawdown [%] -12.7409
Avg. Drawdown [%] NaN
Max. Drawdown Duration NaN
Avg. Drawdown Duration NaN
# Trades 80
Win Rate [%] 11.25
Best Trade [%] 0.759737
Worst Trade [%] -0.839769
Avg. Trade [%] -0.167007
Max. Trade Duration 0 days 05:48:49
Avg. Trade Duration 0 days 00:17:43
Expectancy [%] 0.217416
SQN -7.46218
Sharpe Ratio -0.848379
Sortino Ratio -1.41337
Calmar Ratio -0.013108
_strategy MachineLearning
dtype: object

ランダムフォレスト・グラジアントブーストともに結果は芳しくない。本来であれば作成したモデルを使用し、2019/9/1以外のデータを試すことで汎化の程度についてより詳細に確認できるが今回は試していない(結果はみえている)。

Left: Gradient Boost, Right: Random Forest

以上から、 対象日(2019/9/1)の分足を基礎とした機械学習による3値分類によるトレードでは収益を上げられないことが判明した。説明変数の選択(例えばテクニカル指標)やアルゴリズムの最適なパラメータ探索(グリッドサーチ)を行うことで収益性の高いアルゴリズムを発見できる可能性がある。本稿で使用したpipenvプロジェクトおよびJupyter Notebookを以下リポジトリへアップロードした。[6]

https://github.com/yuyasugano/ml-classifier-ohlc

以上、次回以降はより有効と考えられる特徴量(テクニカル指標の一部)を用いた分析や時系列分析の手法を探ってみたい。

--

--

Yuya Sugano
Yuya Sugano

Written by Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。

No responses yet