Низкочастотные количественные стратегии в MetaTrader 5: (Часть 1) Настройка OLAP-ориентированного хранилища данных
Введение
Мы только что завершили серию статей, в которой представили базовые концепции статистического арбитража для среднего розничного трейдера, имеющего лишь обычный ноутбук, стандартное интернет-соединение и ограниченный капитал. Чтобы это было возможно, мы сохраняем фокус на низкочастотных стратегиях возврата к среднему, предполагая, что высокочастотная торговля (HFT) остаётся капиталоёмкой областью, обычно доступной институциональным участникам. Надеемся, нам удалось показать, что розничные трейдеры могут добиться успеха в низкочастотной количественной торговле при наличии достаточного объёма данных, поскольку необходимая вычислительная мощность уже широко доступна. Достаточно данных — значит достаточно информации для поиска нишевых возможностей. Достаточно вычислительной мощности — значит аппаратное и программное обеспечение, способное обработать данные за требуемое время.
В последней статье той серии, мы предложили использовать специализированную базу данных для анализа данных — бесплатную базу с открытым исходным кодом, которую можно использовать в связке с SQLite в нашем конвейере. Основная идея этого предложения в том, что по мере развития и роста набора данных система OLTP, такая как SQLite, уже не является лучшим инструментом для аналитических задач, хотя на начальном этапе она подходила для демонстрации вводных концепций. Иными словами, по мере движения к реальной торговой системе нам нужен специализированный инструмент анализа данных, и первым, возможно самым важным, нам нужна специализированная OLAP-СУБД.
Теперь, в этой новой серии, которую можно воспринимать как новую главу той же истории, пришло время начать реализацию системы анализа данных, удобной для OLAP. Однако если вы следовали предыдущей серии, запускали инструменты и экспериментировали с предоставленными скриптами и статистическими методами, у вас уже может быть много данных, сохранённых в базе SQLite, и часть этих данных может быть результатами предыдущего анализа, например выводами тестов коинтеграции и системы скоринга. Возможно, вы захотите сохранить ту часть данных, которую необходимо перенести или преобразовать в новую систему. Но даже если эти данные кажутся вам нерелевантными, в будущем вы можете столкнуться с необходимостью импортировать данные из других систем баз данных (Postgres, MySQL и т. д.), включая SQLite. Поэтому наша первая задача в этой новой главе — показать, насколько быстрым и простым может быть такой перенос.
После переноса любых устаревших или дополнительных данных в новую систему следующим шагом будет получение новых данных непосредственно в неё, поддерживая их в актуальном и аккуратно очищенном состоянии, без пропусков и дублирования. Для этого мы будем использовать отраслевой стандарт файлового формата (Parquet) и отраслевую схему разбиения файловой системы (Hive), обеспечивая переносимость данных между локальными системами и большинством облачных провайдеров, высокую производительность и простоту запросов.
После развёртывания этой OLAP-ориентированной конфигурации, мы можем запускать анализ данных для оценки торговых идей, поиска нишевых закономерностей и бэктестинга известных торговых сетапов, чтобы квалифицировать их (или отклонить) для разработки полнофункциональных Expert Advisors. То есть анализ данных должен экономить нам время, позволяя фильтровать новые идеи за секунды вместо часов или даже дней разработки высокопроизводительных MQL5 Expert Advisor для бэктестинга. Благодаря объединению данных из нескольких источников (брокеров или коммерческих поставщиков) мы можем исследовать необычные комбинации, включая нефинансовые данные, такие как фрахтовые ставки и агрегированные макроэкономические наборы данных.
Важно отметить: хотя высокочастотная торговля (HFT) обычно ассоциируется с операциями порядка миллисекунд или даже микросекунд, в наших целях любая частота ниже нескольких секунд находится вне области рассмотрения. В этой серии статей нас интересует разработка нишевых стратегий для любых таймфреймов, но никогда ниже нескольких секунд. Мы не хотим конкурировать там, где наши шансы близки к нулю. Вместо этого мы хотим использовать наш небольшой размер как преимущество. Скорость, конечно, важна, но скорость маршрутизации ордеров или скорость исполнения — не единственная значимая скорость. Мы также можем получить торговое преимущество, повышая скорость обнаружения возможностей. Если мы найдём преимущество, то сможем исследовать его в «голубом океане» без необходимости бороться за миллисекунды.
Имейте в виду, что принципы, которые мы обсудим здесь (партиционирование на разделы, структура файловой системы, чтение данных без копирования и использование нативных функций базы данных), применимы к любой OLAP-системе. Для удобства мы будем использовать ту же бесплатную систему с открытым исходным кодом, которую описывали в той статье, — DuckDB. Если вы с ней не знакомы, рекомендуем обратиться к той статье, чтобы лучше понять причины этого выбора.
Преобразование базы данных SQLite в файлы Parquet
Представляя результаты нашего исследования по разработке фреймворка статистического арбитража для среднего розничного трейдера, мы создали минимальную схему базы данных с использованием встроенной SQLite в Metatrader 5. Эта схема развивалась органично по мере роста наших знаний и появления новых требований. В последней версии (statarb-0.5) она выглядела так:

