backtestingを使用した投資自動売買botの性能評価

## モチベーション
[前回の記事](https://qiita.com/k_0/items/e36162b9c623d27583c9)ではOANDA REST APIを使用したFXの過去データ収集を行いました。
今回はそのデータをもとに、投資自動売買botの戦略評価の方法について記載します。
TL;DR → [backtestingのドキュメント](https://kernc.github.io/backtesting.py/doc/backtesting/)

## やり方
### 環境構築
単純に↓だけです

```
pip install backtesting
```

### 説明いらない人向けコード
`usd_jpy_4h`というDBに[前回の記事](https://qiita.com/k_0/items/e36162b9c623d27583c9)で取得したデータが入っている想定で記載します。
またテクニカル分析用のパッケージである`talib`も使用しています。こちらもpipで簡単に落とせます。
talibのドキュメントは[こちら](https://mrjbq7.github.io/ta-lib/doc_index.html)

```
import talib as ta
import pandas as pd
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
import db_access

def get_raw_data(query):
  engine = db_access.get_engine()
  return pd.read_sql(query, engine)

def get_macd(close, fastperiod=6, slowperiod=13, signalperiod=4):
  macd, macdsignal, macdhist = ta.MACD(close, fastperiod=fastperiod, slowperiod=slowperiod, signalperiod=signalperiod)
  return macd, macdsignal, macdhist

query = "select distinct * from usd_jpy_4h where datetime between '2019/09/01' and '2020/03/01' order by datetime"

raw_data = get_raw_data(query)
df = raw_data[["datetime","open","high","low","close","volume"]].copy()
df.columns = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume']
df['Datetime'] = pd.to_datetime(df['Datetime'])
df = df.reset_index().set_index('Datetime')

class MacdStrategy(Strategy):
  macd_fastperiod = 6
  macd_slowperiod = 13
  macd_signalperiod = 4
  profit_pips = 50
  loss_pips = 50
  profit = profit_pips * 0.01
  loss = loss_pips * 0.01
  
  def init(self):
    self.macd, self.signal, self.hist = self.I(get_macd, self.data.Close, self.macd_fastperiod, self.macd_slowperiod, self.macd_signalperiod)

  def next(self):
    if (crossover(self.signal, self.macd)):
        self.buy(sl=self.data.Close[-1] - self.loss, tp=self.data.Close[-1] + self.profit)
    elif (crossover(self.macd, self.signal)):
        self.sell(sl=self.data.Close[-1] + self.loss, tp=self.data.Close[-1] - self.profit)

bt = Backtest(df, MacdStrategy, cash=100000, commission=.00004)
output = bt.run()
print(output)
```

output

```
Start                     2019-09-01 20:00:00
End                       2020-02-28 20:00:00
Duration                    180 days 00:00:00
Exposure [%]                          74.9074
Equity Final [$]                        98647
Equity Peak [$]                        101404
Return [%]                           -1.35297
Buy & Hold Return [%]                 1.81654
Max. Drawdown [%]                     -2.7724
Avg. Drawdown [%]                   -0.518637
Max. Drawdown Duration      162 days 00:00:00
Avg. Drawdown Duration       21 days 18:00:00
# Trades                                   66
Win Rate [%]                               50
Best Trade [%]                       0.457344
Worst Trade [%]                      -0.68218
Avg. Trade [%]                     -0.0215735
Max. Trade Duration          10 days 00:00:00
Avg. Trade Duration           2 days 02:00:00
Expectancy [%]                       0.275169
SQN                                 -0.538945
Sharpe Ratio                       -0.0655224
Sortino Ratio                       -0.107821
Calmar Ratio                      -0.00778153
_strategy                        MacdStrategy
dtype: object
```

### コード説明
#### inputデータ作成
```
raw_data = get_raw_data(query)
df = raw_data[["datetime","open","high","low","close","volume"]].copy()
df.columns = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume']
df['Datetime'] = pd.to_datetime(df['Datetime'])
df = df.reset_index().set_index('Datetime')
```

ここでDBにあるデータを取得し、backtesting用の整形しています。
backtestinではpandasのDataframeで、カラム名が'Open', 'High', 'Low', 'Close', 'Volume'(なくてもOK)、となっていることを期待してます。
またdatetime型でindexをつけておくことを推奨しています。
[https://kernc.github.io/backtesting.py/doc/examples/Quick%20Start%20User%20Guide.html](https://kernc.github.io/backtesting.py/doc/examples/Quick%20Start%20User%20Guide.html)

> You bring your own data. Backtesting ingests data as a pandas.DataFrame with columns 'Open', 'High', 'Low', 'Close', and (optionally) 'Volume'. Such data is easily obtainable (see e.g. pandas-datareader, Quandl, findatapy, ...). Your data frames can have other columns, but these are necessary. DataFrame should ideally be indexed with a datetime index (convert it with pd.to_datetime()), otherwise a simple range index will do.


#### 売買戦略作成
```
class MacdStrategy(Strategy):
  macd_fastperiod = 6
  macd_slowperiod = 13
  macd_signalperiod = 4
  profit_pips = 50
  loss_pips = 50
  profit = profit_pips * 0.01
  loss = loss_pips * 0.01
  
  def init(self):
    self.macd, self.signal, self.hist = self.I(get_macd, self.data.Close, self.macd_fastperiod, self.macd_slowperiod, self.macd_signalperiod)

  def next(self):
    if (crossover(self.signal, self.macd)):
        self.buy(sl=self.data.Close[-1] - self.loss, tp=self.data.Close[-1] + self.profit)
    elif (crossover(self.macd, self.signal)):
        self.sell(sl=self.data.Close[-1] + self.loss, tp=self.data.Close[-1] - self.profit)
```
売買戦略は`from backtesting import Strategy`を継承して作成します。
今回はテクニカル指標の１つであるMacdがクロスした際に売買を行う戦略でプログラムを書いています。
投資戦略においては、ある時系列曲線が別の曲線と交差した際に発火する戦略が多いです。`crossover()`を使用することでその戦略を簡単に記載することができます。
[https://kernc.github.io/backtesting.py/doc/backtesting/lib.html#backtesting.lib.crossover](https://kernc.github.io/backtesting.py/doc/backtesting/lib.html#backtesting.lib.crossover)
他にも便利な機能がたくさんあるのでぜひドキュメントを見てみてください。


## 変数の最適化
```
stats = bt.optimize(
    macd_fastperiod = [6, 8, 10, 12],
    macd_slowperiod = [13, 19, 26],
    macd_signalperiod = [4, 7, 9],
    profit_pips = [30, 50, 70, 90],
    loss_pips = [20, 30, 40, 50]
)

```
`Backtest.optimize()`を使用することで、売買戦略でしようした変数に対して最適な値・組み合わせをテストすることができます。
ここでは手である程度決めた値を定義していますが`[i for i in range(10)]`みたいな形で定義しても良いかもしれません(処理重くなりそうですが)
最適パラメータはoptimize()実行後、`stats._strategy`にて確認できます