Причинный граф как признак машинного обучения: от условной причинности Грейнджера к CatBoost в MQL5

Аннотация. В первой части цикла мы построили ориентированный граф причинности между валютными парами и отделили его от обычной корреляционной матрицы. Но сам факт существования ребра X→Y ещё не отвечает на главный инженерный вопрос: даёт ли динамическая структура графа дополнительную информацию модели машинного обучения или только красиво описывает уже известное движение цены?

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

тывается через Newey–West/HAC, а множественные проверки контролируются методом Benjamini–Hochberg. Затем узловые, рёберные и глобальные метрики графа присоединяются к ценовым строкам только через backward as-of join — последняя точка графа должна быть сформирована не позже момента решения.

Эксперимент построен как абляция. На одинаковых ежедневных решениях сравниваются три CatBoost-модели: price-only, graph-only и price+causal graph. Начальный train заканчивается 31 декабря 2009 года, тест занимает 2010–2023 годы, а чистый форвард — период с 1 января 2024 по 31 июля 2026 года. Результат оказался содержательнее простого «граф улучшил модель»: graph-only почти не предсказывает направление, на тесте совместная модель немного улучшает AUC и LogLoss, а на форварде снижает просадку и повышает Profit Factor, но не даёт статистически доказанного прироста AUC. Это и есть правильный вывод: причинный граф полезен как контекст и фильтр режима, но не должен превращаться в самостоятельную кнопку Buy/Sell.


Рисунок 1. Полный исследовательский контур: синхронизированные закрытые бары, условный VAR-граф, статистическая фильтрация, backward as-of join, три абляционные CatBoost-модели и отдельный форвард.


Почему граф должен быть признаком, а не торговым сигналом

Корреляция отвечает на вопрос, насколько два ряда движутся совместно. Она симметрична: корреляция EURUSD с GBPUSD равна корреляции GBPUSD с EURUSD. Причинность Грейнджера проверяет другую гипотезу: уменьшают ли прошлые значения одного ряда ошибку прогноза другого ряда после учёта собственной истории цели.

Но даже статистически значимая связь не означает готовую сделку. Ребро может отражать:

Поэтому граф используется не как правило «если CHFJPY ведёт GBPCHF — купить GBPCHF», а как описание состояния рынка. Модель получает ответ на более широкий набор вопросов: кто сейчас ведёт, кто принимает импульс, насколько граф разрежен, стабильно ли ребро, не устарел ли последний снимок и насколько численно надёжно была оценена VAR-система.

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


Данные и временной протокол

Использован архив RoboForex_Top_History200001.zip. В него входят десять пар:

AUDUSD, CADJPY, CHFJPY, EURJPY, EURUSD, GBPCHF, GBPUSD, USDCAD, USDCHF и USDJPY.

Для каждой пары доступны H1, M30 и M15. После пересечения временных меток по всем десяти символам осталось:

Таймфрейм Синхронных доходностей Назначение
H1 130 017 медленный причинный режим и основной торговый горизонт
M30 246 692 промежуточное подтверждение направленности
M15 479 593 краткосрочная структура передачи информации

Граф строится на последовательных окнах по 2 500 наблюдений. Получено 371 состояние графа и выполнено 33 390 направленных тестов. Полный фильтр прошли 664 наблюдения рёбер.

Для ML-эксперимента используется одно решение в день по каждой паре — закрытый H1-бар с меткой 23:00 серверного времени. Цель:

target(t) = 1, если Close(t+24 H1) > Close(t), иначе 0.

Цена будущего бара не входит ни в один признак. После формирования rolling-признаков и присоединения графа получено 66 443 решения. Граф начинается не с 2000 года, а после накопления первого окна в 2 500 наблюдений, поэтому фактическая ML-выборка начинается в 2005 году.

Период Строк Как используется
2005–2009 10 575 начальное обучение для теста 2010–2023
2010–2023 35 776 длинный out-of-sample test
2005–2023 46 351 финальное обучение перед форвардом
2024–31.07.2026 6 543 чистый forward

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


Почему парного Granger-теста недостаточно

Парный тест проверяет X→Y, учитывая историю X и Y. Для валютной корзины этого мало. EURUSD и GBPUSD могут выглядеть причинно связанными просто потому, что обе пары реагируют на скрытый USD-фактор. Поэтому новая версия использует условный многомерный тест.

Для N=10 пар и порядка лагов p строится единая матрица:

Y(t) = c + A₁R(t−1) + ... + AₚR(t−p) + ε(t),

где R — вектор доходностей всех десяти пар. Для проверки ребра X→Y из полной модели удаляются только лаги X, а лаги Y и остальных восьми пар остаются. Если restricted-модель заметно хуже полной, прошлое X содержит дополнительную предсказательную информацию для Y.

Такое условие не устраняет все скрытые факторы мира, но значительно уменьшает ложные связи, которые возникают из наблюдаемой общей валютной структуры.


Статистический конвейер одного окна

Каждое состояние графа проходит десять этапов.

  1. Закрытые бары всех десяти пар синхронизируются по времени.
  2. Цена переводится в логарифмические доходности в базисных пунктах.
  3. Экстремальные значения ограничиваются робастным MAD-порогом.
  4. ADF-защита проверяет стационарность каждого ряда.
  5. Для каждой целевой пары порядок VAR от 1 до 3 выбирается по BIC.
  6. Полная VAR-регрессия решается один раз сразу для всех целей.
  7. Для каждого источника рассчитывается HAC/Newey–West Wald-тест.
  8. По всем 90 направлениям применяется Benjamini–Hochberg FDR 5%.
  9. Кандидат дополнительно подтверждается classical partial-F и частичным ΔR².
  10. Связь должна сохраняться на первой и второй половинах окна.

Важное отличие от первой упрощённой реализации — точные q-value вместо эвристического повышения F-порога. Ниже приведена функция Benjamini–Hochberg из Python-эталона.

def bh_qvalues(pvalues): p = np.asarray(pvalues, dtype=float) order = np.argsort(p) ranked = p[order] m = len(ranked) q = ranked * m / np.arange(1, m + 1) q = np.minimum.accumulate(q[::-1])[::-1] q = np.clip(q, 0.0, 1.0) result = np.empty_like(q) result[order] = q return result

HAC/Newey–West нужен потому, что финансовые остатки гетероскедастичны и могут иметь краткую автокорреляцию. Обычная OLS-ковариация способна принять совместный всплеск волатильности за значимую направленную связь.

def hac_covariance(X, resid, xtx_inv, max_lag): xe = X * resid[:, None] S = xe.T @ xe for lag in range(1, max_lag + 1): w = 1.0 - lag / (max_lag + 1.0) gamma = xe[lag:].T @ xe[:-lag] S += w * (gamma + gamma.T) cov = xtx_inv @ S @ xtx_inv return 0.5 * (cov + cov.T)

Ребро признаётся активным только при одновременном выполнении условий:

Вес активного ребра равен:

weight = sqrt(Wald / p) × 1000 × ΔR² × (0.25 + 0.75 × stability).

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


Какие признаки извлекаются из графа

После статистической фильтрации матрица превращается в ориентированный взвешенный граф. Для каждого момента времени формируются четыре группы признаков.

Группа Примеры Что описывает
Узловые in/out degree, in/out strength, net leadership, PageRank, hub, authority роль конкретной пары в передаче и приёме информации
Рёберные входящий/исходящий stable weight для каждой другой пары конкретные направления влияния относительно целевого символа
Статистические min q, max ΔR², mean stability, persistence значимость, эффект и устойчивость активных связей
Глобальные density, reciprocity, edge count, total weight, condition number состояние всего мультивалютного рынка и качество оценки
Эксплуатационные graph age, availability свежесть последнего снимка и готовность таймфрейма

Признаки рассчитываются отдельно для H1, M30 и M15. В ML-эксперимент вошло 150 графовых полей. Они дополняют 36 локальных ценовых признаков и десять one-hot идентификаторов пары.

Важная инженерная находка: модель активно использует не только вес рёбер. Среди наиболее важных графовых полей оказались:

То есть полезной информацией оказывается не только «кто ведёт», но и «насколько свежа и надёжна текущая причинная карта». Если граф старый или VAR-система плохо обусловлена, модель учится снижать доверие к его рёбрам.


Рисунок 2. Наиболее используемые графовые признаки финальной совместной модели. Высокая важность age и condition number показывает, что метаданные качества графа не менее важны, чем сами рёбра.


Главный контракт: только backward as-of join

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

Разрешён только следующий контракт:

для решения t используется последний graph_end_time, удовлетворяющий graph_end_time ≤ t.

features = pd.merge_asof( price_rows.sort_values("decision_time"), graph_snapshots.sort_values("graph_time"), left_on="decision_time", right_on="graph_time", direction="backward" ) features["graph_age_days"] = ( features.decision_time - features.graph_time ).dt.total_seconds() / 86400.0

Интерполяция между соседними графами также запрещена: правая точка интервала ещё не существовала в момент решения. Если история одного таймфрейма не готова, его графовые поля обнуляются, а отдельный availability-признак сообщает модели о недоступности.


Три модели для честной абляции

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

Модель Признаков Назначение
Price only 46 36 локальных price/volume/regime + 10 one-hot символов
Graph only 160 150 причинных + 10 one-hot символов
Price + causal graph 196 полная совместная модель

Все модели содержат 100 симметричных деревьев глубины 4, learning rate 0.06 и одинаковые параметры регуляризации. Это не попытка выжать максимальный результат из конкретной выборки, а воспроизводимый эксперимент на добавочную ценность признаков.

model = CatBoostClassifier( iterations=100, depth=4, learning_rate=0.06, loss_function="Logloss", l2_leaf_reg=10.0, random_strength=0.5, rsm=0.85, random_seed=24081986, verbose=False )

Первый цикл обучается только на данных до 2010 года и проверяется на 2010–2023. После фиксации метода модель переобучается на всей истории до 2024 года и один раз запускается на форварде 2024–2026.


Метрики прогноза на тесте 2010–2023

Модель AUC LogLoss Brier Balanced accuracy
Price only 0.51590 0.69402 0.25043 0.50779
Graph only 0.50605 0.69470 0.25076 0.50208
Price + causal graph 0.51874 0.69333 0.25009 0.51482

На длинном тесте совместная модель улучшила все четыре прогнозные метрики. Разница AUC относительно price-only составила +0.00284, а LogLoss уменьшился примерно на 0.00069.

Но month-block bootstrap не позволяет объявить этот прирост доказанным:

Интервалы пересекают ноль. Граф выглядит полезным, но эффект слаб и режимно зависим.


Рисунок 3. Graph-only почти не отличается от случайного классификатора. Причинная структура начинает работать только совместно с локальной ценой.


Исследовательский портфель: одинаковое торговое правило

Прогнозная метрика не равна торговому результату. Для одинакового сравнения используется простое правило без индивидуальной настройки пар:

  1. каждый день в 23:00 рассчитываются вероятности всех десяти пар;
  2. выбираются две пары с максимальным |p−0.5|;
  3. направление равно знаку p−0.5;
  4. позиция удерживается 24 H1-бара;
  5. из результата вычитается текущий spread и 0.10 базисного пункта условного slippage.

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

Test 2010–2023 Net, bps PF Max DD, bps
Price only +2 212.7 1.0139 −6 114.7
Graph only −16 611.9 0.9039 −19 235.8
Price + causal graph −112.7 0.9993 −4 928.4

Тест показывает важное противоречие: совместная модель лучше классифицирует направление и имеет меньшую просадку, но её простейшее ранжирование двух пар не превосходит price-only по Net. Следовательно, граф меняет форму вероятностей и выбор состояний, но его полезность нельзя оценивать одной метрикой.


Чистый форвард 2024–2026

Перед форвардом модели переобучаются на всей истории до 1 января 2024 года. Параметры, признаки и торговое правило остаются прежними.

Модель AUC LogLoss Net, bps PF Max DD, bps
Price only 0.53406 0.69146 +4 170.1 1.1966 −1 803.9
Graph only 0.49973 0.69289 +343.8 1.0148 −1 682.2
Price + causal graph 0.52917 0.69181 +4 470.9 1.2164 −1 225.7

На форварде граф не улучшил AUC: совместная модель уступила price-only примерно 0.0049. LogLoss также немного хуже. При этом портфельный результат оказался лучше:

Это выглядит как полезная режимная фильтрация, но paired month bootstrap снова не подтверждает устойчивый прирост:

Вероятность положительной разницы Net в month-block bootstrap равна 58.7%. Этого недостаточно, чтобы объявить граф источником доказанного edge.


Рисунок 4. На форварде совместная модель имеет более ровную кривую и меньшую просадку, но преимущество над price-only невелико относительно межмесячной вариативности.



Рисунок 5. Причинный граф улучшил форвардный PF только в совместной модели. Graph-only остаётся почти нейтральным.



Рисунок 6. Наиболее заметный практический эффект graph-features — уменьшение просадки, а не рост AUC.


Где граф помог и где навредил

Агрегат скрывает различия между парами. На форварде добавление графа изменило all-daily результат относительно price-only следующим образом:

Пара Price only Price + graph Разница, bps
GBPCHF +24.2 +1 317.8 +1 293.6
CHFJPY +1 133.3 +2 014.0 +880.8
USDCHF +1 066.4 +1 867.2 +800.8
USDCAD +1 315.3 +1 403.9 +88.6
EURUSD +693.2 +732.4 +39.2
AUDUSD −459.6 −861.2 −401.6
USDJPY +1 288.1 +225.1 −1 063.0
GBPUSD +1 746.0 +479.9 −1 266.0
CADJPY +2 367.8 +1 056.3 −1 311.5
EURJPY +3 251.7 +1 870.6 −1 381.1

Наиболее сильный положительный эффект наблюдается вокруг CHF-структуры: GBPCHF, CHFJPY и USDCHF. Это согласуется с текущим мульти-ТФ графом, где GBPCHF является главным принимающим узлом, а CHFJPY — одним из лидеров.

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


Что показывает текущий реальный граф

Последние снимки на 31 июля 2026 года содержат:

Источник Цель Активных TF Средний stable weight Минимальный q
CHFJPY GBPCHF 2 18.7158 0.000052
EURJPY GBPCHF 2 2.3137 0.003881
EURUSD GBPCHF 2 1.1352 0.000052

Отсутствие M15-рёбер не считается ошибкой. Строгий граф должен уметь быть пустым. Если статистические фильтры не подтверждают связь, нулевой граф честнее принудительно заполненной сети.


Рисунок 7. Последний мульти-ТФ снимок. Толщина ребра отражает устойчивый вес, а не прогнозируемую прибыль.



Рисунок 8. Граф остаётся разреженным и меняется по режимам. Плотная постоянная сеть была бы признаком слишком слабой статистической фильтрации.


Как считать граф в MQL5

Python остаётся исследовательским эталоном: он использует HAC/Newey–West и точный BH-FDR. В MQL5 реализован автономный runtime-граф с условным VAR, выбором лагов по BIC, точными F-p-value, BH-FDR, ADF и half-window stability.

Сначала загружаются строго синхронные закрытые бары:

bool CG_LoadSyncReturns(const ENUM_TIMEFRAMES tf, const int bars, const string prefix, const string suffix) { datetime reference = 0; for(int s=0; s<CG_N; s++) { MqlRates rates[]; ArraySetAsSeries(rates, true); if(CopyRates(CG_Symbols[s], tf, 1, bars+1, rates) != bars+1) return(false); if(s==0) reference=rates[0].time; else if(rates[0].time!=reference) return(false); for(int t=0; t<bars; t++) CG_Returns[s][t]=MathLog(c0/c1)*10000.0; } return(true); }

Далее для каждого кандидата строится restricted-модель без лагов источника. После BH-FDR рёбра проходят проверку на двух половинах окна:

if(pv[order[rank]] <= alpha*(rank+1.0)/edge) cutoff=rank; for(int e=0; e<edge; e++) { if(!selected[e]) continue; CG_PartialFRange(source,target,p,p,half,f1,p1,d1); CG_PartialFRange(source,target,p,half,CG_Bars,f2,p2,d2); double stability = MathMin(f1,f2) / MathMax(MathMax(f1,f2),1e-12); if(stability < min_stability) continue; }

Полный HAC-пересчёт тяжелее и остаётся в Python. Поэтому production-контракт должен быть двухуровневым:

  1. Research graph. Python HAC-граф используется для аудита, отбора устойчивых семейств рёбер и научного эксперимента.
  2. Runtime-compatible graph. Модель для реальной торговли должна обучаться на признаках, рассчитанных Python-зеркалом формул MQL5, либо использовать live-граф только как внешний regime/veto слой.

Нельзя обучить CatBoost на одной математике графа, а в терминале незаметно подавать другую. Parity распространяется не только на деревья, но и на весь контракт причинных признаков.


Как интегрировать признаки в советник

Практически графовые поля лучше использовать в трёх ролях.

1. Контекст direction-модели. Price-признаки отвечают за локальное состояние пары, а incoming/outgoing strength и leadership сообщают, является ли движение локальным или частью передачи импульса по корзине.

2. Meta-фильтр. Отдельная модель может решать, разрешать ли основной сигнал. Для meta-модели особенно полезны graph age, condition number, density, q-value и stability: они описывают качество контекста, а не направление.

3. Распределение риска. Мультиклассовая или регрессионная allocation-модель может снижать лот в плотном, взаимном или плохо обусловленном графе и повышать его только при устойчивом одностороннем лидерстве.

Самостоятельное направление сделки по одному ребру использовать не рекомендуется. Даже самое устойчивое направление CHFJPY→GBPCHF не содержит информации о знаке будущего движения GBPCHF — оно сообщает только о добавочной предсказательной ценности прошлых CHFJPY-доходностей.


Self-test и parity

Перед реальными данными граф проверяется на синтетическом процессе с заранее заданным ответом:

x(t)=0.3x(t−1)+εx(t),

y(t)=0.2y(t−1)+0.5x(t−1)+εy(t).

Ожидается ребро x→y и отсутствие y→x. Python-self-test прошёл. В комплект входит отдельный MIDAS_GRANGER_SELFTEST.mq5, который должен вывести PASS перед включением live-графа.

Для ML-контура нужны три разные проверки:

PASS модели при несовпадающем графе ничего не доказывает: CatBoost точно вычислит прогноз для неправильного входного вектора.


Ограничения эксперимента


Что находится в приложенном пакете

Файл Назначение
run_ablation.py полный сбор датасета и первоначальный абляционный эксперимент
train_one.py воспроизводимое обучение отдельной модели и периода
ablation_metrics_final.csv общие test/forward метрики трёх моделей
forward_by_symbol_final.csv форвардная разбивка по десяти парам
paired_bootstrap_final.json month-block bootstrap различий AUC, LogLoss и Brier
portfolio_difference_bootstrap.json bootstrap разницы портфельного результата
combined_feature_importance_v2.csv важность price и causal graph признаков
MIDAS_REAL_CAUSAL_GRAPH.py точный Python-движок conditional Granger/HAC/BH-FDR
MIDAS_REAL_CAUSAL_GRAPH_CORE.mqh ядро live-графа MQL5
MIDAS_REAL_CAUSAL_GRAPH_LIVE.mq5 автономный терминальный граф
MIDAS_GRANGER_SELFTEST.mq5 синтетическая проверка направления причинности
all_rolling_edges.csv 33 390 последовательных направленных тестов
all_rolling_node_features.csv узловые признаки всех 371 снимков
figures/ архитектура, граф, ablation и форвардные иллюстрации


Заключение

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

Эксперимент дал три принципиальных результата.

Поэтому правильное место причинного графа в MIDAS — не отдельный «предсказатель будущего», а слой контекста, meta-фильтрации и управления риском. Следующий строгий шаг — обучить runtime-compatible графовые признаки с полной Python/MQL5 parity, заморозить модель и проверить её на новом периоде, который не использовался ни в одном из текущих экспериментов.

Литература

  1. Granger C. W. J. Investigating Causal Relations by Econometric Models and Cross-Spectral Methods. Econometrica, 1969.
  2. Dickey D. A., Fuller W. A. Distribution of the Estimators for Autoregressive Time Series with a Unit Root. JASA, 1979.
  3. Newey W. K., West K. D. A Simple, Positive Semi-definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix. Econometrica, 1987.
  4. Benjamini Y., Hochberg Y. Controlling the False Discovery Rate. Journal of the Royal Statistical Society, Series B, 1995.
  5. Kleinberg J. M. Authoritative Sources in a Hyperlinked Environment. Journal of the ACM, 1999.
  6. Page L., Brin S., Motwani R., Winograd T. The PageRank Citation Ranking. Stanford InfoLab, 1999.
  7. Prokhorenkova L. et al. CatBoost: Unbiased Boosting with Categorical Features. NeurIPS, 2018.