[Перевод] Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 4
В предыдущих статьях мы говорили о том, что такое событийно-ориентированная система бэктестинга, разобрали иерархию классов, необходимую для ее функционирования, обсудили то, как подобные системы используют рыночные данные, а также осуществляют отслеживание позиций и генерацию приказов на покупку.
Сегодня речь пойдет об исполнении ордеров с помощью создания иерархии классов, которая будет представлять симулированный механизм обработки приказов, связанный с брокерской системой или другим интерфейсом доступа на рынок. Также мы рассмотрим метрики для оценки производительности тестируемой стратегии.
Иерархия классов для обработки приказов
Компонент 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)
На этом мы закончили разработку иерархий классов для нашего бэктестера. Теперь поговорим о том, как высчитывать метрики производительности для тестируемой стратегии.
Метрики производительности
В одной из статей Майк Халлс-Мур останавливался на понятии коэффициента Шарпа. Подсчитать его можно по следующей формуле:
Где 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 и наши партнеры проводят онлайн-курсы по данной тематике.