Рис. 1. ER-диаграмма базы данных SQLite statarb-0.5
Схема была фактически реализована, и мы использовали эту базу данных в экспериментах и бэктестах. В ней мы хранили не только рыночные данные, но и результаты множества статистических тестов и экспериментов с нашей системой скоринга. В зависимости от того, как вы работали с концепциями, эти данные могут быть ценными и потенциально большими. Даже если вы считаете их одноразовыми и полезными только для обучения, следует учитывать, что в какой-то момент работы вам может понадобиться повторно использовать данные, уже сохранённые в OLTP-системе. Например, у вас могут быть платные данные, уже сохранённые в базе Postgres или MySQL. В таком случае для масштабируемого анализа данных в количественных стратегиях нужно преобразовать их в формат, более подходящий для загрузки в OLAP-систему. К счастью, такая необходимость — обычное дело в количественной торговле, и с DuckDB это легко сделать.
Предположим, DuckDB уже установлен в вашей системе. Используя его CLI, вы можете выполнить в оболочке DuckDB такую простую команду:
INSTALL sqlite; LOAD sqlite; ATTACH '<path/to/your/sqlite.db>' AS sqlite_db (TYPE SQLITE); USE sqlite_db; EXPORT DATABASE '<folder_name>' (FORMAT PARQUET);
<folder_name> — это выбранное вами место в локальной файловой системе, где будут храниться преобразованные данные. Эта команда создаст несколько файлов Parquet в выбранной папке: по одному файлу на каждую таблицу базы данных, включая системные таблицы, а также два дополнительных файла: schema.sql и load.sql.

Рис. 2. Вывод PowerShell, показывающий все таблицы SQLite, преобразованные в файлы Parquet
«Файл schema.sql содержит инструкции схемы, найденные в базе данных. Он содержит все команды CREATE SCHEMA, CREATE TABLE, CREATE VIEW и CREATE SEQUENCE, необходимые для повторного создания базы данных.
Файл load.sql содержит набор инструкций COPY, которые можно использовать для повторного чтения данных из CSV-файлов. Файл содержит одну инструкцию COPY для каждой таблицы, найденной в схеме.» (документация DuckDB)
Возможно, вы также захотите преобразовать только отдельные таблицы; вероятно, именно так вы будете поступать в большинстве случаев, поскольку некоторые таблицы связаны с торговлей, а не с анализом данных. Например, таблицы ‘trade’ и ‘strategy’ используются соответственно для хранения торговых метаданных и для предоставления Expert Advisor данных об обновлениях стратегии в реальном времени.
Вот как можно преобразовать одну таблицу из командной строки. Вместо экспорта базы данных вы COPY нужную таблицу в ‘output.parquet’.
duckdb -c "INSTALL sqlite; LOAD sqlite;
ATTACH 'my_database.db' AS sqlite_db (TYPE SQLITE);
COPY sqlite_db.my_table TO 'output.parquet' (FORMAT PARQUET);" После преобразования таблиц в файлы Parquet в выходной папке вы можете запрашивать их как обычную таблицу базы данных с помощью нашего старого доброго SQL. Например, к таблице coint_rank, где мы хранили результаты системы скоринга, можно обратиться так:
SELECT timeframe,lookback,assets,rank_score FROM 'parquet.database/coint_rank.parquet' LIMIT 10;
Вывод командной строки похож на вывод SQLite.

Рис. 3. Пример вывода DuckDB после запроса таблицы из базы данных Parquet
С этого момента можно выполнять обычные операции баз данных над этими файлами Parquet: запрашивать сразу множество файлов с использованием шаблона “glob”, создавать представления и выполнять соединения. На сайте DuckDB есть подробная документация о том, как читать и записывать файлы Parquet.
В этой серии мы чаще всего будем использовать Python для запросов к этим файлам Parquet. Это поддерживается Python Relational API, который предоставляет множество специализированных лениво вычисляемых аналитических функций, позволяющих очень быстро сканировать файл Parquet, поскольку читаются только метаданные файла «до тех пор, пока не будет вызван метод, запускающий выполнение».
Отличается ли это от чтения CSV-файлов в SQLite?
Хотя мы придерживаемся той же цели, что и в предыдущей серии, — избегать математических или технических объяснений и сохранять фокус на торговой стратегии, — важно понять разницу между хранением файлов Parquet и хранением файлов CSV.
Метаданные
CSV — это простой текстовый, читаемый человеком формат, тогда как Parquet — бинарный формат с большим количеством метаданных, встроенных в каждый файл. Встроенные метаданные легко проверить, выполнив этот запрос в оболочке DuckDB.
.mode line select * from parquet_metadata('parquet.database/coint_rank.parquet');
Замените путь на собственную папку и имя файла Parquet. (Первая строка нужна только для того, чтобы видеть все метаданные по одному элементу в строке без усечения вывода.) После выполнения запроса вы должны увидеть вертикальный список, похожий на этот:
file_name = parquet.database/coint_rank.parquet row_group_id = 0 row_group_num_rows = 7505 row_group_num_columns = 12 row_group_bytes = 533723 column_id = 0 file_offset = 0 num_values = 7505 path_in_schema = tstamp type = INT64 stats_min = 1760920476215768 stats_max = 1765924659832034 stats_null_count = 0 stats_distinct_count = NULL stats_min_value = 1760920476215768 stats_max_value = 1765924659832034 compression = SNAPPY encodings = PLAIN index_page_offset = NULL dictionary_page_offset = NULL data_page_offset = 4 total_compressed_size = 30166 total_uncompressed_size = 60069 key_value_metadata = {} bloom_filter_offset = NULL bloom_filter_length = NULL min_is_exact = true max_is_exact = true row_group_compressed_bytes = 1 geo_bbox = NULL geo_types = NULL
Напомним, что таблица cointrank в SQLite, которую мы преобразовали в Parquet, имеет следующую схему:

Рис. 4. Снимок таблицы coint_rank из диаграммы statarb-0.5.db
Чтобы лучше понять разницу между запросами к файлам Parquet с DuckDB и к CSV-файлам, предположим, что мы хотим выполнить агрегацию или сортировку по столбцу p_value. Для CSV системе пришлось бы читать столбцы tstamp, timeframe, lookback и assets для каждой строки только чтобы добраться до поля p_value. В файле Parquet благодаря колоночной модели хранения система сначала может прочитать метаданные столбцов. Она прочитает column_id = 0 для tstamp. Поскольку каждый столбец хранится отдельно, а нам нужен только p_value, система может перейти напрямую к следующему столбцу, пока не найдёт column_id для p_value. Поэтому она избегает чтения большого объёма данных, не нужных для ответа на запрос.
В зависимости от запроса, поскольку во встроенных метаданных есть статистика, некоторые запросы вообще не требуют чтения файла: система поймёт, какие файлы стоит читать, просто изучив метаданные. Допустим, мы выполнили запрос на данные за 2024 год. Метаданные столбца tstamp (выше) содержат раздел stats_min и stats_max со следующими значениями:
stats_min = 1760920476215768 (понедельник, 20 октября 2025 г., 12:34:36.215 AM)
stats_max = 1765924659832034 (вторник, 16 декабря 2025 г., 10:37:39.832 PM)
Минимальное значение столбца tstamp начинается далеко в будущем. Системе даже не нужно открывать этот файл, потому что метаданные сообщают, что нужных данных там нет. CSV-файл не знает, что внутри, пока не прочитает всё целиком.
Сжатие
В качестве бонуса вы бесплатно получаете оптимизированную систему хранения.
Итого Несжатый размер = 60,069
Итого Сжатый размер = 30,166
Parquet смог уменьшить размер файла вдвое.
Ключевое различие заключается в том, как система находит информацию. CSV похож на книгу, где нужно прочитать каждую страницу, чтобы найти одно конкретное слово. Parquet похож на книгу с идеальным указателем и кратким содержанием каждой главы. Можно сказать, это ещё и сжатая книга.
Читатель Parquet без копирования
Помимо чтения метаданных перед тем, как избегать ненужного чтения всего файла, и сжатия, есть ещё одна функция, резко повышающая производительность: чтение без копирования. Вы часто встретите это выражение при изучении DuckDB и формата Parquet. Хотя его реализация требует сложных низкоуровневых навыков программирования, с точки зрения конечного пользователя это легко понять.
Чтобы прочитать CSV-файл, системе вроде SQLite (и другим СУБД в этом отношении) нужно считать данные с диска в буфер, разобрать CSV-текст и создать новые копии данных в собственном внутреннем формате памяти. Запрос может выполняться только по этой второй копии. Это похоже на импорт: прочитать, перевести, скопировать и наконец выполнить запрос.
DuckDB может работать с данными с минимальным копированием, отображая страницы Parquet в память и напрямую сопоставляя файл со своей памятью. Вторичная копия не создаётся.
Хранение рыночных данных для анализа
Как вы знаете, Metatrader 5 имеет очень эффективный локальный механизм хранения/кэша и не обращается к серверу, если данные уже запрашивались ранее. Однако мы хотим, чтобы наши рыночные данные всегда были доступны как файлы Parquet. Такая практика обеспечит переносимость исторических данных (мы можем захотеть работать на другой машине или хранить данные на внешнем диске). Кроме того, у вас может быть платная подписка на рыночные данные, содержимое которой, вероятно, придётся объединять с историческими данными Metatrader 5. По этим причинам с этого момента мы будем использовать рабочий процесс, при котором запрашиваем данные из Metatrader 5 и сохраняем файлы Parquet локально.
Денормализация
Преобразование таблицы или всей базы данных в Parquet может существенно помочь с производительностью, удобством использования и упрощением запросов, но наша схема не масштабируется. Сейчас история цен разделена на две таблицы: ‘market_data’ и ‘symbol’. Для SQLite она была нормализована для обеспечения целостности данных и уменьшения объёма хранения, но OLAP-схемам нужно сокращать дорогостоящие соединения таблиц ради скорости. Из-за этого они обычно денормализованы. Поскольку история цен является главным компонентом нашего анализа данных, мы начнём с её денормализации.
Партиционирование на разделы
Однако одной денормализации недостаточно, чтобы получить масштабируемую систему хранения/базу данных, подходящую для больших наборов данных. Скоро мы начнём запрашивать больше исторических ценовых данных, возможно из нескольких источников. Вполне разумно предположить, что через несколько недель у нас будут, скажем, тиковые данные по пятидесяти символам и десяти или более таймфреймам. Это уже немалый объём данных. Но для количественных стратегий не будет чем-то необычным анализировать тиковые данные по пятистам или пяти тысячам символов в двадцати или более таймфреймах. Поэтому нам нужна схема хранения, способная масштабироваться. Партиционирование на разделы — это первый метод, который мы будем использовать для хранения денормализованных исторических данных.
Мы не планируем перезаписывать эти данные. Это хранилище только для чтения. Поэтому мы будем хранить данные в файлах Parquet, а не в едином файле базы DuckDB. Мы можем получать данные из нескольких источников, хранить их отдельно и аккуратно организованно, а затем объединять по запросу. Это идеальный сценарий для файлов Parquet вместо одного файла DuckDB. (Единый файл базы DuckDB будет полезен для других сценариев позже, когда потребуется выполнять записи.)
Но как мы будем называть эти файлы? Это важный вопрос, замаскированный под тривиальную задачу. Нам нужно сделать именование файлов согласованным, последовательным и удобным для запросов. Должны ли имена файлов включать имя символа (тикер), таймфрейм и диапазон истории? Должны ли они включать источник как ссылку? Есть ли ещё какая-то важная информация, которую я упускаю?
К счастью, этот вопрос уже решён. Конечно, нет правила, высеченного в камне, но есть множество практических правил и лучших практик, выведенных из реальных потребностей. Мы воспользуемся этим накопленным опытом.
Папка файловой системы как виртуальные столбцы
Мы будем использовать папки (директории) нашей файловой системы как виртуальные столбцы. Самое важное практическое правило для такой организации состоит в том, что структура папок должна как можно точнее отражать структуру наиболее ожидаемых запросов. Поэтому такая структура может сильно меняться в зависимости от потребностей. Вы можете использовать показанную здесь структуру как отправную точку и развивать собственную по мере знакомства с системой и появления новых требований. Такой способ организации данных называется партиционирование Hive. Он очень производителен, интуитивен, гибок и хорошо документирован в документации DuckDB:
"Партиционирование Hive" — это стратегия партиционирования, используемая для разделения таблицы на несколько файлов на основе ключей разделения. Файлы организованы в папки. В каждой папке ключ партиционирования имеет значение, определяемое именем папки.»
Партиционирование Hive — известный способ организации больших наборов данных (а мы ожидаем, что наши данные скоро станут большими), который DuckDB и другие системы, включая Apache Spark и AWS Athena, могут понимать как виртуальные столбцы. Здесь мы ориентируемся на совместимость.
Сейчас у нас шесть ключей партиционирования:
- source = поставщик данных или имя серверa брокера
- klass = класс актива (назван klass, потому что class — зарезервированный идентификатор Python)
- ticker = имя символа
- tf = таймфрейм
- year
- month
Это приведёт к такой структуре:
market_data\source=MetaQuotes-Demo\klass=Forex\ticker=AUDCAD\tf=H1\year=2026\month=2\data_0.parquet

Рис. 5. Снимок древовидной структуры для примера папки рыночных данных, разделённой по Hive
Когда мы просим DuckDB прочитать market_data/**/*.parquet, он автоматически добавляет в результирующий набор столбцы для всех наших ключей на основе пути к папке. Кроме того, когда мы выполняем такой запрос:
SELECT * FROM market_data WHERE ticker='AUDCAD' AND year=2025 Система открывает только файлы в соответствующих папках. Она даже не рассматривает данные за 2024 год. Это значительно ускоряет запросы. Некоторые источники в интернете и ИИ-ассистенты говорят об ускорении в 10–100 раз по мере роста истории.
Вы уже могли заметить, что такая организация также упрощает обслуживание нашего нового хранилища данных. Если мы хотим удалить или обновить блок данных (например, год или месяц), это можно сделать простым удалением папки.
Убедитесь, что имена столбцов в файле Parquet записаны в нижнем регистре и не содержат пробелов (open, high, low, close, tick_volume). Во многих конфигурациях DuckDB по умолчанию чувствителен к регистру. Кроме того, всегда храните временные метки в UTC. Время брокера Metatrader 5 различается, а переходы на летнее время могут внезапно сместить ваши метки времени, если смешивать offset-aware и offset-naive метки. См. итоговую рекомендацию о часовых поясах в конце статьи.
В подвале статьи вы найдёте Python-скрипт, который запрашивает ценовые данные из вашего терминала Metatrader 5, добавляет виртуальные столбцы на основе описанных выше ключей и экспортирует данные в сжатый файл Parquet во вложенную иерархию папок. Он может загружать данные по символу или по ‘path’ символа (группе символов в именованном классе Metatrader 5). Кроме того, он будет загружать только недостающие данные, необходимые для анализа, заполняя пропуски, и делать паузы между запросами, чтобы обращаться к серверу брокера с разумным темпом.
Когда данные уже есть, выполнять запросы к ним так же просто, как к любой реляционной базе данных с помощью SQL. Например, чтобы найти бычьи свечи во всех таймфреймах EURUSD, доступных локально:
SELECT * FROM read_parquet('data/**/*.parquet', hive_partitioning = 1) WHERE ticker = 'EURUSD' AND year = 2026 AND close > open; -- Find bullish candles
Однако, несмотря на простоту запросов, вы получаете все преимущества отраслевой OLAP-системы вместе со специализированными нативными статистическими функциями, такими как коэффициент корреляции Пирсона и другие, которые мы уже использовали. Подробнее см. документацию DuckDB.
Исследование данных для возможных стратегий
Сейчас наша цель — прояснить, какие запросы нас интересуют в OLAP-системе, а не оценивать жизнеспособность торговой стратегии.
Пожалуй, лучший способ анализировать данные в поиске торговых возможностей — начинать с гипотезы. Когда мы находим что-то достойное изучения, мы проводим бэктест и иногда получаем многообещающие результаты. Как и в нашей серии о статистическом арбитраже через коинтегрированные акции, где наша гипотеза заключалась в том, что можно найти устойчивую коинтеграционную связь между некоторыми высоколиквидными акциями полупроводниковой отрасли и акциями Nvidia, мы также начнём исследовательский анализ данных с гипотезы.
Чтобы упростить задачу и не смещать фокус на результаты, оставляя внимание на процессе, мы начнём с очень известной гипотезы: импульсная стратегия Golden Cross может давать сильно различающуюся доходность на Forex и индексах. Чтобы проверить эту гипотезу, мы запустим её по всем символам Forex и индексов, доступным на сервере нашего брокера (в данном случае сервер MetaQuotes-Demo), с периодом ретроспективы 1250 дней и проверим 30-дневную будущую доходность, чтобы сравнить результаты.
Что такое импульсная стратегия Golden Cross?
Если вы когда-либо изучали торговые стратегии, то, вероятно, уже знаете, что такое стратегия Golden Cross. Тем не менее, поскольку существуют варианты параметров двух её индикаторов, уточним значения, которые мы используем в этом примере.
Golden Cross — это всего лишь звучное название простого пересечения скользящих средних, применяемого к долгосрочному горизонту. Он определяет паттерн технического анализа, возникающий, когда краткосрочная скользящая средняя пересекает долгосрочную снизу вверх. Смысл интуитивен: это указывает на потенциальный разворот от медвежьего к бычьему импульсу. Можно сказать, что Golden Cross показывает: цена могла достичь дна, и с этого момента можно ожидать начала нового долгосрочного бычьего рынка. Обычно для краткосрочной и долгосрочной средних используются 50-дневная и 200-дневная простые скользящие средние (SMA) соответственно.
В приведённых ниже примерах красная линия представляет краткосрочную, или быструю, SMA, а синяя — долгосрочную, или медленную, SMA.

Рис. 6. USDJPY на дневном таймфрейме с успешным Golden Cross 03 декабря 2012 г.
Обратите внимание, что Golden Cross, как и любой индикатор, может давать ложные сигналы. Это часто происходит, когда рынок движется в боковике.

Рис. 7. AUDUSD на дневном таймфрейме с неудачным Golden Cross 26 января 2023 г.
Golden Cross берёт своё начало на фондовом рынке — как и многие торговые стратегии и индикаторы, популярные среди розничных трейдеров, — но также используется в криптовалютах и Forex. В этом случае его часто применяют с более короткими SMA, например 20/50 дней, и с множеством других вариантов этих значений, которые трейдеры могут придумать и протестировать.
Скрипт тестирования Golden Cross
Протестировать Golden Cross на паре символов относительно легко, но тестирование сразу по всем символам Forex и индексов — именно та причина, по которой нам нужны масштабируемое хранилище данных и OLAP-система: мы хотим запускать это за секунды на любом разумном потребительском ноутбуке. В подвале статьи вы найдёте простой Python-скрипт, который поможет выполнить эту задачу. Его стоит рассматривать как отправную точку для собственных экспериментов и разработки.
Это его основная функция — метод, запускающий анализ Golden Cross по списку символов с выбранным периодом просмотра. Также можно выбрать имя портфеля — для необязательных графиков — и указать, экспортировать результаты в CSV-файл или нет. Этот CSV-файл предназначен для будущего использования Expert Advisor, когда мы будем отбирать варианты для бэктестов.
def golden_cross(self, symbols, lookback=2500, portfolio_name="Portfolio", plot=True, export_csv=True): # 1. Ensure we have data self._ensure_data(symbols, "D1", lookback) # 2. Query the Hive-partitioned Parquet files parquet_path = f"{self.base_path}/**/*.parquet"
Он начинается с проверки наличия достаточного объёма ценовых данных уже сохранённых в нашем хранилище Parquet. Если данных недостаточно, он заполнит пропуски, запросив их из MT5.

Рис. 8. Снимок вывода Python-скрипта strategy analyzer при запросе недостающих данных
Это ядро скрипта — SQL-запрос для расчёта Golden Cross по созданным выше разделам файловой системы и проверки 30-дневной будущей доходности.
query = f"""
WITH base_ma AS (
SELECT
time, ticker as symbol, close,
AVG(close) OVER (PARTITION BY ticker ORDER BY time ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) as ma50,
AVG(close) OVER (PARTITION BY ticker ORDER BY time ROWS BETWEEN 199 PRECEDING AND CURRENT ROW) as ma200
FROM read_parquet('{parquet_path}', hive_partitioning=1)
WHERE ticker IN ({str(symbols)[1:-1]})
),
lagged_data AS (
SELECT
*,
LAG(ma50) OVER (PARTITION BY symbol ORDER BY time) as prev_ma50,
LAG(ma200) OVER (PARTITION BY symbol ORDER BY time) as prev_ma200,
LEAD(close, 30) OVER (PARTITION BY symbol ORDER BY time) as future_price
FROM base_ma
),
signals AS (
SELECT
*,
((future_price - close) / close) * 100 as pct_return_30d
FROM lagged_data
WHERE prev_ma50 < prev_ma200 AND ma50 > ma200
)
SELECT * FROM signals;
""" Обратите внимание на строку внизу запроса:
WHERE prev_ma50 < prev_ma200 AND ma50 > ma200
Как легко заметить, она содержит определение Golden Cross — пересечение SMA. Простая система, которую мы строим для запуска на потребительском ноутбуке, может просканировать миллионы строк исторических данных, найти каждый случай такого события и рассчитать доходность за следующие 30 дней. Мы превращаем субъективную визуализацию графического паттерна в объективное статистическое измерение, которое за несколько секунд подтверждает или опровергает прибыльность стратегии для выбранных символов, периода ретроспективы и таймфрейма. Вы можете бэктестировать целую группу акций, индексов, валют — в любой комбинации, которую сочтёте полезной, — и делать это гораздо быстрее, чем традиционными циклами Python. Только после обнаружения чего-то перспективного вы будете тратить усилия на разработку EA для бэктеста в среде MT5, оптимизацию и, возможно, запуск стратегии на демо-счёте для финальной проверки. Если вы серьёзно относитесь к количественной торговле, освоение такого предварительного анализа может сэкономить дни или даже недели тяжёлой работы.
В конце скрипт агрегирует результаты по символу.
results = self.con.execute(query).df() if results.empty: print("No Golden Cross signals found in the dataset.") return # 3. CSV Export Logic (Aggregated by Symbol) if export_csv: summary = results.groupby('symbol').agg( frequency=('symbol', 'count'), avg_return_30d=('pct_return_30d', 'mean') ).sort_values(by='avg_return_30d', ascending=False) filename = f"golden_cross_summary.csv" summary.to_csv(filename) print(f"\nSummary exported to {filename}") print(summary) # Display table in console for quick check
Вывод для индексов, доступных в Metatrader 5 на момент написания.

Рис. 9. Снимок вывода Python-скрипта strategy analyzer со сводкой по символам
Те же данные выше будут сохранены в CSV-файл.
Golden Cross на Forex и индексах
Как уже сказано выше, цель этой простой проверки различий в результатах при применении сигнала Golden Cross к Forex и индексам — показать, как создаваемое нами OLAP-дружественное хранилище данных помогает проверять жизнеспособность известных торговых стратегий, а также искать новые торговые возможности.
На дневном таймфрейме за последние 1250 торговых дней, то есть почти пять лет, 30-дневная будущая доходность на Forex и индексах выглядела так.

Рис. 10. 10 - График 30-дневной будущей доходности Golden Cross на таймфрейме D1 и периоде ретроспективы 1250 дней для Forex

Рис. 11. 11 - График 30-дневной будущей доходности Golden Cross на таймфрейме D1 и периоде ретроспективы 1250 дней для индексов
На Forex наши результаты были практически безубыточными с практической точки зрения (и мы ещё не учитывали транзакционные издержки). С другой стороны, по индексам можно увидеть некоторую прибыль, главным образом у восьми лучших индексов, представленных выше на рисунке 9. В любом случае на дневном таймфрейме сигналов очень мало.
Что данные могут сказать о нашей гипотезе?
Несмотря на популярность, Golden Cross, похоже, лучше работает для индексов. Напомним, что он был разработан в контексте фондового рынка. На Forex он показывает слабые результаты. По крайней мере, на дневном таймфрейме и периоде ретроспективы 1250 дней.
В этом последнем утверждении заключается всё важное понимание темы статьи: нам необходимо уметь выполнять анализ данных очень быстро и легко, потому что придётся запускать его с множеством разных параметров, неожиданно их комбинируя для поиска нишевых возможностей, а потенциально — выполнять такие анализы при мониторинге реальной торговли.
В этом весь смысл OLAP-дружественного хранилища данных, которое мы здесь создаём.
Итоговая рекомендация о часовых поясах
Помните, что мы имеем дело с временными рядами. Согласованность datetime имеет первостепенное значение. Мы постоянно соединяем таблицы по временной метке. Если котировки окажутся несогласованными, анализ будет скомпрометирован и, что хуже, может тихо дать сбой, если в коде нет защитных мер. Одна из самых распространённых ошибок — хранить данные в локальном datetime и/или смешивать их с данными в UTC. Возможное наличие перехода на летнее время на серверах поставщиков данных — ещё один источник проблем. Лучший способ избежать этих проблем — хранить всё в UTC в хранилище данных, соблюдать эту согласованность в Python-запросах и всегда инициализировать систему DuckDB в timezone-aware/naive datetime.
В DuckDB способ обеспечить согласованность — рассматривать базу как независимую от часового пояса: хранить всё в UTC и выполнять преобразование часового пояса только на последнем шаге анализа или визуализации, если это нужно. Мы можем принудительно перевести всю сессию DuckDB в UTC, добавив эту строку сразу после инициализации соединения DuckDB:
con = duckdb.connect()
con.execute("SET TimeZone='UTC';") После этого любая функция вроде now() или today() внутри SQL будет возвращать UTC вместо локального времени компьютера.
Если когда-нибудь потребуется сравнить наши данные с другим источником (например, другим брокером), следует использовать AT TIME ZONE синтаксис. Это гарантирует, что обе стороны сравнения находятся в UTC.
SELECT
time AT TIME ZONE 'UTC' as utc_time,
close
FROM read_parquet('...') Наконец, будьте осторожны при передаче объекта Python datetime в запрос.
Не делайте так:
con.execute("SELECT * FROM df WHERE time > ?", [datetime.now()]) Всегда убедитесь, что объект Python имеет информацию о часовом поясе, прежде чем передавать его в DuckDB.
con.execute("SELECT * FROM df WHERE time > ?", [datetime.now(timezone.utc)])
Наш пример скрипта, приложенный ниже, уже добавляет настройку часового пояса в метод `__init__`:
class StrategyAnalyser: def __init__(self, base_path="market_data"): self.base_path = base_path self.con = duckdb.connect(database=':memory:') # FORCE UTC AT THE START self.con.execute("SET TimeZone='UTC';") self.downloader = DataDownloader(base_path)
Заключение
Мы предложили минимальную, но масштабируемую и перспективную настройку OLAP-дружественного хранилища данных на основе коллекции файлов Parquet, организованных в удобную для запросов и обслуживания файловую систему с разделением по Hive. Мы показали простой способ преобразовать ранее сохранённые данные из других РСУБД, таких как SQLite, Postgres или MySQL, в эту новую систему.
Для будущих разработок в этой серии статей мы предложили создать локально хранимый набор исторических рыночных цен, чтобы упростить объединение нескольких источников данных и повысить производительность хранилища.
Наконец, мы представили пример анализа, сравнивающего известную импульсную стратегию Golden Cross при применении к двум разным классам активов — Forex и индексам — для всех символов, доступных на текущем сервере брокера (MetaQuotes-Demo). Это подчёркивает, что цель анализа — показать тип задач, которые должны решаться с помощью нашего OLAP-ориентированного хранилища и DuckDB на обычном ноутбуке с коллекцией файлов Parquet и несколькими строками SQL-кода.
| Название файла | Описание |
|---|---|
| data_downloader.py | Python-скрипт для поддержания локального хранилища данных в актуальном состоянии с нужными данными |
| strategy_analyser.py | Python-скрипт для запуска примерного анализа Golden Cross по списку символов |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21679
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создание торговой системы (Часть 3): Определение минимального уровня риска для достижения реалистичных целей по прибыли
Нейросети в трейдинге: Внимание, память и рыночные паттерны в GDformer (Окончание)
Разработка инструментария для анализа Price Action (Часть 44): Создание в MQL5 сигнального советника на основе пересечений VWMA
Оптимизация долгосрочных сделок: Свечи поглощения и стратегии работы с ликвидностью
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования