Статистический арбитраж на коинтегрированных акциях (Часть 3): Настройка базы данных
Введение
В предыдущей статье этой серии (часть 2) мы провели бэктест стратегии статистического арбитража, состоящей из корзины коинтегрированных акций из сектора микропроцессоров (акции Nasdaq). Сначала мы отфильтровали среди сотен тикеров те, которые наиболее коррелируют с Nvidia. Затем мы протестировали отобранную группу на коинтеграцию с помощью теста Йохансена, стационарность спрэда — с использованием тестов ADF и KPSS, и, наконец, получили относительные веса портфеля, извлекая собственный вектор Йохансена для первого ранга. Результаты бэктеста оказались многообещающими.
Однако два или более актива могли быть коинтегрированы в течение последних двух лет и уже завтра начать терять эту коинтеграцию. Иными словами, нет гарантии, что пара или группа коинтегрированных активов останется коинтегрированной. Изменения в управлении компанией, макроэкономическая ситуация или отраслевые изменения могут повлиять на фундаментальные факторы, которые изначально обусловили коинтеграцию активов. И наоборот: активы, которые ранее не были коинтегрированы, могут начать двигаться коинтегрированно уже в следующий момент по тем же причинам. Рынок — это "загадка в состоянии непрерывного изменения". Нам необходимо адаптироваться к этим изменениям. [PALOMAR, 2025]
Корзина коинтегрированных акций будет практически непрерывно менять относительные веса портфеля, а именно веса портфеля определяют не только объём (количество) наших ордеров, но и их направление (покупка или продажа). Следовательно, нам нужно уметь адаптироваться и к этим изменениям. Хотя коинтеграция является более долгосрочной зависимостью, веса портфеля меняются постоянно. Поэтому их необходимо проверять чаще и обновлять модель сразу же при изменениях. Как только мы обнаруживаем, что наша модель устарела, нужно действовать немедленно — нам требуется мгновенная замена устаревшей модели.
Наш торговый советник (Expert Advisor) должен в реальном времени понимать, остаются ли используемые веса портфеля актуальными или уже изменились. Если они изменились, советник должен как можно быстрее получать информацию о новых весах. Кроме того остаётся ли сама модель валидной. Если нет, советник должен получить информацию о том, какие активы необходимо заменить, и ротация должна быть немедленно применена к текущему портфелю.
Мы использовали интеграцию Metatrader 5 с Python и профессиональные статистические функции из библиотеки statsmodels, но до сих пор работали только с данными в реальном времени, загружая котировки (ценовые данные) по мере необходимости. Такой подход удобен на этапе исследования благодаря своей простоте. Однако если мы собираемся ротировать портфель, обновлять модели или веса портфеля, стоит задуматься о хранении данных. Иными словами, нам необходимо начать сохранять данные в базе, поскольку скачивать их каждый раз становится непрактично. Более того, нам может понадобиться искать взаимосвязи между различными классами активов и между инструментами, которые не участвовали в наших первоначальных тестах на коинтеграцию.
Высококачественная, масштабируемая и насыщенная метаданными база данных является основой любого серьёзного проекта в области статистического арбитража. Учитывая, что проектирование базы данных — задача во многом индивидуальная (хорошая база данных — это та, которая соответствует требованиям конкретного бизнеса), в этой статье мы рассмотрим один из возможных подходов к созданию базы данных, ориентированной на задачи статистического арбитража.
Какие вопросы должна решать наша база данных?
В нашем стремлении создать «бюджетный фреймворк для статистического арбитража», то есть систему, подходящую для среднего розничного трейдера с обычным ноутбуком и стандартной скоростью интернета, мы сталкиваемся с рядом сложностей, связанных с недостатком специализации в необходимых областях — таких как статистика и разработка программного обеспечения. Проектирование баз данных НЕ является исключением из этого списка. Это обширная область сама по себе — можно написать целые книги на эту тему и всё равно не исчерпать её полностью. В идеале, конечно, стоит привлечь одного или нескольких специалистов для проектирования, реализации и сопровождения базы данных.
Однако, поскольку мы создаём этот фреймворк для обычного трейдера, нам приходится работать с тем, что есть: изучать книги, специализированные форумы и ресурсы, учиться у опытных специалистов, пробовать, ошибаться, экспериментировать, рисковать и быть готовыми изменить архитектуру базы данных, если она окажется неподходящей. Нам нужна гибкость и постепенный подход — лучше начинать с малого, двигаясь снизу вверх (bottom-up), а не сверху вниз (top-down), чтобы избежать избыточной сложности.
В конечном итоге, наша база данных должна отвечать на очень простой вопрос: что нам сейчас торговать, чтобы получить максимально возможную прибыль?
Обращаясь к предыдущей статье, где направления и объёмы ордеров определялись на основе весов портфеля корзины акций, возможный ответ базы данных может выглядеть примерно так:
| Символы | Веса | Timeframe |
|---|---|---|
| "MU", "NVDA", "MPWR", "MCHP" | 2.699439, 1.000000, -1.877447, -2.505294 | D1 |
Таблица 1 — Пример ожидаемого ответа запроса для обновления модели в реальном времени
Если наша база данных способна предоставлять такую относительно простую информацию с подходящей частотой обновления, то у нас есть всё необходимое для непрерывной торговли на максимально оптимизированном уровне.
Обновление базы данных как сервис
До настоящего момента мы проводили анализ данных, используя котировки в реальном времени из терминала Metatrader 5 через код на Python (технически, большую часть времени мы использовали котировки, уже сохранённые внутренним движком терминала). После определения символов и весов портфеля мы вручную обновляли наш торговый советник, задавая новые символы и/или новые веса портфеля.
Начиная с этого момента, мы отделяем процесс анализа данных от терминала и переходим к использованию данных, хранящихся в нашей базе данных, чтобы обновлять Expert Advisor сразу после появления новых весов портфеля, останавливать торговлю при утрате коинтеграционной связи или переключаться на другую, более перспективную группу инструментов. Иными словами, мы стремимся улучшить рыночную экспозицию по каждому инструменту, обновляя веса портфеля в реальном времени и/или выполняя ротацию портфеля каждый раз, когда результаты анализа данных это рекомендуют.
Для обновления базы данных мы реализуем сервис Metatrader 5.
Из документации Metatrader 5 мы узнаём следующее:
- сервисы не привязаны к конкретному графику.
- сервисы загружаются сразу после запуска терминала, если они были активны на момент его закрытия.
- сервисы работают в собственном потоке.
Из этого можно сделать вывод, что сервис является идеальным способом поддерживать базу данных в работоспособном состоянии. Если сервис был запущен во время завершения торговой сессии и закрытия терминала, он автоматически возобновит работу при следующем запуске терминала, независимо от того, какой график или символ открыт в данный момент. Кроме того, поскольку он работает в отдельном потоке, его работа не будет влиять на другие сервисы, индикаторы, скрипты или советники и не будет зависеть от них.
Таким образом, наш рабочий процесс будет выглядеть следующим образом:
- Весь анализ данных будет выполняться в Python вне среды Metatrader 5. Для запуска анализа мы будем загружать исторические данные и сохранять их в базе данных.
- Каждый раз при изменении активного портфеля — добавлении или удалении символа — мы будем обновлять входные параметры сервиса, передавая массив символов и массив таймфреймов.
- Каждый раз при изменении активного портфеля — добавлении или удалении символа — мы также будем обновлять входные параметры советника.
На данный момент обновления на шагах 2 и 3 будут выполняться вручную. Позже мы автоматизируем этот процесс.
Настройка базы данных
В процессе подготовки этой статьи я вновь убедился, что идеальным инструментом для данной задачи является специализированная колоночная база данных для временных рядов. На рынке существует множество решений, удовлетворяющих этим требованиям — как платных, так и бесплатных, как проприетарных, так и с открытым исходным кодом. Они предназначены для обработки высокоспециализированных нагрузок, огромных объёмов данных и обеспечивают отклик менее чем за секунду как при загрузке данных, так и при выполнении запросов в реальном времени.
Однако в нашем случае приоритетом является не масштабируемость. Основной акцент мы делаем на простоте и применимости для одного пользователя, а не для команды высококвалифицированных администраторов баз данных (DBA) и специалистов по системам хранения временных рядов. Поэтому мы начнём с самого простого решения, осознавая его ограничения и понимая, что система будет развиваться по мере необходимости.
Мы начнём с встроенной базы данных SQLite в Metatrader 5. Существует большое количество материалов о том, как создавать и использовать встроенную SQLite-базу в среде Metatrader 5. Найти их можно:
- в документации 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 в папке MQL5/Files — он предназначен исключительно для отладки. Если возникнут проблемы, вы можете проверить этот файл, чтобы понять, как система читает файл схемы. Если всё работает корректно, этот файл можно безопасно удалить.
Альтернативно, вы можете создать базу данных другими способами и загрузить схему, используя любой клиент SQLite3, интерфейс MetaEditor, Windows PowerShell или командную строку SQLite3. Тем не менее, рекомендуется хотя бы в первый раз создать базу данных с помощью прилагаемого скрипта. Позже вы сможете адаптировать процесс под свои нужды.
Схема базы данных
Начальная схема базы данных состоит всего из четырёх таблиц, две из которых являются заготовками для следующих этапов. То есть на текущем этапе мы будем использовать только таблицы "symbol" и "market_data".
На рисунке 1 представлена диаграмма «сущность–связь» (ERD) для данной начальной схемы.

