Performance evaluation of investment automatic trading bot using backtesting

motivation

In Previous article, we collected past data of FX using OANDA REST API. This time, based on that data, we will describe the method of strategic evaluation of investment automatic trading bots. TL; DR → backtesting documentation

manner

Environment

It's just ↓

pip install backtesting

Code for people who don't need explanation

It is assumed that the DB called ʻusd_jpy_4hcontains the data acquired in [Previous article](https://qiita.com/k_0/items/e36162b9c623d27583c9). We also usetalib`, a package for technical analysis. This can also be easily dropped with pip. The talib documentation is here [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

Code description

input data creation

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')

Here, the data in the DB is acquired and formatted for backtesting. In backtestin, I expect the column names to be'Open','High','Low','Close','Volume' (OK without it) in the pandas Dataframe. It is also recommended to add an index with the datetime type. 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.

Creating a trading strategy

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)

The trading strategy is created by inheriting from backtesting import Strategy. This time, I am writing a program with a strategy of buying and selling when Macd, which is one of the technical indicators, crosses. In investment strategies, there are many strategies that fire when one time series curve intersects another. You can easily describe the strategy by using crossover (). https://kernc.github.io/backtesting.py/doc/backtesting/lib.html#backtesting.lib.crossover There are many other useful features, so please take a look at the documentation.

Variable optimization

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]
)

By using Backtest.optimize (), you can test the optimum value / combination for the variables used in the trading strategy. Here, the value decided by hand is defined to some extent, but it may be defined in the form of [i for i in range (10)] (although it seems to be heavy processing). Optimal parameters can be confirmed in stats._strategy after executing optimize ().

Recommended Posts

Performance evaluation of investment automatic trading bot using backtesting
Automatic collection of stock prices using python
Operation of virtual currency automatic trading script
Basic level performance evaluation of programming languages
Automatic launch of Raspberry Pi programs using Systemd