📜 ⬆️ ⬇️

How-to: Object-Oriented Python Backtesting



Well-known British trader and developer Mike Halls-Moore wrote in his blog an article about how to create an object-oriented backtesting system for financial trading strategies on the stock exchange. We bring to your attention the main thoughts of this material.

What is backtesting


By backtesting we understand the process of applying a specific trading strategy to a historical date in order to evaluate its possible performance in the past. This, however, does not provide any guarantee that the system will be successful in the future. Nevertheless, there are ways to “filter out” strategies that are definitely not worthy of spending time and money on them in the course of real trading.
')
Creating a reliable backtesting system is not easy, because it must be able to successfully simulate the behavior of various components that affect the performance of an algorithmic trading strategy. Lack of data, problems on the communication channels between the client and the broker, delays in the execution of the application - these are just a few factors that can affect whether the transaction will be successful or not.

Not all of these factors are known in advance, so as the trader finds out what else affects the trading process on the stock exchange, the backtesting system is usually added to reflect this new knowledge. In this article we will look at an example of creating such a simple system using Python.

Types of backtesting systems


There are two main types of backtesting systems. One of them is called “research-based” and is used mostly in the early stages of evaluating strategies when it is necessary to select the most promising for further work. Such systems are often written in Python, R or Matlab, since in this case the speed of development is more important than the speed of work.

The next type is event-oriented back-testers (event-based). In their case, the backtesting process follows the scenario as close as possible (if not identical) to real trading. The system realistically models market data and the process of order execution, which allows for a deeper assessment of the analyzed strategy.

Often, such systems are written in C ++, because the speed of their work already plays an important role. Although for testing strategies that do not imply extremely high speed, you can still use Python (here we considered creating an event-oriented back tester in this language).

Python object oriented backtester


The object-oriented approach to developing a backtester has its advantages:


In our example, we will create a simple backtester who can work with a strategy that uses only one financial instrument (for example, a stock). For such a system will require the following components:



As it is easy to see, in this case we exclude objects related to risk management, application processing (that is, the system does not know how to work with limit orders) or complex modeling of transaction costs. The challenge here is to create a basic backtester that can be improved afterwards.

Implementation


Now consider the implementation of each object used:

Strategy

The Strategy object will process pricing, mean-reversion, momentum, and volatility strategies. The strategies that are considered in this example are always based on time series, that is, “price driven” (price driven). In particular, this implies that the object will receive not ticks of bidding information as input, but a set of OHLCV indicators. Thus, the maximum possible detail here is 1 second bars.

In addition, the Strategy class will generate alarm recommendations. This means that he will advise the Portfolio object what action is best to prepend. The Portfolio class will then analyze the data along with these recommendations in order to generate a set of signals to enter or exit a position.

The class interface will be implemented using the methodology of abstract base classes . The Python code will be in the backtest.py file. The Strategy class requires that any implemented subclass use the generate_signals method.

In order for the Strategy not to create an instance (also known as asbtraktny), you must use the ABCMeta and abstractmethod objects from the abc module. Set the _metaclass_ class _metaclass_ to ABCMeta and decorate the generate_signals method using the abstractmethod decorator.

 # backtest.py from abc import ABCMeta, abstractmethod class Strategy(object): """Strategy —    ,           Strategy     ,      - pandas.          .""" __metaclass__ = ABCMeta @abstractmethod def generate_signals(self): """    ,        ,    (1, -1 or 0).""" raise NotImplementedError("Should implement generate_signals()!") 

Portfolio

The Portfolio class contains most of the trading logic. For this backtester, this object will be responsible for determining the position size, risk analysis and transaction costs. In the course of further development, these tasks need to be separated into separate components, but now they can be combined in one class.

To implement this class, we will use pandas - this library can save a lot of time here. The only point is to avoid iterating the dataset using the for d in … syntax. The fact is that NumPy optimizes loops using vectorized operations. Therefore, when using pandas, direct iterations are almost never met.

The task of the Portfolio class is to ultimately generate a sequence of transactions and a capital curve, which will then be analyzed by the Performance class. In order to do this, the class must receive a series of recommendations from the Strategy object (in more complex cases, there may be many such objects).

The Portfolio class needs to be told how to apply capital to a particular set of trading signals, how to account for transaction costs, and what types of exchange orders should be used. The Strategy object works with data bars, so assumptions must be made on the basis of the price that exists at the time the order is executed. Since the maximum and minimum prices of any current bar are a priori unknown, it is only possible to use the opening and closing prices (of the previous bar). In reality, however, when using market orders (market) it is impossible to guarantee the execution of an order at a specific price, so the price here will be no more than an assumption.

In this case, the backtester will also ignore everything related to the concept of collateral and restrictions on the part of the broker, assuming that it is possible to open long and short positions in any financial instrument without any liquidity restrictions. Of course, this is an unrealistic assumption, so during the development of the project it should be removed.

We continue to study the code:

 # backtest.py class Portfolio(object): """      (   ),        Strategy.""" __metaclass__ = ABCMeta @abstractmethod def generate_positions(self): """    ,            . """ raise NotImplementedError("Should implement generate_positions()!") @abstractmethod def backtest_portfolio(self): """              (   ) —     , /    .. Produces a portfolio object that can be examined by other classes/functions.""" raise NotImplementedError("Should implement backtest_portfolio()!") 

These are the basic descriptions of the abstract base classes Strategy and Portfolio. Now it's time to create dedicated implementations of these classes so that the system can more efficiently process the test strategy.

We RandomForecastStrategy start by creating a Strategy subclass called RandomForecastStrategy — its only task is to generate random signals to buy or short sell stocks. At first glance, this makes no sense, however, such a simple strategy will allow us to illustrate the work of the object-oriented backtesting framework.

Create a new file random_forecast.py with the module code with random recommendations:

 # random_forecast.py import numpy as np import pandas as pd import Quandl # Necessary for obtaining financial data easily from backtest import Strategy, Portfolio class RandomForecastingStrategy(Strategy): """  Strategy         long  short.       """ def __init__(self, symbol, bars): """Requires the symbol ticker and the pandas DataFrame of bars""" self.symbol = symbol self.bars = bars def generate_signals(self): """  pandas DataFrame,    .""" signals = pd.DataFrame(index=self.bars.index) signals['signal'] = np.sign(np.random.randn(len(signals))) #         NaN-: signals['signal'][0:5] = 0.0 return signals 

Now, having received a test system for creating recommendations, you must create an implementation of the Portfolio object. This object will include most of the backtesting code. It will create two separate data frames - the first one will contain positions (positions), it will be used to store the number of instruments purchased or sold during the bar. Next - the portfolio contains the market prices of all positions for each bar, as well as the amount of available funds. This allows you to build a capital curve to evaluate strategy performance.

Implementing a Portfolio object requires deciding how to handle transaction costs, market orders, etc. In this example, it is assumed that it is possible to open long and short positions without limitations of the collateral, to buy and sell clearly at the bar’s opening price, transaction costs are zero (price slippage, broker and exchange commissions, etc.) are discarded, and the number of shares for purchase or sale is indicated directly for each transaction.

Next is the continuation of the file random_forecast.py :

 # random_forecast.py class MarketOnOpenPortfolio(Portfolio): """ Portfolio   ,   100   ,       .  ,    ,          . : symbol - ,   . bars -     . signals -  pandas  (1, 0, -1)   . initial_capital -     .""" def __init__(self, symbol, bars, signals, initial_capital=100000.0): self.symbol = symbol self.bars = bars self.signals = signals self.initial_capital = float(initial_capital) self.positions = self.generate_positions() def generate_positions(self): """  'positions'           100 ,    {1, 0, -1}    .""" positions = pd.DataFrame(index=signals.index).fillna(0.0) positions[self.symbol] = 100*signals['signal'] return positions def backtest_portfolio(self): """       —           ( ).          —       .   portfolio,     .""" #   'pos_diff'   portfolio     ,      ,      portfolio = self.positions*self.bars['Open'] pos_diff = self.positions.diff() #             'holdings'  'cash' portfolio['holdings'] = (self.positions*self.bars['Open']).sum(axis=1) portfolio['cash'] = self.initial_capital - (pos_diff*self.bars['Open']).sum(axis=1).cumsum() #       ('cash')          portfolio['total'] = portfolio['cash'] + portfolio['holdings'] portfolio['returns'] = portfolio['total'].pct_change() return portfolio 

Now you need to tie everything together using the _main_ function:

 if __name__ == "__main__": #    SPY (ETF,      S&P500)  Quandl (     'pip install Quandl' symbol = 'SPY' bars = Quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily") #          SPY rfs = RandomForecastingStrategy(symbol, bars) signals = rfs.generate_signals() #   SPY portfolio = MarketOnOpenPortfolio(symbol, bars, signals, initial_capital=100000.0) returns = portfolio.backtest_portfolio() print returns.tail(10) 

The output of the program is presented below (in each particular case it will differ due to the different selected date ranges and the use of randomization):



In this case, it is clear that the strategy leads to losses, which is not surprising, given its features. In order to transform this test case into a more efficient backtester, you also need to create a Performance object that will accept input from Portfolio and issue a set of performance metrics on which to base your decision on filtering the strategy.

In addition, you can improve the Portfolio object so that it more realistic takes into account information about transaction costs (for example, slippage or broker commission). You can also include the “predictive engine” directly into the Strategy object, which should also allow you to achieve the best results.

That's all for today! Thank you for your attention, and do not forget to subscribe to our blog .

Other articles about creating trading robots:


Source: https://habr.com/ru/post/276151/


All Articles