Исследование Backtesting среды в Python с пандами

Автор:Доброта, Создано: 2019-03-16 11:58:20, Обновлено:

Бэктестинг - это исследовательский процесс применения идеи торговой стратегии к историческим данным с целью установления прошлых результатов. В частности, бэктестинг не гарантирует будущих результатов стратегии.

В этой статье (и тех, которые последуют за ней) будет описана основная объектно-ориентированная система обратного тестирования, написанная в Python. Эта ранняя система будет в первую очередь "учебным пособием", используемым для демонстрации различных компонентов системы обратного тестирования. По мере прогресса в статьях будет добавляться более сложная функциональность.

Обзор обратного тестирования

Процесс разработки надежной системы обратного тестирования чрезвычайно сложен. Эффективное моделирование всех компонентов, влияющих на производительность алгоритмической торговой системы, является сложной задачей. Плохая гранулярность данных, непрозрачность маршрутизации ордеров у брокера, задержка ордеров и множество других факторов влияют на "истинную" производительность стратегии по сравнению с проверенной производительностью.

При разработке системы бэкстетинга очень хочется постоянно "переписывать ее с нуля", поскольку для оценки производительности имеет решающее значение больше факторов.

Учитывая эти проблемы, представленный здесь обратный тест будет несколько упрощенным. по мере изучения дальнейших вопросов (оптимизация портфеля, управление рисками, управление затратами на транзакции) обратный тест станет более надежным.

Типы систем обратного тестирования

В целом существует два типа систем обратного тестирования, которые будут интересны. Первый основан на исследованиях, используется в основном на ранних стадиях, где будут проверены многие стратегии для выбора тех, для более серьезной оценки.

Второй тип системы обратного тестирования основан на событиях, то есть он выполняет процесс обратного тестирования в цикле исполнения, аналогичной (если не идентичной) самой системе исполнения торговли. Он будет реалистично моделировать данные рынка и процесс исполнения ордеров, чтобы обеспечить более строгую оценку стратегии.

Последние системы часто пишутся на высокопроизводительном языке, таком как C ++ или Java, где важна скорость выполнения.

Объектно-ориентированные исследования Backtester в Python

Теперь будет обсуждаться проектирование и внедрение объектно-ориентированной исследовательской среды обратного тестирования.

  • Интерфейсы каждого компонента могут быть заранее определены, а внутренние компоненты каждого компонента могут быть изменены (или заменены) по мере прогресса проекта
  • Уточняя интерфейсы заранее, можно эффективно проверить поведение каждого компонента (через единичное тестирование)
  • При расширении системы новые компоненты могут быть построены на других или в дополнение к ним, либо путем наследования, либо композиции

На данном этапе бэкстестер разработан для простоты внедрения и разумной степени гибкости, в ущерб истинной точности рынка. В частности, этот бэкстестер сможет обрабатывать только стратегии, действующие на одном инструменте.

  • Стратегия - класс Стратегии получает Pandas DataFrame из баров, то есть список точек данных Open-High-Low-Close-Volume (OHLCV) на определенной частоте.
  • Портфель - большая часть работы по обратному тестированию будет происходить в классе Портфель. Он получит набор сигналов (как описано выше) и создаст серию позиций, распределенных по отношению к денежному компоненту. Задача объекта Портфеля заключается в создании кривой собственности, включении основных затрат на транзакции и отслеживании сделок.
  • Performance - Объект Performance берет портфель и производит набор статистических данных о его производительности. В частности, он будет выводить характеристики риска/возврата (Sharpe, Sortino и информационные коэффициенты), показатели торговли/прибыли и информацию о привлечении.

Чего не хватает?

Как видно, этот бэкстестер не включает в себя никаких ссылок на управление портфелем / рисками, обработку исполнения (т.е. без лимитных ордеров), а также не будет предоставлять сложное моделирование затрат на транзакции. На данном этапе это не является большой проблемой. Он позволяет нам познакомиться с процессом создания объектно-ориентированного бэкстера и библиотеки Pandas / NumPy. Со временем он будет улучшен.

Использование

Теперь мы перейдем к описанию реализаций для каждого объекта.

Стратегия

На данном этапе объект Strategy должен быть довольно общим, поскольку он будет обрабатывать стратегии прогнозирования, среднего обратного движения, импульса и волатильности. Рассматриваемые здесь стратегии всегда будут основываться на временных рядах, т.е. по цене . Ранним требованием для этого бэктестера является то, что производные классы Strategy будут принимать список баров (OHLCV) в качестве ввода, а не клещи (цены по торговле) или данные книги ордеров. Таким образом, наилучшая гранулированность, рассматриваемая здесь, будет составлять 1 секунду.

Класс Strategy также всегда будет производить рекомендации сигналов. Это означает, что он будет консультировать экземпляр портфеля в смысле длинного / короткого или удержания позиции. Эта гибкость позволит нам создать несколько Strategy advisors, которые предоставляют набор сигналов, которые более продвинутый класс Portfolio может принять для определения фактических позиций, которые вводятся.

Интерфейс классов будет применяться с помощью методологии абстрактного базового класса. Абстрактный базовый класс - это объект, который не может быть инстанцирован, и, следовательно, могут быть созданы только производные классы.backtest.py. Класс Strategy требует, чтобы любой подкласс реализовывал метод generate_signals.

Чтобы предотвратить непосредственное инстанцирование класса Strategy (поскольку он абстрактный!), необходимо использовать объекты ABCMeta и abstractmethod из модуля abc.метаклассчтобы быть равным ABCMeta, а затем украсить метод generate_signals с помощью декоратора abstractmethod.

# backtest.py

from abc import ABCMeta, abstractmethod

class Strategy(object):
    """Strategy is an abstract base class providing an interface for
    all subsequent (inherited) trading strategies.

    The goal of a (derived) Strategy object is to output a list of signals,
    which has the form of a time series indexed pandas DataFrame.

    In this instance only a single symbol/instrument is supported."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_signals(self):
        """An implementation is required to return the DataFrame of symbols 
        containing the signals to go long, short or hold (1, -1 or 0)."""
        raise NotImplementedError("Should implement generate_signals()!")

В то время как вышеуказанный интерфейс прост, он станет более сложным, когда этот класс будет унаследован для каждого конкретного типа стратегии.

Портфель

На более позднем этапе эти задачи будут разбиты на отдельные компоненты. На данный момент они будут объединены в один класс.

Этот класс широко использует панды и дает отличный пример того, как библиотека может сэкономить огромное количество времени, особенно в отношении boilerplate ссорки данных. Кроме того, главный трюк с пандами и NumPy состоит в том, чтобы избежать итерации над любым набором данных с использованием синтаксиса d в.... Это связано с тем, что NumPy (который лежит в основе панд) оптимизирует петлю путем векторизированных операций. Таким образом, вы увидите мало (если вообще!) прямых итераций при использовании панд.

Цель класса Portfolio заключается в том, чтобы в конечном итоге создать последовательность сделок и кривую акций, которые будут проанализированы классом Performance.

Класс портфеля должен знать, как распределять капитал для определенного набора торговых сигналов, как обрабатывать расходы на транзакции и какие формы ордеров будут использоваться. Объект Стратегии работает на панелях данных, и, следовательно, должны быть сделаны предположения относительно цен, достигнутых при исполнении ордера. Поскольку высокая/низкая цена любой панели неизвестна априори, для торговли можно использовать только цены открытия и закрытия. На самом деле невозможно гарантировать, что заказ будет выполнен по одной из этих конкретных цен при использовании рыночного ордера, поэтому это будет, в лучшем случае, приближение.

В дополнение к предположениям о выполнении заказов, этот бэктестер будет игнорировать все концепции ограничений маржи / брокерства и будет предполагать, что можно свободно идти длинным и коротким в любом инструменте без каких-либо ограничений ликвидности.

Следующий список продолжается:backtest.py:

# backtest.py

class Portfolio(object):
    """An abstract base class representing a portfolio of 
    positions (including both instruments and cash), determined
    on the basis of a set of signals provided by a Strategy."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_positions(self):
        """Provides the logic to determine how the portfolio 
        positions are allocated on the basis of forecasting
        signals and available cash."""
        raise NotImplementedError("Should implement generate_positions()!")

    @abstractmethod
    def backtest_portfolio(self):
        """Provides the logic to generate the trading orders
        and subsequent equity curve (i.e. growth of total equity),
        as a sum of holdings and cash, and the bar-period returns
        associated with this curve based on the 'positions' DataFrame.

        Produces a portfolio object that can be examined by 
        other classes/functions."""
        raise NotImplementedError("Should implement backtest_portfolio()!")

На данном этапе были введены абстрактные базовые классы "Стратегия" и "Портфель".

Мы начнем с создания подкласса стратегии под названием RandomForecastStrategy, единственной задачей которого является создание случайным образом выбранных длинных/коротких сигналов! Хотя это явно бессмысленная торговая стратегия, она будет служить нашим потребностям, демонстрируя объектно-ориентированную структуру обратного тестирования. Таким образом, мы начнем новый файл под названием random_forecast.py, с перечнем случайного прогноза следующим образом:

# 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):
    """Derives from Strategy to produce a set of signals that
    are randomly generated long/shorts. Clearly a nonsensical
    strategy, but perfectly acceptable for demonstrating the
    backtesting infrastructure!"""    
    
    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):
        """Creates a pandas DataFrame of random signals."""
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = np.sign(np.random.randn(len(signals)))

        # The first five elements are set to zero in order to minimise
        # upstream NaN errors in the forecaster.
        signals['signal'][0:5] = 0.0
        return signals

Теперь, когда у нас есть конкретная система прогнозирования, мы должны создать реализацию объекта портфеля. Этот объект охватит большую часть кода бэкстетинга. Он предназначен для создания двух отдельных DataFrames, первая из которых является рамкой позиций, используемой для хранения количества каждого инструмента, удерживаемого в любой конкретной панели. Вторая, портфель, фактически содержит рыночную цену всех активов для каждой панели, а также сумму наличности, предполагая начальный капитал. Это в конечном итоге обеспечивает кривую капитала, на которой можно оценить эффективность стратегии.

Объект портфеля, хотя и чрезвычайно гибкий в своем интерфейсе, требует конкретных выборов в отношении того, как обрабатывать затраты на транзакции, рыночные заказы и т. Д. В этом базовом примере я считаю, что будет возможно легко купить/сократить инструмент без ограничений или маржи, купить или продать непосредственно по открытой цене ствола, нулевые затраты на транзакции (включая скольжение, сборы и влияние на рынок) и указал количество запасов непосредственно покупать для каждой сделки.

Вот продолжение списка random_forecast.py:

# random_forecast.py

class MarketOnOpenPortfolio(Portfolio):
    """Inherits Portfolio to create a system that purchases 100 units of 
    a particular symbol upon a long/short signal, assuming the market 
    open price of a bar.

    In addition, there are zero transaction costs and cash can be immediately 
    borrowed for shorting (no margin posting or interest requirements). 

    Requires:
    symbol - A stock symbol which forms the basis of the portfolio.
    bars - A DataFrame of bars for a symbol set.
    signals - A pandas DataFrame of signals (1, 0, -1) for each symbol.
    initial_capital - The amount in cash at the start of the portfolio."""

    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):
    	"""Creates a 'positions' DataFrame that simply longs or shorts
    	100 of the particular symbol based on the forecast signals of
    	{1, 0, -1} from the signals DataFrame."""
        positions = pd.DataFrame(index=signals.index).fillna(0.0)
        positions[self.symbol] = 100*signals['signal']
        return positions
                    
    def backtest_portfolio(self):
    	"""Constructs a portfolio from the positions DataFrame by 
    	assuming the ability to trade at the precise market open price
    	of each bar (an unrealistic assumption!). 

    	Calculates the total of cash and the holdings (market price of
    	each position per bar), in order to generate an equity curve
    	('total') and a set of bar-based returns ('returns').

    	Returns the portfolio object to be used elsewhere."""

    	# Construct the portfolio DataFrame to use the same index
    	# as 'positions' and with a set of 'trading orders' in the
    	# 'pos_diff' object, assuming market open prices.
        portfolio = self.positions*self.bars['Open']
        pos_diff = self.positions.diff()

        # Create the 'holdings' and 'cash' series by running through
        # the trades and adding/subtracting the relevant quantity from
        # each column
        portfolio['holdings'] = (self.positions*self.bars['Open']).sum(axis=1)
        portfolio['cash'] = self.initial_capital - (pos_diff*self.bars['Open']).sum(axis=1).cumsum()

        # Finalise the total and bar-based returns based on the 'cash'
        # and 'holdings' figures for the portfolio
        portfolio['total'] = portfolio['cash'] + portfolio['holdings']
        portfolio['returns'] = portfolio['total'].pct_change()
        return portfolio

Это дает нам все, что нам нужно, чтобы создать кривую собственности на основе такой системы.главныйФункция:

if __name__ == "__main__":
    # Obtain daily bars of SPY (ETF that generally 
    # follows the S&P500) from Quandl (requires 'pip install Quandl'
    # on the command line)
    symbol = 'SPY'
    bars = Quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily")

    # Create a set of random forecasting signals for SPY
    rfs = RandomForecastingStrategy(symbol, bars)
    signals = rfs.generate_signals()

    # Create a portfolio of SPY
    portfolio = MarketOnOpenPortfolio(symbol, bars, signals, initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    print returns.tail(10)

Выход программы выглядит следующим образом. Ваш будет отличаться от выхода ниже в зависимости от диапазона дат, который вы выберете, и используемого случайного семена:

          SPY  holdings    cash  total   returns

Дата
2014-01-02 -18398 -18398 111486 93088 0.000097 2014-01-03 18321 18321 74844 93165 0.000827 2014-01-06 18347 18347 74844 93191 0.000279 2014-01-07 18309 18309 74844 93153 -0.000408 2014-01-08 -18345 -18345 111534 93189 0.000386 2014-01-09 -18410 -18410 111534 93124 -0.000698 2014-01-10 -18395 -18395 111534 93139 0.000161 2014-01-13 -18371 -18371 111534 93163 0.000258 2014-01-14 -18228 -18228 111534 93306 0.001535 2014-01-15 18410 18410 74714 93124 -0.001951

В этом случае стратегия потеряла деньги, что неудивительно, учитывая стохастическую природу прогнозирующего! Следующие шаги заключаются в создании объекта производительности, который принимает экземпляр портфеля и предоставляет список показателей производительности, на основе которых можно принять решение отфильтровать стратегию или нет.

Мы также можем улучшить объект портфеля, чтобы иметь более реалистичное управление затратами на транзакции (например, комиссионные и скольжение интерактивных брокеров). Мы также можем просто включить двигатель прогнозирования в объект Стратегии, который (надеюсь) принесет лучшие результаты. В следующих статьях мы рассмотрим эти концепции более подробно.


Больше