Причинный граф как признак машинного обучения: от условной причинности Грейнджера к 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.
Такое условие не устраняет все скрытые факторы мира, но значительно уменьшает ложные связи, которые возникают из наблюдаемой общей валютной структуры.
Статистический конвейер одного окна
Каждое состояние графа проходит десять этапов.
Важное отличие от первой упрощённой реализации — точные 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 почти не отличается от случайного классификатора. Причинная структура начинает работать только совместно с локальной ценой.
Исследовательский портфель: одинаковое торговое правило
Прогнозная метрика не равна торговому результату. Для одинакового сравнения используется простое правило без индивидуальной настройки пар:
Это 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-контракт должен быть двухуровневым:
Нельзя обучить 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, заморозить модель и проверить её на новом периоде, который не использовался ни в одном из текущих экспериментов.
Литература