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


  1. lol_wat
    23.07.2015 12:43

    Интересно, спасибо за перевод.


    1. itinvest Автор
      23.07.2015 12:47

      Спасибо, что читаете. Скоро будет продолжение!


  1. avk1651
    07.08.2015 14:39

    Знаю, что перевод (кстати, спасибо огромное переводчику), но метод calculate_ib_commission выглядит чужеродно в FillEvent. Лучше добавить абстракцию типа Brokerage, которая бы считала вознаграждение. Это даст возможность проверять стратегии в брокерах с разными условиями.