【Python】Backtesting.pyで株取引をバックテストして戦略を最適化

6月 21, 2021

今回は,Pythonによる株取引シミュレーションのバックテストについて解説します

バックテストとは

株取引やFXには,移動平均線やローソク足,MACD,ボリンジャーバンドなどのテクニカル指標を用いて売買する手法があります.

詳しくは,MACDの説明記事ボリンジャーバンドの説明記事をご覧ください.

ときには,オリジナルな売買手法を考えるときもあるでしょう.

売買の手法を考えたら,それがどれくらい良い戦略なのか(どれくらい儲けが出るのか)を試すために過去のデータを使ってシミュレーションを行いたいです.なぜなら,考えた戦略でいきなり取引するのは少し怖いですから.

この「過去のデータで手法の精度を確認する」ことをバックテストといいます.

Pythonによるバックテスト

Pythonでバックテストを行う方法はいくつかあると思いますが,ここでは『Backtesting.py』というライブラリを用いてバックテストを行います.

Backtesting.pyでは,戦略を最適化する機能(例えば,リターンが最大になるように移動平均線の日数を変化させる)もあるので,そちらも試してみます.

Backtesting.pyのインストール

pip install backtesting

でできます.

株価データの取得

まずは,バックテストを行うための株価を取得しましょう.

今回は,株価の取得にはpandas-datareaderを用います.

ただし,一度株価を取得したらCSVファイルに保存し,初回以降はそれを読み込むことをおすすめします(方法はこちら).

今回は,2018/1/1~現在までのKO(コカ・コーラ)の株価を取得したいと思います.

import pandas_datareader.data as web
import datetime

start = datetime.date(2018,1,1)
end = datetime.date.today()
data = web.DataReader('KO', 'yahoo', start, end)

ここに関しては,Pythonのpandas datareaderをインストールして株価データを取得する方法【日本株・米国株】を参考にして好きな銘柄の株価を取得して下さい.

pandas-datareaderで取得したデータは以下のように,High(高値),Low(低値),Open(始値),Close(終値),Volume(出来高),Adj Close(調整後終値)を持つDataFarameとなっています.

                 High        Low       Open      Close      Volume  Adj Close
Date                                                                         
2018-01-02  45.939999  45.509998  45.910000  45.540001  10872200.0  40.913391
2018-01-03  45.689999  45.340000  45.490002  45.439999  12635600.0  40.823551
2018-01-04  46.220001  45.450001  45.560001  46.080002  12709400.0  41.398529
2018-01-05  46.200001  45.790001  46.020000  46.070000  13113100.0  41.389545
2018-01-08  46.099998  45.880001  45.950001  46.000000   7068600.0  41.326656
...               ...        ...        ...        ...         ...        ...
2021-04-12  53.549999  53.099998  53.330002  53.349998   8565300.0  53.349998
2021-04-13  53.290001  52.810001  53.040001  53.090000  11071700.0  53.090000
2021-04-14  53.189999  52.650002  52.980000  53.080002   9787600.0  53.080002
2021-04-15  53.660000  53.119999  53.130001  53.330002  13078100.0  53.330002
2021-04-16  53.799999  53.380001  53.740002  53.680000  17958400.0  53.680000

Backtesting.pyを使用するには,データが"Open", “High", “Low", “Close"のカラムを持つ必要があります.pandas-datareaderで取得した場合はすでにこれらのカラムを持っているので問題ないですね.

Backtesting.pyでバックテスト

まずは,Backtesting.pyを動かすのに必要なモジュールをインポートしましょう.

from backtesting import Backtest, Strategy 
from backtesting.lib import crossover
from backtesting.test import SMA

売買手法を作成

次に,手法を書きましょう.

今回は2つのSMA(単純移動平均線)によるゴールデンクロス,デッドクロスで売買をする手法で行います.この手法の説明は,【投資の基本】移動平均線とは?ゴールデンクロスとデッドクロスで売買をご覧ください.

class SmaCross(Strategy):
    n1 = 10 # 短期SMA
    n2 = 30 # 長期SMA

    def init(self):
        self.sma1 = self.I(SMA, self.data.Close, self.n1) 
        self.sma2 = self.I(SMA, self.data.Close, self.n2)

    def next(self): #チャートデータの行ごとに呼び出される
        if crossover(self.sma1, self.sma2): #sma1がsma2を上回った時
            self.buy() # 買い
        elif crossover(self.sma2, self.sma1):
            self.position.close() # 売り
2021.4 追記

売り判定のところでself.position.close()ではなくself.sell()にしてしまうと,空売りありになってしまうようです.空売りを禁止するならposition.close()を使いましょう.

バックテストを実行

バックテストの条件は以下のようにbt = Backtest( )の中に書きます.あとは以下のようにbt.run()とすればバックテストが実行されます.

bt = Backtest(
    data, # チャートデータ
    SmaCross, # 売買戦略
    cash=1000, # 最初の所持金
    commission=0.00495, # 取引手数料
    margin=1.0, # レバレッジ倍率の逆数(0.5で2倍レバレッジ)
    trade_on_close=True, # True:現在の終値で取引,False:次の時間の始値で取引
    exclusive_orders=True #自動でポジションをクローズ
)

output = bt.run() # バックテスト実行
print(output) # 実行結果(データ)
bt.plot() # 実行結果(グラフ)

所持金は株価よりも多く設定しておかないと購入できずに終わりますから,適切な値にしましょう.

取引手数料は楽天証券の米国株式手数料の0.495%としました.

結果

上のコードを実行すると,以下のような結果(データ)とグラフが表示されると思います.

Start                     2018-01-02 00:00:00
End                       2021-04-16 00:00:00
Duration                   1200 days 00:00:00
Exposure Time [%]                     63.0435
Equity Final [$]                      938.099
Equity Peak [$]                       1205.96
Return [%]                           -6.19008
Buy & Hold Return [%]                 17.8744
Return (Ann.) [%]                    -1.92598
Volatility (Ann.) [%]                 14.1436
Sharpe Ratio                                0
Sortino Ratio                               0
Calmar Ratio                                0
Max. Drawdown [%]                    -28.3291
Avg. Drawdown [%]                    -3.40125
Max. Drawdown Duration      420 days 00:00:00
Avg. Drawdown Duration       50 days 00:00:00
# Trades                                   15
Win Rate [%]                          33.3333
Best Trade [%]                        15.0922
Worst Trade [%]                      -7.21554
Avg. Trade [%]                      -0.449168
Max. Trade Duration         177 days 00:00:00
Avg. Trade Duration          49 days 00:00:00
Profit Factor                        0.881756
Expectancy [%]                      -0.293761
SQN                                 -0.288313
_strategy                            SmaCross
_equity_curve                             ...
_trades                       Size  EntryB...
bokeh_plot.png
bokeh_plot (1).png
bokeh_plot (2).png
bokeh_plot (3).png

結果の見方は,記事の最後に載せておきます.

15回取引を行い,所持金1000に対して最終資産は938,リターンは-6.2%という結果になりました.

この手法(SMAの期間)では損してしまうということですね.それでは,手法を最適化してみましょう.

売買手法を最適化

SMAの期間を変えることで,最終資産額が大きくなるように最適化してみたいと思います.

Backtesting.pyでは,以下のようにすることでSMAを10~70日の範囲で5日刻みで変化させてEquity Final(最終資産額)が最大になるようなSMAの期間を探してくれます.

※maximizeはデフォルトではSQN(System Quality Number)です

#最適化
output2=bt.optimize(n1=range(10, 70, 5), n2=range(10, 70, 5), maximize='Equity Final [$]')
print(output2)
bt.plot()
2021.6 追記

consraintオプションを付けることで短期SMAが長期SMAより長くなることを禁止できます.もしも結果が長期SMAよりも短期SMAの方が長くなってしまった場合は以下のようにしましょう.

output2=bt.optimize(n1=range(10, 70, 5), n2=range(10, 70, 5), maximize='Equity Final [$]', constraint=lambda p: p.n1 < p.n2)

最適化の結果は以下のようになりました.

最適化結果

Start                     2018-01-02 00:00:00
End                       2021-04-16 00:00:00
Duration                   1200 days 00:00:00
Exposure Time [%]                     68.7198
Equity Final [$]                      3217.68
Equity Peak [$]                       3414.71
Return [%]                            221.768
Buy & Hold Return [%]                 211.529
Return (Ann.) [%]                      42.715
Volatility (Ann.) [%]                 32.9843
Sharpe Ratio                          1.29501
Sortino Ratio                         2.87914
Calmar Ratio                          1.69859
Max. Drawdown [%]                    -25.1473
Avg. Drawdown [%]                    -3.24492
Max. Drawdown Duration      227 days 00:00:00
Avg. Drawdown Duration       24 days 00:00:00
# Trades                                   13
Win Rate [%]                          61.5385
Best Trade [%]                        55.2328
Worst Trade [%]                       -6.4954
Avg. Trade [%]                        9.68661
Max. Trade Duration         175 days 00:00:00
Avg. Trade Duration          62 days 00:00:00
Profit Factor                          7.3015
Expectancy [%]                        10.9874
SQN                                   1.86309
_strategy                 SmaCross(n1=20,n...
_equity_curve                             ...
_trades                       Size  EntryB...
bokeh_plot (4).png
bokeh_plot (5).png
bokeh_plot (6).png
bokeh_plot (7).png

最終資産は3217,リターンは221%という結果になりました.

移動平均線が20日と25日,取引回数が13回となっています.

このような取引をしていれば,利益がプラスとなることになります.もちろん,これは「過去のデータでおこなった場合」なので,この日数が未来においても最適なのかを考えるにはいろいろな期間を設定して良い結果を出すのかを調べてみる必要があります.

まとめコード


import pandas_datareader.data as web
import datetime

start = datetime.date(2018,1,1)
end = datetime.date.today()
data = web.DataReader('KO', 'yahoo', start, end) #データの取得

from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA

class SmaCross(Strategy):
    n1 = 10 # 短期SMA
    n2 = 30 # 長期SMA

    def init(self):
        self.sma1 = self.I(SMA, self.data.Close, self.n1)
        self.sma2 = self.I(SMA, self.data.Close, self.n2)
    def next(self): # チャートデータの行ごとに呼び出される
        if crossover(self.sma1, self.sma2): # sma1がsma2を上回った時
            self.buy() # 買い
        elif crossover(self.sma2, self.sma1):
            self.position.close() # 売り

# バックテストを設定
bt = Backtest(
    data, # チャートデータ
    SmaCross, # 売買戦略
    cash=1000, # 最初の所持金
    commission=0.00495, # 取引手数料
    margin=1.0, # レバレッジ倍率の逆数(0.5で2倍レバレッジ)
    trade_on_close=True, # True:現在の終値で取引,False:次の時間の始値で取引
    exclusive_orders=True #自動でポジションをクローズ
)

output = bt.run() # バックテスト実行
print(output) # 実行結果(データ)
bt.plot() # 実行結果(グラフ)

#最適化
output2=bt.optimize(n1=range(10, 70, 5),n2=range(10, 70, 5))
print(output2)
bt.plot()

結果の見方

データ

Start                     2018-01-02 00:00:00 #バックテスト開始日
End                       2021-04-09 00:00:00 #バックテスト終了日
Duration                   1193 days 00:00:00 #バックテスト期間
Exposure Time [%]                     34.5079 #ポジション保有期間
Equity Final [$]                  1.25808e+06 #最終資産額
Equity Peak [$]                   1.31175e+06 #期間中の最高資産額
Return [%]                            25.8079 #リターン
Buy & Hold Return [%]                 16.7765 #|終了時の価格-開始時の価格|/開始時の価格
Return (Ann.) [%]                     7.28285 #年平均リターン
Volatility (Ann.) [%]                 18.9324 #年平均ボラティリティ(標準偏差)
Sharpe Ratio                         0.384676 #シャープレシオ(Return/Volatility)
Sortino Ratio                        0.574517 #ソルティノレシオ(下落リスクに対するリターン)
Calmar Ratio                         0.209777 #カルマーレシオ(最大損失率に対する年間平均収益)
Max. Drawdown [%]                     -34.717 #最大下落率
Avg. Drawdown [%]                    -2.96757 #平均下落率
Max. Drawdown Duration      382 days 00:00:00 #最長下落期間
Avg. Drawdown Duration       43 days 00:00:00 #平均下落期間
# Trades                                   12 #トレード回数
Win Rate [%]                          83.3333 #勝率(勝ち回数/取引回数)
Best Trade [%]                        11.0289 #1回の取引での利益の最大値/所持金
Worst Trade [%]                        -21.52 #1回の取引での利益の最小値/所持金
Avg. Trade [%]                        1.93162 #取引での利益の平均値/所持金
Max. Trade Duration          63 days 00:00:00 #1回の取引最長期間
Avg. Trade Duration          33 days 00:00:00 #取引平均期間
Profit Factor                         2.15154 #総利益/総損失
Expectancy [%]                        2.29776 #損益期待値
SQN                                  0.770112 #SQN(SystemQualityNumber)
_strategy                 SmaCross(n1=42,n... #手法の関数名とパラメータ
_equity_curve                             ...
_trades                        Size  Entry...

グラフ(上から)

1つ目:資産額(Equity)の推移

2つ目:各取引での損益状況

緑と赤の三角の見方:

上緑:売りによる益

上赤:売りによる損

(下緑:空売りによる益)

(下赤:空売りによる損)

3つ目:株価推移とSMAおよび取引(縞の直線)

緑の縞:その取引による益

赤の縞:その取引による損

4つ目:出来高(Volume)

参考記事

『Backtesting.py』でFXのバックテストをする!:Python(モーリーのメモ)