Рис. 1 – Исходная ERD (диаграмма «сущность-связь») схемы базы данных
Таблица "corporate_event", как нетрудно догадаться, предназначена для хранения событий, связанных с компаниями из нашего портфеля, таких как размер дивидендных выплат, дробление акций, обратный выкуп акций, слияния и другие. Пока мы не будем с ней работать.
Таблица "trade" будет хранить наши торговые сделки. Собирая эти данные, мы получим уникальную коллекцию данных для агрегации и анализа. Мы будем использовать её только когда начнём торговлю.
Таблица "market_data" будет хранить наши данные OHLC со всеми полями MqlRates. Она имеет составной первичный ключ (symbol_id, timeframe и timestamp), чтобы гарантировать уникальность каждой записи. Таблица "market_data" связана с таблицей "symbol" внешним ключом.
Как видите, это единственная таблица, в которой используется составной первичный ключ. Во всех остальных таблицах в качестве первичного ключа используется timestamp, хранящийся как INTEGER. Для такого выбора есть причина. Согласно документации SQLite3:
"Данные для таблиц с rowid хранятся в виде B-дерева, содержащего одну запись для каждой строки таблицы, с использованием значения rowid в качестве ключа. Это означает, что извлечение или сортировка записей по rowid выполняется быстро. Поиск записи с определённым rowid или всех записей с rowid в указанном диапазоне примерно в два раза быстрее, чем аналогичный поиск по любому другому ПЕРВИЧНОМУ КЛЮЧУ или индексированному значению.
(...) если таблица с rowid имеет первичный ключ, состоящий из одного столбца, и объявленный тип этого столбца — "INTEGER" в любом регистре, то такой столбец становится псевдонимом для rowid. Такой столбец обычно называют "целочисленным первичным ключом".
(...) "вы можете добиться производительности запросов 'примерно в два раза быстрее', если используете INTEGER в качестве первичного ключа. Метки времени Unix epoch можно вставлять как INTEGER в базах данных SQLite3"."
(...) "SQLite хранит целые числа в 64-битном дополнительном коде¹. Это обеспечивает диапазон хранения от -9223372036854775808 до +9223372036854775807 включительно. Целые числа в этом диапазоне являются точными. (Документация SQLite3)".
Таким образом, мы можем заменить наши строки даты и времени на метки времени Unix epoch и вставлять их в качестве первичного ключа, чтобы получить прирост скорости, превышающий скорость света. 🙂
Приведённые ниже таблицы содержат полную документацию по схеме (словарь данных) для использования нашей командой и нами в будущем.
Tаблица: symbol
Хранит метаданные о торгуемых или отслеживаемых финансовых инструментах.
| Поле | Тип данных | Null | Ключ | Описание |
|---|---|---|---|---|
| symbol_id | INTEGER | NO | PK | Уникальный идентификатор для каждого финансового инструмента. |
| ticker | TEXT(≤10) | NO | Тикер символа актива (например, "AAPL", "MSFT"). | |
| exchange | TEXT(≤50) | NO | Биржа, где котируется актив (например, "NASDAQ", "NYSE"). | |
| asset_type | TEXT(≤50) | YES | Тип актива (например, "Equity", "ETF", "FX", "Crypto"). | |
| sector | TEXT(≤50) | YES | Классификация экономического сектора (например, "Technology", "Healthcare"). | |
| industry | TEXT(≤50) | YES | Классификация отрасли внутри сектора. | |
| currency | TEXT(≤50) | YES | Валюта номинации актива (например, "EUR", "USD"). |
Таблица 2 – Описание словаря данных таблицы 'symbol' (v0.1)
Таблица: corporate_event
Фиксирует события, влияющие на активы, такие как дивиденды, дробление акций или объявления о прибыли.
| Поле | Тип данных | Null | Ключ | Описание | Пример |
|---|---|---|---|---|---|
| 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(symbol_id); связывает событие с активом. | 1 |
Таблица 3 – Описание словаря данных таблицы 'corporate_event' (v0.1)
Таблица: market_data
Хранит данные OHLCV (open, high, low, close, volume) и связанные временные ряды для активов.
| Поле | Тип данных | Null | Ключ | Описание | Пример |
|---|---|---|---|---|---|
| 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 | Средний или мгновенный спред (bid-ask) за период бара. | 0.02 | |
| symbol_id | INTEGER | NO | PK*, FK | Ссылка на symbol(symbol_id). | 1 |
Таблица 4 – Описание словаря данных таблицы 'market_data' (v0.1)
Таблица: trade
Отслеживает реальные или имитационные сделки для стратегий.
| Поле | Тип данных | Null | Ключ | Описание | Пример |
|---|---|---|---|---|---|
| tstamp | INTEGER | NO | PK | Метка времени Unix исполнения сделки. | 1678905600 |
| ticket | INTEGER | NO | Тикет/ID торгового ордера. | 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(symbol_id). | 1 |
Таблица 5 – Описание словаря данных таблицы 'trade' (v0.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)
ПРОВЕРКА ДЛИНЫ
Кроме того, мы вводим проверку длины для нескольких текстовых полей. Это связано с тем, что SQLite не принудительно ограничивает длину или не усекает строки на основе значения (n), указанного в CHAR(n) или VARCHAR(n), как это принято в других СУБД.
А что насчёт индексов?
Вы можете спросить, почему мы не создали никаких индексов. Мы создадим их, как только начнём выполнять запросы, чтобы точно знать, где они необходимы.
Первоначальная вставка данных
Предполагается, что база данных будет заполняться незаметно в ходе выполнения нашего анализа данных из интеграции MQL5 с Python. Однако для удобства прилагается скрипт Python (db_store_quotes.ipynb), который поможет вам сохранить котировки для списка символов, с определённого таймфрейма и за выбранный временной интервал. В дальнейшем мы будем выполнять наш анализ данных (тесты на корреляцию, коинтеграцию и стационарность) с использованием этих сохранённых данных.
Рисунок 2 – Таблица 'Symbol' после первоначальной вставки данных с помощью скрипта Python
Как видите, большая часть метаданных «symbol» имеет значение '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 также прилагается к этой статье.
# keep this file at the root of your project # or in the same folder of the Python script that uses it STATARB_DB_PATH="your/db/path/here"
Основной вызов db_store_quotes.ipynb находится в нижней части скрипта.
symbols = ['MPWR', 'AMAT', 'MU'] # Symbols from Market Watch timeframe = mt5.TIMEFRAME_M5 # 5-minute timeframe start_date = '2024-02-01' end_date = '2024-03-31' db_path = os.getenv('STATARB_DB_PATH') # Path to your SQLite database if db_path is None: print("Error: STATARB_DB_PATH environment variable is not set.") else: print("db_path: " + db_path) # Download historical quotes and store them in the database download_mt5_historical_quotes(symbols, timeframe, start_date, end_date, db_path)
Обновления
Основа нашего обслуживания базы данных для автоматического обновления модели и ротации портфеля заключается в нашем фоновом MQL5 Сервисе. По мере развития нашей базы данных этот Сервис также потребует обновлений.
Сервис подключается к локальному файлу базы данных SQLite (или создает его).
ПРЕДУПРЕЖДЕНИЕ: При создании совершенно новой базы данных с использованием Сервиса обновления базы данных не забудьте инициализировать базу данных с помощью скрипта db_setup, как упоминалось выше.
Затем Сервис устанавливает необходимые ограничения для целостности данных и переходит в бесконечный цикл, в котором проверяет наличие
новых рыночных данных. Для каждого символа и таймфрейма он:
- получает последний завершенный ценовой бар (включая open, high, low, close, volume и spread),
- проверяет, существует ли он уже в базе данных,
- и вставляет новые котировки, если их еще нет
Сервис оборачивает вставку в транзакцию базы данных для обеспечения атомарности. Если что-то идет не так (например, ошибка базы данных), он выполняет повторные попытки до установленного лимита (по умолчанию 3 раза) с паузами в одну секунду. Логирование на ваше усмотрение. Цикл делает паузу между обновлениями и останавливается только при ручной остановке сервиса.
Итак, давайте взглянем на некоторые его компоненты.
Во входных параметрах вы можете выбрать путь к базе данных в файловой системе, частоту обновления в минутах, максимальное количество повторных попыток при неудачной вставке, а также хотите ли вы выводить сообщения об успехе/неудаче в журнал советника. Этот последний параметр может быть полезен, когда мы выйдем на стабильный код после завершения разработки.
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string InpDbPath = "StatArb\\statarb-0.1.db"; // Database filename input int InpUpdateFreq = 1; // Update frequency in minutes input int InpMaxRetries = 3; // Max retries input bool InpShowLogs = true; // Enable logging?
Рисунок 3 – Диалоговое окно MetaTrader 5 для входных параметров сервиса обновления базы данных
Вы должны выбрать символы и соответствующие таймфреймы, которые будут обновляться. Эти параметры будут автоматизированы на следующем этапе.
//+------------------------------------------------------------------+ //| Global vars | //+------------------------------------------------------------------+ string symbols[] = {"EURUSD", "GBPUSD", "USDJPY"}; ENUM_TIMEFRAMES timeframes[] = {PERIOD_M5};
Здесь мы инициализируем дескриптор базы данных как INVALID_HANDLE. Он будет проверен далее при открытии базы данных.
// Database 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' и передаем управление обратно в основной цикл. Таким образом, если у вас возникли проблемы с инициализацией базы данных, вы можете спокойно оставить Сервис запущенным, пока устраняете проблему инициализации базы данных. На следующем цикле он попытается снова.
//+------------------------------------------------------------------+ //| Update market data for multiple symbols and timeframes | //+------------------------------------------------------------------+ bool UpdateMarketData(string &symbols_array[], ENUM_TIMEFRAMES &time_frames[], int max_retries = 3) { // Initialize database if(!InitializeDatabase()) { LogMessage("Failed to initialize database"); return false; } bool allSuccess = true; If the database is initialized (open), we start processing each symbol and timeframe. // Process each symbol for(int i = 0; i < ArraySize(symbols_array); i++) { string symbol = symbols_array[i]; // Process each timeframe for(int j = 0; j < ArraySize(time_frames); j++) { ENUM_TIMEFRAMES timeframe = time_frames[j]; int retryCount = 0; bool success = false;
В этом цикле while мы контролируем максимальное количество повторных попыток и непосредственно вызываем функцию обновления базы данных.
// Retry logic 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_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 с запущенным Сервисом обновления БД
…вы должны увидеть нечто подобное на вкладке журнала Experts.
Рисунок 7 – Вкладка журнала Experts MetaTrader 5 с выводом Сервиса обновления базы данных
Ваша таблица ‘market_data’ должна выглядеть следующим образом. Обратите внимание, что пока мы храним все рыночные данные для всех символов и таймфреймов в одной таблице. Позже мы улучшим это, но только по мере возникновения необходимости. На данный момент этого более чем достаточно, чтобы начать наш анализ данных более устойчивым образом.
Рисунок 8 – Вкладка интегрированной SQLite в Metaeditor с обновлениями базы данных
Заключение
В этой статье мы увидели, как переходим от эфемерных загруженных ценовых данных к первой версии базы данных для нашего фреймворка статистического арбитража. Мы рассмотрели обоснование её первоначальной схемы, способы инициализации и вставки начальных данных.
Все таблицы, поля и связи схемы теперь задокументированы с описаниями таблиц, ограничениями и примерами.
Мы также подробно описали шаги по поддержанию базы данных в актуальном состоянии с помощью создания Сервиса MetaTrader 5, предоставили реализацию этого сервиса, а также Python-скрипты для вставки начальных данных для любого доступного символа (тикера) и таймфрейма.
С помощью этих инструментов, и без необходимости писать ни строчки кода, обычный розничный трейдер — целевая аудитория нашего фреймворка статистического арбитража — может начать хранить не только текущие рыночные данные (ценовые котировки), но также некоторую метаинформацию об акциях, участвующих в нашей стратегии коинтеграции, и историю сделок.
Эта первоначальная схема будет развиваться прямо на следующем этапе, когда мы будем обновлять веса портфеля в реальном времени и осуществлять ротацию портфеля, если коинтеграция корзины ослабевает, заменяя и/или добавляя символы без ручного вмешательства.
Источники
Daniel P. Palomar (2025). Portfolio Optimization: Theory and Application. Cambridge University Press.
* Заметное ограничение SQLite по сравнению с некоторыми базами данных, ориентированными на временные ряды, — отсутствие "асоф-джойнов" (as-of joins). Поскольку мы будем индексировать наши рыночные данные по временной метке, мы почти наверняка не сможем использовать "джойны" (соединения) между таблицами в будущем, потому что мы используем временные метки в качестве первичных ключей, и они редко, возможно, никогда, не будут совпадать в двух или более таблицах, а "джойны" (inner, left и outer joins) зависят от этого совпадения индексов. Поэтому при использовании «джойнов» мы будем получать пустые (null) результирующие наборы. В этом посте блога подробно объясняется данная проблема.
| Название файла | Описание |
|---|---|
| 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 блокнот с кодом 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+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования