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

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

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

Работа с рыночными данными

Одной из задач при создании событийно ориентированной торговой системы является минимизация необходимости писать разный код для одних и тех же задач в контексте тестирования на исторических данных и для реальной торговли. В идеале, следует использовать единую методологию генерации сигналов и управления портфолио для каждого из этих случаев. Чтобы этого добиться, объект Strategy , который генерирует торговые сигналы ( Signals ), и объект Portfolio , который на их основе генерирует ордера ( Orders ), должны использовать один интерфейс доступа к рыночным данным как в контексте исторического тестирования, так и работы в реальном времени.

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

Среди таких подклассов могут быть HistoricCSVDataHandler , QuandlDataHandler , SecuritiesMasterDataHandler , InteractiveBrokersMarketFeedDataHandler и так далее. Здесь мы рассмотрим только создание обработчика CSV с историческими данными, который будет загружать соответствующий CSV-файл финансовых данных внутри дня в формате баров (значения цены Low, High, Close, а также объем торгов Volume и открытый интерес OpenInterest). На основе этих данных при каждом «ударе сердца» системы (heartbeat) можно уже проводить углубленный анализ компонентами Strategy и Portfolio , что позволит избежать различных искажений.

На первом шаге нужно импортировать требуемые библиотеки, в частности pandas и abstract base class. Поскольку DataHandler генерирует события MarketEvents, нужно также импортировать и event.py:

DataHandler — это абстрактный базовый класс (АБК), что означает невозможность создания экземпляра напрямую. Это можно сделать только с помощью подклассов. Обоснование этого заключается в том, что АБК, предоставляет интерфейс для подлежащих подклассов DataHandler, который они должны использовать, что позволяет добиться совместимости с другими классами, с которыми может осуществляться взаимодействие.

Чтобы Python «понял», что имеет дело с абстрактным базовым классом, мы будем использовать свойство _metaclass_ . Также с помощью декоратора @abstractmethod указывается, что метод будет переопределен в подклассах (в точности аналогично полностью виртуальному методу в C++).

Два интересующих нас метода — это get_latest_bars и update_bars . Первый из них возвращает последние N баров из текущей временной метки «удара сердца» системы, что полезно для осуществления вычислений для классов Strategy . Последний метод предоставляет механизм анализа для наложения информацию бара на новую структуру данных, что полностью позволяет избавиться от прогнозных искажений. Если произойдет попытка созданий экземпляра класса, возникнет исключение:

После описания класса DataHandler следующим шагом является создание обработчика для исторических CSV-файлов. HistoricCSVDataHandler будет брать множество CSV-файлов (по одному для каждого финансового инструмента) и конвертировать их в словарь фреймов DataFrames для pandas.

Обработчику нужно несколько параметров — очередь событий ( Event Queue ), в которую публиковать рыночную информацию MarketEvent , абсолютный путь к CSV-файлам и список инструментов. Вот так выглядит инициализация класса:

Он будет пытаться открыть файлы в формате “SYMBOL.csv”, в которым SYMBOL — это тикер инструмента. Использованный здесь формат совпадает с предлагаемым поставщиком данных DTN IQFeed, но его легко можно модифицировать для работы с другими форматами. Открытие файлов обрабатывается методом _open_convert_csv_files.

Одно из преимуществ использования пакета pandas для хранения данных внутри HistoricCSVDataHandler заключается в том, что индексы всех отслеживаемых инструментов можно слить воедино. Это позволяет интерполировать даже отсутствующие данные, что полезно для побарового сравнения инструментов (бывает нужно в стратегиях mean reversion). При комбинировании индексов для инструментов используются методы union и reindex :

Метод _get_new_bar создает генератор для создания форматированной версии данных в барах. Это означается, что последующие вызовы метода результируются в новом баре (и так до того момента, пока не будет достигнут конец строки данных по инструментам):

Первый абстрактный метод из DataHаndler , который нужно реализовать — это get_latest_bars . Он просто выводит список последних N баров из структуры latest_symbol_data . Установка N = 1 позволяет получать текущий бар:

Последний метод — update_bars , это второй абстрактный метод из DataHandler . Он генерирует события ( MarketEvent ), которые попадают в очередь, как последние бары добавляются в latest_symbol_data :

Таким образом, у нас есть DataHandler — выделенный объект, который используется остальными компонентами системы для отслеживания рыночных данных. Для работы объектам Stragety , Portfolio и ExecutionHandler требуется текущая рыночная информация, поэтому имеет смысл работать с ней централизованно, чтобы избежать возможного дублировани хранения.

От информации до торгового сигнала: стратегия

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

Иерархия стратегии относительно проста — она состоит из абстрактного базового класса с единственным виртуальным методом для создания объектов SignalEvents . Для создания иерархии стратегии необходимо импортировать NumPy, pandas, объект Queue, инструмент abstract base tools и SignalEvent:

Абстрактный базовый класс Strategy определяет виртуальный метод calculate_signals . Он используется для обработки создания объектов SignalEvent на основе обновлений рыночных данных:

Определение абстрактного базового класса Strategy довольно проста. Первый пример использования подклассов в объекте Strategy заключается в использовании стратегий buy и hold и создании соответствующего класса BuyAndHoldStrategy . Он будет покупать конкретную акцию в определенный день и удерживает позицию. Таким образом на одну акцию генерируется только один сигнал.

Конструктор ( __init__ ) требует наличия обработчика рыночных данных bars и объекта очереди событий events :

При инициализации стратегии BuyAndHoldStrategy в словаре bought содержится набор ключей для каждого инструмента, которые установлены в False. Когда определенный инструмент покупается (открывается длинная позиция), то ключ переводится в положение True. Это позволяет объекту Strategy понимать, открыта ли позиция:

Виртуальный метод calculate_signals имплементирован именно в этом классе. Метод проходит по всем инструментам в списке и получает последний бар из обработчика bars. Затем он проверяет, был ли инструмент «куплен» (находимся ли мы в рынке по нему, или нет), а затем создается сигнальный объект SignalEvent . Затем он помещается в очередь событий, а словарь bought обновляется соответствующей информацией (True для купленного инструмента):

Это очень простая стратегия, но ее достаточно для того, чтобы продемонстрировать природу иерархии событийно ориентированной стратегии. В следующей статьей мы рассмотрим более сложные стратегии, например, парную торговлю. Также в следующей статье речь пойдет о создании иерархии Portfolio , которая будет отслеживать прибыль и убыток по позициям (profit and loss, PnL).

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

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

📎📎📎📎📎📎📎📎📎📎