Ранее в нашем блоге на Хабре мы рассматривали различные этапы разработки торговых систем (есть и онлайн-курсы по теме), среди которых одним из наиболее важных является тестирование на исторических данных (бэктестинг). Сегодня речь пойдет о практической релизации событийно-ориентированного бэктест-модуля с помощью Python.
Событийно-ориентированный софт
Прежде, чем погрузиться в разработку бэктестера, следует разобраться с понятием событийно-ориентированных систем. Одним из наиболее очевидных примеров подобных программ являются компьютерные игры. В видеоигре есть множество компонентов, которые взаимодействуют друг с другом в режиме реального времени с высоким фреймрейтом. Справляться с нагрузкой помогает осуществление всех вычислений внутри «бесконечной» петли, которую еще называют петлей событий или игровой петлей.
На каждом тике петли вызывается функция для получения последнего события, которое было сгенерировано каким-либо действием в игре. В зависимости от природы этого события (нажатие клавиши, клик мыши) предпринимается последующее действие, которое либо прерывает петлю, либо создает дополнительные события, и процесс продолжается. Проиллюстрировать все это можно таким псевдокодом:
while True: # Петля продолжается бесконечно
new_event = get_new_event() # Получаем последнее событие
# В зависимости от типа события выполняем действие
if new_event.type == "LEFT_MOUSE_CLICK":
open_menu()
elif new_event.type == "ESCAPE_KEY_PRESS":
quit_game()
elif new_event.type == "UP_KEY_PRESS":
move_player_north()
# ... and many more events
redraw_screen() # Обновляем экран для отображения соответствующей анимации
tick(50) # Ждем 50 миллисекунд
Код будет снова и снова проверять наличие новых событий и выполнять действия на их основе. В частности, благодаря этому создается иллюзия ответа в реальном времени. Как станет понятно далее, это — как раз то, что нам нужно для запуска симуляции высокочастотного трейдинга.
Почему именно событийно-ориентированный бэктестер
Событийно-ориентированные системы обладают рядом преимуществ перед векторизированным подходом:
- Повторное использование кода. Благодаря своей природе событийно-ориентированный модуль тестирования может быть использован как для работы с историческими данными, так и при реальной торговле на бирже при необходимости лишь минимальной «доводки» компонентов. В случае векторизированных бэктестеров нам необходимо иметь весь набор данных сразу для проведения статистического анализа.
- Предугадывание искажений. Событийно-ориентированные бэктестеры воспринимают рыночные данные, в качестве «событий», на которые нужно как-то реагировать. Таким образом, можно «скормить» модулю информацию, реакция на которую будет максимально соответствовать тому, что будет наблюдаться впоследствии в реальной торговле.
- Реализм. Событийно-ориентированные бэктестеры позволяют значительно кастомизировать процесс выполнения ордеров и оптимизировать транзакционные издержки. Важно уметь работать с базовыми типами ордеров (market, limit) и более сложными (market-on-open, MOO и market-on-close, MOC) — таким образом можно создаться «кастомный» обработчик исключений.
Однако не все так безоблачно, и у событийно-ориентированных систем есь свои недостатки. Во-первых, их значительно сложнее создавать и тестировать — больше «подвижных частей», а значит и больше багов. Поэтому для их создания рекомендуется применять разработку через тестирование. Во-вторых, они работают медленнее векторизированных систем.
Обзор бэктестера
Чтобы применить событийно-ориентированный подход, прежде всего необходимо разобраться с частями нашей системы, которые будут отвечать за определенные участки работы:
- Событие (
event
) — это фундаментальная единица класса событийно-ориентированной системы. Содержит тип (например, «MARKET», «SIGNAL», «ORDER» или «FILL»), который влияет на то, как событие будет обрабатываться в петле. - Очередь событий (
event queue
) — in-memory-объект Python, который хранит все объекты подклассовEvent
, сгенерированные остальными частями системы. DataHandler
— это абстрактный базовый класс (АБК), который представляет собой интерфейс для обработки исторических и текущих рыночных данных. Это повышает гибкость системы, поскольку модули стратегии и управления портфолио могут быть использованы как для тестирования на исторических данных так и для работы на «живом» рынке. DataHandler генерирует событиеMarketEvent
при каждом «ударе сердца» (heartbeat) системы.- Модуль стратегии (
Strategy
) — еще один АБК, который представляет собой интерфейс для забора рыночных данных и генерации на их основе сигнальных событий (SignalEvent
), которые используются объектомPortfolio
.SignalEvent
содержит символ биржевого тикера, направление ордера (Long, Short) и временную метку. Portfolio
— также АБК, отвечающий за обработку приказов, связанных с текущей и последующими позициями, подразумевающимися стратегией (Strategy
). Сюда же входит риск-менеджмент портфолио, включая контроль размеров позиций и анализ секторов рынка. В более сложных реализациях эта часть работы может быть передана классуRiskManagement
.Portfolio
беретSignalEvent
из очереди и генерирует события ордеров (OrderEvent
), которые также попадают в очередь.ExecutionHandler
— в нашем случае симулирует соединение с брокерской системой. Задача обработчика заключается в том, чтобы брать события OrderEvent из очереди, выполнять их (в режиме симуляции или через реальное подключение к брокеру). Когда ордер выполнен, обработчик создает событиеFillEvent
, которое описывает транзакцию, включая комиссии брокера и биржи, а также проскальзывание (если оно учитывается в модели).- Петля (
Loop
) — все описанные компоненты включены в петлю событий, которая обрабатывает все типы событий, направляя их к соответствующему модулю системы.
Выше мы описали базовую модель торгового дивжка, которую можно усложнять и расширять по многим направлениям, например, в области работы модуля
Portfolio
. Кроме того, можно вынести разные модели транзакционных издержек в отдельную иерархию классов. В нашем случае, однако, это только создаст лишние сложности, поэтому мы будем лишь постепенно привносить в систему больше реализма.Ниже представлен кусок кода на Python, который демонстрирует практическую работу бэктестера. В коде возникают две петли. Внешняя петля используется для придания бэктестеру сердцебиения (heartbeat). В онлайн-трейдинге это означает частоту, с которой происходит запрос рыночных данных. Для стратегий тестирования на исторических данных — это не обязательный компонент, поскольку рыночные данные вливаются в систему по частям — см. строку
bars.update_bars()
.Внутренняя петля нужна для обработки событий из объекта Queue. Конкретные события делегируются соответствующим компонентам в очередь последовательно добавляются новые события. Когда очередь пустеем петля сердцебиения делает новый виток:
# Объявление компонентнов с соответствующими параметрами
bars = DataHandler(..)
strategy = Strategy(..)
port = Portfolio(..)
broker = ExecutionHandler(..)
while True:
# Обновляем бары (код для бэктестинга, а не живой торговли)
if bars.continue_backtest == True:
bars.update_bars()
else:
break
# Обрабатываем события
while True:
try:
event = events.get(False)
except Queue.Empty:
break
else:
if event is not None:
if event.type == 'MARKET':
strategy.calculate_signals(event)
port.update_timeindex(event)
elif event.type == 'SIGNAL':
port.update_signal(event)
elif event.type == 'ORDER':
broker.execute_order(event)
elif event.type == 'FILL':
port.update_fill(event)
# следующий удар сердца через 10 минут
time.sleep(10*60)
Классы событий
В описанной схеме есть четыре типа событий:
MarketEvent
— инициируется, когда внешняя петля начинает новый «удар сердца». Оно возникает, когда объект DataHandler получает новое обновление рыночных данных для любых отслеживаемых финансовых инструментов. Оно используется для того, чтобы запустить генерацию торговых сигналов объектомStrategy
. Объект события содержит идентификатор того, что это рыночное событие, и никакой другой структуры.SignalEvent
— объектStrategy
использует рыночную информацию для создания нового сигнального событияSignalEvent
. Это событие содержит символ тикера, временную метку генерации и направление ордера (long или short). Такие сигнальные события используются объектомPortfolio
в качестве своеобразных подсказок на тему того, как торговать.OrderEvent
— когда объектPortfolio
получаетSignalEvent
, он использует такие события для более широкого контекста портфолио (расчет рисков и размера позиции). Все это приводит к созданиюOrderEvent
, который затем посылается вExecutionHandler
.FillEvent
— когда ExecutionHandler получаетOrderEvent
, он обязан его выполнить. После того, как произошла транзакция, создается событиеFillEvent
, которое описывает стоимость покупки или продажи и траназкционные издержки (проскальзывания, комиссии и т.п.)
Родительский класс называется
Event
— это базовый класс, который не предоставляет никакой функциональности или специального интерфейса. В дальнейших реализациях класс Event
с большой долей вероятности станет сложнее, поэтому стоит предусмотреть такую возможность заранее, создав иерархию классов:# event.py
class Event(object):
"""
Event — это базовый класс, обеспечивающий интерфейс для последующих (наследованных) событий, которые активируют последующие события в торговой инфраструктуре.
"""
pass
MarketEvent
наследует от Event
и несет в себе чуточку больше, чем простая самоидентификация типа ‘MARKET’:# event.py
class MarketEvent(Event):
"""
Обрабатывает событие получние нового обновления рыночной информации с соответствущими барами.
"""
def __init__(self):
"""
Инициализирует MarketEvent.
"""
self.type = 'MARKET'
SignalEvent
требует наличия символа тикера, временной метки и направления ордера, которые объект портфолио может использовать в качестве «совета» при торговле:# event.py
class SignalEvent(Event):
"""
Обрабатывает событие отправки Signal из объекта Strategy. Его получает объект Portfolio, который предпринимает нужное действие.
"""
def __init__(self, symbol, datetime, signal_type):
"""
Инициализирует SignalEvent.
Параметры:
symbol - Символ тикера, например для Google — 'GOOG'.
datetime - временная метка момента генерации сигнала.
signal_type - 'LONG' или 'SHORT'.
"""
self.type = 'SIGNAL'
self.symbol = symbol
self.datetime = datetime
self.signal_type = signal_type
OrderEvent
сложнее, чем SignalEvent
, и содержит дополнительное поле для указания количества единиц финансового инструмента в ордере. Количество определяется ограничениями объекта Portfolio
. Вдобавок OrderEvent
содержит метод print_order()
, который используется для вывода информация в консоль при необходимости:# event.py
class OrderEvent(Event):
"""
Обрабатывает событие отправки приказа Order в торговый движок. Приказ содержит тикер (например, GOOG), тип (market или limit), количество и направление.
"""
def __init__(self, symbol, order_type, quantity, direction):
"""
Инициализирует тип приказа (маркет MKT или лимит LMT), также устанавливается число единиц финансового инструмента и направление ордера (BUY или SELL).
Параметры:
symbol - Инструмент, сделку с которым нужно осуществить.
order_type - 'MKT' или 'LMT' для приказов Market или Limit.
quantity - Не-негативное целое (integer) для определения количества единиц инструмента.
direction - 'BUY' или 'SELL' для длинной или короткой позиции.
"""
self.type = 'ORDER'
self.symbol = symbol
self.order_type = order_type
self.quantity = quantity
self.direction = direction
def print_order(self):
"""
Выводит значения, содержащиеся в приказе Order.
"""
print "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" % (self.symbol, self.order_type, self.quantity, self.direction)
FillEvent
— это Event
повышенной сложности. Оно содержит временную метку исполнения приказа, тикер и информациб о бирже, на которой он был исполнен, количество единиц финансового инструмента (акций, фьючерсов и т.п.), фактическую цену сделки и сопутствующие комиссии.Сопутствующие издержки вычисляются с помощью API брокерской системы (у ITinvest есть свой API-интерфейс). В нашем примере используется система американского брокера, комиссия которого составляет минимум $1.30 с ордера с единой ставкой от $0,013 или $0,08 за акцию в зависимости от того, превышает ли количество акций 500 единиц или нет.
# event.py
class FillEvent(Event):
"""
Инкапсулирует понятие исполненного ордера (Filled Order), возвращаемое брокером.
Хранит количество единиц инструмента, которые были куплены/проданы по конкретной цене.
Также хранит комиссии сделки.
"""
def __init__(self, timeindex, symbol, exchange, quantity,
direction, fill_cost, commission=None):
"""
Инициализирует объек FillEvent.
Устанавливает тикер, биржевую площадку, количество, направление, цены и (опционально) комиссии.
Если информация о комиссиях отсутствиет, то объект Fill вычислит их на основе объема сделки
и информации о тарифах брокерах (полученной через API)
Параметры:
timeindex - Разрешение баров в момент выполнения ордера.
symbol - Инструмент, по которому прошла сделка.
exchange - Биржа, на которой была осуществлена сделка.
quantity - Количество единиц инструмента в сделке.
direction - Направление исполнения ('BUY' или 'SELL')
fill_cost - Размер обеспечения.
commission - Опциональная комиссия, информация отправляемая бркоером.
"""
self.type = 'FILL'
self.timeindex = timeindex
self.symbol = symbol
self.exchange = exchange
self.quantity = quantity
self.direction = direction
self.fill_cost = fill_cost
# Calculate commission
if commission is None:
self.commission = self.calculate_ib_commission()
else:
self.commission = commission
def calculate_ib_commission(self):
"""
Вычисляет издержки торговли на основе данных API брокера (в нашем случае, американского, т.е. цены в долларах).
Не включает комиссии биржи.
"""
full_cost = 1.3
if self.quantity <= 500:
full_cost = max(1.3, 0.013 * self.quantity)
else: # Greater than 500
full_cost = max(1.3, 0.008 * self.quantity)
full_cost = min(full_cost, 0.5 / 100.0 * self.quantity * self.fill_cost)
return full_cost
На сегодня все, спасибо за внимание. В следующей части мы поговорим об использовании рыночной информации (класс
DataHandler
) для тестирования на исторических данных и при реальной торговле.Продолжение следует…
Комментарии (3)
avk1651
07.08.2015 14:39Знаю, что перевод (кстати, спасибо огромное переводчику), но метод calculate_ib_commission выглядит чужеродно в FillEvent. Лучше добавить абстракцию типа Brokerage, которая бы считала вознаграждение. Это даст возможность проверять стратегии в брокерах с разными условиями.
lol_wat
Интересно, спасибо за перевод.
itinvest Автор
Спасибо, что читаете. Скоро будет продолжение!