В предыдущих статьях мы говорили о том, что такое событийно-ориентированная система бэктестинга, разобрали иерархию классов, необходимую для ее функционирования, обсудили то, как подобные системы используют рыночные данные, а также осуществляют отслеживание позиций и генерацию приказов на покупку.
Сегодня речь пойдет об исполнении ордеров с помощью создания иерархии классов, которая будет представлять симулированный механизм обработки приказов, связанный с брокерской системой или другим интерфейсом доступа на рынок. Также мы рассмотрим метрики для оценки производительности тестируемой стратегии.
Иерархия классов для обработки приказов
Компонент
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 и наши партнеры проводят онлайн-курсы по данной тематике.