PythonのバックテストライブラリBacktesting.pyを試してみました

# 目次
株のデータ収集についての記事一覧を[こちら](https://qiita.com/fjunya/items/dbe5ef36246c88acd983)に記載しております。

# 目的
- ゴールデンクロスが起きたら買い注文を入れ、デッドクロスが起きたら売り注文を出すロジックのバックテストを実施する
- Backtesting.pyを使用する

### バックテストとは
システムトレード（売買ルール）の有効性を検証する際に、過去のデータを用いて、一定期間にどの程度のパフォーマンスが得られたかをシミュレーションすること。

# Backtesting.pyのインストール
```
pip install backtesting
```
# ゴールデンクロスとデッドクロスを算出する処理
過去の株価データと移動平均線は事前にDBに登録されている状態とします。
Djangoを使用します。modelは以下のようになっております。

```python:models.py
class RawPrices(models.Model):
    code = models.IntegerField("銘柄コード")
    date = models.DateField("日付")
    open_price = models.IntegerField("始値")
    close_price = models.IntegerField("終値")
    high_price = models.IntegerField("高値")
    low_price = models.IntegerField("安値")
    volume = models.IntegerField("出来高")
    adjustment_close_price = models.FloatField("調整後終値")
    moving_averages25 = models.FloatField("25日移動平均線")
    moving_averages75 = models.FloatField("75日移動平均線")
```

ゴールデンクロスとデッドクロスはBacktesting.pyの`crossover`関数で算出することが可能です。
DjangoのCommandでスクリプトを作成します。
サイボウズの株価情報を使用してバックテストを実施します。

```python:backtesting_test.py
from django.core.management.base import BaseCommand
from app.models import RawPrices
import pandas as pd
from backtesting import Backtest
from backtesting import Strategy
from backtesting.lib import crossover

import logging
logger = logging.getLogger(__name__)

class Command(BaseCommand):
    help = ''

    def handle(self, *args, **options):
        self.backtest()

    def backtest(self):
        df = self.get_data()
        # cashは運用資金になります。commissionで手数料を指定することも可能です。
        bt = Backtest(df, SmaCross, cash=10000)
        # バックテストの実行/結果を表示
        print(bt.run())

    def get_data(self):
        # DBからデータを取得。4776はサイボウズの証券コード
        raw_prices = RawPrices.objects.filter(code=4776)
        raw_prices = raw_prices.filter(date__gte="2019-01-01")
        raw_prices = raw_prices.exclude(moving_averages75=-1).order_by("date")
        raw_price_list = list(
            map(lambda x: [x.date, x.open_price, x.high_price, x.low_price, x.close_price, x.moving_averages25, x.moving_averages75], raw_prices))
        """        
        Backtesting.pyではdf['Open'] = df['High'] = df['Low'] = df['Close']を指定する必要があります。
        オプションでVolumeも指定することが可能です。
        """
        columns = ["date", "Open", "High", "Low", "Close", "sma25", "sma75"]
        df = pd.DataFrame(raw_price_list, columns=columns)
        df.date = pd.to_datetime(df.date)
        df.set_index("date", inplace=True)
        return df

class SmaCross(Strategy):

    def init(self):
        """
        sma1に25日平均線のデータをセット
        sma2に75日平均線のデータをセット
        :return:
        """
        self.sma1 = self.I(sma, self.data.sma25) #Iの引数に関数と関数の引数を渡す
        self.sma2 = self.I(sma, self.data.sma75)

    def next(self):
        """
        ゴールデンクロスの時に買い、デッドクロスの時に売却する
        :return:
        """
        # crossoverはゴールデンクロス/デッドクロスを算出する関数
        if crossover(self.sma1, self.sma2):
            self.buy()
        elif crossover(self.sma2, self.sma1):
            self.position.close()

def sma(data: pd.Series):
    return data
```
実行結果

```
python manage.py backtesting_test
Start                     2019-01-04 00:00:00
End                       2019-09-27 00:00:00
Duration                    266 days 00:00:00
Exposure [%]                          63.5338
Equity Final [$]                      15853.7
Equity Peak [$]                       20200.9
Return [%]                            58.5366
Buy & Hold Return [%]                 56.1934
Max. Drawdown [%]                    -22.0881
Avg. Drawdown [%]                    -3.10143
Max. Drawdown Duration       10 days 00:00:00
Avg. Drawdown Duration        5 days 00:00:00
# Trades                                    1
Win Rate [%]                              100
Best Trade [%]                        57.0629
Worst Trade [%]                       57.0629
Avg. Trade [%]                        57.0629
Max. Trade Duration         169 days 00:00:00
Avg. Trade Duration         169 days 00:00:00
Expectancy [%]                            NaN
SQN                                       NaN
Sharpe Ratio                              NaN
Sortino Ratio                             NaN
Calmar Ratio                          2.58343
_strategy                            SmaCross
dtype: object
```
**事項結果の概要**
`start/end` -> バックテストで使った為替レートのデータの最初の最後の時間
`Duration` -> 期間
`Equity Final [$]` -> 最終的な金額
`Trades` -> トレード回数
`Win Rate [%]` -> トレードの勝率
今回のバックテストの結果では10000ドルの運用資金で開始して、最終的に16872.3ドルになっており、+6872.3ドルの利益になっております。

`bt.plot()`を実行することによって、実行結果を可視化してブラウザで表示することができます。
<img width="1373" alt="SmaCross_html.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/7a288fe4-272f-445b-4d97-44e31444026c.png">


2019/3/8にゴールデンクロスが起きて購入し、2019/8/23にデッドクロスが起きて売却しております。
~~上記、チャートで気になる点があります。2019/8/23にデッドクロスが起きて売却しているのですが、それ以降もEquityが変動しております。
売却は空売り扱いのようになっているようです。空売り扱いなので売却後に株価が下がった場合に利益として計算されているようです。
空売り扱いにならないようなオプションが無いか探してみましたが見つけることができませんでした。~~

Strategyクラスのbuy()とsell()メソッドはオプションでストップリミットとテイクプロフィット価格を指定できるようです。
https://kernc.github.io/backtesting.py/doc/backtesting/backtesting.html#backtesting.backtesting.Strategy.buy

```python
def buy(self, price=None, *, sl=None, tp=None)
```

```python
def sell(self, price=None, *, sl=None, tp=None)
```