【Python】Backtesting.pyで株売買のバックテスト・最適化してみる


トレードや株取引の戦略・手法を考えたら，それがどれくらい良い戦略なのかを試すために過去のデータを使ってシミュレーションを行うと思います．考えた戦略でいきなり取引するのは少し怖いですからね．これをバックテストといいます．

今回は，簡単にバックテストできるPythonライブラリ『Backtesting.py』を使ってみます．

Backtesting.pyでは，変数を移動させて戦略を最適化する機能（例えば，移動平均線の日数を変化させる）もあるので，そちらも試してみます．

#Backtesting.pyのインストール
pipなら

```
pip install backtesting
```

でできます．

#株価データの取得
今回は，2018/1/1~現在までのKO（コカ・コーラ）の株価を取得したいと思います．

株価はpandas_datareaderで取得します．もちろんウェブやローカルにあるCSVファイルを読み込むなどでも構いません．

```Python
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)
```

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でバックテスト

##事前準備（戦略記述）

今回はお試しなので，2つのSMA（単純移動平均線）によるゴールデンクロス，デッドクロスで売買をする戦略でいきたいと思います．

SMAには，10日と30日を使ってみます（特に深い意味はありません）．

```Python
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() # 売り
```

##バックテストを実施

```Python
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() # 実行結果（グラフ）
```

##結果

```
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...
```

※各項目の意味は[こちら](https://optrip.xyz/?p=3348)の記事の一番下に記載しています

![bokeh_plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/7ed4f4bf-0105-25e5-56dd-010c4a692497.png)
![bokeh_plot (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/17f1bcf8-53cb-0e41-bb6e-c91a89be693d.png)
![bokeh_plot (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/4f4f577b-5d11-ed86-8266-31c5a656e50d.png)
![bokeh_plot (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/3d08f39a-77af-9e02-66a2-b27d99b98c92.png)


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

この戦略では損をしてしまうようですね．

戦略を少し変えてみましょう．

##戦略を最適化

戦略を変えると言っても，2つのSMA（単純移動平均線）によるゴールデンクロス，デッドクロスで売買をする戦略はそのままで，SMAの期間を変えてみたいと思います．

以下のようにすることで，SMAを10~70日の範囲で5日刻みで変化させてEquity Final（最終資産額）が最大になるようなSMAの期間を探してくれます．
※maximizeはデフォルトではSQN(System Quality Number)です

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

※ちなみに，以下のようにconsraintオプションを付けることで短期SMAが長期SMAより長くなることを禁止できます．

```Python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/700f303c-2734-7954-c4af-4b71a60c0656.png)
![bokeh_plot (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/85b72603-cd94-864c-0969-2679218154dd.png)
![bokeh_plot (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/1e93024e-1510-0ce9-0f89-0a1cef0855dc.png)
![bokeh_plot (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1298289/34ff4745-4f30-9985-3b1c-36839e3c2a52.png)


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

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

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


#まとめコード

```Python

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), maximize='Equity Final [$]')
print(output2)
bt.plot()
```

#他の戦略をバックテスト

今後は，色々な投資戦略を試していきたいと思います．

手始めに，MACDでやってみました．

https://qiita.com/Fujinoinvestor/items/780b138e441531860e60


ちなみに，SMAやMACDのような指標の生成はTA-Libを使うと楽だと思います．

https://optrip.xyz/?p=2534

https://qiita.com/Fujinoinvestor/items/405c292cb262d8ca27bf


**投資に関する免責事項**
プログラムや考え方の情報の提供・作業代行を目的としており，投資勧誘を目的とするものではありません．また，この記事は投資成績を保証するものではありません．投資はあくまで自己責任でお願いします．