English Deutsch 日本語
preview
Интеграция MQL5 с пакетами обработки данных (часть 8): Использование графовых нейронных сетей для выявления зон ликвидности

Интеграция MQL5 с пакетами обработки данных (часть 8): Использование графовых нейронных сетей для выявления зон ликвидности

MetaTrader 5Примеры |
54 1
Hlomohang John Borotho
Hlomohang John Borotho

Содержание:

  1. Введение
  2. Обзор модели и системы
  3. Получение исторических данных
  4. Приступаем
  5. Объединяем все в MQL5
  6. Тестирование на исторических данных
  7. Заключение


Введение

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

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


Обзор модели и системы

Модель и советник работают совместно, распределяя задачи между подготовкой данных, инференсом машинного обучения и исполнением сделок. Советник выступает в MetaTrader 5 как интерфейс работы с рынком в реальном времени, непрерывно собирая значения максимумов, минимумов и закрытия, а также тиковые объемы. На основе этих данных советник строит структурное представление рынка, определяя максимумы и минимумы свингов. Затем они организуются в граф, где каждая точка свинга становится узлом, а структурные связи между ними образуют ребра. Эти узлы дополняются признаками: ценовым уровнем, типом свинга и относительным расстоянием до соседних свингов. Подготовленные признаки узлов и ребра графа передаются обученной модели графовой нейронной сети (GNN). Модель обрабатывает граф и возвращает прогнозы, указывающие на вероятность или силу присутствия ликвидности вокруг каждого структурного уровня.

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


Получение исторических данных

from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import pytz

# Display MetaTrader5 package information
print("MetaTrader5 package author:", mt5.__author__)
print("MetaTrader5 package version:", mt5.__version__)

# Pandas display settings
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1500)

# Initialize MT5 connection
if not mt5.initialize():
    print("initialize() failed, error code =", mt5.last_error())
    quit()

# Define symbol
symbol = "XAUUSD.m"

# Ensure the symbol is available
if not mt5.symbol_select(symbol, True):
    print("Failed to select symbol:", symbol)
    mt5.shutdown()
    quit()

# Set timezone to UTC
timezone = pytz.timezone("Etc/UTC")

# Define date range
utc_from = datetime(2025, 11, 1, tzinfo=timezone)
utc_to   = datetime(2026, 2, 28, tzinfo=timezone)

# Get historical rates
rates = mt5.copy_rates_range(symbol, mt5.TIMEFRAME_H1, utc_from, utc_to)

# Shutdown MT5 connection
mt5.shutdown()

# Validate data
if rates is None or len(rates) == 0:
    print("No data retrieved. Check symbol or date range.")
else:
    
    print("First 10 bars:")
    for rate in rates[:10]:
        print(rate)

    # Convert to DataFrame
    rates_frame = pd.DataFrame(rates)

    # Convert time
    rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')

    # Save to CSV
    filename = "XAUUSD_H1.csv"
    rates_frame.to_csv(filename, index=False)

    print("\nData saved to:", filename)

Вывод:

Этот скрипт устанавливает соединение между Python и терминалом MetaTrader 5 для получения исторических рыночных данных, используемых для анализа и обучения модели. Скрипт начинается с импорта необходимых библиотек, вывода информации о пакете MetaTrader 5 и настройки параметров отображения pandas для более удобного просмотра данных. После инициализации подключения к MetaTrader 5 скрипт выбирает символ XAUUSD.m и задает определенный исторический диапазон дат по времени UTC. Затем скрипт запрашивает почасовые ценовые данные за этот период с помощью функции copy_rates_range, которая возвращает структурированные данные: цену открытия, цену максимума, цену минимума, цену закрытия и тиковый объем.


Приступаем

import pandas as pd
import numpy as np
import torch
import torch.nn.functional as F

from torch_geometric.data import Data
from torch_geometric.nn import GCNConv

from sklearn.preprocessing import StandardScaler

df = pd.read_csv("XAUUSD_H1.csv")
df['time'] = pd.to_datetime(df['time'])
df.head()

Вывод:

Для начала импортируем необходимые библиотеки для обработки данных, числовых вычислений и построения модели графовой нейронной сети. Затем загружается исторический набор данных XAUUSD H1 из ранее созданного CSV-файла, а столбец времени преобразуется в формат datetime для более удобного анализа по времени. Наконец, просматриваются первые несколько строк набора данных, чтобы убедиться, что данные корректно загружены и готовы к дальнейшей предварительной обработке.

def detect_swings(df, window=3):

    swing_highs = []
    swing_lows = []

    for i in range(window, len(df)-window):

        high = df['high'][i]
        low = df['low'][i]

        if high == max(df['high'][i-window:i+window]):
            swing_highs.append(i)

        if low == min(df['low'][i-window:i+window]):
            swing_lows.append(i)

    return swing_highs, swing_lows

highs, lows = detect_swings(df)

nodes = []
node_prices = []

for i in highs:

    price = df['close'][i]
    volume = df['tick_volume'][i]

    nodes.append([price, volume, 1])
    node_prices.append(price)

for i in lows:

    price = df['close'][i]
    volume = df['tick_volume'][i]

    nodes.append([price, volume, -1])
    node_prices.append(price)

nodes = np.array(nodes)
node_prices = np.array(node_prices)

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

После выявления точек свинга создаются узлы графа, которые позднее используются графовой нейронной сетью. Для каждого выявленного максимума и минимума свинга из набора данных извлекаются соответствующие признаки, например цена закрытия и тиковый объем. Эти значения объединяются со структурной меткой: 1 для максимумов свинга и -1 для минимумов свинга. Так описывается тип рыночной структуры, представляемый каждым узлом. Затем все признаки узлов собираются в массив NumPy, а соответствующие ценовые уровни хранятся отдельно. Данные подготавливаются к следующему этапу, на котором структура графа будет построена и обработана моделью.

scaler = StandardScaler()
nodes = scaler.fit_transform(nodes)
edges = []

for i in range(len(nodes)-1):

    edges.append([i, i+1])
    edges.append([i+1, i])

edge_index = torch.tensor(edges, dtype=torch.long).t().contiguous()

labels = []

for i in range(len(node_prices)):

    price = node_prices[i]

    # count nearby swing prices
    touches = np.sum(np.abs(node_prices - price) < 1.0)

    if touches >= 3:
        labels.append(1)
    else:
        labels.append(0)

labels = torch.tensor(labels)

unique, counts = np.unique(labels.numpy(), return_counts=True)

print(dict(zip(unique, counts)))

Вывод:

{np.int64(0): np.int64(311), np.int64(1): np.int64(103)}

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

Затем для каждого узла создаются метки: измеряется, сколько других цен свингов располагается около того же ценового уровня. Если ценовой уровень многократно достигается в пределах небольшого диапазона, он маркируется как потенциальная зона ликвидности; в противном случае – как обычный уровень. Наконец, эти метки преобразуются в тензоры и выводится их распределение. Это позволяет быстро проверить баланс между узлами ликвидности и узлами без ликвидности перед обучением модели.

x = torch.tensor(nodes, dtype=torch.float)

data = Data(
    x=x,
    edge_index=edge_index,
    y=labels
)

print(data)

Вывод:

Data(x=[414, 3], edge_index=[2, 826], y=[414])

На этом этапе обработанные данные признаков узлов преобразуются в тензор PyTorch, что позволяет использовать их в графовой нейронной сети. Затем создается объект данных графа с использованием структуры данных из PyTorch Geometric, объединяющей признаки узлов, ребра графа и целевые метки в единое графовое представление. Наконец, объект графа выводится для проверки корректности структуры набора данных и его готовности к обучению модели.

class LiquidityGNN(torch.nn.Module):

    def __init__(self):

        super().__init__()

        self.conv1 = GCNConv(3, 32)
        self.conv2 = GCNConv(32, 32)
        self.conv3 = GCNConv(32, 2)

    def forward(self, x, edge_index):

        x = self.conv1(x, edge_index)
        x = F.relu(x)

        x = self.conv2(x, edge_index)
        x = F.relu(x)

        x = self.conv3(x, edge_index)

        return x

model = LiquidityGNN()

optimizer = torch.optim.Adam(model.parameters(), lr=0.005)

weights = torch.tensor([1.0, 3.0])  # give liquidity higher importance

loss_fn = torch.nn.CrossEntropyLoss(weight=weights)

for epoch in range(800):

    model.train()
    optimizer.zero_grad()
    out = model(data.x, data.edge_index)
    loss = loss_fn(out, data.y)
    loss.backward()
    optimizer.step()

    if epoch % 50 == 0:
        print("Epoch:", epoch, "Loss:", loss.item())

Вывод:

В этом разделе определяется модель LiquidityGNN – графовая нейронная сеть для анализа взаимосвязей между точками свинга на рынке. Модель построена с использованием трех слоев графовой сверточной сети (GCN). Первый слой преобразует входные признаки узлов в пространство признаков большей размерности, второй уточняет усвоенную структурную информацию, а последний формирует прогнозы для каждого узла. Во время прямого прохода операции графовой свертки используют признаки узлов и ребра графа для передачи информации между соседними узлами. Функция активации ReLU вводит нелинейность, позволяя сети изучать более сложные структурные паттерны, связанные с зонами ликвидности.

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

model.eval()

pred = model(data.x, data.edge_index).argmax(dim=1)

print(pred)

Вывод:

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

liquidity_nodes = (pred == 1).nonzero(as_tuple=True)[0]

print("Liquidity nodes:", liquidity_nodes)

Вывод:

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

import os

mt5_path = r"C:\Users\...\AppData\Roaming\MetaQuotes\Terminal\...\MQL5\Files"

model_path = os.path.join(mt5_path, "liquidity_gnn.onnx")

dummy_x = data.x
dummy_edge = data.edge_index
model.eval()

torch.onnx.export(
    model,
    (dummy_x, dummy_edge),
    model_path,
    input_names=["node_features","edge_index"],
    output_names=["output"],
    opset_version=17,
    do_constant_folding=True,
    external_data=False,
    dynamic_axes={
        "node_features":{0:"nodes"},
        "edge_index":{1:"edges"},
        "output":{0:"nodes"}
    }
)
print("Model saved to:", model_path)

Вывод:

Определяется путь к каталогу Files MetaTrader 5 и расположение файла модели ONNX. Затем обученная модель GNN переводится в режим оценки и экспортируется в ONNX с помощью torch.onnx.export. Для обработки графов переменного размера задаются динамические оси узлов и ребер, после чего подтверждается сохранение модели по указанному пути.


Объединяем все в MQL5

//+------------------------------------------------------------------+
//|                                                     Graph NN.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/ru/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/ru/users/johnhlomohang/"
#property version   "1.00"
#property strict

#resource "\\Files\\liquidity_gnn.onnx" as uchar ExtModel[]

#include <Trade/Trade.mqh>
#include <ONNXRuntime/ONNXRuntime.mqh>

//--- trading
CTrade trade;

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input          string            ModelFile="liquidity_gnn.onnx";
input          int               SwingWindow=3;
input          int               BarsToProcess=500;
input          int               MaxNodes=50;
input          int               MaxEdges=100;
input          double            MinLiquidityScore=0.7;
input          double            LotSize=0.1;
input          int               MagicNumber=777;
input          bool              UseStopLoss=true;
input          int               StopLossPoints=200;
input          bool              UseTakeProfit=true;
input          int               TakeProfitPoints=400;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+

double               node_features[];
long                 edge_index[];
double               predictions[];
double               nodePrices[];
int                  swingHighs[];
int                  swingLows[];
int                  nodeCount=0;
int                  actualEdgeCount=0;
datetime             lastBarTime=0;
//--- scaler values
double               scalerMean[3]= {1950,500,0};
double               scalerStd[3]= {100,200,1};
long                 onnx_handle;
//--- Model dimensions from ONNX
long                 model_nodes = 0;
long                 model_features = 0;
long                 model_edges = 0;
long                 model_classes = 0;

В этом разделе кода обученная модель ONNX GNN сначала встраивается как ресурс с помощью #resource. Это позволяет советнику обращаться к ней непосредственно из каталога Files, без зависимости от внешних путей к файлам. Затем подключаются необходимые библиотеки MQL5 для торговли, включая CTrade, и для работы с ONNX runtime. Определяются входные параметры: окно определения свингов, число обрабатываемых баров, максимальное число узлов и ребер графа, минимальная оценка ликвидности для исполнения сделки, размер позиции, настройки стоп-лосса и тейк-профита, а также уникальное магическое число MagicNumber для идентификации сделок.

Также объявляются глобальные переменные для хранения структур данных графа и выходных данных модели: массивы признаков узлов, индексов ребер, прогнозов, цен узлов, а также максимумов и минимумов свингов. Дополнительные глобальные переменные отслеживают текущее количество узлов, фактическое число ребер и временную метку последнего обработанного бара. Для нормализации данных в массивах scalerMean и scalerStd сохраняются заранее вычисленные средние значения и стандартные отклонения. Инициализируются переменные для хранения размеров модели ONNX – узлов, признаков, ребер и классов – а также хэндл сессии ONNX. Благодаря этому советник готов загружать модель, обрабатывать поступающие ценовые данные и принимать торговые решения.

//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.SetExpertMagicNumber(MagicNumber);

   onnx_handle = OnnxCreateFromBuffer(ExtModel, ONNX_DEFAULT);

   if(onnx_handle == INVALID_HANDLE)
     {
      Print("ONNX create failed: ", GetLastError());
      return INIT_FAILED;
     }

   Print("ONNX model loaded");

   OnnxTypeInfo type_info;

   long input_count = OnnxGetInputCount(onnx_handle);
   Print("Model inputs: ", input_count);

   for(long i = 0; i < input_count; i++)
     {
      string name = OnnxGetInputName(onnx_handle, i);
      Print("Input ", i, " name: ", name);

      if(OnnxGetInputTypeInfo(onnx_handle, i, type_info))
        {
         Print("Input ", i, " dimensions:");
         ArrayPrint(type_info.tensor.dimensions);

         //--- Store model dimensions
         if(name == "node_features")
           {
            if(ArraySize(type_info.tensor.dimensions) >= 2)
              {
               model_nodes = type_info.tensor.dimensions[0];
               model_features = type_info.tensor.dimensions[1];
               Print("Model expects node_features: [", model_nodes, ", ", model_features, "]");
              }
           }
         else
            if(name == "edge_index")
              {
               if(ArraySize(type_info.tensor.dimensions) >= 2)
                 {
                  model_edges = type_info.tensor.dimensions[1];
                  Print("Model expects edge_index: [2, ", model_edges, "]");
                 }
              }
        }
     }

   long output_count = OnnxGetOutputCount(onnx_handle);
   Print("Model outputs: ", output_count);

   for(long i = 0; i < output_count; i++)
     {
      string name = OnnxGetOutputName(onnx_handle, i);
      Print("Output ", i, " name: ", name);

      if(OnnxGetOutputTypeInfo(onnx_handle, i, type_info))
        {
         Print("Output ", i, " dimensions:");
         ArrayPrint(type_info.tensor.dimensions);

         if(ArraySize(type_info.tensor.dimensions) >= 2)
           {
            model_classes = type_info.tensor.dimensions[1];
            Print("Model output classes: ", model_classes);
           }
        }
     }

//--- Don't set fixed shapes - the model has dynamic axes
//--- Instead, we'll use the actual node count from detection

   Print("ONNX initialization complete");
   Print("Model expects: nodes=", model_nodes, ", features=", model_features,
         ", edges=", model_edges, ", classes=", model_classes);

   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Print("EA deinitializing. Reason code: ", reason);

//--- Release ONNX model session
   if(onnx_handle != INVALID_HANDLE)
     {
      OnnxRelease(onnx_handle);
      onnx_handle = INVALID_HANDLE;
      Print("ONNX session released");
     }

//--- Delete all objects created by this EA
//--- Delete liquidity objects on the chart
   int deleted = 0;
   for(int i = ObjectsTotal(0, -1, OBJ_RECTANGLE) - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, "Liq_") == 0) // Objects starting with "Liq_"
        {
         ObjectDelete(0, name);
         deleted++;
        }
     }

   if(deleted > 0)
      Print("Deleted ", deleted, " rectangle objects");

//--- Optionally close all open positions (if you want this behavior)
   bool closePositions = false; // Set to true if you want to close all positions on deinit

   if(closePositions)
     {
      int closed = 0;
      for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
         ulong ticket = PositionGetTicket(i);
         if(PositionSelectByTicket(ticket))
           {
            if(PositionGetInteger(POSITION_MAGIC) == MagicNumber)
              {
               trade.PositionClose(ticket);
               closed++;
              }
           }
        }

      if(closed > 0)
         Print("Closed ", closed, " positions");
     }

//--- Clear global arrays to free memory
   ArrayFree(node_features);
   ArrayFree(edge_index);
   ArrayFree(predictions);
   ArrayFree(nodePrices);
   ArrayFree(swingHighs);
   ArrayFree(swingLows);

   Print("EA deinitialization complete");
  }

В функции OnInit() советник инициализируется: объекту CTrade сначала присваивается магическое число, обеспечивающее уникальную идентификацию сделок, открытых этим советником. Затем встроенная модель ONNX загружается из массива uchar ExtModel с помощью OnnxCreateFromBuffer, после чего проверяется успешность создания сессии. После активации сессии модели запрашиваются спецификации ее входных и выходных данных с помощью OnnxGetInputCount, OnnxGetInputName, OnnxGetInputTypeInfo и аналогичных функций для выходных данных. Это позволяет динамически сохранять ожидаемое количество узлов, признаков, ребер и выходных классов, которые советник позднее использует для корректного форматирования данных графа перед инференсом.

Функция OnDeinit() обеспечивает корректное завершение работы и управление ресурсами при удалении советника или закрытии терминала. Сначала функция освобождает сессию ONNX, высвобождая память, затем перебирает все объекты на графике и удаляет объекты ликвидности, созданные советником. При необходимости функция может закрыть все открытые позиции, связанные с магическим числом советника. Наконец, функция очищает глобальные массивы признаков узлов, индексов ребер, прогнозов, цен и свингов, высвобождая память.

//+------------------------------------------------------------------+
//| Expert tick                                                      |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime currentBar = iTime(_Symbol, _Period, 0);

   if(currentBar == lastBarTime)
      return;

   lastBarTime = currentBar;

   if(!DetectSwings())
      return;

   if(!BuildGraph())
      return;

   if(!RunInference())
      return;

   ExecuteTrades();
  }

//+------------------------------------------------------------------+
//| Detect swing highs/lows                                          |
//+------------------------------------------------------------------+
bool DetectSwings()
  {
   ArrayResize(swingHighs, 0);
   ArrayResize(swingLows, 0);

   double high[], low[];

   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);

   if(CopyHigh(_Symbol, _Period, 0, BarsToProcess, high) <= 0)
     {
      Print("Failed to copy high data");
      return false;
     }

   if(CopyLow(_Symbol, _Period, 0, BarsToProcess, low) <= 0)
     {
      Print("Failed to copy low data");
      return false;
     }

   for(int i = SwingWindow; i < BarsToProcess - SwingWindow; i++)
     {
      bool swingHigh = true;
      bool swingLow = true;

      for(int j = -SwingWindow; j <= SwingWindow; j++)
        {
         if(j == 0)
            continue;

         if(high[i] <= high[i + j])
            swingHigh = false;

         if(low[i] >= low[i + j])
            swingLow = false;
        }

      if(swingHigh)
        {
         int s = ArraySize(swingHighs);
         ArrayResize(swingHighs, s + 1);
         swingHighs[s] = i;
        }

      if(swingLow)
        {
         int s = ArraySize(swingLows);
         ArrayResize(swingLows, s + 1);
         swingLows[s] = i;
        }
     }

   Print("Detected ", ArraySize(swingHighs), " swing highs and ",
         ArraySize(swingLows), " swing lows");

   return true;
  }

