[Перевод] Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 4

af33ffd497c1458cac21a2724bd2cb76.jpg

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

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

Иерархия классов для обработки приказов


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

Как и с использованными ранее иерархиями абстрактных базовых классов, необходимо импортировать нужные сущности и декораторы из библиотеки abc. Также необходимо импортировать FillEvent и OrderEvent<code>: <source lang="python"> # execution.py import datetime import Queue from abc import ABCMeta, abstractmethod from event import FillEvent, OrderEvent</source> <code>ExecutionHandler похож на использованные ранее абстрактные базовые классы и содержит один полностью виртуальный метод execute_order:

# execution.py

class ExecutionHandler(object):
    """
    Абстрактный класс ExecutionHandler обрабатывает взаимодействие между набором объектов приказов, сгенерированных Portfolio и полным набором объектов Fill, которые возникают на рынке.


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

    __metaclass__ = ABCMeta

    @abstractmethod
    def execute_order(self, event):
        """
        Берет событие Order и выполняет его, получая событие Fill, которе помещается в очередь Events. 

        Параметры:
        event - Содержит объект Event с информацией о приказе
        """
        raise NotImplementedError("Should implement execute_order()")


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

Учтем, что событие FillEvent содержит значение fill_cost равное None (см. предпоследнюю строку в execute_order), поскольку мы уже позаботились о цене исполнения в объекте NaivePortfolio (он описан в прошлой статье). В более реалистичной реализации мы бы использовали значение рыночных данных “value”, чтобы получить реальную стоимость сделки.

В нашем примере для тестирования используется биржа ARCA. Для реальной торговли конечная биржа будет оказывать важное влияние.

# execution.py

class SimulatedExecutionHandler(ExecutionHandler):
    """
    Симулированный обработчик конвертирует все объекты приказов в их эквивалентные объекты Fill, автоматически и без задержки, проскальзывания и т.п. Это позволяет быстро протестировать стратегию в первом приближении перед разработкой более сложных реализаций обработчиков.

    """
    
    def __init__(self, events):
        """
        Инициализирует обработчик, устанавливает внутренние очереди событий. 

        Параметры:
        events - Очередь событий Event.
        """
        self.events = events

    def execute_order(self, event):
        """
        Просто наивно конвертирует объекты Order в объекты Fill, то есть не учитывается задержка, проскальзывание или количество акций в приказе, которые можно купить/продать по заданной цене.

        Параметры:
        event - Содержит объект Event с информацией о приказе.
        """
        if event.type == 'ORDER':
            fill_event = FillEvent(datetime.datetime.utcnow(), event.symbol,
                                   'ARCA', event.quantity, event.direction, None)
            self.events.put(fill_event)


На этом мы закончили разработку иерархий классов для нашего бэктестера. Теперь поговорим о том, как высчитывать метрики производительности для тестируемой стратегии.

Метрики производительности


В одной из статей Майк Халлс-Мур останавливался на понятии коэффициента Шарпа. Подсчитать его можно по следующей формуле:

8723681aa02a42e5b9b54517e4ebf01d.png

Где Ra это поток возврата кривой капитала, а Rb — это бенчмарк, например показатель интереса или индекс.

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

В нашем бэктестере будут использоваться коэффициент Шарпа и показатели максимальной просадки и ее длительности.

Реализация на Python


Для начала нужно создать файл performance.py, который хранит функции для подсчета коэффициента Шарпа и информацию о просадке. Как и в случае других наших классов, требующих большого объёма вычислений, нужно импортировать NumPy и Pandas:

# performance.py

import numpy as np
import pandas as pd


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

Обычно это число устанавливается на уровне 252 — количество торговых дней в США. Однако, если тестируемая стратегия предполагает торговлю на часовых интервалах, то нужно соответственно изменить коэффициент Шарпа, чтобы получить корректное значение для года. В данном случае нужно установить период так: 252 ∗ 6.5 = 1638 (количество торговых часов в США за год). Если торговля идет на минутном интервале, то нужно еще умножить все на 60: 252∗6.5∗60 = 98280.

Функция create_sharpe_ratio оперирует объектом библиотеки Pandas Series под названием returns и просто подсчитывает отношение среднего значения процента прибыли и стандартного отклонения прибыли в процентах с учетом разного числа торговых периодов.

# performance.py

def create_sharpe_ratio(returns, periods=252):
    """
    Создает коэффициент Шарпа для стратегии, основанной на бенчмарке ноль (нет информации о рисках ).

    Параметры:
    returns -  Series из Pandas представляет процент прибыли за период - Дневной (252), Часовой (252*6.5), Минутный (252*6.5*60) и т.п..
    """
    return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)


Коэффициент Шарпа описывает, насколько большой риск (определенный стандартным отклонение цены активов) берется при работе с единицей инвестирования, а просадка определяется как наибольшее снижение объёма средства с максимума до минимума.

Функция create_drawdowns, представленная ниже, представляет оба показателя — максимальная просадка и максимальная длительность просадки. В определении длительности просадки есть тонкость — при ее определении нельзя оперировать общими понятиями типа «день», учитываются только торговые периоды.

Функция начинает работу с создания двух объектов pandas Series, представляющих просадку и длительность на каждом торговом баре. Затем устанавливается текущий показатель HWM (high water mark) — он означает, что объём капитала превышает предыдущие максимумы.

Просадка будет являться разницей между текущим HWM и кривой капитала. Если значение отрицательно, то длительность увеличивается для каждого бара, пока наблюдается это явление, до достижения следующего HWM. Затем функция возвращает максимум каждой из двух серий:

# performance.py

def create_drawdowns(equity_curve):
    """
    Вычисляет крупнейшее падение от пика до минимума кривой PnL и его длительность. Требует возврата  pnl_returns в качестве pandas Series.
 
    Параметры:
    pnl - pandas Series, представляющая процент прибыли за период. 

    Прибыль:
    drawdown, duration - Наибольшая просадка и ее длительность 
    """

    # Подсчет общей прибыли 
    # и установка High Water Mark
    # Затем создаются серии для просадки и длительности
    hwm = [0]
    eq_idx = equity_curve.index
    drawdown = pd.Series(index = eq_idx)
    duration = pd.Series(index = eq_idx)

    # Цикл проходит по диапазону значений индекса
    for t in range(1, len(eq_idx)):
        cur_hwm = max(hwm[t-1], equity_curve[t])
        hwm.append(cur_hwm)
        drawdown[t]= hwm[t] - equity_curve[t]
        duration[t]= 0 if drawdown[t] == 0 else duration[t-1] + 1
    return drawdown.max(), duration.max()


Для того, чтобы применить эти метрики производительности, нужны средства их подсчета после тестирования на исторических данных. Также необходимо связать вычисления с определенным объектом в иерархии. Учитывая, что метрики производительности вычисляются на базе портфолио, имеет смысл применить вычисления для метода в иерархии классов Portfolio, которую мы обсуждали в прошлых статьях.

Прежде всего нужно открыть portfolio.py и импортировать функции производительности:

# portfolio.py

..  # импорт других функций

from performance import create_sharpe_ratio, create_drawdowns


Поскольку Portfolio — это абстрактный базовый класс, то добавить метод нужно к одному из его производных классов. В данном случае это NaivePortfolio. Таким образом мы создадим метод под названием output_summary_stats — он будет работать с кривой доступных средств портфолио для генерирования коэффициента Шарпа и информации по просадке.

Метод довольно прост. Он использует две метрики производительности и применяет их напрямую к кривой капитала в датафрейме pandas, а затем выводит статистику в качестве отформатированного списка:

# portfolio.py

..
..

class NaivePortfolio(object):

    ..
    ..

    def output_summary_stats(self):
        """
        Создает список статистических показателей для портфолио — коэффициент Шарпа и данные по просадке. 
        """
        total_return = self.equity_curve['equity_curve'][-1]
        returns = self.equity_curve['returns']
        pnl = self.equity_curve['equity_curve']

        sharpe_ratio = create_sharpe_ratio(returns)
        max_dd, dd_duration = create_drawdowns(pnl)

        stats = [("Total Return", "%0.2f%%" % ((total_return - 1.0) * 100.0)),
                 ("Sharpe Ratio", "%0.2f" % sharpe_ratio),
                 ("Max Drawdown", "%0.2f%%" % (max_dd * 100.0)),
                 ("Drawdown Duration", "%d" % dd_duration)]
        return stats


Данный анализ производительности является сильно упрощенным. Он не учитывает аналитику на уровне сделок или другие измерения соотношения риска и прибыли. Однако его довольно просто расширить и добавить дополнительные методы в performance.py с последующим внедрением их в output_summary_stats.

Продолжение следует…

P. S. Ранее в нашем блоге на Хабре мы уже рассматривали различные этапы разработки торговых систем. ITinvest и наши партнеры проводят онлайн-курсы по данной тематике.

© Habrahabr.ru