株価分析(SMA) - Backtestingを使ってリターンを計算する

# はじめに

前回はこんな記事を書きました。

https://qiita.com/mahoutsukaino-deshi/items/67f3258b26199b35c5b8

今回はリターンがどれくらいになるのか求めてみたいと思います。
リターンの計算ですが、今まではしこしことトレーディングクラスなどを書いていたのですが、Backtestingという便利なライブラリを見つけたのでこれを使用することにします。

https://pypi.org/project/Backtesting/

インストールは上のページにも書いてありますが下記のように行います。

```sh
$ pip install Backtesting
```

# まずはリターンを求めてみる

Backtestingを使用するときには、Strategyクラスを実装する必要があります。
ここではゴールデンクロスで買う、デッドクロスで売る、という戦略でいくことにします。

- long_term, short_termをクラス変数で定義しています。クラス変数として定義することで、後で書きますがoptimizeメソッドを使用して最適な組み合わせを求めることができるようになります。
- initメソッドでSMAを求めています。Backtestingの中にもSMAを計算する処理はあるのですが、今後のことも考えて、TA-Libを使用することにしています。
- nextメソッドは時系列データごとに呼び出されます。ここではゴールデンクロスで買い、デッドクロスで売るという単純な戦略を取ってます。

```python
class SmaCross(Strategy):
    '''
    SMS Cross Strategy
    '''
    long_term = 75
    short_term = 25

    def init(self):
        close = self.data['Close']
        self.long_sma = self.I(talib.SMA, close, self.long_term)
        self.short_sma = self.I(talib.SMA, close, self.short_term)

    def next(self):
        if crossover(self.short_sma, self.long_sma):
            self.buy()
        elif crossover(self.long_sma, self.short_sma):
            self.sell()
```

では、リターンを求めてみます。

- 株価の取得は前と同じget_stockメソッドを使っています。
- Backtestクラスのインスタンスを作成します。ここで先ほど作成したStrategyクラスを指定します。
- trade_on_closeで取引のタイミングを指定します。Trueにするとその日の終値で取引を行い、Falseにすると翌日の始値で取引を行います。その日の終値を確認して翌日に仕掛けるということでFalseにしています。
- exclusive_ordersは取引時にそれまでのポジションをクローズするかどうかを指定します。毎回ポジションをクローズしたいのでTrueとしています。
- runで計算を開始します。

```python
    df = get_stock(TICKER, START_DATE, END_DATE)

    bt = Backtest(
        df,
        SmaCross,
        cash=INIT_CASH,
        trade_on_close=False,
        exclusive_orders=True
    )

    output = bt.run()
    print(output)
    bt.plot()
```

print(output)の結果です。

```text:結果
Start                     2011-01-04 00:00:00
End                       2020-12-30 00:00:00
Duration                   3648 days 00:00:00
Exposure Time [%]                     94.9775
Equity Final [$]                       650714
Equity Peak [$]                   1.42691e+06
Return [%]                           -34.9286
Buy & Hold Return [%]                 163.934
Return (Ann.) [%]                     -4.3251
Volatility (Ann.) [%]                 18.8042
Sharpe Ratio                                0
Sortino Ratio                               0
Calmar Ratio                                0
Max. Drawdown [%]                    -65.8071
Avg. Drawdown [%]                    -6.89812
Max. Drawdown Duration     2779 days 00:00:00
Avg. Drawdown Duration      192 days 00:00:00
# Trades                                   41
Win Rate [%]                          36.5854
Best Trade [%]                        40.7543
Worst Trade [%]                      -13.8896
Avg. Trade [%]                       -1.04275
Max. Trade Duration         288 days 00:00:00
Avg. Trade Duration          85 days 00:00:00
Profit Factor                        0.806629
Expectancy [%]                      -0.650414
SQN                                 -0.722688
_strategy                            SmaCross
_equity_curve                             ...
_trades                       Size  EntryB...
dtype: object
```

"Return [%]"の行にリターンが出ています。結果は残念ながら**マイナス39.4%**でした。
bt.plot()でトレードの結果のグラフを出力できます。2013年半ばにピークを迎えた後、下降の一途を辿っているように見えます。

![strategy1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/8a2ffce0-f926-3f14-2391-9e8b6b210333.png)

# 最適な組み合わせを探す(1)

Backtestのoptimizeメソッドを使用すると、短期と長期の期間の最適な組み合わせを求めることができます。

- SmaCrossクラスのクラス変数で定義したlong_term, short_termを範囲で指定します。するとこの範囲から最適な組み合わせを見つけてくれます。
- return_heatmapをTrueにすると、組み合わせのヒートマップを取得することができます。plot_heatmapsメソッドを使用すればグラフを表示できます。

```python
    stats, heatmap = bt.optimize(
        long_term=range(3, MAX_LONG_TERM + 1, 101),
        short_term=range(2, MAX_LONG_TERM, 100),
        return_heatmap=True,
        constraint=lambda p: p.short_term < p.long_term)
```

statsの内容は以下の通りです。

```text:結果
Start                     2011-01-04 00:00:00                                             
End                       2020-12-30 00:00:00
Duration                   3648 days 00:00:00
Exposure Time [%]                      98.775
Equity Final [$]                  1.97874e+06
Equity Peak [$]                   1.97939e+06
Return [%]                            97.8745
Buy & Hold Return [%]                 163.934
Return (Ann.) [%]                     7.27493
Volatility (Ann.) [%]                 21.6981
Sharpe Ratio                          0.33528
Sortino Ratio                         0.52895
Calmar Ratio                         0.152433
Max. Drawdown [%]                    -47.7255
Avg. Drawdown [%]                    -4.58205
Max. Drawdown Duration     2490 days 00:00:00
Avg. Drawdown Duration      108 days 00:00:00
# Trades                                  141
Win Rate [%]                           48.227
Best Trade [%]                         35.591
Worst Trade [%]                      -13.1921
Avg. Trade [%]                       0.490535
Max. Trade Duration         192 days 00:00:00
Avg. Trade Duration          26 days 00:00:00
Profit Factor                         1.44673
Expectancy [%]                       0.629784
SQN                                   1.34157
_strategy                 SmaCross(long_te...
_equity_curve                             ...
_trades                        Size  Entry...
dtype: object
```
リターンはlong_term=23, short_term=17のときに**プラス97.9%**となってます。資産が約2倍になったことになりますね。

トレードの結果をグラフにしてみます。

```python
    bt.plot()
```

![strategy2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/d790d08a-dfc3-6af5-d50c-794a60f1c13d.png)

下記のようにするとヒートマップを表示できます。

```python
    plot_heatmaps(heatmap, agg='mean', plot_width=2048, filename='heatmap')
```

long_term=23, short_term=17のところが、明るい色になってることがわかりますが、ここが最適な組み合わせの場所となります。

![strategy2_heatmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/301161a8-f5ab-8e8b-5108-5304ec89d83a.png)

# 最適な組み合わせを探す(2)

上では2〜100日の5日刻みで最適な組み合わせを求めています。今度は250日までの期間を1日刻みで求めてみたいと思います。プログラム的にはほぼ同じで、期間と刻みが違うだけです。私のPCで1時間ちょっとかかりました。

statsの内容は下記の通りです。リターンは**プラス144.6%**で、資産が約2.4倍になったことになります。

```text:結果
Start                     2011-01-04 00:00:00
End                       2020-12-30 00:00:00
Duration                   3648 days 00:00:00
Exposure Time [%]                     86.2393
Equity Final [$]                  2.44551e+06
Equity Peak [$]                   2.44631e+06
Return [%]                            144.551
Buy & Hold Return [%]                 163.934
Return (Ann.) [%]                     9.63845
Volatility (Ann.) [%]                   21.03
Sharpe Ratio                         0.458318
Sortino Ratio                        0.731527
Calmar Ratio                         0.309028
Max. Drawdown [%]                    -31.1895
Avg. Drawdown [%]                    -4.22459
Max. Drawdown Duration      452 days 00:00:00
Avg. Drawdown Duration       50 days 00:00:00
# Trades                                   13
Win Rate [%]                          69.2308
Best Trade [%]                        44.9915
Worst Trade [%]                      -17.4933
Avg. Trade [%]                        7.17498
Max. Trade Duration         629 days 00:00:00
Avg. Trade Duration         243 days 00:00:00
Profit Factor                         4.90971
Expectancy [%]                        8.24431
SQN                                   2.07852
_strategy                 SmaCross(long_te...
_equity_curve                             ...
_trades                       Size  EntryB...
```

続いてヒートマップをグラフにします。
右上の方が明るい色になってます。この辺の組み合わせが良いようです。

![strategy4_heatmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/beeaa39d-8111-200d-81fb-6729a9d6a60c.png)

このヒートマップですが、軸のラベルがつぶれてしまって見えなくなってます。色なども調整したいので、Seabornのheatmapでグラフを作成してみます。

```python
    sns.set(font='IPAexGothic')

    d = heatmap.reset_index().pivot('short_term', 'long_term', 'SQN')
    fig, ax = plt.subplots(figsize=(12, 9))
    sns.heatmap(d, square=True, cmap='seismic', center=0, ax=ax)
    ax.set_title('移動平均期間の組み合わせによるSQN')
    ax.invert_yaxis()
    ax.grid()
    plt.show()
```

赤の濃いところが良い組み合わせ、青の濃いところが悪い組み合わせになってます。

![strategy4_heatmap2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/a1534020-bc53-25d2-ba5f-e52b4ab4ec57.png)

# 最後に

今回はSMAを使用して、長期と短期がどの組み合わせが最適化を求めました。結果、リターンは144%となりました。これは資産が2.4倍になったことになり年利にすると9%です(複利)。
しかしよく考えてみると、2011年当時10000円くらいだった日経平均株価は2020年末頃には26000円くらいになってます。2011年に買って何もしなければ資産が2.6倍（160%の増加）になった計算になります。
この結果だけを見るとSMAを使った戦略はもう一つなのかな、と思ってしまいます。ただし、今回はゴールデンクロスで買ってデッドクロスで売るという単純な戦略です。他の戦略を取れば結果も変わると思います。

今回のソースは[GitHub](https://github.com/mahoutsukaino-deshi/stock_sma_strategy.git)に置いてます。