В функции OnTick() на каждом входящем тике выполняется основная логика советника. Сначала функция проверяет, сформировался ли новый бар, сравнивая временную метку текущего бара с lastBarTime. Если нет, она завершает работу, чтобы избежать избыточных вычислений. После обнаружения нового бара советник последовательно вызывает DetectSwings() для определения последних максимумов и минимумов свингов, BuildGraph() для создания графового представления для модели ONNX, RunInference() для получения прогнозов ликвидности от GNN и ExecuteTrades() для размещения ордеров Buy или Sell на основе прогнозируемых сигналов.

Функция DetectSwings() анализирует последние бары для выявления локальных максимумов и минимумов, соответствующих максимумам и минимумам свингов. Используя настраиваемый параметр SwingWindow, функция перебирает массивы цен максимумов и минимумов и сравнивает каждый бар с соседними, чтобы определить, является ли он свингом. Выявленные свинги сохраняются в динамических массивах swingHighs и swingLows. Функция также обрабатывает ошибки копирования ценовых данных и выводит сводку о количестве выявленных свингов, что упрощает контроль и отладку. Этот шаг важен, поскольку формирует исходные узлы для построения графа, используемого GNN для анализа ликвидности.

//+------------------------------------------------------------------+
//| Build graph                                                      |
//+------------------------------------------------------------------+
bool BuildGraph()
  {
   double close[];
   long volume[];

   ArraySetAsSeries(close, true);
   ArraySetAsSeries(volume, true);

   if(CopyClose(_Symbol, _Period, 0, BarsToProcess, close) <= 0)
     {
      Print("Failed to copy close data");
      return false;
     }

   if(CopyTickVolume(_Symbol, _Period, 0, BarsToProcess, volume) <= 0)
     {
      Print("Failed to copy volume data");
      return false;
     }

   nodeCount = 0;
   ArrayResize(nodePrices, MaxNodes);

//--- swing highs
   for(int i = 0; i < ArraySize(swingHighs) && nodeCount < MaxNodes; i++)
     {
      int bar = swingHighs[i];
      if(bar < BarsToProcess)
        {
         nodePrices[nodeCount] = close[bar];
         nodeCount++;
        }
     }

//--- swing lows
   for(int i = 0; i < ArraySize(swingLows) && nodeCount < MaxNodes; i++)
     {
      int bar = swingLows[i];
      if(bar < BarsToProcess)
        {
         nodePrices[nodeCount] = close[bar];
         nodeCount++;
        }
     }

   if(nodeCount < 2)
     {
      Print("Not enough nodes: ", nodeCount);
      return false;
     }

   Print("Building graph with ", nodeCount, " nodes");

//--- Build node features [nodes, 3]
   ArrayResize(node_features, nodeCount * 3);
   ArrayInitialize(node_features, 0);

   int f = 0;
   for(int i = 0; i < nodeCount; i++)
     {
      double price = nodePrices[i];
      double vol = 0;

      //--- Get volume for this bar
      if(i < BarsToProcess)
         vol = (double)volume[i];

      //--- Determine swing type (1 for highs, -1 for lows)
      double swingType = (i < ArraySize(swingHighs)) ? 1.0 : -1.0;

      //--- Normalize features using scaler
      node_features[f++] = (price - scalerMean[0]) / scalerStd[0];  // price
      node_features[f++] = (vol - scalerMean[1]) / scalerStd[1];    // volume
      node_features[f++] = (swingType - scalerMean[2]) / scalerStd[2]; // swing type
     }

//--- Build edges
//--- For a chain graph with bidirectional connections between consecutive nodes:
//--- Number of edges = (nodeCount - 1) * 2
   actualEdgeCount = (nodeCount - 1) * 2;

//--- Edge_index should be a flattened array of shape [2, actualEdgeCount]
//--- That means: [row0_edge0, row1_edge0, row0_edge1, row1_edge1, ...]
//--- So total array size = 2 * actualEdgeCount
   int edgeArraySize = 2 * actualEdgeCount;
   ArrayResize(edge_index, edgeArraySize);
   ArrayInitialize(edge_index, 0);

   Print("Creating ", actualEdgeCount, " edges, array size: ", edgeArraySize);

   int e = 0;
   for(int i = 0; i < nodeCount - 1; i++)
     {
      //--- Forward edge: from i to i+1
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 0;     // row 0 (source row)
         e++;
         edge_index[e] = i;      // source node index
         e++;
        }

      //--- Forward edge: target
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 1;      // row 1 (target row)
         e++;
         edge_index[e] = i + 1;  // target node index
         e++;
        }

      //--- Backward edge: from i+1 to i (source)
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 0;      // row 0 (source row)
         e++;
         edge_index[e] = i + 1;  // source node index
         e++;
        }

      //--- Backward edge: target
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 1;      // row 1 (target row)
         e++;
         edge_index[e] = i;      // target node index
         e++;
        }
     }

   Print("Added ", e/2, " edge entries (", e, " array elements used)");

//--- Verify we used the correct number of elements
   if(e != edgeArraySize)
     {
      Print("Warning: Expected ", edgeArraySize, " elements but used ", e);
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Run ONNX inference                                               |
//+------------------------------------------------------------------+
bool RunInference()
  {
//--- Set dynamic shapes before inference
   long input0_shape[] = {nodeCount, 3};
   if(!OnnxSetInputShape(onnx_handle, 0, input0_shape))
     {
      Print("Failed to set input shape for node_features: ", GetLastError());
      return false;
     }

//--- actualEdgeCount is the number of edges (not the array size)
//--- Edge_index shape is [2, actualEdgeCount]
   long input1_shape[] = {2, actualEdgeCount};
   if(!OnnxSetInputShape(onnx_handle, 1, input1_shape))
     {
      Print("Failed to set input shape for edge_index: ", GetLastError());
      return false;
     }

   long output_shape[] = {nodeCount, 2};
   if(!OnnxSetOutputShape(onnx_handle, 0, output_shape))
     {
      Print("Failed to set output shape: ", GetLastError());
      return false;
     }

//--- Debug: Print array sizes
   Print("Running inference with: nodes=", nodeCount,
         ", edges=", actualEdgeCount,
         ", features array size=", ArraySize(node_features),
         ", edges array size=", ArraySize(edge_index));

//--- Run inference
   ArrayResize(predictions, nodeCount * 2);

   if(!OnnxRun(onnx_handle, ONNX_NO_CONVERSION, node_features, edge_index, predictions))
     {
      Print("ONNX inference failed: ", GetLastError());
      return false;
     }

   ProcessPredictions();

   return true;
  }

Функция BuildGraph() строит графовое представление недавних рыночных свингов, которое передается модели ONNX GNN. Сначала функция собирает последние цены закрытия и тиковые объемы для указанного числа баров, затем объединяет выявленные максимумы и минимумы свингов в массив узлов. Каждому узлу с использованием заранее заданных значений масштабировщика присваиваются три признака: нормализованная цена, объем и тип свинга (high=1, low=-1). Функция также создает двунаправленные ребра между последовательными узлами, формируя цепной граф, и сохраняет их в развернутом массиве edge_index. Одновременно отслеживается общее количество узлов и ребер для динамического задания размеров входных данных ONNX.

Затем функция RunInference() подготавливает динамические формы входных и выходных данных на основе фактического количества узлов и ребер, созданных в BuildGraph(). Функция изменяет размеры входных тензоров ONNX, node_features и edge_index, а также выходного тензора, выводит отладочную информацию для проверки и выполняет ONNX-модель с помощью OnnxRun(). Прогнозы сохраняются в массиве predictions и обрабатываются функцией ProcessPredictions(). Этот шаг напрямую преобразует графовое представление рыночных свингов в применимые сигналы ликвидности, которые советник позднее использует для открытия сделок. При этом инференс в реальном времени согласуется с динамически определяемой рыночной структурой.

//+------------------------------------------------------------------+
//| Process predictions                                              |
//+------------------------------------------------------------------+
void ProcessPredictions()
  {
   int liquidityCount = 0;

   for(int i = 0; i < nodeCount; i++)
     {
      double p0 = predictions[i * 2];
      double p1 = predictions[i * 2 + 1];

      //--- Apply softmax manually
      double maxv = MathMax(p0, p1);
      double e0 = MathExp(p0 - maxv);
      double e1 = MathExp(p1 - maxv);
      double prob = e1 / (e0 + e1);  // Probability of class 1 (liquidity)

      if(prob > MinLiquidityScore)
        {
         liquidityCount++;
         DrawLiquidityZone(i, nodePrices[i], prob);
        }
     }

   if(liquidityCount > 0)
      Print("Found ", liquidityCount, " liquidity zones");
  }

//+------------------------------------------------------------------+
//| Execute trades                                                   |
//+------------------------------------------------------------------+
void ExecuteTrades()
  {
   if(CountPositions() > 0)
      return;

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   for(int i = 0; i < nodeCount; i++)
     {
      if(MathAbs(nodePrices[i] - ask) < 10 * _Point)
        {
         if(ask < nodePrices[i])
            OpenTrade(ORDER_TYPE_BUY);
         else
            OpenTrade(ORDER_TYPE_SELL);
         break;
        }
     }
  }

//+------------------------------------------------------------------+
//| Open trade                                                       |
//+------------------------------------------------------------------+
void OpenTrade(ENUM_ORDER_TYPE type)
  {
   double price, sl = 0, tp = 0;

   if(type == ORDER_TYPE_BUY)
      price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   else
      price = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   if(UseStopLoss)
     {
      if(type == ORDER_TYPE_BUY)
         sl = price - StopLossPoints * _Point;
      else
         sl = price + StopLossPoints * _Point;
     }

   if(UseTakeProfit)
     {
      if(type == ORDER_TYPE_BUY)
         tp = price + TakeProfitPoints * _Point;
      else
         tp = price - TakeProfitPoints * _Point;
     }

   if(type == ORDER_TYPE_BUY)
      trade.Buy(LotSize, _Symbol, price, sl, tp);
   else
      trade.Sell(LotSize, _Symbol, price, sl, tp);
  }

//+------------------------------------------------------------------+
//| Draw zone                                                        |
//+------------------------------------------------------------------+
void DrawLiquidityZone(int id, double price, double probability)
  {
   string name = "Liq_" + IntegerToString(id) + "_" + IntegerToString(TimeCurrent());

   datetime t1 = iTime(_Symbol, _Period, 0);
   datetime t2 = t1 + PeriodSeconds(_Period) * 5;

   if(!ObjectCreate(0, name, OBJ_RECTANGLE, 0,
                    t1, price - 5 * _Point,
                    t2, price + 5 * _Point))
     {
      Print("Failed to create rectangle object");
      return;
     }

   ObjectSetInteger(0, name, OBJPROP_COLOR, clrGold);
   ObjectSetInteger(0, name, OBJPROP_FILL, true);
   ObjectSetString(0, name, OBJPROP_TEXT, "Liquidity: " + DoubleToString(probability * 100, 1) + "%");
  }

Функция ProcessPredictions() получает необработанные выходные данные модели ONNX GNN и преобразует их в пригодные для использования оценки вероятности наличия ликвидности для каждого выявленного узла. Для каждого узла вручную выполняется вычисление softmax, определяющее вероятность того, что узел представляет значимую зону ликвидности. Узлы, превышающие заданный пользователем порог MinLiquidityScore, подсчитываются и визуализируются на графике с помощью DrawLiquidityZone(). Вокруг прогнозируемых ценовых уровней создаются прямоугольные зоны с цветовой маркировкой и метками вероятности. Это позволяет трейдерам видеть, где модель определяет потенциальные области высокой ликвидности на рынке.

Последующая торговая логика использует выявленные зоны ликвидности для автоматического открытия позиций. Функция ExecuteTrades() сначала проверяет отсутствие открытых позиций, затем сравнивает текущие цены Ask и Bid с ценами выявленных узлов. Когда цена находится близко к цене узла, система открывает сделку Buy или Sell с помощью функции OpenTrade(), применяющей настраиваемые уровни стоп-лосса и тейк-профита. Функция DrawLiquidityZone() обеспечивает четкое обозначение каждой зоны ликвидности на графике прямоугольником и меткой вероятности. Это обеспечивает визуальный контекст и сигналы для автоматического исполнения сделок советником.

//+------------------------------------------------------------------+
//|                                                  ONNXRuntime.mqh |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/ru/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/ru/users/johnhlomohang/"

//--- ONNX flags (keep it in sync with MT5 if changed)
#define ONNX_DEFAULT       0
#define ONNX_DEBUG_LOGS    1
#define ONNX_NO_CONVERSION 2

//--- Simple ONNX model wrapper that calls MT5 built-in Onnx* functions.
//--- This wrapper DOES NOT redeclare OnnxTypeInfo and DOES NOT import any DLL.

//+------------------------------------------------------------------+
//|                     Simple ONNX Runtime Wrapper                  |
//+------------------------------------------------------------------+
class CSimpleONNXModel
  {
private:
   long              session;
   bool              initialized;

public:

                     CSimpleONNXModel()
     {
      session = INVALID_HANDLE;
      initialized = false;
     }

                    ~CSimpleONNXModel()
     {
      Release();
     }

   //--- Initialize model from resource
   bool              InitFromResource(const uchar &model[])
     {
      session = OnnxCreateFromBuffer(model, ONNX_DEFAULT);

      if(session == INVALID_HANDLE)
        {
         Print("ONNX create failed: ", GetLastError());
         return false;
        }

      initialized = true;

      Print("ONNX model loaded successfully");

      return true;
     }

   bool              SetInputShape(int index,long &shape[])
     {
      return OnnxSetInputShape(session,index,shape);
     }

   bool              SetOutputShape(int index,long &shape[])
     {
      return OnnxSetOutputShape(session,index,shape);
     }

   bool              Run(double &input_features[],
            long &edge_index[],
            double &output[])
     {
      if(!initialized)
         return false;

      bool result = OnnxRun(session,
                            ONNX_NO_CONVERSION,
                            input_features,
                            edge_index,
                            output);

      if(!result)
         Print("ONNX run failed: ",GetLastError());

      return result;
     }

   void              Release()
     {
      if(session!=INVALID_HANDLE)
        {
         OnnxRelease(session);
         session=INVALID_HANDLE;
        }
     }
  };
//+------------------------------------------------------------------+

Наконец, имеется подключаемый файл ONNXRuntime, выступающий легковесной оберткой и позволяющий советнику взаимодействовать с ONNX-моделью машинного обучения внутри MetaTrader 5. Этот файл определяет небольшой класс CSimpleONNXModel, упрощающий использование встроенных ONNX-функций MetaTrader 5: OnnxCreateFromBuffer, OnnxSetInputShape, OnnxSetOutputShape и OnnxRun. Вместо прямого вызова этих функций по всему коду советника обертка объединяет их в четкие методы для инициализации модели, настройки форм входных и выходных данных, выполнения инференса и управления сессией. Это делает интеграцию советника с обученной моделью более понятной и удобной для сопровождения.

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


Результаты тестирования моделей на исторических данных

Представленные ниже результаты тестирования на исторических данных показывают, что интеграция обнаружения ликвидности на основе графовой нейронной сети в торговую систему позволяет определять пригодные для торговли зоны рыночной структуры. Это дает положительную чистую прибыль и профит-фактор выше 1, что соответствует цели алгоритмического выявления возможностей, связанных с ликвидностью. Тестирование на исторических данных проводилось на инструменте XAUUSD на таймфрейме H1 в течение двухмесячного тестового периода с 1 декабря 2025 года по 30 января 2026 года с настройками по умолчанию:


Заключение

В итоге задача определения зон ликвидности – областей, где цена, вероятно, отреагирует из-за скопления ордеров, – решается сочетанием машинного обучения на графах и автоматизированной торговой системы на MQL5. Вместо опоры только на традиционные индикаторы или ручной анализ графика рыночная структура была представлена в виде графа, где максимумы и минимумы свингов стали узлами, а связи между ними – ребрами. На основе этой структуры была обучена графовая нейронная сеть (GNN) для распознавания паттернов, связанных с накоплением ликвидности. Затем обученная модель была экспортирована в формат ONNX и интегрирована в советник. Это позволяет системе обрабатывать рыночные данные в реальном времени, динамически строить графы, выполнять инференс модели и определять потенциальные зоны ликвидности непосредственно в торговой платформе.

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

Имя файла
Описание
GNN for LqZs.ipynb Файл, содержащий блокнот для обучения и сохранения модели.
ONNXRuntime.mqh Файл, содержащий ONNXRuntime, являющийся подключаемым файлом.
Graph NN.mq5 Файл, содержащий советника MQL5, который загружает модель и исполняет сделки.
XAUUSD_H1.csv Файл, содержащий исторические ценовые данные XAUUSD.

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

Прикрепленные файлы |
GNN_for_LqZs.ipynb (14.45 KB)
ONNXRuntime.mqh (2.7 KB)
Graph_NN.mq5 (18.5 KB)
XAUUSD_H1.csv (117.25 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Stanislav Korotky
Stanislav Korotky | 19 мар. 2026 в 11:53
Одна картинка стоит тысячи слов. Почему бы в статье о графике не привести хотя бы одну схему этого графика?
Возможности Мастера MQL5, которые вам нужно знать (Часть 92): Использование индексации B-дерева и байесовской нейронной сети в пользовательском классе сигнала Возможности Мастера MQL5, которые вам нужно знать (Часть 92): Использование индексации B-дерева и байесовской нейронной сети в пользовательском классе сигнала
В этой статье представлен очередной пользовательский класс сигнала MQL5 – CSignalBTreeBayesian. Здесь алгоритм сбалансированного дерева объединяется с нейронной сетью, построенной на байесовских принципах, для создания еще одного пользовательского сигнала. Благодаря Мастеру MQL5 его можно тестировать отдельно или вместе с другими сигналами.
От начального до среднего уровня: Классы (II) От начального до среднего уровня: Классы (II)
Эта статья призвана быть как можно более дидактичной, поскольку тема, которую мы будем рассматривать, сама по себе нередко вызывает немалую путаницу. Поэтому, уважаемый читатель, постарайтесь применить на практике то, что здесь объясняется. Если у вас возникнут какие-либо вопросы, обязательно оставляйте комментарии, ведь понять деструкторы — задача не из самых простых.
Разработка инструментария для анализа Price Action (Часть 71): Картирование структуры гэпов выходного дня в MQL5 Разработка инструментария для анализа Price Action (Часть 71): Картирование структуры гэпов выходного дня в MQL5
В статье представлена реализация на MQL5 на основе графических объектов, которая определяет гэпы выходного дня по разрывам времени и отображает их непосредственно на графике. Она управляет графическими объектами, отслеживает переходы состояний – новый (FRESH), частично заполнен (PARTIAL), реакция (REACTION), заполнен (FILLED) – и сохраняет завершенные гэпы как исторические зоны. В результате создается воспроизводимая система для отслеживания возврата цены к структурам гэпов выходного дня и их заполнения.
Изучение стандартной библиотеки MQL5 (Часть 14): Создание советника для динамического хеджирования с использованием ALGLIB (ap.mqh) Изучение стандартной библиотеки MQL5 (Часть 14): Создание советника для динамического хеджирования с использованием ALGLIB (ap.mqh)
В этой статье представлен ap.mqh, порт ALGLIB для MQL5, и продемонстрировано его использование в многоактивных сценариях работы, требующих надежной линейной алгебры. Рассматривается почему встроенных индикаторов недостаточно, а затем реализуются полиномиальная регрессия, индикатор скользящей корреляционной матрицы и адаптивный оценщик коэффициента хеджа с использованием гребневой ​​регрессии с разложением Холецкого. Практический код показывает, как вычислять z-показатели спреда и согласованно исполнять парные сделки полностью в MetaTrader 5.