Интеграция MQL5 с пакетами обработки данных (часть 8): Использование графовых нейронных сетей для выявления зон ликвидности
Содержание:
- Введение
- Обзор модели и системы
- Получение исторических данных
- Приступаем
- Объединяем все в MQL5
- Тестирование на исторических данных
- Заключение
Введение
Одна из постоянных проблем трейдеров – определение зон ликвидности до того, как цена на них отреагирует. Во многих случаях пулы ликвидности, например кластеры стоп-ордеров вокруг максимумов и минимумов свингов или диапазонов консолидации, удается определить лишь после того, как рынок уже совершил движение. Традиционные индикаторы часто анализируют цену строго последовательно, рассматривая каждый бар независимо, и не учитывают более глубокие структурные связи между различными ценовыми точками на рынке. Для системных трейдеров, разрабатывающих алгоритмические стратегии, это ограничение затрудняет программное определение формирования ликвидности вокруг структурных уровней. В результате возникают задержки входа, упускаются возможности или сделки открываются после того, как ликвидность уже была поглощена.
Для устранения этого ограничения рыночную структуру можно представить в виде графа, где максимумы и минимумы свингов, а также структурные точки разворота становятся узлами, а связи между ними образуют ребра, описывающие, как цена движется в рынке. Применение графовых нейронных сетей (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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 92): Использование индексации B-дерева и байесовской нейронной сети в пользовательском классе сигнала
От начального до среднего уровня: Классы (II)
Разработка инструментария для анализа Price Action (Часть 71): Картирование структуры гэпов выходного дня в MQL5
Изучение стандартной библиотеки MQL5 (Часть 14): Создание советника для динамического хеджирования с использованием ALGLIB (ap.mqh)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования