Статистический арбитраж на основе коинтегрированных акций (Часть 3): Настройка базы данных
Введение
В предыдущей статье этой серии (часть 2) мы провели бэктестинг стратегии статистического арбитража, состоящей из корзины коинтегрированных акций из сектора микропроцессоров (акции Nasdaq). Мы начали отбирать из сотен биржевых кодов те, которые наиболее тесно коррелируют с акциями Nvidia. Затем мы проверили группу с фильтрацией на коинтеграцию с помощью теста Йохансена, стационарность спреда с помощью тестов ADF и KPSS, и, наконец, получили относительные веса портфеля, выделив собственный вектор Йохансена первого ранга. Результаты бэктеста оказались обнадеживающими.
Однако два или более актива могли демонстрировать коинтеграцию в течение последних двух лет и уже завтра начать терять эту коинтеграцию. То есть нет гарантии, что коинтегрированная пара или группа активов останется коинтегрированной. Изменения в руководстве компании, макроэкономическая ситуация или отраслевые изменения могут повлиять на фундаментальные факторы, которые изначально определяли коинтеграцию данных активов. И наоборот. Активы, которые ранее не были коинтегрированы, могут в следующую минуту начать двигаться по коинтегрированной траектории по тем же причинам. Рынок — это «загадка, постоянно пребывающая в состоянии изменения». Нам нужно справиться с этими изменениями. [ПАЛОМАР, 2025]
В корзине коинтегрированных акций относительные веса в портфеле будут меняться практически непрерывно, причем эти веса определяют не только объем (количество) наших ордеров, но и их направление (покупка или продажа). Итак, нам тоже нужно приспособиться к этим изменениям. В то время как коинтеграция представляет собой более долгосрочную взаимосвязь, веса в портфеле постоянно меняются. Поэтому нам нужно проверять их чаще и обновлять нашу модель, как только они изменятся. Как только мы обнаруживаем, что наша модель устарела, нам необходимо немедленно принять меры; нам нужна мгновенная замена устаревшей модели.
Наш советник должен в режиме реального времени отслеживать, остаются ли в силе веса портфеля, которые мы использовали ранее, или они изменились. В случае их изменения необходимо как можно скорее сообщить EA о новых весах портфеля. Кроме того, наш экспертный советник должен определять, остается ли сама модель актуальной. В противном случае необходимо проинформировать управляющего активами о том, какие активы подлежат замене, и как можно скорее внести соответствующие изменения в активный портфель.
Мы используем интеграцию Python в MetaTrader 5 и профессионально разработанные статистические функции из библиотеки statsmodels, но до сих пор работали исключительно с данными в режиме реального времени, загружая котировки (данные о ценах) по мере необходимости. Этот подход полезен на этапе предварительного изучения благодаря своей простоте. Но если мы собираемся обновлять наш портфель, модели или веса в портфеле, нам, возможно, стоит задуматься о сохранности данных. То есть нам нужно начать задумываться о хранении данных в базе данных, поскольку загружать данные каждый раз, когда они нужны, нецелесообразно. Более того, нам, возможно, придётся искать взаимосвязи между различными классами активов, между показателями, которые не учитывались в наших первых тестах на коинтеграцию.
Высококачественная, масштабируемая база данных с обширными метаданными является основой любой серьезной деятельности в области статистического арбитража. Учитывая, что проектирование баз данных — это весьма индивидуальная задача, поскольку хорошая база данных — это та, которая соответствует требованиям конкретного бизнеса, в данной статье мы рассмотрим один из возможных подходов к созданию базы данных, ориентированной на статистический арбитраж.
На какие вопросы должна отвечать наша база данных?
В поисках «доступной системы статистического арбитража» — то есть системы, подходящей для обычного розничного трейдера, имеющего стандартный ноутбук и доступ к сети со средней пропускной способностью, — мы сталкиваемся с рядом трудностей, связанных с отсутствием специальных знаний в необходимых областях, таких как статистика и разработка программного обеспечения. Проектирование баз данных НЕ является исключением в этом списке необходимых навыков. Проектирование баз данных — это обширная область сама по себе. О проектировании баз данных можно написать целые книги, и все равно не исчерпать эту тему. Идеальным решением было бы привлечение специалиста, а возможно, и нескольких специалистов, для разработки, внедрения и обслуживания нашей базы данных.
Но поскольку мы разрабатываем эту систему арбитража для обычного розничного трейдера, нам приходится работать с тем, что есть: искать информацию в книгах, на специализированных интернет-форумах и каналах; учиться у опытных профессионалов; действовать методом проб и ошибок; проводить эксперименты; идти на риск и быть готовыми изменить наш подход, если он окажется неподходящим для поставленной задачи. Нам нужна гибкость, чтобы вносить изменения, и нам нужно начинать с малого, работая по принципу «снизу вверх», а не «сверху вниз», чтобы избежать излишней усложненности.
В конечном итоге наша база данных должна дать ответ на один очень простой вопрос: чем нам сейчас торговать, чтобы получить максимально возможную прибыль?
Ссылаясь на предыдущую статью из этой серии, в которой мы определяли направления и объемы наших ордеров на основе весов в портфеле акций, один из возможных ответов мог бы выглядеть примерно так:
| Символы | Веса | Сроки |
|---|---|---|
| «MU», «NVDA», «MPWR», «MCHP» | 2,699439, 1,000000, -1,877447, -2,505294 | D1 |
Таблица 1 — Примерный вариант ожидаемого ответа на запрос для обновления модели в режиме реального времени
Если наша база данных сможет предоставлять нам эту довольно простую информацию с достаточной периодичностью, у нас будет всё необходимое для непрерывной торговли на максимально оптимальном уровне.
Обновление баз данных как услуга
До сих пор мы проводили анализ данных, используя котировки в реальном времени из терминала MetaTrader 5 с помощью кода на Python (технически, в большинстве случаев мы использовали котировки, сохраненные в движке самого терминала). После определения тикеров и весов портфеля мы вручную обновляли наш советник, добавляя в него новые тикеры и/или новые веса портфеля.
С этого момента мы отделим анализ данных от терминала и начнем использовать данные, хранящиеся в нашей базе данных, для обновления нашего советника, как только появятся новые веса портфеля, а также для остановки торговли в случае потери коинтеграционных отношений или если другая группа символов будет признана более перспективной. То есть мы хотим оптимизировать наше присутствие на рынке по каждому инструменту, обновляя веса в портфеле в режиме реального времени и/или перебалансируя портфель каждый раз, когда это рекомендует анализ данных.
Для обновления базы данных мы реализуем сервис Metatrader 5.
Из документации по MetaTrader 5 мы узнаем, что
- Услуги не привязаны к конкретному графику.
- Сервисы загружаются сразу после запуска терминала, если они были запущены на момент его завершения работы.
- Сервисы работают в собственном потоке.
Можно сделать вывод, что сервис — это идеальный способ поддерживать нашу базу данных в актуальном состоянии. Если он работает в момент завершения торговой сессии и закрытия терминала, он возобновит работу сразу же после запуска терминала для новой сессии, независимо от того, какой график или инструмент мы будем использовать в этот момент. Кроме того, поскольку он будет работать в отдельном потоке, он не будет влиять на другие запущенные сервисы, индикаторы, скрипты или советники, а также не будет подвергаться их влиянию.
Итак, наш рабочий процесс будет выглядеть следующим образом:
- Весь анализ данных будет проводиться на языке Python вне среды MetaTrader 5. Чтобы провести анализ, мы загрузим исторические данные и внесем их в базу данных.
- Каждый раз, когда мы вносим изменения в наш активный портфель, добавляя или удаляя инструмент, мы обновляем входные параметры Сервиса, передавая массив инструментов и массив таймфреймов.
- Каждый раз, когда мы вносим изменения в наш активный портфель, добавляя или удаляя инструмент, мы обновляем входные параметры советника.
Пока что мы будем выполнять обновления в шагах 2 и 3 вручную. Позже мы автоматизируем этот процесс.
Настройка базы данных
В ходе подготовки этой статьи я вновь убедился, что идеальным инструментом для этой задачи является специализированная столбцовая база данных, предназначенная для работы с временными рядами. На рынке представлено множество продуктов, отвечающих этим требованиям: как платных, так и бесплатных, как проприетарных, так и открытых. Они предназначены для обработки узкоспециализированных рабочих нагрузок и огромных объемов данных с временем отклика менее секунды — как при приеме данных, так и при обработке запросов в режиме реального времени.
Но в данном случае речь идет не о масштабах. Наше основное внимание сосредоточено на простоте и доступности для отдельного пользователя, а не для команды высококвалифицированных профессиональных администраторов баз данных (DBA) и специалистов по проектированию систем управления временными рядами. Поэтому мы начнём с самого простого решения, осознавая его ограничения и помня о том, что в будущем система будет развиваться по мере необходимости.
Начнём со встроенной в MetaTrader 5 базы данных SQLite. Существует огромное количество информации о том, как создать и использовать встроенную базу данных SQLite в среде MetaTrader 5. Вы можете найти это:
- В документации по Metaeditor описано, как использовать графический интерфейс Metaeditor для создания, использования и управления базами данных
- Кроме того, в документации Metaeditor по функциям API для работы с базами данных
- В статье-введении от MetaQuotes, посвященной обзору возможностей работы с базами данных SQL на языке MQL5
- В книге «MQL5» — подробное руководство по использованию встроенной базы данных SQLite
Если вы планируете серьезно работать со встроенной базой данных SQLite, настоятельно рекомендуем вам внимательно изучить все эти ссылки. Ниже приводится краткое изложение шагов для нашего конкретного примера использования, описанного в данной статье, с некоторыми пояснениями относительно обоснования сделанного выбора.
Схема
В приложении к этой статье вы найдете файл db-setup.mq5 — это скрипт на языке MQL5. В качестве входных параметров она принимает имя файла базы данных SQLite и имя файла схемы базы данных. По умолчанию для этих параметров используются файлы statarb-0.1.db и schema-0.1.sql соответственно.
ВНИМАНИЕ: Настоятельно рекомендуется хранить файл схемы базы данных в версионном хранилище и не создавать его дубликаты в файловой системе. Metatrader 5 предлагает надежную систему управления версиями, уже встроенную в платформу.
Запустив этот скрипт с параметрами по умолчанию, вы создадите базу данных SQLite в папке MQL5/Files/StatArb вашего терминала (НЕ в общей папке всех клиентских терминалов), содержащую все таблицы и поля, описанные ниже. Скрипт также создаст файл output.sql в папке MQL/Files исключительно для целей отладки. Если у вас возникнут какие-либо проблемы, вы можете проверить этот файл, чтобы увидеть, как ваша система считывает файл схемы. Если всё прошло успешно, этот файл можно смело удалить.
В качестве альтернативы вы можете создать базу данных другим способом и загрузить файл схемы из базы данных в любом клиенте SQLite3, в интерфейсе Metaeditor, в Windows PowerShell или в командной строке SQLite3. Я рекомендую, по крайней мере в первый раз, создать базу данных, запустив прилагаемый скрипт. Вы всегда сможете настроить этот процесс позже.
Схема базы данных
Наша исходная схема базы данных состоит всего из четырёх таблиц, две из которых являются заполнителями для последующих этапов. То есть на данном этапе мы будем использовать только таблицы «symbol» и «market_data».
На рисунке 1 представлена ER-диаграмма «сущность-связь» (Entity-Relationship Diagram, ERD) этой начальной схемы.

Рис. 1 — Исходная схема базы данных (диаграмма «сущность-связь»)
Таблица «corporate_event», как и следовало ожидать, предназначена для хранения данных о событиях, связанных с компаниями нашего портфеля, таких как суммы дивидендов, сплиты, выкупы, слияния и т. д. Пока что мы не будем этим заниматься.
В таблице «trade» будут храниться наши трейды (сделки). Собрав эти данные, мы получим уникальный массив данных для обобщения и анализа. Мы будем использовать его только тогда, когда начнём торговать.
В таблице «market_data» будут храниться наши данные OHLC со всеми полями MqlRates. Он имеет составной первичный ключ (symbol_id, timeframe и timestamp), чтобы гарантировать уникальность каждой записи. Таблица «market_data» связана с таблицей «symbol» посредством внешнего ключа.
Как видите, это единственная таблица, в которой используется составной первичный ключ. Во всех остальных таблицах в качестве первичного ключа используется временная метка, хранящаяся в виде целого числа (INTEGER). У этого выбора есть своя причина. Согласно документации SQLite3,
«Данные для таблиц rowid хранятся в виде структуры B-дерева, содержащей по одной записи на каждую строку таблицы, причем в качестве ключа используется значение rowid. Это означает, что поиск или сортировка записей по rowid происходит быстро. Поиск записи по конкретному rowid или всех записей с rowid в заданном диапазоне выполняется примерно в два раза быстрее, чем аналогичный поиск по любому другому PRIMARY KEY или индексированному значению.
(...) если таблица rowid имеет первичный ключ, состоящий из одного столбца, и объявленный тип этого столбца — «INTEGER» (в любом сочетании заглавных и строчных букв), то этот столбец становится псевдонимом для rowid. Такую колонку обычно называют «целочисленным первичным ключом».
(...) «Выполнение запросов может ускориться примерно вдвое, если использовать INTEGER в качестве первичного ключа». «Временные метки эпохи Unix можно вставлять в базы данных SQLite3 в виде целых чисел (INTEGER)».
(...) «SQLite хранит целочисленные значения в 64-битном формате с дополнением до двух¹. Таким образом, диапазон значений составляет от -9223372036854775808 до +9223372036854775807 включительно. Целые числа в этом диапазоне являются точными. (Документация SQLite3)»
Таким образом, мы можем преобразовать наши даты и время из строковых значений в временные метки эпохи Unix и ввести их в качестве первичного ключа, чтобы превзойти скорость света. 🙂
В приведенных ниже таблицах представлена полная документация по схеме (словарь данных) для использования в качестве справочного материала нашей командой и нами самими в будущем.
Таблица: symbol
Хранит метаданные о финансовых инструментах, торгуемых или отслеживаемых.
| Поле | Тип данных | Пустое значение | Ключ | Описание |
|---|---|---|---|---|
| symbol_id | INTEGER | NO | PK | Уникальный идентификатор для каждого финансового инструмента |
| ticker | TEXT(≤10) | NO | Тикер актива (например, «AAPL», «MSFT») | |
| exchange | TEXT(≤50) | NO | Биржа, на которой котируется данный актив (например, «NASDAQ», «NYSE») | |
| asset_type | TEXT(≤50) | YES | Тип актива (например, «Акции», «Биржевые инвест-фонды», «Форекс», «Криптовалюта») | |
| sector | TEXT(≤50) | YES | Классификация по отраслям экономики (например, «Технологии», «Здравоохранение») | |
| industry | TEXT(≤50) | YES | Классификация отраслей в рамках сектора | |
| currency | TEXT(≤50) | YES | Валюта номинала актива (например, «EUR», «USD») |
Таблица 2 — Описание словаря данных таблицы «symbol» (версия 0.1)
Таблица: corporate_event
Отслеживает события, влияющие на активы, такие как выплата дивидендов, дробление акций или публикация отчётов о прибылях.
| Поле | Тип данных | Пустое значение | Ключ | Описание | Пример |
|---|---|---|---|---|---|
| tstamp | INTEGER | NO | PK | Время в формате Unix, когда событие вступает в силу | 1678905600 |
| event_type | TEXT ENUM {'dividend', 'split', 'earnings'} | NO | Тип корпоративного события | "dividend" | |
| event_value | REAL | YES | Числовое значение события: • Размер дивиденда на акцию • Коэффициент сплита • Прибыль на акцию (EPS) | 0.85, 2.0, 1.35 | |
| details | TEXT(≤255) | YES | Дополнительные примечания или контекст | "Q2 dividend payout" | |
| symbol_id | INTEGER | NO | FK | Идентификатор синструмента (symbol_id); связывает событие с активом | 1 |
Таблица 3 — Описание словаря данных таблицы «corporate_event» (версия 0.1)
Таблица: market_data
Хранит данные OHLCV (открытие, максимум, минимум, закрытие, объем) и связанные временные ряды по активам.
| Поле | Тип данных | Пустое значение | Ключ | Описание | Пример |
|---|---|---|---|---|---|
| tstamp | INTEGER | NO | PK* | Время в формате Unix для бара/свечи | 1678905600 |
| timeframe | TEXT ENUM {M1,M2,M3,M4,M5,M6, M10,M12,M15,M20,M30,H1,H2,H3, H4,H6,H8,H12,D1,W1,MN1} | NO | PK* | Таймфрейм данных временных рядов | "M5", "D1" |
| price_open | REAL | NO | Цена открытия бара | 145.20 | |
| price_high | REAL | NO | Максимальная цена за бар | 146.00 | |
| price_low | REAL | NO | Минимальная цена за бар | 144.80 | |
| price_close | REAL | NO | Цена закрытия бара | 145.75 | |
| tick_volume | INTEGER | YES | Тиковый объем | 200 | |
| real_volume | INTEGER | YES | Реальный объем (если доступен) | 15000 | |
| spread | REAL | YES | Средний спред за бар | 0.02 | |
| symbol_id | INTEGER | NO | PK*, FK | Идентификатор синструмента (symbol_id). | 1 |
Таблица 4 — Описание словаря данных таблицы «market_data» (версия 0.1)
Таблица: trade
Отслеживает реальные или моделируемые сделки по стратегиям.
| Поле | Тип данных | Пустое значение | Ключ | Описание | Пример |
|---|---|---|---|---|---|
| tstamp | INTEGER | NO | PK | Время сделки в формате Unix | 1678905600 |
| ticket | INTEGER | NO | Тикет сделки/идентификатор ордера | 20230001 | |
| side | TEXT ENUM {'buy', 'sell'} | NO | Направление сделки | "buy" | |
| quantity | INTEGER (>0) | NO | Торговый объем в акциях/контрактах | 100 | |
| price | NO | Цена исполнения | 145.50 | ||
| strategy | YES | Идентификатор торговой стратегии, на основании которой совершена сделка | "StatArb_Pairs" | ||
| symbol_id | INTEGER | NO | FK | Идентификатор синструмента (symbol_id). | 1 |
Таблица 5 — Описание словаря данных таблицы «trade» (версия 0.1)
STRICT
Если вы просмотрите файл схемы, то увидите, что все таблицы являются таблицами типа STRICT .
CREATE TABLE symbol(
symbol_id INTEGER PRIMARY KEY,
ticker TEXT CHECK(LENGTH(ticker) <= 10) NOT NULL,
exchange TEXT CHECK(LENGTH(exchange) <= 50) NOT NULL,
asset_type TEXT CHECK(LENGTH(asset_type) <= 50),
sector TEXT CHECK(LENGTH(sector) <= 50),
industry TEXT CHECK(LENGTH(industry) <= 50),
currency TEXT CHECK(LENGTH(currency) <= 10)
) STRICT;Это означает, что мы выбираем строгую типизацию в наших таблицах вместо удобного механизма приведения типов SQLite. Мы считаем, что этот выбор поможет избежать проблем в будущем.
«Если в операторе CREATE TABLE в конце, после закрывающей скобки «)», добавлено ключевое слово опции таблицы «STRICT», то к этой таблице применяются строгие правила типизации.» (Документация SQLite)
CHECK LENGTH
Кроме того, мы требуем проверки длины нескольких полей TEXT. Это связано с тем, что SQLite не обеспечивает фиксированную длину и не обрезает строки в соответствии со значением (n), указанным в типах CHAR(n) или VARCHAR(n), как это обычно делается в других СУБД.
А как насчет индексов?
Вы, возможно, задаетесь вопросом, почему мы не создали никаких индексов. Мы это сделаем, как только начнём запрашивать информацию, чтобы точно понять, где она нужна.
Ввод исходных данных
Предполагается, что база данных будет заполняться автоматически и незаметно в процессе анализа данных через интеграцию MQL5 и Python. Тем не менее, для удобства прилагается скрипт на Python (db_store_quotes.ipynb), который поможет вам сохранить котировки по списку символов, с определенного таймфрейма и за выбранный период времени. С этого момента мы будем проводить анализ данных (проверку на корреляцию, коинтеграцию и стационарность) на основе этих сохраненных данных.
Рисунок 2 — Таблица «Symbol» после первоначальной вставки данных с помощью скрипта на Python
Как видно, большинство метаданных «символа» имеют значение «UNKNOWN». Это связано с тем, что функции Python для SymbolInfo не охватывают все метаданные символов, доступные в MQL5 API. Мы восполним эти пробелы позже.
# Insert new symbol cursor.execute(""" INSERT INTO Symbol (ticker, exchange, asset_type, sector, industry, currency) VALUES (?, ?, ?, ?, ?, ?) """, ( mt5_symbol, # some of this data will be filled by the MQL5 DB Update Service # because some of them are not provided by the Python MT5 API symbol_info.exchange or 'UNKNOWN', symbol_info.asset_class or 'UNKNOWN', symbol_info.sector or 'UNKNOWN', symbol_info.industry or 'UNKNOWN', symbol_info.currency_profit or 'UNKNOWN' ))
Путь к базе данных должен передаваться в виде переменной среды, и мы используем модуль python-dotenv для загрузки этой переменной. Это должно помочь избежать проблем, связанных с тем, что редактор не распознает переменные среды терминала и/или PowerShell.
В самом начале скрипта вы найдёте Jupyter-ноутбук, который загружает расширение python-dotenv и соответствующий файл *.env.
%load_ext dotenv %dotenv .env
К этой статье также прилагается пример файла *.env.
# Сохраните этот файл в корневом каталоге вашего проекта # или в той же папке, что и Python-скрипт, который его использует STATARB_DB_PATH="путь/к/вашей/базе/данных"
Основной вызов в файле db_store_quotes.ipynb находится в конце скрипта.
symbols = ['MPWR', 'AMAT', 'MU'] # Символ из Market Watch timeframe = mt5.TIMEFRAME_M5 # пятиминутный таймфрейм start_date = '2024-02-01' end_date = '2024-03-31' db_path = os.getenv('STATARB_DB_PATH') # Путь в Вашей БД SQLite if db_path is None: print("Error: STATARB_DB_PATH environment variable is not set.") else: print("db_path: " + db_path) # Загрузка исторических данных и сохранение в БД download_mt5_historical_quotes(symbols, timeframe, start_date, end_date, db_path)
Обновления
Основой нашего обслуживания базы данных для автоматического обновления моделей и ротации портфелей является работающий в фоновом режиме сервис MQL5. По мере развития нашей базы данных данному Сервису также потребуются обновления.
Сервис подключается к (или создает) локальному файлу базы данных SQLite.
ВНИМАНИЕ: Если вы создаете совершенно новую базу данных с помощью Сервиса обновления баз данных, не забудьте инициализировать базу данных с помощью скрипта db_setup, как упоминалось выше.
Затем сервис устанавливает необходимые ограничения для обеспечения целостности данных, после чего работает в бесконечном цикле, в котором проверяет
новые рыночные данные. Для каждого символа и таймфрейма
- извлекает данные о последнем сформированном баре (включая цены открытия и закрытия, максимальную и минимальную цены, объем и спред),
- проверяет, есть ли он уже в базе данных,
- и вставляет новую котировку (или котировки), если их нет
Для обеспечения атомарности сервис оборачивает операцию вставки в транзакцию базы данных. Если что-то пойдет не так (например, произойдет ошибка базы данных), система будет повторять попытку до установленного предела (по умолчанию 3 раза) с паузами в одну секунду. Ведение журнала не является обязательным. Цикл приостанавливается между обновлениями и останавливается только при ручной остановке сервиса.
Итак, давайте рассмотрим некоторые из его компонентов.
В настройках можно выбрать путь к базе данных в файловой системе, частоту обновления в минутах, максимальное количество попыток в случае сбоя при вставке, а также указать, следует ли выводить сообщения об успешном выполнении или сбое в журнал. Этот последний параметр может пригодиться, когда после разработки мы получим стабильный код.
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string InpDbPath = "StatArb\\statarb-0.1.db"; // Имя файла базы данных input int InpUpdateFreq = 1; // Частота обновления в минутах input int InpMaxRetries = 3; // Максимальное количество попыток input bool InpShowLogs = true; // Включить ведение журнала?
Рисунок 3 — Диалоговое окно Metatrader 5 для ввода параметров службы обновления базы данных
Необходимо выбрать символы и соответствующие периоды графика, которые будут обновляться. На следующем этапе эти записи будут вноситься автоматически.
//+------------------------------------------------------------------+ //| Global vars | //+------------------------------------------------------------------+ string symbols[] = {"EURUSD", "GBPUSD", "USDJPY"}; ENUM_TIMEFRAMES timeframes[] = {PERIOD_M5};
Здесь мы инициализируем хендл базы данных как INVALID_HANDLE. Это будет проверено при следующем открытии базы данных.
// Хендл базы данных int dbHandle = INVALID_HANDLE;
OnStart()
В обработчике событий OnStart сервиса мы лишь запускаем бесконечный цикл и вызываем функцию UpdateMarketData, в которой собственно и начинается работа. В функцию Sleep() передается количество миллисекунд, которое необходимо выждать между каждым циклом (каждым запросом обновления котировок). Мы переводим это в минуты, чтобы было удобнее для пользователей. Кроме того, мы не ожидаем обновлений на таймфреймах короче одной минуты.
//+------------------------------------------------------------------+ //| Main Service function | //| Parameters: | //| symbols - Array of symbol names to update | //| timeframes - Array of timeframes to update | //| InpMaxRetries - Maximum number of retries for failed operations| //+------------------------------------------------------------------+ void OnStart() { do { printf("Updating db: %s", InpDbPath); UpdateMarketData(symbols, timeframes, InpMaxRetries); Sleep(1000 * 60 * InpUpdateFreq); // 60 secs } while(!IsStopped()); }
UpdateMarketData()
Здесь мы вызываем функцию, которая инициализирует базу данных. Если что-то пойдет не так, мы возвращаем значение false и передаем управление обратно в основной цикл. Итак, если у вас возникли проблемы с инициализацией базы данных, вы можете смело оставить службу запущенной, пока устраняете эту проблему. В следующем цикле программа повторит попытку.
//+------------------------------------------------------------------+ //| Обновление рыночных данных для нескольких символов и временных интервалов | //+------------------------------------------------------------------+ bool UpdateMarketData(string &symbols_array[], ENUM_TIMEFRAMES &time_frames[], int max_retries = 3) { // Инициализировать базу данных if(!InitializeDatabase()) { LogMessage("Не удалось инициализировать базу данных"); return false; } bool allSuccess = true; Если база данных инициализирована (открыта), мы приступаем к обработке каждого символа и каждого таймфрейма. // Обработать каждый символ for(int i = 0; i << ArraySize(symbols_array); i++) { string symbol = symbols_array[i]; // Обработать каждый временной интервал for(int j = 0; j << ArraySize(time_frames); j++) { ENUM_TIMEFRAMES timeframe = time_frames[j]; int retryCount = 0; bool success = false;
//+------------------------------------------------------------------+ //| Обновление данных множества символов и периодов графика | //+------------------------------------------------------------------+ bool UpdateMarketData(string &symbols_array[], ENUM_TIMEFRAMES &time_frames[], int max_retries = 3) { // Инициализация БД if(!InitializeDatabase()) { LogMessage("Failed to initialize database"); return false; } bool allSuccess = true; // Если база данных инициализирована (открыта), приступаем к обработке каждого символа и каждого таймфрейма // Обработка каждого символа for(int i = 0; i < ArraySize(symbols_array); i++) { string symbol = symbols_array[i]; // Обработка каждого таймфрейма for(int j = 0; j < ArraySize(time_frames); j++) { ENUM_TIMEFRAMES timeframe = time_frames[j]; int retryCount = 0; bool success = false;
В этом цикле while мы контролируем максимальное количество попыток и фактически вызываем функцию для обновления базы данных.
while(retryCount < max_retries && !success) { success = UpdateSymbolTimeframeData(symbol, timeframe); if(!success) { retryCount++; Sleep(1000); // Wait before retry } } if(!success) { LogMessage(StringFormat("Failed to update %s %s after %d retries", symbol, TimeframeToString(timeframe), max_retries)); allSuccess = false; } } } DatabaseClose(dbHandle); return allSuccess; }
UpdateSymbolTimeframeData()
Поскольку таблица market_data требует идентификатор символа (symbol_id) в качестве внешнего ключа, нам необходимо сначала создать эту таблицу. Поэтому мы проверяем наличие этого идентификатора или создаём новый symbol_id в таблице «symbol», если это новый символ.
//+------------------------------------------------------------------+ //| Update market data for a single symbol and timeframe | //+------------------------------------------------------------------+ bool UpdateSymbolTimeframeData(string symbol, ENUM_TIMEFRAMES timeframe) { ResetLastError(); // Get symbol ID (insert if it doesn't exist) long symbol_id = GetOrInsertSymbol(symbol); if(symbol_id == -1) { LogMessage(StringFormat("Failed to get symbol ID for %s", symbol)); return false; }
Преобразуем тип временного интервала из MQL5 ENUM_TIMEFRAMES в строковый (TEXT) тип, требуемый нашей таблицей.
string tfString = TimeframeToString(timeframe); if(tfString == "") { LogMessage(StringFormat("Unsupported timeframe for symbol %s", symbol)); return false; }
Копируем котировки последнего закрытого бара.
// Get the latest closed bar MqlRates rates[]; if(CopyRates(symbol, timeframe, 1, 1, rates) != 1) { LogMessage(StringFormat("Failed to get rates for %s %s: %d", symbol, tfString, GetLastError())); return false; }
Проверяем, существует ли эта запись. Если да, регистрируем это и возвращаем true.
if(MarketDataExists(symbol_id, rates[0].time, tfString)) { LogMessage(StringFormat("Data already exists for %s %s at %s", symbol, tfString, TimeToString(rates[0].time))); return true; }
Если это новая запись или новая котировка, мы запускаем транзакцию базы данных, чтобы обеспечить атомарность. Если что-то пойдет не так, мы откатываем изменения. Если всё прошло успешно, фиксируем транзакцию и возвращаем значение true.
// Start transaction if(!DatabaseTransactionBegin(dbHandle)) { LogMessage(StringFormat("Failed to start transaction: %d", GetLastError())); return false; } // Insert the new data if(!InsertMarketData(symbol_id, tfString, rates[0])) { DatabaseTransactionRollback(dbHandle); return false; } // Commit transaction if(!DatabaseTransactionCommit(dbHandle)) { LogMessage(StringFormat("Failed to commit transaction: %d", GetLastError())); return false; } LogMessage(StringFormat("Successfully updated %s %s data for %s", symbol, tfString, TimeToString(rates[0].time))); return true; }
InitializeDatabase()
Здесь мы открываем базу данных и проверяем хендл.
//+------------------------------------------------------------------+ //| Initialize database connection | //+------------------------------------------------------------------+ bool InitializeDatabase() { ResetLastError(); // Open database (creates if it doesn't exist) dbHandle = DatabaseOpen(InpDbPath, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { LogMessage(StringFormat("Failed to open database: %d", GetLastError())); return false; }
Эта директива «PRAGMA», включающая поддержку foreign_keys в SQLite, не является строго необходимой при работе со встроенной базой данных SQLite, поскольку мы знаем, что она была скомпилирована с включенной этой функцией. Это всего лишь мера безопасности на случай, если вам придется работать с внешней базой данных.
// Enable foreign key constraints if(!DatabaseExecute(dbHandle, "PRAGMA foreign_keys = ON")) { LogMessage(StringFormat("Failed to enable foreign keys: %d", GetLastError())); return false; } LogMessage("Database initialized successfully"); return true; }
TimeframeToString()
Функция преобразования MQL5 ENUM_TIMEFRAMES в строку представляет собой простой оператор switch.
//+------------------------------------------------------------------+ //| Convert MQL5 timeframe to SQLite format | //+------------------------------------------------------------------+ string TimeframeToString(ENUM_TIMEFRAMES tf) { switch(tf) { case PERIOD_M1: return "M1"; case PERIOD_M2: return "M2"; case PERIOD_M3: return "M3"; (...) case PERIOD_MN1: return "MN1"; default: return ""; } }
MarketDataExists()
Чтобы проверить, существуют ли уже рыночные данные, выполняем простой запрос по составному первичному ключу таблицы market_data.
//+------------------------------------------------------------------+ //| Check if market data exists for given timestamp and timeframe | //+------------------------------------------------------------------+ bool MarketDataExists(long symbol_id, datetime tstamp, string timeframe) { ResetLastError(); int stmt = DatabasePrepare(dbHandle, "SELECT 1 FROM market_data WHERE symbol_id = ? AND tstamp = ? AND timeframe = ? LIMIT 1"); if(stmt == INVALID_HANDLE) { LogMessage(StringFormat("Failed to prepare market data existence check: %d", GetLastError())); return false; } if(!DatabaseBind(stmt, 0, symbol_id) || !DatabaseBind(stmt, 1, (long)tstamp) || !DatabaseBind(stmt, 2, timeframe)) { LogMessage(StringFormat("Failed to bind parameters for existence check: %d", GetLastError())); DatabaseFinalize(stmt); return false; } bool exists = DatabaseRead(stmt); DatabaseFinalize(stmt); return exists; }
InsertMarketData()
Наконец, чтобы внести новые рыночные данные (обновление), мы выполняем запрос на вставку, используя функцию MQL5 StringFormat, как рекомендуется в документации.
Будьте осторожны со строками при подстановке в VALUES. Их нужно заключать в кавычки. Поблагодарите меня позже. 🙂
//+------------------------------------------------------------------+ //| Insert market data into database | //+------------------------------------------------------------------+ bool InsertMarketData(long symbol_id, string timeframe, MqlRates &rates) { ResetLastError(); string req = StringFormat( "INSERT INTO market_data (" "tstamp, timeframe, price_open, price_high, price_low, price_close, " "tick_volume, real_volume, spread, symbol_id) " "VALUES(%d, '%s', %G, %G, %G, %G, %d, %d, %d, %d)", rates.time, timeframe, rates.open, rates.high, rates.low, rates.close, rates.tick_volume, rates.real_volume, rates.spread, symbol_id); if(!DatabaseExecute(dbHandle, req)) { LogMessage(StringFormat("Failed to insert market data: %d", GetLastError())); return false; } return true; }
Пока служба работает…
Рисунок 5 — Навигатор Metaeditor при запущенной службе обновления базы данных
… на вкладке «Журнал экспертов» должно появиться примерно следующее.
Рисунок 7 — Вкладка «Журнал экспертов» в MetaTrader 5 с результатами работы службы обновления базы данных
Ваша таблица «market_data» должна выглядеть следующим образом. Обратите внимание, что на данный момент мы храним все рыночные данные по всем символам и таймфреймам в одной таблице. Позже мы это доработаем, но только по мере необходимости. На данный момент этого более чем достаточно, чтобы начать анализ данных достаточно надёжно.
Рисунок 8 — Встроенный в Metaeditor вкладка SQLite с обновлениями базы данных
Заключение
В этой статье мы рассмотрели, как мы переходим от кратковременных загруженных данных о ценах к первой версии базы данных для нашей системы статистического арбитража. Мы рассмотрели принципы, лежащие в основе первоначальной структуры схемы, а также способы её инициализации и ввода начальных данных.
Теперь все таблицы, поля и связи схемы задокументированы, включая описания таблиц, ограничения и примеры.
Кроме того, мы подробно описали шаги по обновлению базы данных путем создания сервиса Metatrader 5, предоставили один вариант реализации этого сервиса, а также скрипты на языке Python для ввода исходных данных по любому доступному символу и таймфрейму.
Благодаря этим инструментам и без необходимости писать ни одной строчки кода обычный трейдер — целевая аудитория нашей платформы для статистического арбитража — может начать хранить не только оперативные рыночные данные (ценовые котировки), но и некоторую метаинформацию об акциях, задействованных в нашей стратегии коинтеграции, а также историю сделок.
Эта первоначальная схема будет доработана уже на следующем этапе, когда мы будем обновлять веса портфеля в режиме реального времени и перебалансировать портфель в случае ослабления коинтеграции корзины, заменяя и/или добавляя инструменты без ручного вмешательства.
Ссылки
Дэниел П. Паломар (2025). Оптимизация портфеля: Теория и практика. Издательство Кембриджского университета.
*Одним из существенных недостатков SQLite по сравнению с некоторыми базами данных, ориентированными на временные ряды, является отсутствие «соединений по дате». Поскольку мы будем индексировать наши рыночные данные по временным меткам, в будущем мы почти наверняка не сможем использовать «соединения» между таблицами, так как временные метки используются в качестве первичных ключей, а их расположение в двух или более таблицах редко, а возможно, и никогда не будет совпадать, а «соединения» (внутренние, левые и внешние) зависят именно от такого совпадения индексов.Поэтому при использовании «соединений» мы получим пустые (нулевые) наборы результатов. В этой записи блога подробно объясняется данная проблема.
| Название файла | Описание |
|---|---|
| StatArb/db-setup.mq5 | Скрипт MQL5 для создания и инициализации базы данных SQLite путем чтения файла схемы schema-0.1.sql |
| StatArb/db-update-statarb-0.1.mq5 | Сервис MQL5 для обновления базы данных SQLite самыми последними закрытыми ценовыми барами |
| StatArb/schema-0.1.sql | Файл схемы SQL (DDL) для инициализации базы данных (создание таблиц, полей и ограничений) |
| db_store_quotes.ipynb | Jupyter Notebook, содержащий код на Python. Вспомогательный файл для заполнения базы данных SQLite котировками символов за определенный период времени и таймфрейм |
| .env | Пример файла с переменными среды, которые должны считываться из приведенного выше вспомогательного файла Python. (необязательно) |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19242
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание самооптимизирующихся советников на MQL5 (Часть 13): Введение в теорию управления с использованием факторизации матриц
Разработка инструментария для анализа Price Action (Часть 28): Инструмент для торговли пробоя диапазона открытия
Python + MetaTrader 5: быстрый исследовательский контур для данных, признаков и прототипов
От начального до среднего уровня: Объекты (II)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования