Статистический арбитраж на основе коинтегрированных акций (заключительная часть): Анализ данных с помощью специализированной БД
Введение
В начале 2025 года мы приняли вызов — разработать систему статистического арбитража для рядового трейдера, того одинокого воина, вооруженного лишь обычным ноутбуком, стандартным подключением к Интернету и небольшим торговым капиталом. С тех пор мы опубликовали десять статей, в которых описали наши исследования и эксперименты, а также успехи и неудачи наших попыток обыграть рынок с помощью математики.
Все эти статьи были написаны с целью дать читателю доступное введение в тему статистического арбитража с точки зрения трейдера. В совокупности они должны составить минимальный набор знаний, необходимый обычному человеку, имеющему среднее математическое образование и базовые навыки программиста, для того, чтобы начать понимать и реализовывать стратегии статистического арбитража.
Тесты на корреляцию, коинтеграцию и стационарность были рассмотрены в самом начале серии, поскольку эти темы являются основополагающими. Там мы рассмотрели тесты коинтеграции Энгл-Грейнджера и Йохансена, а также расширенный тест Дики — Фуллера (ADF) и тест стационарности Квятковского-Филлипса-Шмидта-Шина (KPSS). Зная их назначение, интерпретацию и значение, мы могли бы с самого начала приступить к формированию наших парных и корзинных портфелей. Использование статистических библиотек Python с открытым исходным кодом и профессионального уровня, таких как statsmodels, избавило нас от необходимости углубляться в математику, лежащую в основе этих тестов, и позволило сосредоточиться на их применении на высоком уровне.
Конечно, когда мы говорим о статистическом арбитраже, мы имеем в виду анализ данных и базы данных. Поскольку в MetaTrader 5 встроена база данных SQLite, мы позаботились и об этом. В третьей части этой серии мы рассмотрели настройку базы данных, предложили начальную схему базы данных и показали, как поддерживать её актуальность с помощью сервиса MQL5. Внедрение данной службы стало первым шагом, который мы предприняли для отделения анализа данных от нашей торговой среды. В этой заключительной статье мы завершим это разделение торгового контура и аналитического контура.
После того как мы создали базу данных и наладили стабильный процесс проверки на коинтеграцию и стационарность, следующим логичным шагом стало формирование портфелей. Поэтому в следующих трёх статьях мы предложили минималистичную систему отбора и оценки, основанную на иерархии объективных критериев, таких как сила коинтеграции, стабильность весов портфеля и время, необходимое для возврата к среднему значению. В качестве гипотезы для нашего скрининга мы использовали коинтеграцию между акциями высоколиквидных компаний микропроцессорной отрасли, котирующихся на бирже Nasdaq, однако ее принципы применимы к любому другому коинтегрированному портфелю.
Наконец, в трех последних статьях мы рассмотрели некоторые широко используемые методы мониторинга сделок в реальном времени, такие как сравнение собственных векторов скользящих окон (RWEC) и ADF внутривыборочного/вневыборочного анализа (IS/OOS ADF), которые мы использовали в сочетании для выявления необходимости перебалансировки весов портфеля, а также тест Чау и оценку кумулятивной суммы квадратов (CUSUM) для выявления структурных разрывов в коинтегрированных отношениях.
Исследования и эксперименты, результаты которых мы изложили в этих десяти статьях, сыграли решающую роль в том, что за последние десять–пятнадцать лет барьер для входа на рынок статистического арбитража для рядового розничного трейдера значительно снизился. Перенеся акцент с скорости торговли, то есть общей скорости исполнения ордеров, на поиск практически безграничных возможностей, скрытых в неожиданных взаимосвязях между инструментами с разных рынков и на различных комбинациях таймфреймов, мы лишь подтвердили то предположение, которое у нас было, когда мы приняли этот вызов: в наши дни стратегии статистического арбитража вполне доступны для обычного розничного трейдера. Поскольку статистический арбитраж позволяет избежать непомерных затрат, характерных для сферы HFT, уже в 2026 году обычный розничный трейдер может находить весьма привлекательные возможности для применения таких стратегий.
Единственное, чего не хватает в описанном нами наборе инструментов, — это специализированная база данных, которая взяла бы на себя аналитическую нагрузку вместо SQLite, оставив нашей замечательной встроенной базе данных MetaTrader 5 те задачи, для которых она и была создана, — а именно обработку транзакций. Взаимодействие с EA — нашим так называемым «единым источником достоверной информации» о входе на рынок и выходе из него — должно по-прежнему осуществляться через SQLite, однако анализ данных, лежащий в основе нашей системы отбора и оценки, а также мониторинга торговых операций в режиме реального времени, должен выполняться специализированной системой. Последний этап этого введения в статистический арбитраж для розничных трейдеров должен подготовить нашу систему анализа данных к мониторингу больших массивов данных в режиме реального времени, а также сделать её перспективной, поскольку она сможет адаптироваться к расширению масштабов нашей деятельности без необходимости инвестиций в дорогостоящее оборудование.
Значение вычислительной мощности для статистического арбитража
Для любой торговой деятельности необходимы данные. Как минимум, для совершения сделки на покупку или продажу необходимо знать текущую цену актива. Как правило, нам потребуется немного больше данных; по крайней мере, история цен поможет понять, как менялась стоимость актива с течением времени. Вскоре мы начнём рассчитывать максимумы и минимумы, диапазоны, средние значения, средние истинные диапазоны, средние значения с учетом объёма, а также формировать графики ценовой истории. Имея больше данных, мы сможем лучше понять, как цена достигла своего нынешнего уровня. Если у нас будет достаточно данных, возможно, мы сможем также предположить, как цена может измениться в ближайшие часы, дни или месяцы. Мы можем использовать данные о ценах активов для построения и анализа свечных моделей, а также для создания более или менее сложных индикаторов, облегчающих визуализацию ценовой истории. Нередко бывает так, что для одного актива нам приходится собирать исторические данные о ценах за десятки месяцев.
Когда речь заходит о парной торговле, нам требуется как минимум вдвое больше данных. Нам необходимо постоянно рассчитывать средний спред, и нам потребуется ещё больше исторических данных, если мы хотим проверить, достаточно ли устойчива связь между двумя инструментами. Возможно, нам нужно рассчитать динамический порог стандартного отклонения с учетом волатильности. Таким образом, помимо увеличения объема данных, нам также требуется больше вычислительных ресурсов для загрузки данных по обоим символам, расчета спреда, среднего значения, порога стандартного отклонения, а теперь ещё и волатильности в режиме реального времени.
Объем данных и вычислительные мощности, необходимые для стратегий парной торговли, обычно составляют лишь небольшую часть того, что требуется для стратегий статистического арбитража. У нас есть корзины, состоящие из неограниченного числа акций, для которых необходимо рассчитать вектор коинтеграции и соответствующие веса портфеля; провести сравнение собственных векторов на скользящих окнах (RWEC) для проверки стабильности весов портфеля; выполнить тест Чоу с использованием кумулятивной суммы квадратов для выявления структурных разрывов; при этом все эти вычисления должны выполняться в режиме реального времени для мониторинга текущих торговых операций. В конечном итоге нам потребуется выполнять все эти расчёты параллельно для нескольких корзин, то есть нам нужно будет получать данные о ценах по десяткам инструментов и обрабатывать их как можно быстрее, чтобы успевать своевременно реагировать на сигналы, прорывы и рыночные сбои.
Когда Джим Саймонс начал применять стратегии статистического арбитража в конце 80-х годов, вычислительные мощности, необходимые для обработки огромных объемов данных, которые они собирали ежедневно, были очень дорогими. Расходы на оборудование и специалистов, необходимых для разработки программного обеспечения и обеспечения бесперебойной работы системы, были непомерно высокими для любого розничного торговца, даже самого состоятельного. Препятствием для входа на рынок для розничных трейдеров были не высокоскоростные оптоволоконные соединения или размещение серверов в дата-центрах. В то время высокочастотная торговля (HFT) ещё не была распространена.
В конце 1980-х годов стоимость бессрочной лицензии на Oracle, Ingres или IBM составляла примерно 14–15 тысяч долларов. С поправкой на инфляцию эта сумма сегодня составила бы примерно сто тысяч долларов. Мы не учитываем расходы на персонал и оборудование. Это лишь приблизительная стоимость лицензии на программное обеспечение. Статистический арбитраж был прерогативой институциональных трейдеров, в основном из-за необходимой вычислительной мощности для анализа данных и высокой стоимости используемых для этого инструментов. Но в последующие годы ситуация кардинально изменилась, как мы увидим ниже.
На стандартном демо-счете MetaQuotes доступно около 9000 уникальных инструментов и более 20 таймфреймов.

Рис. 1. Скриншот, демонстрирующий количество символов, доступных на стандартном демо-счете MetaQuotes
Поскольку статистический арбитраж заключается в поиске взаимосвязей между ценами активов, в идеале нам следовало бы проверить все возможные комбинации всех доступных активов на всех доступных временных интервалах. Поиск взаимосвязей во всех возможных комбинациях привел бы к комбинаторному взрыву. Итак, мы выбираем пары и корзины в соответствии с системой скоринга.
До сих пор мы использовали только SQLite
Для удобства до сих пор мы использовали встроенный в MetaTrader 5 модуль SQLite в качестве нашей базы данных. Но не поймите меня неправильно. Наряду с удобством вы получаете самую популярную в мире базу данных — блестящее и надежное программное обеспечение, проверенное в реальных условиях и отличающееся исключительной надежностью. Помимо того, что она является частью MetaTrader 5, что позволяет избежать дополнительной установки и настройки, в платформе доступен нативный набор функций для работы с базами SQLite. Кроме того, в MetaEditor доступен встроенный графический интерфейс пользователя (GUI) для запросов и управления встроенными базами данных SQLite в MetaTrader 5.

Рис. 2. Снимок экрана, демонстрирующий графический интерфейс MetaEditor для работы с SQLite
Эти особенности сделали SQLite идеальным выбором для создания нашей платформы статистического арбитража, предназначенной для обычного розничного трейдера. Он надежен, масштабируем, встроен, поддерживает встроенные функции MQL5 и имеет графический интерфейс MetaEditor. В MetaTrader 5 это очень удобно в использовании. Но мы можем добиться лучших результатов, не отказываясь от SQLite. Мы можем использовать его по назначению, а для анализа данных — прибегнуть к помощи специализированной базы данных.
SQLite — это система OLTP (Online Transaction Processing), предназначенная для очень быстрой обработки небольших транзакций. SQLite справляется с этой задачей довольно хорошо, но когда речь заходит о специализированной базе данных для анализа данных, мы имеем в виду систему OLAP (Online Analytical Processing), то есть систему, предназначенную для проведения сложных и требующих интенсивных математических вычислений операций над историческими данными, как правило, над большими массивами данных. Хотя их функции в некоторых случаях могут пересекаться — то есть мы можем использовать систему OLAP для хранения и запроса ежедневных транзакционных данных, а систему OLTP — для анализа исторических данных, как мы это делали здесь, — это разные системы.
Чтобы различия между ними стали заметны, нам нужно провести анализ данных в полном объеме. До сих пор в наших примерах мы анализировали лишь крошечную часть истории. Обычно наши периоды ретроспективного анализа составляли не более двух-трех лет. Мы ни при каких обстоятельствах не анализировали исторические данные за последние десять или двадцать лет. Мы проверяли наличие коинтеграции в корзинах из трёх или четырёх символов, но никогда больше. Кроме того, в соответствии с нашей первоначальной гипотезой о коинтеграции в полупроводниковой отрасли мы ограничили нашу систему отбора и оценки одним временным интервалом (H4).
Однако если вы серьезно относитесь к статистическому арбитражу, даже будучи скромным рядовым трейдером, в какой-то момент вам придется анализировать более длительные исторические периоды в поисках закономерностей, выходящих за рамки коинтеграции; либо вам может понадобиться протестировать корзины из пяти, а может быть, и десяти активов в различных комбинациях с использованием нескольких таймфреймов, в конечном итоге включая нефинансовые данные (например, ставки фрахта или объемы производства сырьевых товаров); вероятно, этот анализ станет основой вашего мониторинга реальных торговых операций. Понимаете?
По мере того как вы начинаете использовать больше символов, больше временных интервалов и больше исторических данных, у вас внезапно появляется собственный массив данных, который необходимо обрабатывать как можно быстрее. Такой рост является естественным явлением в статистическом арбитраже. Предполагается, что ваши потребности в области обработки данных будут развиваться именно таким образом, и вам следует быть готовыми к поиску специализированных инструментов, которые помогут справиться с необходимостью обработки все больших объемов данных. Хорошая новость заключается в том, что в 2025 году вам не придётся, так сказать, продавать землю, чтобы купить лопату. Если еще недавно для доступа к высокопроизводительной системе OLAP приходилось приобретать дорогостоящую лицензию на программное обеспечение, то сегодня вы можете получить доступ к высококачественной системе, которая не только бесплатна, но и имеет открытый исходный код: DuckDB.
DuckDB: бесплатная система OLAP с открытым исходным кодом
Существует распространенное заблуждение, что стратегии статистического арбитража являются разновидностью высокочастотной торговли (HFT) или, по крайней мере, зависят от высокоскоростных соединений и маршрутизации ордеров с низкой задержкой. В результате многие считают, что в статистическом арбитраже доминируют институциональные трейдеры именно из-за этой предполагаемой зависимости от скорости. Это только отчасти верно.
Хотя многие стратегии статистического арбитража рассчитаны на работу на низких таймфреймах (обычно менее 5 минут), это не является обязательным условием. На более крупных таймфреймах вполне нормально обнаруживать корреляции, коинтеграции и другие статистические связи. Скорость имеет большое значение и лежит в основе многих стратегий, но это была не единственная причина, по которой институциональные игроки до недавнего времени монополизировали эту сферу торговли. Основной причиной стала высокая стоимость вычислительных ресурсов, необходимых для анализа данных, а также сложность их эксплуатации.
Чтобы лучше понять ситуацию, я провел небольшое исследование в Интернете и обнаружил, что всего пятнадцать лет назад «разрыв между розничным трейдером и хедж-фондом представлял собой «капитальную пропасть», основанную на собственном аппаратном и программном обеспечении OLAP». В середине 2000-х годов, помимо высоких финансовых затрат, существовал высокий технический барьер для входа на рынок. То есть, вложив от 100 000 до 250 000 долларов только на лицензии программного обеспечения, позволяющие осуществлять запросы к данным в системе OLAP, в некоторых случаях вам придётся также иметь дело со стойкой специализированных серверов и ежегодной платой за обслуживание в размере около 20 %. Технические барьеры для внедрения также включали предварительную обработку кубов OLAP — поскольку эти старые системы основывались на многомерной аналитике OLAP — и использование языка MDX (Multidimensional Expressions) для управления этими многомерными агрегатами и выполнения запросов к ним. В этих системах корпоративного уровня нельзя было использовать старый добрый SQL для сканирования необработанных файлов CSV или Parquet, как мы увидим ниже.
За последние пятнадцать лет разрыв между розничным трейдером и хедж-фондом значительно сократился. Хотя скорость по-прежнему вызывает опасения, а высокочастотная торговля (HFT) остаётся запретной зоной для розничных трейдеров, теперь мы можем запустить полнофункциональную систему OLAP прямо на своих ноутбуках. Неважно, проводим ли мы анализ на компьютере под управлением Mac, Windows или Linux: нам достаточно скачать исполняемый файл размером около 50 МБ, чтобы в мгновение ока приступить к анализу больших наборов данных на профессиональном уровне.
Совместимость форматов данных
Пожалуй, одной из главных особенностей DuckDB является способность взаимодействовать (и поддерживать совместимость) с наиболее распространенными базами данных, механизмами обработки массивов данных, облачными хранилищами и форматами файлов в сфере анализа данных. Это достигается с помощью расширений. После установки клиента вы можете ознакомиться с доступными расширениями. DuckDB имеет систему расширений, которую поддерживает основная команда и дополняют разработки сообщества. Это позволяет нам читать и записывать данные в:
- библиотеки и среды работы с датафреймами, такие как Pandas, Apache Arrow, NumPy и Polars;
- такие базы данных, как MySQL, Postgres и SQLite;
- сервисы облачного хранения данных, такие как Amazon S3, Google Cloud Storage и Azure Blob Storage;
- Форматы файлов CSV, JSON, Parquet и Iceberg.
На момент написания этой статьи мне не известно ни об одной другой системе с открытым исходным кодом, обладающей такой гибкостью.
Простота в эксплуатации
DuckDB привносит в наш конвейер ту же простоту работы, что и SQLite. Подобно SQLite, DuckDB также представляет собой встроенную базу данных, работающую в рамках одного процесса и не требующую администрирования, которая не только упрощает анализ данных и повышает производительность, но и способствует совместимости между различными источниками данных и форматами файлов, упрощая агрегирование данных из нескольких разных источников, а также перенос данных из локальной среды в облако и наоборот.
Мы НЕ предлагаем замену SQLite. Вместо этого мы интегрируем DuckDB в нашу систему с четко очерченной сферой применения: аналитическая обработка данных. В то время как SQLite представляет собой универсальную реляционную базу данных, ориентированную на обработку транзакций, DuckDB — это специализированный инструмент OLAP. Взаимодействие с EA осталось прежним: используется встроенный SQLite и нативная библиотека MQL5.
Различия между OLAP и OLTP на практике
Чтобы лучше понять разницу между системой OLTP на базе SQLite и системой OLAP на базе DuckDB, давайте посмотрим, как они работают с типичными торговыми данными разного объема. Для этого у нас есть очень простой скрипт на Python, состоящий из трёх шагов, который сгенерирует синтетические данные по нескольким биржевым тикерам (символам) и выполнит над ними стандартную агрегацию, рассчитав среднюю цену по каждому тикеру. Это поможет нам увидеть архитектурные различия на практике.
Существуют клиентские пакеты DuckDB для нескольких языков программирования. Чтобы использовать его в Python, достаточно просто вызвать
pip install duckdb
У него нет внешних зависимостей, за исключением Windows, где требуется пакет C++ Redistributable, содержащий компоненты библиотеки выполнения.
После этого мы сможем просто импортировать его в наши скрипты, как любой другой пакет Python.
import sqlite3 import duckdb import pandas as pd import numpy as np import time
Чтобы продемонстрировать различия между SQLite и DuckDB при анализе данных, мы проведем упрощенный тест производительности на основе синтетических данных. Для начала мы сгенерируем миллион строк с синтетическими данными о цене и объеме торгов по восьми тикерам. С самой первой секунды 2023 года мы генерируем один миллион «тиков», что в сумме составляет 11,5 торговых дней. Обратите внимание, что это данные почти за полмесяца. В реальной жизни следует ожидать, что эти цифры будут на несколько порядков выше.
def generate_data(filename="finance_data.csv"): print("Generating synthetic data...") tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'NVDA', 'META', 'NFLX'] n_rows = 1_000_000 df = pd.DataFrame({ 'timestamp': pd.date_range(start='2023-01-01', periods=n_rows, freq='s'), 'ticker': np.random.choice(tickers, n_rows), 'price': np.random.uniform(100, 500, n_rows), 'volume': np.random.randint(1, 1000, n_rows) }) df.to_csv(filename, index=False) print(f"Created {filename} with {n_rows} rows.")
В результате будет сгенерирован датафрейм, содержащий миллион строк, который будет выглядеть следующим образом:
| временная метка | тикер | цена | объём |
|---|---|---|---|
| 1 января 2023 г. 00:00:00 | TSLA | 432,15 | 842 |
| 01.01.2023 00:00:01 | AAPL | 120,50 | 12 |
| 01.01.2023 00:00:02 | MSFT | 315,88 | 500 |
Таблица 1. Пример синтетических табличных данных для сравнительного тестирования SQLite и DuckDB
После сохранения этих табличных данных в формате CSV мы запрашиваем SQLite, работающий в памяти, чтобы он рассчитал среднюю цену для каждого тикера.
def benchmark_sqlite(filename): conn = sqlite3.connect(":memory:") # Using memory for a fair speed test cursor = conn.cursor() # Load data df = pd.read_csv(filename) df.to_sql("prices", conn, index=False) start_time = time.perf_counter() query = "SELECT ticker, AVG(price) FROM prices GROUP BY ticker" cursor.execute(query) results = cursor.fetchall() end_time = time.perf_counter() conn.close() return end_time - start_time
Мы передаем тот же файл CSV в DuckDB для выполнения того же вычисления, но на этот раз нам не нужно загружать данные, поскольку DuckDB может обращаться к файлу CSV напрямую.
def benchmark_duckdb(filename): conn = duckdb.connect(database=':memory:') start_time = time.perf_counter() query = f"SELECT ticker, AVG(price) FROM '{filename}' GROUP BY ticker" results = conn.execute(query).fetchall() end_time = time.perf_counter() return end_time - start_time if __name__ == "__main__": csv_file = "finance_data.csv" generate_data(csv_file) print("\nStarting Benchmarks...") sqlite_time = benchmark_sqlite(csv_file) print(f"SQLite execution time: {sqlite_time:.4f} seconds") duckdb_time = benchmark_duckdb(csv_file) print(f"DuckDB execution time: {duckdb_time:.4f} seconds") speedup = sqlite_time / duckdb_time print(f"\nDuckDB was {speedup:.1f}x faster than SQLite for this query.")
Этот скрипт прилагается здесь под именем bench.py. После обработки миллиона строк у вас должен получиться файл CSV объёмом около 50 МБ. На ноутбуке низкого уровня с двумя ядрами я обнаружил следующую зависимость между временем выполнения запросов в SQLite и DuckDB.
Generating synthetic data... Created finance_data.csv with 1000000 rows. Starting Benchmarks... SQLite execution time: 1.3408 seconds DuckDB execution time: 1.3442 seconds DuckDB was 1.0x faster than SQLite for this query.
При таком объёме данных обе системы работали одинаково. Когда мы увеличили объем данных в десять раз, запустив тот же скрипт на десять миллионов строк и сгенерировав CSV-файл размером около 500 МБ, разница стала заметна.
Generating synthetic data... Created finance_data.csv with 10000000 rows. Starting Benchmarks... SQLite execution time: 18.0329 seconds DuckDB execution time: 9.0836 seconds DuckDB was 2.0x faster than SQLite for this query.
По совпадению, у нас здесь круглые цифры. Это просто совпадение. Можно с уверенностью сказать, что после многократного запуска этого скрипта на трёх разных компьютерах ваши результаты будут отличаться от приведённых здесь значений абсолютного времени выполнения. Конечно, эти результаты в значительной степени зависят от технических характеристик вашего компьютера, в частности от количества ядер процессора. Так что не стоит зацикливаться на абсолютных значениях. Просто обратите внимание на соотношение значений — соотношение между временем выполнения SQLite и DuckDB при изменении объема данных. Предлагаю вам попробовать изменить это значение на сто миллионов строк.
Дело в том, что у SQLite время запуска равно нулю. Когда мы даём ему задание обработать миллион строк, он сразу же приступает к обработке. С другой стороны, DuckDB оснащен оптимизатором запросов и механизмом векторизованного выполнения, который работает за несколько миллисекунд (или секунд на этих двухъядерных машинах низкого уровня). Кроме того, поскольку DuckDB представляет собой столбцовую систему хранения данных, она считывает только тот столбец, который необходим для вычисления средней цены, то есть, естественно, столбец «price», в то время как SQLite, будучи строко-ориентированной системой, загружает каждую полную строку, содержащую поля «timestamp», «ticker», «price» и «volume». DuckDB на физическом уровне не читает эти данные с диска. Таким образом, при увеличении количества строк с одного миллиона до десяти миллионов DuckDB считывает значительно меньше данных, чем SQLite.
Наконец, SQLite обрабатывает по одной строке за раз и обычно работает в однопоточном режиме при выполнении отдельных запросов, тогда как DuckDB использует так называемое векторизированное выполнение, при котором данные передаются в процессор пакетами и одновременно задействуются все доступные ядра для обеспечения полной параллельности. По мере увеличения объёма данных разрыв в производительности между этими двумя системами также увеличивается. Чтобы быть честными и показать ситуацию, где SQLite выигрывает, мы могли бы попросить его найти конкретный ID сделки среди этих десяти миллионов строк, и он оставит DuckDB далеко позади, поскольку это именно та область, в которой SQLite силен — именно для этой задачи он и был разработан.
Стоит отметить, что в данном случае мы используем файл CSV, однако возможность DuckDB выполнять запросы к файлам Parquet или к собственному формату .duckdb еще больше увеличит это преимущество. Однако эффективность анализа данных и общая скорость работы — это лишь одно из преимуществ, которые мы получаем благодаря внедрению специализированной системы OLAP в нашу систему статистического арбитража. Еще одним значительным преимуществом является встроенное временное соединение.
ASOF JOIN
В статистическом арбитраже время играет ключевую роль. Это не фигура речи. Это буквально так. Если вы посмотрите на схему базы данных SQLite, которую мы разрабатывали в этой серии статей, то заметите, что у большинства таблиц в качестве первичного ключа используется временная метка. Поскольку большая часть наших данных представляет собой временные ряды, это является естественным выбором, благодаря чему временные метки становятся своего рода естественным ключом домена не только в статистическом арбитраже, но и в торговле в целом.
Учитывая это, предположим, что нам нужно использовать две приведенные ниже таблицы, чтобы ответить на следующий вопрос: какова была цена символа A в момент торговли символом B? Поскольку цена символа A хранится в таблице «market_data», а временная метка сделки по символу B — в таблице «trade», нам необходимо выполнить соединение этих двух таблиц.

Рис. 3. Схема, отображающая таблицы market_data и trade из схемы schema-0.5
Как вы, наверное, знаете, для выполнения условия соединения в SQL необходимо, чтобы в участвующих таблицах были совпадающие поля, иначе в результате мы получим ноль строк. В данном случае искомым полем для сопоставления является временная метка. Наш запрос будет выглядеть примерно так:
SELECT t.ticket, t.side, t.price AS trade_price, m.price_open, m.price_high, m.price_low, m.price_close, m.timeframe FROM trade t JOIN market_data m ON t.tstamp = m.tstamp AND t.symbol_id = m.symbol_id WHERE t.tstamp = 1708531200 -- The timestamp of our reference trade AND m.timeframe = 'M1'; -- We need the timeframe because of the market_data composite primary key
Проблема заключается в том, что для того, чтобы этот запрос сработал, нам потребуется, чтобы временная метка рыночных данных (m.tstamp) совпадала с временной меткой сделки (t.tstamp) в точности до секунды. В случае приведенного выше примера нам потребуется значение с датой и временем 21 февраля 2024 года, 16:00:00 по UTC. Ровно в 16:00:00 по Гринвичу. Как вы, возможно, уже заметили, нереально ожидать, что сделки и котировки будут происходить в одну и ту же секунду. Если бы наша система зависела от подобных запросов, она была бы крайне неустойчивой торговой системой, поскольку большинство, если не все, запросы завершались бы сбоем. К тому же на практике это не то, что нам нужно знать. Нас интересует последняя котировка символа A на момент сделки по символу B.
Эту проблему можно решить с помощью подзапросов или с помощью условия LIMIT, чтобы найти последнюю котировку, зафиксированную до момента сделки или в тот же момент.
SELECT t.*, m.* FROM trade t JOIN market_data m ON m.symbol_id = t.symbol_id WHERE t.tstamp = 1708531200 AND m.timeframe = 'M1' AND m.tstamp <= t.tstamp ORDER BY m.tstamp DESC LIMIT 1;
Этот стандартный способ SQL работает практически в любой базе данных, включая SQLite, однако у него есть некоторые недостатки. Самым важным фактором, как и прежде, является производительность. Чтобы найти наиболее близкую цену, ища максимальную временную метку, которая меньше или равна времени совершения сделки, мы вручную даем базе данных команду отсортировать все исторические записи по данному инструменту и выбрать самую последнюю из них, а это требует значительных ресурсов. При каждой отдельной сделке база данных должна выполнять поиск и сортировку. Если мы сопоставляем 1 000 торговых операций с 1 000 000 котировок — что является типичным сценарием при анализе данных — этот процесс может значительно замедлиться.
DuckDB и другие специализированные системы OLAP решают эту проблему с помощью так называемого ASOF JOIN — временного соединения, разработанного для анализа временных рядов в столбчатых базах данных, которое позволяет объединять таблицы, в которых временные метки не совпадают точно. Чтобы продемонстрировать разницу между выполнением временных соединений с помощью стандартных подзапросов SQL и использованием специализированного соединения DuckDB ASOF JOIN, мы подготовили ещё один простой скрипт для тестирования производительности, который сравнит эффективность:
Запрос SQLite с использованием подзапроса и LIMIT
sql_query = """
SELECT SUM(t.quantity * (
SELECT q.bid_price FROM q
WHERE q.ticker = t.ticker AND q.timestamp <= t.timestamp
ORDER BY q.timestamp DESC LIMIT 1
)) FROM t
""" Запрос DuckDB с использованием соединения ASOF
duck_query = """
SELECT SUM(t.quantity * q.bid_price)
FROM t ASOF JOIN q ON t.ticker = q.ticker AND t.timestamp >= q.timestamp
""" Этот скрипт прилагается здесь под именем bench_asof.py. Запустив программу на компьютере низкого уровня, который является нашим целевым оборудованием, вы должны получить результат, похожий на этот.
--- Generating 1,000,000 trades and 100,000 quotes --- --- Running Engine-Only Benchmark (Computing SUM) --- DuckDB Time: 1.1637s SQLite Time: 2.1329s [WINNER]: DuckDB is 1.8x faster at this scale.
И снова, по мере увеличения объёма данных — в десять раз — разрыв также увеличивается. Помните, что я специально запускаю этот тест на недорогом двухъядерном компьютере. Этот выбор обусловлен стремлением оставаться верными нашей основной цели — разработке данной платформы для статистического арбитража, предназначенной для обычных розничных трейдеров, — однако в то же время он ограничивает встроенный параллелизм DuckDB, который по умолчанию использует все доступные ядра. Поэтому, если вы проведете анализ на современном настольном компьютере или ноутбуке, вы увидите, что эти показатели времени работы DuckDB значительно сократятся.
--- Generating 10,000,000 trades and 1,000,000 quotes --- --- Running Engine-Only Benchmark (Computing SUM) --- DuckDB Time: 8.6416s SQLite Time: 22.2207s [WINNER]: DuckDB is 2.6x faster at this scale.
Наличие ASOF JOIN принципиально меняет ситуацию для розничных трейдеров, работающих в сфере статистического арбитража. Учтите, что выше мы рассматриваем очень простой пример, который служит в качестве эталона. Однако по мере добавления в запрос новых параметров, таких как, например, цена предложения, объем, волатильность и другие, отладка и сопровождение запроса, написанного на стандартном SQL без использования соединения ASOF, предоставляемого DuckDB, становится затруднительным, а производительность снижается ещё больше. Для каждой отдельной сделки базе данных приходится выполнять отдельный запрос к таблице котировок.
С другой стороны, система OLAP рассматривает временные соединения как полноценный компонент и использует специализированный алгоритм, разработанный специально для этой логики «момента времени». Этот запрос отличается элегантностью и высокой вычислительной эффективностью. DuckDB не выполняет такое соединение через вложенные циклы. Именно здесь используется так называемое векторизованное выполнение, о котором мы упоминали выше. Он обрабатывает «блоки» данных через кэш процессора и способен выполнять такие соединения по миллионам строк за миллисекунды на стандартном ноутбуке. Поскольку DuckDB является столбцовой базой данных, она считывает только столбцы с временными метками, символами и ценами, а не весь набор данных.
Векторизированное выполнение особенно хорошо проявляет себя при вычислении показателя «Сравнение собственных векторов на скользящих окнах (RWEC)» (RWEC) для ребалансировки портфеля, о котором мы рассказывали в 8-й части этой серии.
RWEC
DuckDB обладает встроенными функциями для работы с массивами, которые могут значительно повысить производительность наших вычислений RWEC, особенно по мере роста объёма базы данных. Вот тот метод на Python, который мы ранее использовали для вычисления косинусного сходства между последовательными векторами:
def vector_similarity(self, vectors_df): """Compute cosine similarity between consecutive vectors""" similarities = [] for i in range(1, len(vectors_df)): vec1 = vectors_df.iloc[i-1].values vec2 = vectors_df.iloc[i].values cos_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) angle_deg = np.degrees(np.arccos(np.clip(cos_sim, -1, 1))) similarities.append({ 'date': vectors_df.index[i], 'cosine_similarity': cos_sim, 'angle_degrees': angle_deg, 'stable': angle_deg < 30 # Threshold }) return pd.DataFrame(similarities).set_index('date')
Среди функций массива DuckDB есть прямой аналог этой логики на Python: array_cosine_similarity(v1, v2). Обратите внимание, что для вычисления косинусного сходства мы используем скалярное произведение двух векторов, деленное на произведение их модулей. Это точное математическое определение косинусного сходства. Чтобы реализовать наш метод vector_similarity в DuckDB, мы можем использовать оконные функции (LAG) для сравнения текущего вектора с предыдущим. Таким образом, DuckDB сможет обрабатывать эти операции с массивами с помощью инструкций SIMD (Single Instruction, Multiple Data), что сделает их гораздо быстрее, чем цикл в Python, по мере того как наш репозиторий исторических данных и торговых операций будет разрастаться до большого набора данных.
Мы также должны учитывать, что DuckDB поддерживает выполнение за пределами оперативной памяти, то есть может обрабатывать объемы данных, превышающие размер доступной оперативной памяти. С другой стороны, наш нынешний подход с использованием pandas ограничен объемом памяти. Чтобы увидеть разницу между итерацией в Python на основе строк и векторизованным движком системы OLAP, мы можем запустить третий тест производительности. Мы сравним цикл на языке Python/NumPy (используемый в прилагаемом скрипте rwec.py) с функцией array_cosine_similarity из библиотеки DuckDB.
import duckdb import pandas as pd import numpy as np import time def generate_eigen_dataset(n_rows=1_000_000, vec_dim=2): """Generates a large set of synthetic eigenvectors for testing.""" print(f"--- Generating {n_rows:,} eigenvectors (dim={vec_dim}) ---") # Generate random vectors and normalize them data = np.random.randn(n_rows, vec_dim) norms = np.linalg.norm(data, axis=1, keepdims=True) normalized_vecs = data / norms df = pd.DataFrame({ 'date': pd.date_range(start='2000-01-01', periods=n_rows, freq='h'), 'vec': list(normalized_vecs) }) return df
Не забывайте всегда нормировать сгенерированные векторы, как это делается при тесте коинтеграции Йохансена. При расчете RWEC это обязательный этап, поскольку формула косинусного сходства измеряет угол между двумя векторами и предполагает, что их величины нормированы.
В статистическом арбитраже вектор коинтеграции отражает коэффициент хеджирования между активами. Собственный вектор вида [1, -2] отражает точно такую же зависимость, как и [10, -20]. Оба они указывают на соотношение 1:2. Если при расчете скользящего окна в одном периоде мы получаем [1, -2], а в следующем — [10, -20], это не означает, что взаимосвязь действительно изменилась. Нормирование гарантирует, что наш тест измеряет направление (коэффициент хеджирования), а не произвольную шкалу результатов теста Йохансена.
def benchmark_rwec(df): # --- SETUP DUCKDB --- con = duckdb.connect(":memory:") # Register the dataframe as a virtual table con.register("eigen_table", df) print("\n--- Running RWEC Benchmark (Cosine Similarity + Angle) ---") # 1. DuckDB Benchmark (Vectorized SQL) # Uses array_cosine_similarity and LAG to compare consecutive rows duck_query = """ SELECT AVG(DEGREES(ACOS(inner_sim))) FROM ( SELECT LEAST(GREATEST(array_cosine_similarity( vec::DOUBLE[2], LAG(vec::DOUBLE[2]) OVER (ORDER BY date) ), -1), 1) as inner_sim FROM eigen_table ) WHERE inner_sim IS NOT NULL """ start = time.perf_counter() duck_res = con.execute(duck_query).fetchone()[0] duck_time = time.perf_counter() - start print(f"DuckDB Time: {duck_time:.4f}s (Result Avg Angle: {duck_res:.2f}°)") # 2. Python/NumPy Loop Benchmark # We iterate through the dataframe as in the rwec.py script start = time.perf_counter() similarities = [] # Replicating the logic from rwec.py vector_similarity() vecs = np.stack(df['vec'].values) for i in range(1, len(vecs)): vec1 = vecs[i-1] vec2 = vecs[i] # Manual cosine similarity calculation cos_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) angle_deg = np.degrees(np.arccos(np.clip(cos_sim, -1, 1))) similarities.append(angle_deg) py_res = np.mean(similarities) py_time = time.perf_counter() - start print(f"Python Loop Time: {py_time:.4f}s (Result Avg Angle: {py_res:.2f}°)") print(f"\n[WINNER]: DuckDB is {py_time/duck_time:.1f}x faster for RWEC logic.") if __name__ == "__main__": # Test with 500,000 rows to see the gap eigen_df = generate_eigen_dataset(n_rows=500_000) benchmark_rwec(eigen_df)
Этот скрипт прилагается здесь под именем bench_rwec.py. Если запустить программу на слабом компьютере, вы должны получить примерно такие результаты.
--- Generating 500,000 eigenvectors (dim=2) --- --- Running RWEC Benchmark (Cosine Similarity + Angle) --- DuckDB Time: 0.9603s (Result Avg Angle: 90.01°) Python Loop Time: 21.9560s (Result Avg Angle: 90.01°) [WINNER]: DuckDB is 22.9x faster for RWEC logic.
В отличие от двух других тестов, которые мы рассмотрели выше, здесь разница оказывается огромной с самого начала, даже без увеличения объёма входных данных свыше 500 000 двумерных собственных векторов. Функция `array_cosine_similarity` заменяет как ручную операцию `np.dot`, так и нормирование (`norm`), которые мы использовали в Python, а отказ от цикла Python (`for i in range`) позволяет избежать накладных расходов, возникающих из-за того, что интерпретатор при обработке каждой строки вынужден переключаться между кодом Python и базовыми библиотеками C. Кроме того, как уже упоминалось выше, DuckDB обрабатывает эти массивы частями. Он не вычисляет углы по одному за раз. Вместо этого он вычисляет их параллельными партиями, благодаря чему мы наблюдаем столь значительное повышение производительности по сравнению с нашим предыдущим алгоритмом на Python/SQLite.
Вышесказанное справедливо для двумерных собственных векторов, то есть для двух символов. Если мы хотим рассмотреть наборы из трёх или более символов, мы можем просто передать размерность как векторную переменную.Тип преобразования ::DOUBLE[n] должен соответствовать фактическому количеству элементов в нашем векторе коинтеграции. Мы можем использовать f-строку Python, чтобы встроить размерность (vec_dim) непосредственно в запрос.
# vec_dim is the number of assets (2 for a pair) vec_dim = len(df['vec'].iloc[0]) duck_query = f""" SELECT AVG(DEGREES(ACOS(inner_sim))) FROM ( SELECT LEAST(GREATEST(array_cosine_similarity( vec::DOUBLE[{vec_dim}], LAG(vec::DOUBLE[{vec_dim}]) OVER (ORDER BY date) ), -1), 1) as inner_sim FROM eigen_table ) WHERE inner_sim IS NOT NULL """
Это важно, поскольку функция `array_cosine_similarity` в DuckDB требует, чтобы входные массивы имели фиксированную длину. Если мы попытаемся сравнить массив DOUBLE[2] с массивом DOUBLE[3], движок выдаст ошибку сопоставления типов на этапе привязки.
Заключение
В этой заключительной статье нашей вводной серии, посвящённой статистическому арбитражу с использованием коинтегрированных акций, мы предложили включить в наш рабочий процесс специализированную базу данных для анализа данных. В частности, мы рекомендуем использовать бесплатную систему OLAP DuckDB с открытым исходным кодом, систему корпоративного уровня.
Мы отметили, что еще несколько лет назад главным барьером для входа розничных трейдеров в сферу статистического арбитража были вычислительные мощности, необходимые для анализа данных в режиме реального времени, а не скорость исполнения ордеров. В подтверждение этого мы привели некоторые оценки стоимости аналогичных систем, действовавших примерно в 2010 году, а также кратко описали технические сложности, связанные с их внедрением.
Наконец, чтобы продемонстрировать, как внедрение системы OLAP может повысить эффективность нашего этапа анализа данных в реальном времени, мы представили три теста производительности, в которых сравнивались показатели DuckDB и SQLite при выполнении наиболее часто используемых в нашей системе вычислений: простое агрегирование для расчета средней цены, соединение двух таблиц для сравнения исторических котировок и расчет RWEC с использованием встроенных функций DuckDB вместо вложенного цикла на Python.
Результаты тестирования на компьютере низкого уровня показывают, что вычисления в DuckDB выполняются от 2 до 23 раз быстрее; причём разрыв в производительности увеличивается с ростом объёма данных. Это подтверждает основную причину, по которой мы предлагаем включить эту специализированную систему в наш конвейер: она позволит обеспечить его готовность к расширению и пригодность для работы с большими наборами данных в будущем.
| Имя файла | Описание |
|---|---|
| bench.py | Скрипт на Python для проведения простого сравнительного тестирования агрегации данных между SQLite и DuckDB |
| bench_asof.py | Скрипт на Python для проведения сравнительного тестирования стандартного временного соединения SQLite и соединения ASOF в DuckDB |
| bench_rwec.py | Скрипт на Python для проведения сравнительного тестирования между вложенным циклом Numpy и функцией косинусного сходства DuckDB при расчете RWEC |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21507
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка инструментария для анализа Price Action (Часть 40): ДНК-профиль рынка
Разработка инструментария для анализа Price Action (Часть 39): Автоматизация обнаружения BOS и ChOCh на MQL5
Тестер стратегий для Python и MetaTrader 5 (Часть 1): Торговый симулятор
Разработка инструментария для анализа Price Action (Часть 38): VWAP на основе тикового буфера и модуль расчета дисбаланса на коротком окне
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования