English
preview
Низкочастотные количественные стратегии в MetaTrader 5: (Часть 2) Бэктестинг lead/lag-анализа в SQL и MetaTrader 5

Низкочастотные количественные стратегии в MetaTrader 5: (Часть 2) Бэктестинг lead/lag-анализа в SQL и MetaTrader 5

MetaTrader 5Торговые системы |
112 0
Jocimar Lopes
Jocimar Lopes

Введение 

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

Этот образ также прекрасно иллюстрирует центральную идею данной статьи и описываемого в ней анализа. Финансовые рынки похожи на сложные системы. Их часто моделируют как стохастические процессы, то есть процессы, в которых будущее состояние нельзя предсказать точно из-за влияния случайных факторов. Каждый тик, каждое изменение цены заключает в себе эти случайные факторы — так сказать, взмахи крыльев множества бабочек. Одни находятся рядом с изменением и оказывают прямое влияние; другие удалены и влияют косвенно. Но все они присутствуют и формируют цену.

Несмотря на эту неопределенность, специалисты в финансах пытаются проследить первопричины таких событий, как изменения цен. Они надеются, что понимание движущих факторов позволит им оценивать будущие исходы с разумными вероятностями. В этих поисках проявилась как минимум одна теперь уже очевидная истина: независимо от природы случайных факторов изменение распространяется на разные активы с разной скоростью. Поскольку время, необходимое изменению для распространения, может быть ценной информацией для лучшего понимания поведения вовлеченных активов, были разработаны методы его измерения. В этой статье мы рассмотрим один из таких методов.

Lead/Lag-анализ позволяет увидеть, опережают ли изменения цены актива A изменения цены актива B, то есть выступает ли актив A лидером, а актив B — последователем. Обычно между обоими активами уже установлена известная корреляционная или коинтеграционная связь, поэтому предполагается, что они движутся вместе. Но это не означает, что они всегда будут двигаться одновременно. Между движением A и движением B может существовать временной интервал. Характеризуя величину и частоту этого интервала, мы можем получить ценные знания о коррелированном или коинтегрированном портфеле и потенциально выявить торговые возможности возврата к среднему с очень низким риском.

Даже без известной корреляции или коинтеграции lead/lag-анализ может выявить паттерн лидер/последователь. В таком случае можно искать возможности для торговли по импульсу, но риск будет выше. В этой статье мы опишем первый сценарий — торговлю возврата к среднему по схеме лидер/последователь с коинтегрированными активами. Со временем сценарий импульсной торговли для некоррелированных активов станет темой будущей части.

Из-за этого выбора в статье используются техники, которые мы обсуждали в серии «Статистический арбитраж через коинтегрированные акции», в частности использование hedge ratio для рыночно-нейтральной портфельной торговли. В последней статье той серии мы привели причины, почему в конвейер статистического арбитража следует интегрировать систему, удобную для OLAP-аналитики, а в предыдущей статье этой серии, предложили для нее простую настройку. Теперь мы будем использовать эту систему для выполнения lead/Lag-анализа.

К концу статьи вы должны понимать, чего ожидать от низкочастотного lead/lag-анализа. Вы также узнаете, как построить анализатор на Python, избежать распространенных ошибок, интерпретировать результаты и выявлять торговые возможности.

Мы начинаем искать низкочастотные торговые возможности Lead/Lag, потому что именно там розничные трейдеры с обычным ноутбуком, стандартным сетевым подключением и ограниченным капиталом могут превратить небольшой масштаб в преимущество.


Что такое низкочастотный lead/lag-анализ?

Когда розничный трейдер видит выражение «lead/lag торговля», он почти автоматически может отвергнуть саму идею разработки торговой стратегии вокруг него. Это нормальная реакция. Lead/Lag-торговля обычно ассоциируется с высокочастотной торговлей (HFT), борьбой за микросекунды при маршрутизации ордеров, коллокацией в дата-центрах, высокопроизводительным кодом и, в конечном счете, опережением более медленных заявок. Для такой реакции есть веские причины, поскольку эксплуатация временных ценовых расхождений между коррелированными активами — или даже между одним и тем же активом на разных биржах — является самой понятной и популярной тактикой HFT-проп-десков.

Однако в низкочастотном lead/lag (LFLL) существуют реальные и часто упускаемые возможности. То, что считать низкой частотой, сильно зависит от точки зрения. Для свинг-трейдера это может быть месячный таймфрейм, а для DOM-скальпера — пятиминутный график. Поэтому с самого начала уточним: для наших целей высокочастотным является все, что ниже нескольких секунд. Мы будем искать торговые возможности на минутном таймфрейме и выше. Именно там можно разумно ожидать исполнения ордеров с управляемым проскальзыванием. На этих низких частотах преимущество можно искать в глубине и точности анализа, а не в скорости исполнения. На этих частотах любое разумное сетевое соединение делает задержку несущественной, а альфа должна приходить из способности лучше понимать путь и время распространения информации.

Нас интересует, например, время, за которое ценовое движение ведущей голубой фишки отрасли распространяется вниз по ее цепочке поставок, пока воздействие не дойдет до конкретного тикера; или мы можем анализировать эффект пост-отчетного дрейфа, когда объявление прибыли выше ожиданий может распространяться на некоторые тикеры в течение нескольких дней, пока крупные игроки корректируют позиции. В любом случае мы не будем бороться за миллисекунды. Мы будем находиться в диапазоне от часов до нескольких дней, где малый размер может быть преимуществом.

У каждого из этих примеров есть своя специфика — не только на этапе анализа данных, но и в торговых правилах. В следующие недели мы можем подробно обсудить некоторые из них. Пока же возьмем в качестве кейса время распространения от лидера цепочки поставок к последователю. Такой вид lead/lag-анализа часто называют — анализ кросс-активной информационной диффузии.


Построение анализатора

Анализ кросс-активной информационной диффузии включает расчет кросс-корреляции между лог-доходностями актива-лидера в момент t-n и лог-доходностями актива-последователя в момент t. Этот расчет кросс-корреляции может выявить Lead/Lag-связи, где движение цены актива-лидера предшествует движению цены последователя со статистически значимой регулярностью временного интервала.

Кросс-корреляция действительно является большой темой для специалистов, но для наших целей ее можно понять очень интуитивно. Согласно Wikipedia, 

математические понятия ковариации и корреляции очень похожи. Оба описывают степень, в которой две случайные величины или наборы случайных величин склонны отклоняться от своих ожидаемых значений похожим образом”.

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

Чтобы облегчить понимание понятия кросс-корреляции, полезно увидеть его на упрощенном, хорошо известном «наборе данных». Для этого мы создали скрипт, который генерирует синтетические данные (контролируемые значения) для лидера и последователя на 5-минутном таймфрейме (частота дискретизации). Цена лидера сдвинута во времени на 3 периода (15 минут), и кросс-корреляция рассчитывается для разных временных сдвигов (лагов) от 0 до 4. Переменная lag_0 хранит коэффициент кросс-корреляции при нулевом лаге, то есть без лага. lag_1 соответствует лагу в один период, то есть пять минут, и так далее. Поскольку мы знаем, что последователь отстает на 3 периода, ожидаем пик корреляции в lag_3.

--- Step 1: Synthetic Cross-Correlation Results ---                                                                                                               

             Correlation                                                                                                                                               
 lag_0_corr    -0.031678                                                                                                                                               
 lag_1_corr    -0.038247                                                                                                                                               
 lag_2_corr    -0.016094                                                                                                                                               
 lag_3_corr     0.996679                                                                                                                                               
 lag_0_corr    -0.031678                                                                                                                                               
 lag_1_corr    -0.038247                                                                                                                                               
 lag_2_corr    -0.016094                                                                                                                                               
 lag_3_corr     0.996679                                                                                                                                               
 lag_1_corr    -0.038247                                                                                                                                               
 lag_2_corr    -0.016094                                                                                                                                               
 lag_3_corr     0.996679                                                                                                                                               
 lag_2_corr    -0.016094                                                                                                                                               
 lag_3_corr     0.996679                                                                                                                                               
 lag_4_corr    -0.015409                                                                                                                                               

Эти искусственные результаты говорят нам, что информационная диффузия занимает 15 минут. Если мы видим движение лидера сейчас и дискретизируем данные каждые 5 минут, можно ожидать, что последователь сдвинется через 3 периода, или через 15 минут.

Назначение этого простого скрипта — лишь показать, как должны выглядеть результаты кросс-корреляции: коэффициент должен достигать пика на лучшем лаге, указывая временной интервал, необходимый для того, чтобы цена последователя действительно последовала за ценой лидера. Скрипт приложен внизу статьи.

Как учитывать недетерминизм в SQL

В приведенном выше условном примере мы запрашиваем DataFrame Pandas всего со 100 точками данных, сдвинутыми относительно самих себя. В production мы запрашиваем Hive-партиционированное хранилище с тысячами или миллионами точек. Мы также объединяем как минимум две временные серии, которые должны быть идеально выровнены по времени, чтобы дать детерминированные результаты. Если мы запускаем один и тот же тест на одних и тех же данных, мы должны получать одинаковые результаты.

Однако в SQL-движках вроде DuckDB строки не обрабатываются в естественном порядке. документация DuckDB четко говорит:

“без [упорядочивания] результаты оконных функций общего назначения и чувствительных к порядку агрегатных функций, а также порядок фреймов не определены однозначно.”

Среди оконных функций общего назначения есть функции LEAD() и LAG(). Документация говорит, что простой SELECT без явного ORDER BY может каждый раз возвращать строки в другой последовательности. Если оконные функции вроде LAG() или CORR() применяются к неупорядоченному набору, корреляции и другие результаты будут меняться между запусками. Такой неопределенности нам не нужно.

Помимо общего SQL-понятия неупорядоченного множества, колонко-ориентированная модель хранения, которую использует DuckDB, хранит данные чанками для обеспечения массового параллелизма.

“Типичный запрос к хранилищу данных содержит несколько операций соединения, фильтрации, группировки и агрегации. Оптимизатор MPP [massively parallel processing] разбивает этот сложный запрос на ряд стадий выполнения и партиций, многие из которых могут выполняться параллельно на разных узлах кластера базы данных. Запросы, включающие сканирование больших частей набора данных, особенно выигрывают от такого параллельного выполнения.” (Mark Kleppmann, Designing Data-Intensive Applications, O’Reilly, 2017)

То есть база данных читает разные столбцы или чанки параллельно на разных ядрах CPU. Восстановленные или «составленные» строки поступают на этап оконной функции в том порядке, в котором закончилось их чтение. Без ORDER BY база данных просто обрабатывает их в порядке поступления.

Мы с партнером совершили эту ошибку, когда начинали самостоятельно, не имея прежнего опыта такого анализа в колонковых базах данных. Пока мы не поняли, что база данных как бы «перемешивает» записи, мы постоянно получали противоречивые результаты. Иногда это не так очевидно. Если не решить проблему, результаты могут стать ненадежными. Чтобы сделать проблему видимой, мы написали небольшой скрипт, имитирующий «перемешивание» записей на уровне базы данных.

Мы создали простую временную шкалу с пятью точками данных.

data = {
    'time': pd.to_datetime(['10:00', '10:05', '10:10', '10:15', '10:20']),
    'price': [100.0, 102.0, 101.0, 105.0, 104.0]
}
df = pd.DataFrame(data)

Затем, чтобы принудительно вызвать «сбой», физически перемешали строки.

df_shuffled = df.sample(frac=1, random_state=42).reset_index(drop=True)

Обратите внимание: хотя номера строк идут по возрастанию, столбец времени — нет.

 --- Step 2: Visualizing the Non-Determinism Pitfall ---                                                                                                               
                                                                                                                                                                                                                                                      
                  time  price                                                                                                                                          

 0 2026-03-11 10:05:00  102.0                                                                                                                                          
 1 2026-03-11 10:20:00  104.0                                                                                                                                          
 2 2026-03-11 10:10:00  101.0                                                                                                                                          
 3 2026-03-11 10:00:00  100.0                                                                                                                                          
 4 2026-03-11 10:15:00  105.0

Когда мы вызываем функцию LAG(price) без ORDER BY, она получает отстающую цену в физическом порядке памяти:

# 3. THE DANGEROUS QUERY (Physical Order)
query_broken = """
SELECT 
    time, 
    price,
    lag(price) OVER () as prev_price_broken
FROM df_shuffled
"""                                                                                                
                 time  price  prev_price_broken                                                                                                                       

 0 2026-03-11 10:05:00  102.0                NaN                                                                                                                       
 1 2026-03-11 10:20:00  104.0              102.0                                                                                                                       
 2 2026-03-11 10:10:00  101.0              104.0                                                                                                                       
 3 2026-03-11 10:00:00  100.0              101.0                                                                                                                       
 4 2026-03-11 10:15:00  105.0              100.0

Обратите внимание на выделенную вторую строку: цена 104.0 в 10:20. Какой должна быть предыдущая цена с логической точки зрения? Очевидно, это должна быть цена в строке 4, в 10:15, то есть 105.00. Но база данных возвращает цену из строки 0, в 10:05, то есть 102.0. Это не логическая предыдущая цена пять минут назад; это просто предыдущая физическая строка, предыдущая строка в памяти.

Что мы можем сделать, чтобы результаты были осмысленными и детерминированными? Начнем с вызова функции LAG() с предложением ORDER BY. 

# 4. THE ROBUST QUERY (Logical Order)
query_correct = """
SELECT 
    time, 
    price,
    lag(price) OVER (ORDER BY time ASC) as prev_price_correct
FROM df_shuffled
"""

Затем она следует логическому порядку времени и возвращает правильную предыдущую цену в строке 3, время 10:15, то есть 105.0.

                  time  price  prev_price_correct                                                                                                                      

 0 2026-03-11 10:00:00  100.0                 NaN                                                                                                                      
 1 2026-03-11 10:05:00  102.0               100.0                                                                                                                      
 2 2026-03-11 10:10:00  101.0               102.0                                                                                                                      
 3 2026-03-11 10:15:00  105.0               101.0                                                                                                                      
 4 2026-03-11 10:20:00  104.0               105.0

В базе данных физический порядок НЕ является логическим порядком. Если мы явно не вызываем оконную функцию с ORDER BY, нельзя быть уверенными, что не выполняем логический запрос над физически упорядоченными данными. Поэтому это первая мера которую мы принимаем при построении lead/lag-анализатора: каждый расчет, включающий временной сдвиг (LAG), оборачивается в явное предложение OVER (ORDER BY time ASC).

Вторая мера заключается в том, что мы используем функцию row_number() для создания row_id для каждого тикера. Благодаря этому, даже если две свечи имеют одинаковую временную метку, их последовательность уже определена нами. Если у двух строк одинаковое время, база данных может перемешать их случайно. Поэтому мы устанавливаем канонический порядок, создавая уникальную, определенную последовательность для каждой строки.

SELECT 
    time, ticker, close,
    -- Ensure every row has a unique, deterministic ID
    row_number() OVER (PARTITION BY ticker ORDER BY time ASC) as row_id

Когда мы рассчитываем лог-доходности, мы разбиваем данные по тикеру и обязательно сортируем по времени. Мы гарантируем, что результат LAG() для 10:05 всегда рассчитывается относительно 10:00, независимо от того, как данные хранятся на диске. Каждый LAG и каждое предложение OVER теперь явно используют ORDER BY time ASC, row_id ASC. Это заставляет движок выполнения следовать каноническому порядку, а не физическому порядку памяти. Мы никогда не позволяем базе данных самой решать порядок. Каждый раз, когда рассчитываем доходность или сдвиг, мы заново утверждаем хронологическое требование.

returns AS (
    SELECT 
      time, ticker, row_id,
      -- Returns must follow the canonical order
      log(close / lag(close) OVER (PARTITION BY ticker ORDER BY time ASC, row_id ASC)) as ret
    FROM filtered_data

Наконец, в качестве третьей меры, мы внимательно относимся к предложениям ‘JOIN’. Мы обеспечиваем строгое временное выравнивание по временным меткам лидера и последователя перед сдвигом доходностей лидера. Это нужно, чтобы мы всегда сравнивали одни и те же моменты времени. Возьмем, например, такое предложение JOIN:

JOIN ... ON l.time = c.time

Оно остается якорем, обеспечивая идеальную синхронизацию лидера и последователя во времени до вычисления любой кросс-корреляции. Здесь нужно быть внимательными, потому что у лидера и последователя могут быть разные часы торгов или пропущенные бары. Если поспешить и просто поставить строки рядом (строка 1 lead_asset против строки 1 follower_asset), можно сравнить утро понедельника у lead_asset с днем вторника у follower_asset. Такое временное выравнивание гарантирует, что мы сравниваем лидера и последователя только тогда, когда оба существуют в один и тот же момент.

joined AS (
    SELECT 
       l.time,
       l.ret as lead_ret,
       c.ret as lag_ret,
        l.row_id as lead_row_id
     FROM (SELECT * FROM returns WHERE ticker = '{lead_symbol}') l
      -- TEMPORAL ALIGNMENT: Sync the clocks of both assets
     JOIN (SELECT * FROM returns WHERE ticker = '{ticker}') c ON l.time = c.time
),

Это гарантирует, что соединение действует как синхронизированные часы. Оно отбрасывает любые несовпадающие данные и обеспечивает расчет корреляции только при сравнении в одной и той же временной зоне и торговой сессии.

Такого трехшагового подхода должно быть достаточно для получения детерминированных, воспроизводимых результатов, которые нам нужны. Я настоятельно рекомендую не игнорировать их при написании запросов или при запросе таких запросов у ИИ-ассистента.


Запрос диффузии

Предоставленный lead_lag_analyser.py следует трехэтапному конвейеру. Сначала мы синхронизируем локальное хранилище данных, как обычно. Скрипт вызывает класс DataDownloader, чтобы проверить наличие требуемых данных в локальном Hive-партиционированном хранилище, и при необходимости загружает недостающие бары.

Когда необходимые данные доступны локально, запрос диффузии фильтрует их по таймфрейму, периоду ретроспективы и необязательным часам сессии. Обратите внимание, что этот последний фильтр особенно важен при анализе не круглосуточных инструментов.

        for ticker in candidates:

            query = f"""
            WITH filtered_data AS (
                SELECT 
                    time, ticker, close,
                    -- Ensure every row has a unique, deterministic ID
                    row_number() OVER (PARTITION BY ticker ORDER BY time ASC) as row_id
                FROM market_view
                WHERE ticker IN ('{lead_symbol}', '{ticker}')
                AND tf = '{tf_str}'
                AND time >= '{start_date}'
                {session_filter}
            ),

            returns AS (
                SELECT 
                    time, ticker, row_id,
                    -- Returns must follow the canonical order
                    log(close / lag(close) OVER (PARTITION BY ticker ORDER BY time ASC, row_id ASC)) as ret
               FROM filtered_data
            ),
"""

Обратите внимание на использование log(close/lag(close)) для расчета доходности каждого символа; то есть мы оцениваем кросс-корреляцию по лог-доходностям, а не по простым доходностям.

Как сказано выше о мерах, которые мы принимаем, чтобы не попасть в ловушку недетерминированного SQL-упорядочивания, мы выполняем JOIN по времени, чтобы обеспечить временное выравнивание перед расчетом кросс-корреляции.

            joined AS (
                SELECT 
                    l.time,
                    l.ret as lead_ret,
                    c.ret as lag_ret,
                    l.row_id as lead_row_id
                FROM (SELECT * FROM returns WHERE ticker = '{lead_symbol}') l
                -- TEMPORAL ALIGNMENT: Sync the clocks of both assets
                JOIN (SELECT * FROM returns WHERE ticker = '{ticker}') c ON l.time = c.time
            ),

Каждая доходность лидера сдвигается на N периодов. Максимальное число периодов лага является параметром метода analyze_diffusion.

   def analyze_diffusion(self, lead_symbol, candidates, timeframe, lookback_days, max_lag_periods=10, session_start=None, session_end=None):
        """
        Analyzes information diffusion by checking cross-correlation at various lags.
        """
(...)

            lagged AS (
                SELECT 
                    time,
                    lead_ret,
                    lag_ret,
                    -- Shifting the lead returns deterministically
                    {' , '.join([f"lag(lead_ret, {i}) OVER (ORDER BY time ASC, lead_row_id ASC) as lead_lag_{i}" for i in range(1, max_lag_periods + 1)])}
                FROM joined
            )

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

"""
            SELECT 
                corr(lead_ret, lag_ret) as corr_0,
                {' , '.join([f"corr(lead_lag_{i}, lag_ret) as corr_{i}" for i in range(1, max_lag_periods + 1)])},
                count(*) as sample_size
            FROM lagged
            WHERE lead_ret IS NOT NULL AND lag_ret IS NOT NULL
            """

Если вы следите за этой серией статей о статистическом арбитраже, то, вероятно, помните, что при анализе коэффициента корреляции Пирсона между двумя активами для парной торговли, мы искали положительные (или отрицательные) коэффициенты корреляции выше 0.80, в идеале выше 0.90. В Lead/Lag-исследовании мы ищем очень слабые сигналы. Корреляция 0.05 или 0.10 может оказаться значимой — или нет — главным образом в зависимости от размера выборки, то есть числа анализируемых периодов (баров). 

Чтобы не оставлять значимость этого коэффициента полностью на субъективные критерии, анализатор включает тест значимости на основе квадратного корня из размера выборки. 

           # Significance threshold (rough estimate: 2/sqrt(N))
            significance = 2.0 / np.sqrt(sample_size)

Он прост, но работает. Для небольшой выборки, скажем 100 периодов, порог значимости равен 2 / 10 = 0.20. В таком случае корреляция 0.08 незначима, то есть это просто шум. С другой стороны, при 10 000 периодов порог равен 2 / 100 = 0.02, и корреляция 0.08 уже может быть очень значимой. 

Если корреляция на конкретном лаге ниже порога значимости, связь, скорее всего, является шумом, и первым шагом должно быть увеличение размера выборки, прежде чем отбрасывать пару или пытаться торговать ее.

Хотя эта простая и известная формула помогает фильтровать шум, важно помнить, что наш lead/lag-анализ полностью основан на кросс-корреляции, которая является частным случаем корреляции Пирсона. Как статистический тест, корреляция Пирсона имеет доверительный интервал, а это значит, что если вы тестируете 100 разных кандидатов в последователи против одного лидера, то по законам вероятности как минимум у 5 из них значимая корреляция может появиться чисто случайно. Статистики называют это ошибкой I рода.

Кроме того, доходности цен не являются идеально случайными, независимыми и одинаково распределенными (I.I.D.) переменными. Если у актива есть импульс, это может искусственно завысить корреляцию. В таком случае наш тест значимости может переоценивать реальную надежность связи.

Используйте разумный размер выборки (эмпирическое правило: >500 баров). Не тестируйте десятки кандидатов за один запуск без поправки на множественное тестирование. Не торгуйте в реале до обширных out-of-sample бэктестов и мониторинга стабильности.


Эффект Эппса

Прежде чем перейти к главному примеру статьи, я хотел бы обратить ваше внимание на явление, известное как эффект Эппса которое может сбивать с толку, когда вы начнете изменять параметр таймфрейма. Оно описывается как явление, при котором

“эмпирическая корреляция между доходностями двух разных акций уменьшается с длиной интервала, за который измеряются изменения цены.”

Предположим, мы ищем Lead/Lag-возможности на товарном рынке, точнее в секторе добычи. Мы хотим узнать, существует ли разрыв в движении цены лидер-последователь между золотом и некоторыми связанными с золотом бумагами. В качестве лидера берем XAUUSD. В качестве кандидатов-последователей мы выбрали акцию горнодобывающей корпорации (NEM.US) и два ETF, связанных с золотом (SGOL.US и GDX.US).

Затем мы запускаем анализатор без фильтрации по времени сессии на таймфрейме H1 с 30-дневным периодом ретроспективы (размер выборки очень мал, но пока это можно игнорировать, поскольку для понимания эффекта Эппса это несущественно).

lead_lag_analyser.py
Analyzing Information Diffusion for XAUUSD (H1, 30 days)...

=== Diffusion Ranking (Best Non-Zero Lag) ===

 Ticker  Lag_0_Corr  Best_Lag  Best_Lag_Corr  Is_Significant  Sample

 NEM.US      0.6169        10         0.2155            True     119
 GDX.US      0.6968        10         0.1975            True     119
SGOL.US      0.7168        10         0.1899            True     119

Top Candidate: NEM.US
Correlation at Lag 10 (600 mins): 0.2155
>>> CONTEMPORANEOUS: Moves mostly in sync with the lead asset.

Судя по результатам, можно заключить, что все они движутся синхронно, потому что десятичасовой лаг, вероятно, связан с отсутствием фильтра по времени сессии. Лаг одинаков для трех символов, с относительно высокой корреляцией на десятом часу. Поскольку XAUUSD торгуется 24 часа OTC, а остальные три символа торгуются только в часы работы биржевого рынка, они, вероятно, «следуют за лидером», когда их рынок открыт. Такая интерпретация выглядит логичной.

Также результаты говорят, что для этих символов рынок не показывает неэффективности на таймфрейме H1. GDX и NEM представляют акции золотодобывающих компаний, а высокая корреляция на Lag_0 говорит, что они реагируют на изменения спотовой цены золота в течение того же часа. Вероятно, это уже арбитражируется HFT и другими участниками. Что если изменить таймфрейм на 5 минут?

Analyzing Information Diffusion for XAUUSD (M5, 30 days)...

=== Diffusion Ranking (Best Non-Zero Lag) ===

 Ticker  Lag_0_Corr  Best_Lag  Best_Lag_Corr  Is_Significant  Sample

NEM.US      0.4576         6         0.0520           False    1099
GDX.US      0.5217         6         0.0486           False    1099
SGOL.US     0.6450         6         0.0446           False    1099

Top Candidate: NEM.US
Correlation at Lag 6 (30 mins): 0.052
>>> CONTEMPORANEOUS: Moves mostly in sync with Lead.

При переходе с часового на пятиминутный таймфрейм мы видим резкое падение корреляций на лагах. Это эффект Эппса на практике. Однако помимо известного статистического явления, поскольку мы не используем фильтр времени сессии, разные часы работы рынков вносят вклад в исчезающую корреляцию, которую мы видим на таймфрейме M5. Валютная пара XAUUSD очень активна во время лондонской сессии, тогда как SGOL.US и GDX.US стоят на месте, потому что рынки США закрыты. Когда мы рассчитываем корреляцию по 24-часовому окну на M5, мы включаем тысячи баров, где XAUUSD быстро движется, а горнодобывающие бумаги и ETF ждут открытия рынка. Это существенно размывает общий коэффициент корреляции.

Обязательно используйте фильтр времени сессии, чтобы минимизировать это искажение. Используйте параметры session_start и session_end, чтобы сосредоточиться на основных торговых часах активов. Если Lag_0_Corr значительно выше всех лаговых корреляций, как в обоих примерах выше, активы движутся синхронно. Настоящий последователь с лагом покажет отчетливый пик корреляции на ненулевом лаге. Мы увидим много таких примеров в следующем анализе.

Кроме того, при тестах на относительно очень высоких и очень низких таймфреймах убедитесь, что число наблюдений на самом высоком таймфрейме достаточно велико. Как сказано выше, > 500 баров должно быть достаточно для статистической уверенности.


Взгляд на коинтегрированные акции

Мы уже провели несколько экспериментов с коинтегрированными акциями. Мы знаем, что в долгосрочной перспективе они склонны двигаться вместе. В наших экспериментах мы взяли NVDA (Nvidia Co.) как лидера полупроводниковой отрасли, и ранжировали несколько связанных с полупроводниками акций по силе их коинтеграции с NVDA. Поэтому мы знаем, что они склонны двигаться вместе, знаем, что есть лидер, а также есть некоторые «последователи». Что если у некоторых из этих последователей есть постоянный, измеримый лаг движения цены относительно движения цены NVDA? Это разумная гипотеза для проверки, не так ли? 

Этот пример был специально подобран, чтобы проиллюстрировать главную цель lead/lag-анализа.

Мы взяли акции, наиболее высоко ранжированные по коинтеграции с NVDA, и протестировали их на таймфрейме M5 с периодом ретроспективы 60 дней.

Analyzing Information Diffusion for NVDA (M5, 60 days)...

=== Diffusion Ranking (Best Non-Zero Lag) ===

Ticker  Lag_0_Corr  Best_Lag  Best_Lag_Corr  Is_Significant  Sample

 ASX      0.4637         1         0.0933            True    3162
AVGO      0.5514         1         0.0640            True    3199
  MU      0.4594         2         0.0522            True    3199
 AMD      0.4345         2         0.0423            True    3199
LAES      0.3179         9         0.0336           False    3199
INTC      0.2617         2         0.0330           False    3199
NVTS      0.4348         2         0.0329           False    3199

Top Candidate: ASX
Correlation at Lag 1 (5 mins): 0.0933
>>> CONTEMPORANEOUS: Moves mostly in sync with Lead.

Мы ожидали, что результаты идентифицируют их как ‘contemporaneous’ (неинформативные, что было бы идеальным). В конце концов, они коинтегрированы. Это ясно видно по высоким корреляциям на Lag_0, которые значительно выше лаговых корреляций. Это говорит, что рынок здесь очень эффективен. Большая часть движения цены NVDA почти мгновенно, в пределах того же 5-минутного бара, закладывается в ASX или AVGO.

 В столбце Best Lag есть две группы последователей:

  • лаг 5 минут (Best_Lag 1). Эти акции наиболее чувствительны к движению цены NVDA. Значимая корреляция на Lag 1 означает, что если NVDA делает сильное движение, через 5 минут в ASX и AVGO есть статистически измеримое эхо. Если NVDA пробивается, у нас есть 5 минут, чтобы проверить, отреагировали ли ASX или AVGO.
  • лаг 10 минут (Best_Lag 2) — у этих бумаг немного более длинный период. Их пиковая корреляция возникает через 10 минут после движения NVDA. 

Причины различия временного интервала между этими двумя группами здесь не важны. Все, что мы хотим знать, — существует ли лаг и пригоден ли он для торговли. Обратите внимание: хотя ASX выглядит главным кандидатом, сигнал лучшего лага слаб. Относительно небольшая корреляция 0.0933 на Lag 1 означает, что прошлое движение NVDA объясняет только около 9% текущего движения ASX. Можно сказать, что это лишь вероятностное преимущество. Мы не стали бы торговать по одному лишь этому сигналу, но его можно использовать как инструмент подтверждения, если мы уже собирались открыть позицию по ASX. Внезапный скачок NVDA около 5 минут назад может быть зеленым светом.

Главный кандидат ASX — учебный пример того, почему мы называем это анализом информационной диффузии цепочки поставок. ASX часто следует за NVDA с небольшим лагом, потому что это поставщик услуг сборки и тестирования полупроводников. NVDA проектирует чипы. ASX предоставляет услуги бэкенд-производства. Лидерство спроса NVDA за несколько минут распространяется вниз к поставщикам услуг.

Хотя рынок на 90% эффективен (contemporaneous), мы нашли паттерн, который может заслуживать дальнейшего исследования или даже бэктеста. Возможно, мы выявили потенциальную торговую возможность. Как ее исследовать? Возможно, начнем искать дивергенцию, предполагающую последующую конвергенцию. Имеет ли это смысл?

Нет, потому что отсутствует тот пик корреляции на ненулевом лаге, о котором мы говорили выше: “Настоящий последователь с лагом покажет отчетливый пик корреляции на ненулевом лаге.” 

Пара XAUUSD (спот золота) и ETF GDX.US показывает именно это свойство за последние 30 дней на минутном таймфрейме.

Syncing Lead: XAUUSD
[XAUUSD] No local data. Starting from 2026-03-01 02:36:52.562940+00:00
Analyzing Information Diffusion for XAUUSD (M1, 30 days)...

=== Diffusion Ranking (Best Non-Zero Lag) ===
Ticker  Lag_0_Corr  Best_Lag  Best_Lag_Corr  Is_Significant  Sample

GDX.US      0.0764         1         0.0843            True    6198

Top Candidate: GDX.US
Correlation at Lag 1 (1 mins): 0.0843
>>> INFORMATIVE LEAD: This asset follows with a distinct delay.

Именно это мы ищем, чтобы обосновать настоящий бэктест в MetaTrader 5. 


Бэктестинг

Прежде чем разрабатывать полнофункциональный советник для бэктеста, мы можем проверить, была бы связь лидер/последователь между XAUUSD и GDX.US прибыльной, запустив чистый SQL-бэктест. Советник MetaTrader 5 — последний этап нашего конвейера, когда у нас уже есть стратегия, достойная усилий, связанных с разработкой EA. Метод чистого SQL-бэктеста менее распространен в нашем сообществе. Он может быть полезен, чтобы показать практические преимущества OLAP-системы, которую мы представляем в этой серии.

Чистый SQL-бэктест очень ограничен по сложности торговых правил, которые можно тестировать, но это самый простой в реализации и самый быстрый в запуске способ. Поэтому он хорошо подходит для начала конвейера, когда нужно просмотреть десятки, сотни или даже тысячи символов. Эта техника дает мощный инструмент предварительного скрининга, позволяющий масштабировать бэктест стратегии с одной пары на целый сектор без усложнения кода.

Стоит отметить, что независимо от связи лидер/последователь мы должны учитывать различия между торговлей парами или корзинами с коинтеграционной связью и без нее, как в случае пары XAUUSD/GDX.US, которую мы используем здесь в качестве примера.

Коинтегрированные пары или корзины: возврат к среднему

Связь лидер/последователь не привязана к коинтеграции. Она указывает только на лаговую кросс-корреляцию и может возникать там, где коинтеграции нет. Но если коинтеграция есть, она влияет на то, как можно торговать парой или корзиной. Когда lead/lag-связь существует вместе с коинтеграцией, мы имеем систему возврата к среднему. Коинтеграция означает, что, хотя два актива могут расходиться, у них есть долгосрочное равновесие, поэтому любое расхождение в lead/lag-связи рассматривается как временное. Если лидер движется, а последователь — нет, мы знаем, что с высокой вероятностью последователь в итоге последует за лидером и сохранит статистическую связь.

Риск постоянного расхождения низок. Это рыночно-нейтральный способ для коинтегрированных портфелей. Мы устраняем рыночный риск, покупая/продавая последователя и лидера всегда в противоположных направлениях. Объем ордеров определяется hedge ratio, полученным из собственного вектора Йохансена или простой обычной МНК-регрессии (OLS).

Некоинтегрированные пары или корзины: импульс

Но наличие связи лидер/последователь позволяет торговать простой направленный тренд без хеджирования. Если лидер сдвинулся, а последователь остался на месте, можно ставить на то, что последователь двинется в том же направлении в ожидаемый интервал времени. Это более рискованный сценарий. Здесь лидер предсказывает направление или импульс последователя, но они не обязательно "идут вместе". Лидер дает направленный сигнал, но два актива со временем могут расходиться бесконечно далеко. Это чисто импульсная торговля. Мы не торгуем возврат к среднему. Мы не можем спокойно ждать, пока захеджированная пара сойдется. Если последователь не реагирует, убыток может быть бесконечным, потому что спред не стационарен. Поэтому обязательны жесткий стоп-лосс, выход по времени или закрытие по противоположному сигналу. Кроме того, объем ордера не определяется статистическим измерением. Он произволен, что увеличивает риск.


Бэктестинг с помощью чистого SQL

OLAP-система в начале конвейера позволяет анализировать большие исторические наборы данных за секунды, даже офлайн. Мы должны уметь искать паттерны, повторения и аномалии сразу в большом числе символов и получать ответ за несколько секунд на обычном ноутбуке. Мы хотим тестировать десятки комбинаций символов для парной торговли и перестановки коинтегрированных корзин, а затем повторно запускать релевантные варианты в цикле каждые пять минут или каждую минуту для мониторинга торговли, а в итоге — для обновления параметров советника, работающего в реальной торговле. Все это должно быть простым, повторяемым, точным и быстрым.

Чистый SQL-бэктест прекрасно показывает, как можно достичь этой цели. Однако у него есть ограничения: в чистом SQL может быть трудно тестировать сложные торговые правила. Подробная отчетность и построение графиков, которые бесплатно доступны в MetaTester, могут потребовать внешних инструментов. Некоторые статистические функции могут отсутствовать в системе по умолчанию. Их можно реализовать как пользовательские функции (UDF) в соответствии с требованиями конкретной системы, но это ограничение нужно учитывать. При разработке количественных стратегий мы постоянно зависим от статистических функций. Пример такого ограничения мы увидим прямо сейчас. Несмотря на ограничения, это самый быстрый способ тестировать новые идеи, когда ограничения не мешают, как в случае lead/lag-анализа.

Когда исторические данные доступны и система настроена, мы можем проверять гипотезы только с помощью наших данных и нескольких строк SQL.

К этой статье приложен lead_lag_backtest.sql. Поскольку наша цель — не учить SQL и не писать учебник, мы не будем описывать его пошагово. Вместо этого хотим дать общий взгляд на структуру и, что важнее, показать, как она связана с торговыми правилами, которые мы будем использовать при переходе к бэктесту на основе советника. То есть мы хотим использовать этот скрипт как инструмент для понимания принципов lead/lag-бэктеста, применимых к любому методу. 

lead_lag_backtest.sql

Первое, что нужно отметить: это векторизованный бэктест, то есть он рассчитывает все строки параллельно, а не в циклах по каждому бару, как в событийном фреймворке бэктеста. Это дает скорость и низкие требования к памяти, но скрипт ожидает, что минутные цены уже доступны в нашем Hive-партиционированном локальном хранилище.

В этом примере мы используем XAUUSD (спот золота) как актив-лидер и золотой ETF GDX.US как актив-последователь (или «laggard»). Тестируемая стратегия предполагает, что цена ETF следует за спотовой ценой с 5-минутным лагом. 

Когда две ценовые серии выровнены по временным меткам, спотовая цена золота (лидер) сдвигается назад на пять периодов, в данном случае на пять минут. В этот момент у нас есть три выровненные временные серии: цена закрытия ETF, цена закрытия спотового золота и сдвинутая цена закрытия спотового золота. Таблица иллюстрирует это сдвинутое выравнивание.

time закрытие GDX.US закрытие XAUUSD сдвинутое закрытие XAUUSD
5 2 15 17
10 4 17 19
15 6 19 21
20 8 21 ??

Таблица 1 — упрощенное представление трех выровненных временных серий

Затем скрипт рассчитывает скользящий hedge ratio. В предыдущих примерах мы использовали собственные векторы теста коинтеграции Йохансена для получения hedge ratio. Здесь это заменено обычной регрессией наименьших квадратов (OLS), предоставляемой функцией REGR_SLOPE().

REGR_SLOPE(lagg_close, lead_shifted)

Здесь lead_shifted — независимая переменная регрессии, то есть переменная, используемая для «предсказания» lagg_close, которая является зависимой переменной. Функция возвращает скользящий hedge ratio, обычно обозначаемый как Beta. Умножив сдвинутую цену лидера на Beta, мы получаем ожидаемую цену последователя.

Ожидаемая цена последователя = beta * lead_shifted

Поскольку между ожидаемой ценой последователя и фактической ценой последователя есть разница, у нас появляется остаток.

Остаток = lagg_close - (beta * lead_shifted)

Остаток — это значение, которое мы используем для генерации сигналов, отклонение от ожидаемого значения. Затем мы рассчитываем среднее и дисперсию остатка для каждой временной метки (скользящее среднее и скользящую дисперсию) за период ретроспективы. Они будут использоваться для расчета z-score.

z-score: (residual - r_mean) / SQRT(r_var)

Z-score показывает, на сколько стандартных отклонений текущий остаток (остаток в конкретной временной метке) находится от своего скользящего среднего. Это работает как стандартный индикатор, сообщая о возможных состояниях перекупленности/перепроданности. Скрипт использует z-score 2.0 как порог. На основе нарушений этого порога скрипт генерирует торговые сигналы.

Если z-score равен 0, отклонения от скользящего среднего остатка нет. Если он < 0, последователь отстал и «перепродан». И наоборот, если z-score > 0, лидер упал, а последователь снова отстал, но теперь он «перекуплен». В любом случае, когда z-score выше +2.0 или ниже порога -2.0, мы соответственно продаем «перекупленного» или покупаем «перепроданного» последователя.

Как видно, торговые правила очень похожи на обычную стратегию возврата к среднему для коррелированных пар. Мы всегда ожидаем, что отклонения вернутся к среднему. Разница в том, что у нас меньше статистических доказательств, что это произойдет.

Наконец, скрипт рассчитывает лог-доходности для каждого бара.

target_pos * (LN(lagg_close) - LN(LAG(lagg_close)))

Это вычисляет прибыль/убыток от удержания позиции в течение каждой минуты, предполагая, что позиция мгновенно корректируется на основе сигнала. То есть это показывает, сколько мы заработали бы или потеряли, если бы имели открытую позицию на всех сигналах.

В конце скрипт агрегирует результаты по всем барам, где доходности не были null.

Рис. 1 — Снимок результатов lead_lag_backtest.sql для спотового золота и ETF, связанных с золотом.

Рис. 1. Снимок результатов lead_lag_backtest.sql для спота золота x ETF, связанного с золотом

Стоит отметить, что при таком векторизованном подходе разница между запуском бэктеста для простой пары, как выше, и запуском для десяти, двадцати или двухсот символов становится пренебрежимо малой. Мы можем легко просматривать десятки символов одновременно в одном запросе.


Бэктестинг с советником MQL5

Теперь, когда мы отфильтровали кандидатов лидер/последователь из десятков символов, можно разработать более надежный и производительный бэктест в MetaTrader 5 и использовать его возможности оптимизации. Помните, что lead/lag-возможности могут быть крайне эфемерными. Нередко пара отлично работает одну неделю, а на следующей связь уже нарушена. Это требует постоянного мониторинга и обновления портфеля. Самый эффективный путь к обновлению портфеля — чистый SQL-бэктест, который мы видели выше.

Торговый советник приложен внизу статьи для ваших тестов и экспериментов. Я хотел бы лишь обратить внимание на некоторые части кода.

//+------------------------------------------------------------------+
//|                                                     Lead-Lag.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+

(..)

//+------------------------------------------------------------------+
//|                          inputs                                  |
//+------------------------------------------------------------------+

input string  InpLeaderSymbol  = "XAUUSD";     // Leader
input string  InpLaggardSymbol = "GDX.US";     // Laggard (trade one side)

input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1;
input int     InpLagBars      = 5;             // Leader shift
input int     InpWindow       = 60;            // Rolling window
input double  InpZOpen        = 2.0;
input double  InpZClose       = 0.5;
input double  InpLot          = 1.0;
input int     InpSlippage     = 5;
input int     InpMagic        = 20260325;

Среди параметров торгового советника, главные кандидаты для оптимизации — InpWindow, InpZOpen и InpZClose. В нашей оптимизации (см. ниже) мы использовали скользящее окно и порог z-score для закрытия. Не следует менять InpLagBars, потому что именно этот lead/lag анализ указал как статистически значимый.

//+------------------------------------------------------------------+
//|              OnTick Function                                     |
//+------------------------------------------------------------------+

void OnTick()
  {

(...)

// 1) fetch aligned close arrays for laggard and leader
   int bars = MathMax(InpWindow + InpLagBars + 5, 200);
   double laggClose[];
   double leadClose[];
   ArraySetAsSeries(laggClose, true);
   ArraySetAsSeries(leadClose, true);
   if(CopyClose(InpLaggardSymbol, InpTimeframe, 0, bars, laggClose) <= 0 ||
      CopyClose(InpLeaderSymbol,  InpTimeframe, 0, bars, leadClose) <= 0)
     {
      return;
     }

Необходимое выравнивание цен двух символов реализовано с помощью встроенной функции MQL5 CopyClose.

// 2) Build shifted leader series (lag by InpLagBars)
   double leadShifted[];
   ArrayResize(leadShifted, ArraySize(leadClose));
   for(int i = 0; i < ArraySize(leadClose); i++)
      leadShifted[i] = (i + InpLagBars < ArraySize(leadClose)) ? leadClose[i + InpLagBars] : 0.0;

Лаг в 5 баров реализован добавлением значения InpLagBars к индексу массива leadClose.

// 3) compute rolling regression slope (beta), residual, mean, var, zscore
   int idx = 0; // current bar index 0
   if(ArraySize(laggClose) < InpWindow + InpLagBars + 1)
      return;
   int valid = InpWindow;

// compute beta at current bar (using most recent up to InpWindow)
   double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
   int n = 0;
   for(int j = idx; j < idx + InpWindow; j++) // AsSeries so 0==latest
     {
      double x = leadShifted[j];
      double y = laggClose[j];
      if(x == 0.0)
         continue; // skip misaligned values
      sumX += x;
      sumY += y;
      sumXY += x * y;
      sumX2 += x * x;
      n++;
     }
   if(n < 2)
      return;
   double beta = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);

В советнике встроенная функция REGR_SLOPE(), которую мы использовали для получения Beta в SQL, заменена простой моделью линейной регрессии.

// rolling residual stats over InpWindow
   double mean = 0, var = 0;
   int count = 0;
   for(int k = idx; k < idx + InpWindow; k++)
     {
      double x = laggClose[k] - beta * leadShifted[k];
      if(!IsFiniteDouble(x))
         continue;
      mean += x;
      var  += x * x;
      count++;
     }
   if(count < 2)
      return;
   mean /= count;
   var = var / count - mean * mean;
   if(var < 1e-12)
      var = 1e-12;
   double zscore = (resid - mean) / MathSqrt(var);

// 4) position logic
   double wantPos = 0.0;
   if(zscore < -InpZOpen)
      wantPos = +1.0;
   else
      if(zscore > +InpZOpen)
         wantPos = -1.0;
      else
         if(MathAbs(zscore) < InpZClose)
            wantPos = 0.0;
         else
           {
            if(positionLong)
               wantPos = 1.0;
            if(positionShort)
               wantPos = -1.0;
           }
   ManagePosition(wantPos);
  }

Логика входа/выхода основана на пороге z-score, заданном во входных параметрах InpZOpen и InpZClose. Переменная wantPos +1 означает купить laggard/последователя; wantPos -1 означает продать его; wantPos 0 означает ничего не делать. InpZOpen и InpZClose — основные значения по умолчанию, подлежащие оптимизации, вместе с InpWindow.

//+------------------------------------------------------------------+
//| Close positions by opposite signal                               |
//+------------------------------------------------------------------+

void ManagePosition(double wantPos)
  {
   int    total = PositionsTotal();
   bool   hasLong = false;
   bool   hasShort = false;
   for(int i = 0; i < total; i++)
     {
      ulong posTicket = PositionGetTicket(i);
      if(PositionGetString(POSITION_SYMBOL) != InpLaggardSymbol)
         continue;
      ENUM_POSITION_TYPE ptype = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      if(ptype == POSITION_TYPE_BUY)
         hasLong = true;
      if(ptype == POSITION_TYPE_SELL)
         hasShort = true;
     }

// close opposite positions
   if(wantPos == 0.0)
     {
      if(hasLong)
         ClosePosition(POSITION_TYPE_BUY);
      if(hasShort)
         ClosePosition(POSITION_TYPE_SELL);
      positionLong = positionShort = false;
      return;
     }
   if(wantPos > 0 && !hasLong)
     {
      if(hasShort)
         ClosePosition(POSITION_TYPE_SELL);
      OpenPosition(POSITION_TYPE_BUY);
      positionLong = true;
      positionShort = false;
     }
   else
      if(wantPos < 0 && !hasShort)
        {
         if(hasLong)
            ClosePosition(POSITION_TYPE_BUY);
         OpenPosition(POSITION_TYPE_SELL);
         positionLong = false;
         positionShort = true;
        }
  }

Позиции закрываются противоположным сигналом.

Это настройки, использованные в бэктесте. Соответствующий файл *.set приложен.

Настройки MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 2. Настройки MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 3 — Статистика MetaTester для lead/lag-бэктеста XAUUSD (спотовое золото) и ETF GDX.US.

Рис. 3. Статистика MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US.

Рис. 4 — График MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US.

Рис. 4. График MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и GDX.US ETF


Рис. 5 - Время сделок MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 5. Время сделок MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Обратите внимание, что мы торговали только во время сессии США из-за GDX.US, который торгуется преимущественно на NYSE. Также, возможно, следует протестировать фильтр дней недели для торговли только по средам.

Рис. 6 - Корреляция MetaTester (MFE, MAE) для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 6. Корреляция MetaTester (MFE, MAE) для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 7 - Время удержания позиций MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 7. Время удержания позиций MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US


Оптимизация

Мы оптимизировали период ретроспективы скользящего окна и порог z-score для закрытия позиций. Это были входные параметры. Соответствующий файл *.ini приложен.

Входные параметры оптимизации MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 8. Входные параметры оптимизации MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Это были результаты оптимизации. Мы выбрали первый вариант (pass 10), чтобы запустить одиночный тест с результатами выше.

Рис. 9 - Результаты оптимизации MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Рис. 9. Результаты оптимизации MetaTester для lead/lag-бэктеста XAUUSD (спот золота) и ETF GDX.US

Пожалуйста, учитывайте, что результаты не включают транзакционные издержки (комиссии, свопы XAUUSD и т. д.).


Заключение

В этой статье мы представили полный конвейер lead/lag-анализа: от обнаружения связи лидер/последователь, через фильтрацию чистым SQL-бэктестом, до финального бэктеста в MetaTrader 5.

Мы использовали векторизованное выполнение колонковой базы данных и строгое упорядочивание оконных функций, чтобы выполнять lead/lag-скрининг по десяткам активов за секунды. Такой детерминированный подход гарантирует воспроизводимость результатов исследования, что необходимо любой количественной торговой стратегии.

Мы сосредоточились на трех распространенных ошибках в запросах lead/lag-диффузии: отсутствие ORDER BY в оконных функциях, отсутствие детерминированной нумерации строк и несоответствие временных меток между сериями.

Наконец, мы использовали выборку известных и уже ранжированных коинтегрированных акций, чтобы показать очень краткий, но практический пример анализа, который мог выявить потенциальную торговую возможность. Однако отсутствие пика корреляции на ненулевом лаге указывало, что лаг не пригоден для торговли. Поэтому мы выбрали пару, у которой за последние 30 дней был такой пик корреляции на ненулевом лаге, и запустили бэктест за последние две недели, оптимизировав период скользящих окон и порог z-score выхода из позиции для лучших результатов.

Название файла Описание
cross_correlation_synth.py Python-скрипт для имитации кросс-корреляции на синтетических данных
non_determinism_synth.py Python-скрипт для имитации недетерминированного SQL-упорядочивания на синтетических данных
lead_lag_analyser.py Python-скрипт для запуска lead/lag-анализа
lead_lag_backtest.sql  SQL-файл для запуска чистого SQL-бэктеста 
Lead_Lag.mq5 Исходный файл советника MQL5 
tester-set-lead-lag-xauusd-gdxus.ini INI-файл настроек тестера 
expert-set-lead-lag-xauusd-gdxus-opt-params.set SET-файл параметров оптимизации 


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21811

Прикрепленные файлы |
Lead-Lag.mq5 (8.98 KB)
Архитектура машинного обучения для MetaTrader 5 (Часть 9): Интеграция байесовской оптимизации гиперпараметров в производственный пайплайн Архитектура машинного обучения для MetaTrader 5 (Часть 9): Интеграция байесовской оптимизации гиперпараметров в производственный пайплайн
В этой статье бэкенд оптимизации гиперпараметров Optuna (HPO) интегрируется в единый ModelDevelopmentPipeline. Добавлены совместная настройка гиперпараметров модели и схем весов выборки, раннее отсечение с Hyperband и отказоустойчивое SQLite-хранилище исследований. Пайплайн автоматически определяет первичные и вторичные модели, добавляет перед моделью обученный препроцессор удаления столбцов, обеспечивающий безопасный инференс, поддерживает последовательный бутстрэппинг, формирует отчет Optuna и интегрируется с bid/ask-пайплайном и LearnedStrategy. Читатели получают более быстрые, возобновляемые запуски и развертываемые самодостаточные модели.
Автоматизация торговых стратегий в MQL5 (Часть 29): Создание системы торговли по гармоническому паттерну "Гартли" на основе Price Action Автоматизация торговых стратегий в MQL5 (Часть 29): Создание системы торговли по гармоническому паттерну "Гартли" на основе Price Action
В этой статье мы разрабатываем систему распознавания гармонических паттернов "Гартли" (Gartley) на языке MQL5, которая определяет бычьи и медвежьи гармонические паттерны "Гартли" с использованием точек разворота и уровней Фибоначчи, запуская сделки с точными уровнями входа, стоп-лосса и тейк-профита. Мы также улучшим визуальное представление паттерна с помощью графических объектов — треугольников, линий тренда и меток, которые чётко отображают структуру паттерна XABCD.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Архитектура машинного обучения для MetaTrader 5 (Часть 8): Байесовская оптимизация гиперпараметров с Purged Cross-Validation и ранним отсечением испытаний Архитектура машинного обучения для MetaTrader 5 (Часть 8): Байесовская оптимизация гиперпараметров с Purged Cross-Validation и ранним отсечением испытаний
GridSearchCV и RandomizedSearchCV имеют фундаментальное ограничение в финансовом ML: каждое испытание независимо, поэтому качество поиска не улучшается с ростом вычислительного бюджета. В этой статье Optuna — с использованием Tree-structured Parzen Estimator — интегрируется с кросс-валидацией PurgedKFold, ранней остановкой HyperbandPruner и соглашением о двух типах весов, которое разделяет веса обучения и веса оценки. В результате получается система из пяти компонентов: целевая функция с отсечением на уровне фолдов, слой преобразования/подстановки параметров, совместно оптимизирующий схему взвешивания и гиперпараметры модели, финансово откалиброванное отсечение, возобновляемый оркестратор на базе SQLite и конвертер в формат scikit-learn cv_results_. В статье также проводится четкое разграничение — на основе Тимоти Мастерса — между статистическими целями, где направленный поиск полезен, и финансовыми целями, где он вреден.