見出し画像

【初心者botter向け】バックテストとは?過去データで検証してみよう

こんにちは、あいば(https://twitter.com/aiba_algorithm)です。
毎月記事を書こうと思っていましたが、モチベが続かず更新が止まっていました。これから不定期でも書いていこうと思います。

前回の記事

今回は初心者向けにバックテストについて説明します。勝てそうなロジックを思いついて、えいやでbotを作って回し始めてもいいんですが、過去データからロジックを検証するのにバックテストは欠かせません。
バックテストという言葉を聞いたことしかないという方でもわかるように説明していきます。



はじめに

「botter始めたいんだけど、何から始めればいいですか?」と尋ねられたときに、自分は作りたいロジックがない場合は取り敢えずバックテストしてみたらどうでしょうか?ということをお伝えします。
※裁量トレードをbotにしたい、価格差を取りたいなど実装したいbotがある場合は取り敢えず作ってみればいいと思います。まずは裁量トレードをバックテストして勝てるのか確認するっていうのはありです。


バックテストとは?

バックテストとは過去データにおいて、自分の決めたルールで売買すると損益はどうなっていたか?をシュミレーションするものです。そのシュミレーションはよくohlcvと言われるデータで行われることがほとんどです。
高頻度(1秒ごとに注文するようなロジック)であれば約定データからバックテストすることもあります。今回は約定データから作る高頻度のバックテストはコラム的にしてohlcvでのバックテストについて詳しく説明します。

まずohlcvとはopen, high, low, close, volumeの略です。日本語にすると始値(はじめね)、高値(たかね)、安値(やすね)、終値(おわりね)、出来高です。このopen, high, low, closeをバックテストでは使います。

※ ohlcはいわゆるローソク足のデータです。以下の画像の左は陽線(価格が上がったローソク足)、右が陰線(価格が下がったローソク足)です。


成行バックテスト

ohlcvでのバックテストは1時間足だとすると1時間ごとにシグナルを作成します。買いのシグナルが出た時に成行注文したとしたらcloseの価格(またはopenの価格)で約定してポジションを持てることにします。またロングポジションを持っている場合に売りのシグナルが出た場合も成行注文の場合はcloseの価格でポジションを閉じれたというようにします。

以下の画像で具体的に説明します。

BinanceUSDM BTCUSDT perp 1時間足

buy signalとsell signalは適当に入れました。
2行目のopen timeが2024年1月23日22時00分にbuy signalがTrueになっていますね。1時間足なので実際は23時00分に成行注文をします。なのでバックテスト上は2024年1月23日22時00分のcloseの価格39468.3ドルでロングポジションを持てたこととします。

次にopen timeが2024年1月24日03時00分にsell signalがTrueになってますね。上と同様にcloseの価格の39611.2ドルでロングポジションを閉じることになります。ここで今回の取引の利益を確認してみましょう。

エントリー価格 = 39468.3
エグジット価格 = 39611.2
リターン(利益率)= (39611.2 - 39468.3) / 39468.3 =0.0036206…

 になります。バックテストはリターンだけでもいいんですが、実際の損益は100ドル分のポジションを持った場合、リターン✖️100ドルで計算できます。なので実現損益は0.36206…ドルですね。

これらを全ての期間で確認してバックテストが完了します。
次に指値をどうバックテストするかみていきましょう。


指値バックテスト

指値バックテストは成行と違って、指値価格が約定したかを確認しなければなりません。具体的にlongの場合はbuy priceを計算して、(何時間指値注文を出すかによりますが次の足の期間だけ注文を出すとすると)次の足のlowよりもbuy priceが大きければ約定した判定にします。low < buy priceの時に自分の指値を貫通してlowの価格まで下げたわけですから自分の買い指値は約定しているという想定です。shortの場合は同様にsell priceを計算して次の足のhighよりもsell priceが小さければ約定した判定にします。

次に具体的な数字で見ていきましょう。成行バックテストで使用した同じohlcvとsignalを使います。

BinanceUSDM BTCUSDT perp 1時間足

指値位置を決めまます。

ohlcv['buy_price'] = ohlcv['close']*(1-0.0005)
ohlcv['sell_price'] = ohlcv['close']*(1+0.0005)
  • 上はcloseから0.05パーセント離した価格をbuy_priceとsell_priceにしています。上で説明したように次の足のlowとhighの価格と指値価格を比較したいので、buy priceとlow priceを1行ずらします

ohlcv['buy_price'] = ohlcv['buy_price'].shift(1)
ohlcv['sell_price'] = ohlcv['sell_price'].shift(1)

あとはbuy priceとlowを比較し、sell priceとhighを比較してその足で約定したかを確認します。

ohlcv['is_buy_contracted'] = ohlcv['buy_price'] > ohlcv['low']
ohlcv['is_sell_contracted'] = ohlcv['sell_price'] < ohlcv['high']

is_buy_contractedがTrueの時に買い指値が約定しており、is_sell_contractedがTrueの時に売り指値が約定しています。
最後にわかりやすいようにbuy signalとsell signalも1行ずらします。1つ前の足のbuy signal(sell signal)とbuy price(sell price)を見て今の足でエントリー(エグジット)しているかと約定したかを見る感じですね。

ohlcv['buy_signal'] = ohlcv['buy_signal'].shift(1)
ohlcv['sell_signal'] = ohlcv['sell_signal'].shift(1)

buy_signalがTrueかつis_buy_contractedがTrueの時に買いでエントリーできて、sell_signalがTrueかつis_sell_contractedがTrueの時に売りでエグジットできることになります。
上の画像だと2024年01月24日00時00分にbuy_signalがTrueかつis_buy_contractedがTrueなのでbuy priceの39870.05500でエントリーすることができたとします。次に2024年01月24日04時00分にsell_signalがTrueかつis_sell_contractedがTrueなのでsell priceの39631.00560でエグジットできることになります。

ここで今回の取引の利益を確認してみましょう。

エントリー価格 = 39870.05500
エグジット価格 = 39631.00560
リターン(利益率)= (39631.00560 - 39870.05500) / 39870.05500 =-0.0059957…

となりマイナスのリターンとなりました。
100ドル分買っていたとしたら-0.59957…ドルのマイナスですね。

次に簡易コードで実際に期間全体のバックテストを行うコードを作成していきます。まずはohlcvのデータを取引所から取得します。


ohlcvを取得する

まずは取引所からohlcvを取得してみましょう。ccxtというライブラリからデータ取得するのが便利です。https://docs.ccxt.com/#/

以下はbinance futuresから4時間足のデータを取得するものです。
※1回の取得では(取引所によりますが)1500本分がマックスなので全期間のデータを取得するにはfor文を回す必要があります。

import time
import pandas as pd
import ccxt

symbol = 'BTC/USDT:USDT'
timeframe = '4h'
limit = 1500
current_time = int(time.time())*1000 # 現在時間
start_time = current_time - 60*60*24*30*3*1000 # 現在時間から90日前のデータを取得

ex = ccxt.binanceusdm()
ohlcv = ex.fetch_ohlcv(symbol, timeframe=timeframe, since=start_time, limit=limit)
ohlcv = pd.DataFrame(ohlcv, columns=["unixtime", "open", "high", "low", "close", "volume"])
ohlcv[['open', 'high', 'low', 'close', 'volume']] = ohlcv[['open', 'high', 'low', 'close', 'volume']].astype(float)
ohlcv['unixtime'] = ohlcv['unixtime'].astype(int)
ohlcv['time'] = pd.to_datetime(ohlcv['unixtime'], unit='ms', utc=True)
ohlcv

下の画像のようなデータが取得できます。


実際に簡易バックテストのコードを書いてみる

上で取得したohlcvでバックテストを行います。
簡単なロジック例として「24時間移動平均が1週間移動平均を上回ったときに成行buy, 24時間移動平均が1週間移動平均を下回ったときに成行sellでexitするロングオンリー」のロジックでバックテストしてみます。

まずは移動平均を計算してbuy signalとsell signalを求めます。

# 24時間の移動平均と1週間の移動平均を計算
ohlcv['short_ma'] = ohlcv['close'].rolling(window=6).mean()
ohlcv['long_ma'] = ohlcv['close'].rolling(window=6*7).mean()

# 24時間移動平均が1週間移動平均を上回ったときにbuy, 24時間移動平均が1週間移動平均を下回ったときにsellでexit
ohlcv['buy_signal'] = ohlcv['short_ma'] > ohlcv['long_ma']
ohlcv['sell_signal'] = ohlcv['short_ma'] < ohlcv['long_ma']

上のコードを実行したらohlcvは以下のようなデータになりました。
buy_signalとsell_signalという名前の列が追加されてTrueとFalseというデータが入っていますね。

次に1行ずつデータを確認してbuy_signalがTrueの時にlongでエントリーして、ポジションを持っているかつsell_signalがTrueの時にエグジットするようなものを作っていきます。

# 手数料0.04%
fee = 0.0004

pl = []
position = 0
entry_time = None
entry_price = None
exit_time = None
exit_price = None
r = None
for i in range(len(ohlcv)):
    if position == 0:
        # i番目のbuy signalを取得
        buy_signal = ohlcv.loc[i, 'buy_signal']
        # buy signalがTrueの時にlong
        if buy_signal:
            position = 1
            # i番目のclose価格でentry
            entry_price = ohlcv.loc[i, 'close']
            # i番目の時間を記録
            entry_time = ohlcv.loc[i, 'time']

    # positionがlongの場合
    elif position > 0:
        # i番目のsell signalを取得
        sell_signal = ohlcv.loc[i, 'sell_signal']
        # sell signalがTrueの時にexit
        if sell_signal:
            # i番目のclose価格でexit
            exit_price = ohlcv.loc[i, 'close']
            # i番目の時間を記録
            exit_time = ohlcv.loc[i, 'time']
            # Returnを計算(feeはentryとexitを考慮)
            r =  (exit_price - entry_price) / entry_price - fee*2 
            # plに今回の取引を追加
            pl.append(
                {
                    'Entry Time': entry_time,
                    'Entry Price': entry_price,
                    'Exit Time': exit_time,
                    'Exit Price': exit_price,
                    'Direction': 'Long',
                    'Return': r
                }
            )
            # 値を初期化
            position = 0
            entry_time = None
            entry_price = None
            exit_time = None
            exit_price = None
            r = None

for文で回してエントリープライスやエグジットプライス、リターンなどをplに追加していってます。

plは以下のようになりました。

これでバックテストが完了です。これの累積リターンをプロットしてみましょう。

from matplotlib import pyplot as plt

plt.plot(pl['Entry Time'], pl['Return'].cumsum(), label='pl')
plt.xticks(rotation=45)
plt.legend()
plt.show()
出力

この期間では一応プラスのロジックにはなっていますね!(全期間でやると多分全然勝てないロジックです。)

今回は説明のための簡易バックテストコードなので速度も遅いですし、コード自体もそこまで綺麗じゃないです。あと簡単にするためにロングエントリーかつ成行のみのバックテストです。上のように自分でバックテストコードを作成してもいいのですが、すでにあるライブラリなどを使った方が時間節約にもなりますしオススメです。次にバックテストツールを紹介していきます。


バックテストツールの紹介

Backtesting.py

最初はbacktest.pyです。多分一番有名なバックテストのフレームワークな気がします。申し訳ないんですが、自分が使ったことないのでどんな使い心地かはわからないです。


Qさんフレームワーク

次はQさん(https://twitter.com/trading_algo_Q)のフレームワークです。実は自分がbotを最初に教えてもらったのはQさんなので、このフレームワークをがっつり使っていました。初心者でもわかりやすく、自由度も高く、使いやすいです。bitbankのbotter道でも使われていたフレームワークです。


yasutakeさんフレームワーク

次はyasutakeさん(https://x.com/yasstake)のフレームワークです。これも自分は使ったことないので使い心地はわからないのですが、記事を読んでいる限りは使いやすそうだなという印象です。


vectorbt pro

最後は現在自分が使っているフレームワークです。紹介しといてあれですが、初心者には全然お勧めできないです。バックテストの方法も何種類もあったり、できることが多すぎてdocumentとかtutorialがめちゃくちゃ長いです。numbaを使ってたりで速度は結構速いので中級者でバックテストのフレームワーク探しているという方は検討してもいいかなぐらいです。

コラム:高頻度バックテスト

最初に軽く触れましたが、高頻度のバックテストはOHLCVデータではなく、約定履歴からシミュレーションする場合があります。実装も重いですし、しっかり作ったこともないのでおまけ程度に紹介します。OHLCVデータでのバックテストと違って、約定履歴から作るパターンは、約定履歴や板データなどを時間順にソートして、それぞれのデータが来たときに注文するかどうかを判断します。指値注文はOHLCVバージョンのようにlow, highで約定を判断するのではなく、約定履歴で判定します。もっと作り込む場合は、取引量(volume)で自分の注文がどれくらい約定できたかや、成行注文時の板を見てどれくらいスリッページがあるかなども考慮できます。約定履歴は取引所から取得することもできますが、自分でWebSocketから配信されるデータを保存することで遅延具合を知ることもできます。その遅延もバックテストに組み込むとより正確なバックテストになります。

長々と書きましたが、結局のところ、高頻度のバックテストはどれだけ正確に行っても、フォワードテストで成績が変わります。例えば、自分がある価格に注文を出したら、他のプレイヤーはそれを考慮してアクションを変えます。自分のアクションが他のプレイヤーに影響を与える限り、バックテスト通りにはいきません。そのため、高頻度のボッターの中には、バックテストをあまり行わずに直感でボットを動かし始める人も多くいます。1週間ほどPLを監視していれば、約定が多くて勝てるボットかどうかが分かります。高頻度でバックテストしたいという人は、まず5秒足などのOHLCVデータでバックテストしてみるといいでしょう。そこからもっと正確性を高めたいという場合は、約定データからバックテストしてみてはいかがでしょうか。


おわりに

どうだったでしょうか?最初にも書いた通り、この記事はバックテストを知らない方向けの記事です。バックテストは理解できたけれど、どんなロジックをバックテストすればいいか思いつかないという方は、とりあえず有名なテクニカル指標でバックテストしてみたり、Xのトレーダーたちが見ているようなデータでバックテストしてみたらいいと思います。

オーバーフィットなど気を付けることはありますが、とりあえずバックテストで勝てそうなロジックができると「自分も億万長者か!?」とテンションが上がるので、バックテスト上だけでも勝てるロジックを見つけることから始めてみてもいいと個人的には思います。良きbotterライフを!

Xのフォローもよろしくお願いします

https://twitter.com/aiba_algorithm

次の記事はバックテストの評価指標や勝てるロジック作ってみた!みたいな記事を考え中です….。

この記事が気に入ったらサポートをしてみませんか?