English Deutsch
preview
MQL5とデータ処理パッケージの統合(第8回):グラフニューラルネットワークによる流動性ゾーンの検出

MQL5とデータ処理パッケージの統合(第8回):グラフニューラルネットワークによる流動性ゾーンの検出

MetaTrader 5 |
21 1
Hlomohang John Borotho
Hlomohang John Borotho

目次

  1. はじめに
  2. モデルとシステムの概要
  3. 履歴データの取得
  4. 導入手順
  5. MQL5で全体を組み立てる
  6. バックテスト
  7. 結論


はじめに

トレーダーが継続的に直面する課題の一つは、価格が反応する前に流動性ゾーンを特定することです。多くの場合、スイングハイ、スイングロー、あるいは保ち合いレンジ付近に集まるストップ注文などの流動性プールは、市場がすでに動いた後になって初めて認識されます。従来のインジケータは、多くの場合、価格を厳密に逐次的な形で扱い、各バーを独立して分析するため、市場に存在する異なる価格ポイント間のより深い構造的な関係を捉えることができません。アルゴリズム戦略を構築するシステムトレーダーにとって、この制約により、構造的な価格水準周辺で流動性がどのように形成されるかをプログラムで検出することが難しくなり、結果としてエントリーの遅れや機会損失、あるいは流動性がすでに消費された後の取引につながる可能性があります。

この制約に対処するため、本記事では市場構造をグラフとして表現します。このグラフでは、スイングハイ、スイングロー、および構造的な転換点をノードとし、それらの関係を価格が市場内をどのように推移するかを表すエッジとして表現します。この構造に対してGNN(Graph Neural Network:グラフニューラルネットワーク)を適用することで、モデルはノード間の相互作用パターンを学習し、流動性が蓄積される可能性の高い場所や、次に狙われる可能性のある価格帯を認識できるようになります。このアプローチをMQL5と統合することで、高度なデータ処理および機械学習技術を自動売買システムと組み合わせることが可能となり、より高度な流動性ゾーン認識と、より情報に基づいたアルゴリズム取引の意思決定への道が開かれます。


モデルとシステムの概要

モデルとEAは、データ準備、機械学習による推論、および売買執行の役割を分担することで連携します。EAはMetaTrader 5内でリアルタイムで市場と接するインターフェースとして動作し、高値・安値・終値・ティックボリュームなどの価格データを継続的に収集します。このデータから、EAはスイングハイとスイングローを検出し、それらを市場構造として表現します。各スイングポイントはノードとなり、それらの構造的な関係はエッジとして構成されます。各ノードには、価格水準、スイング種別、近傍スイングとの相対距離といった特徴量が付与されます。準備されたノード特徴量とエッジ接続は、学習済みのGNNモデルへ入力され、モデルはグラフを処理した後、各構造水準周辺に流動性が存在する確率またはその強さを示す予測結果を返します。

モデルによる推論が完了すると、EAは返された予測結果を解釈し、売買可能なシグナルを判断します。たとえば、現在の市場構造より上方に流動性が存在する確率が高いとモデルが判断した場合、システムは上昇方向への値動きを想定し、買いポジションの準備をする場合があります。一方、市場構造より下方に流動性が存在すると予測された場合は、売り機会の可能性を示すシグナルとして扱われます。その後、EAはスプレッド条件の確認、市場構造との整合性の検証、ポジションサイズなどの取引パラメータの計算といった基本的な取引管理ルールを適用したうえで、注文を実行します。このワークフローにより、機械学習モデルは複雑な流動性パターンの認識に専念し、EAは売買に関する実務的な処理を担当するため、グラフベースの流動性予測を取引プラットフォーム上で自動的な売買執行へと変換できます。


履歴データの取得

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関数を使用して、その期間の1時間足(H1)の価格データを取得します。この関数は、始値、高値、安値、終値、ティックボリュームなどの構造化されたデータを返します。


導入手順

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()

出力:

まず、データ処理、数値計算、およびGNNモデルの構築に必要なライブラリをインポートします。続いて、前のステップで生成したCSVファイルからXAUUSDのH1履歴データを読み込み、時間列を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という関数を定義します。この関数は、指定されたウィンドウサイズを使用してデータセットを走査し、各ローソク足の高値と安値を周囲のローソク足と比較します。あるローソク足の高値が定義された範囲内で最大値である場合、それはスイングハイとして分類されます。一方、同じ範囲内でその安値が最小値である場合、それはスイングローとして識別されます。検出されたスイングポイントのインデックスは、それぞれ別々のリストに保存されます。これにより、市場構造における重要な転換点を特定できるようになります。

スイングポイントを特定した後、次の段階でGNNによって使用されるグラフノードを構築します。検出された各スイングハイおよびスイングローについて、データセットから終値やティックボリュームなどの関連する特徴量を抽出します。これらの値に、市場構造の種類を示すラベルを組み合わせます。スイングハイには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)}

ここでは、GNNで処理できるようにデータを準備します。まず、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テンソルへ変換し、GNNで利用できる形式にします。次に、PyTorch Geometricのデータ構造を使用してグラフデータオブジェクトを構築します。ノード特徴量、エッジ接続、およびターゲットラベルを1つのグラフ表現にまとめることで、モデルが市場構造全体をグラフとして処理できるようになります。最後に、作成したグラフオブジェクトを表示し、データセットが正しく構造化され、モデル学習の準備が整っていることを確認します。

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())

出力:

このセクションでは、市場におけるスイングポイント間の関係を分析するために設計されたGNNであるLiquidityGNNモデルを定義します。このモデルは3つのGCN(Graph Convolutional Network:グラフ畳み込みネットワーク)レイヤーで構成されています。最初のレイヤーは入力されたノード特徴量をより高次元の特徴空間へ変換し、2番目のレイヤーは学習した市場構造の情報をさらに洗練します。そして最後のレイヤーが、各ノードに対する予測結果を出力します。フォワードパスでは、グラフ畳み込み演算がノード特徴量とエッジ接続の両方を利用して、隣接するノード間で情報を伝播させます。また、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)

出力:

ここでは、MetaTrader 5のFilesディレクトリへのパスを定義し、ONNXモデルファイルの保存先を指定します。次に、学習済みのGNNモデルを評価モードへ切り替え、torch.onnx.exportを使用してONNX形式へエクスポートします。この際、ノード数やエッジ数が異なるグラフサイズにも対応できるよう、動的軸を設定します。最後に、モデルが指定したパスへ正常に保存されたことを確認します。


MQL5で全体を組み立てる

//+------------------------------------------------------------------+
//|                                                     Graph NN.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/ja/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/ja/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;

このコードセクションでは、まず#resourceを使って学習済みONNX GNNモデルをリソースとして埋め込みます。これにより、EAは外部パス参照に依存せず、そのリソースを直接利用できます。次に、取引処理用のMQL5ライブラリ(CTrade)およびONNXランタイム機能を使用するために必要なライブラリをインクルードします。その後、システムの主要な動作を制御する入力パラメータを定義します。これらには、スイング検出用ウィンドウ、処理対象となるバー数、グラフ内で使用する最大ノード数およびエッジ数、取引実行に必要な最小流動性スコア、ポジションサイズ、ストップロス・テイクプロフィット設定、そして取引を識別するための固有のMagicNumberなどが含まれます。

さらに、グラフデータ構造およびモデル出力を保持するためのグローバル変数を宣言します。これには、ノード特徴量、エッジインデックス、予測結果、ノード価格、そして検出されたスイング高値・安値を格納する配列が含まれます。加えて、現在のノード数、実際に使用されているエッジ数、最後に処理したバーのタイムスタンプを管理するためのグローバル変数も定義します。データを正規化するため、事前計算された平均値と標準偏差の値をscalerMeanおよびscalerStd配列へ保存します。また、ONNXモデルの次元情報(ノード数、特徴量数、エッジ数、クラス数)を格納するための変数と、ONNXセッションハンドルを初期化します。これによりEAは、モデルの読み込み、リアルタイム価格データの処理、そしてモデルの予測結果に基づいた売買判断を実行できる準備が整います。

//+------------------------------------------------------------------+
//| 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オブジェクトにマジックナンバーを設定し、EAによって開かれた取引を一意に識別できるようにします。次に、OnnxCreateFromBufferを使用して、ExtModel uchar配列に組み込まれたONNXモデルを読み込み、正常に作成されたかを確認します。モデルセッションが有効になると、OnnxGetInputCount、OnnxGetInputName、OnnxGetInputTypeInfoなどの関数、および対応する出力情報取得関数を使用して、モデルの入力・出力仕様を取得します。これにより、必要なノード数、特徴量数、エッジ数、出力クラス数を動的に取得して保存できます。これらの情報は後の推論処理で使用され、EAがグラフデータを正しい形式で構築してモデルへ入力できるようにします。

OnDeinit()関数は、EAがチャートから削除された場合やターミナルが終了する際に、正常な終了処理とリソース管理をおこないます。まず、ONNXセッションを解放してメモリを空けます。その後、チャート上にEAが作成した流動性オブジェクトを削除するため、すべてのチャートオブジェクトを確認します。また、必要に応じてEAのマジックナンバーに関連付けられたすべての保有ポジションを決済する処理を追加できます。最後に、ノード特徴量、エッジインデックス、予測結果、価格データ、スイングポイントなどに使用したグローバル配列をクリアし、使用していたメモリを解放します。

//+------------------------------------------------------------------+
//| 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()関数では、EAにおける主要な処理ロジックを、受信する各ティックごとに実行します。この関数では、まず現在のバーのタイムスタンプとlastBarTimeを比較し、新しいバーが形成されたかどうかを確認します。新しいバーが形成されていない場合、不要な再計算を避けるため、関数は早期に終了します。新しいバーが検出されると、EAはまずDetectSwings()を呼び出して、直近のスイングハイとスイングローを検出します。次に、BuildGraph()を実行してONNXモデルへ入力するためのグラフ表現を構築します。その後、RunInference()によってGNNモデルから流動性予測を取得し、最後にExecuteTrades()を呼び出して、予測されたシグナルに基づいて買いまたは売り注文を実行します。

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モデルへ入力するために、直近の市場スイングをグラフとして表現する処理を構築します。まず、指定されたバー数分の最新の終値とティックボリュームを取得します。その後、検出されたスイングハイとスイングローを1つのノード配列へ統合します。各ノードには、3つの特徴量が割り当てられます。これらは、正規化された価格、ボリューム、そしてスイングタイプです。スイングハイには1、スイングローには-1が設定され、それぞれのノードが表す市場構造の種類を示します。また、この関数では隣接するノード間に双方向のエッジを構築し、チェーングラフを形成します。これらのエッジはフラット化されたedge_index配列へ保存されます。同時に、動的なONNX入力サイズに対応するため、総ノード数とエッジ数も管理します。

続いて、RunInference()関数は、BuildGraph()で生成された実際のノード数とエッジ数に基づいて、動的な入力・出力シェイプを準備します。この関数では、ONNX入力テンソル(node_featuresおよびedge_index)と出力テンソルのサイズを調整し、確認用のデバッグ情報を表示した後、OnnxRun()を使用してONNXモデルを実行します。予測結果はpredictions配列へ保存され、その後ProcessPredictions()によって処理されます。このステップにより、市場スイングのグラフ表現が、EAが後続の取引判断に利用できる実用的な流動性シグナルへ直接変換されます。これにより、リアルタイム推論が動的に検出された市場構造と一致した状態で実行されます。

//+------------------------------------------------------------------+
//| 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モデルから取得した生の出力結果を処理し、検出された各ノードに対する売買判断に利用できる流動性確率へ変換します。各ノードについて、この関数は手動でソフトマックス計算を適用し、そのノードが重要な流動性ゾーンを表している可能性を算出します。ユーザーが設定したMinLiquidityScoreのしきい値を超えたノードはカウントされ、DrawLiquidityZone()を使用してチャート上に可視化されます。この関数は、予測された価格水準周辺に長方形のゾーンを描画し、色と確率ラベルを表示します。これにより、トレーダーはモデルが市場内で高い流動性が存在すると判断した可能性のあるエリアを視覚的に確認できます。

続く取引ロジックでは、これらの識別された流動性ゾーンを利用して自動的にポジションを実行します。ExecuteTrades()関数は、まず既存のオープンポジションがないことを確認します。その後、現在のAsk価格およびBid価格と、検出されたノード価格を比較します。価格がノード価格の近くにある場合、システムはOpenTrade()を使用して買いまたは売り取引を実行します。この関数では、設定可能なストップロスおよびテイクプロフィット水準が適用されます。DrawLiquidityZone()関数は、各流動性ゾーンをチャート上に明確に表示する役割を持ち、長方形と予測確率を示すラベルを追加します。これにより、EAは視覚的な市場コンテキストと自動売買シグナルの両方を提供します。

//+------------------------------------------------------------------+
//|                                                  ONNXRuntime.mqh |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/ja/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/ja/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インクルードファイルについて説明します。このファイルは、MetaTrader 5内でEAがONNX機械学習モデルと連携するための軽量なラッパーとして機能します。このファイルでは、CSimpleONNXModelという小さなクラスを定義しています。このクラスは、MetaTrader 5に組み込まれているOnnxCreateFromBuffer、OnnxSetInputShape、OnnxSetOutputShape、OnnxRunなどのONNX関数を簡単に利用できるようにするものです。EA全体でこれらの関数を直接呼び出す代わりに、このラッパーは処理を明確なメソッドへ整理します。これにより、モデルの初期化、入力・出力シェイプの設定、推論の実行、セッション管理などを個別の処理として扱うことができ、EAと学習済みモデルの統合をよりシンプルで保守しやすい構造にできます。

また、このラッパーはONNXセッションのライフサイクルも管理します。モデルがリソースから正しく読み込まれ、提供された入力テンソルを使用して実行され、不要になった際には安全に解放されることを保証します。これらの処理を再利用可能な単一クラスへカプセル化することで、MQL5取引環境とエクスポートされたグラフニューラルネットワークの間に構造化されたインターフェースを構築できます。その結果、EAは低水準のONNXランタイム呼び出しを直接処理することなく、効率的に機械学習推論を実行し、モデルの予測結果を売買判断へ変換できるようになります。


モデルのバックテスト結果

 以下のバックテスト結果を見ると、GNNを利用した流動性検出を取引システムへ統合することで、売買機会として活用可能な市場構造上のゾーンを識別でき、プラスの純利益および1を超えるプロフィットファクターを達成していることが確認できます。これは、流動性を基盤とした取引機会をアルゴリズムによって検出するという本システムの目的と一致しています。バックテストは、XAUUSDペアの1時間足(H1)を対象として、2025年12月1日から2026年1月30日までの2か月間にわたって実施しました。使用した設定はデフォルト設定です。


結論

本記事では、価格が反応する可能性が高い、注文が集中するエリアである流動性ゾーンを特定するという課題に対して、グラフベースの機械学習とMQL5による自動売買システムを組み合わせることで対応しました。従来のインジケータや手動によるチャート分析だけに依存するのではなく、市場構造をグラフ表現へ変換し、スイングハイとスイングローをノード、それらの関係性をエッジとして表現しました。この構造を利用して、流動性の蓄積に関連するパターンを認識するためのGNNを学習させました。その後、学習済みモデルをONNX形式へエクスポートし、EAへ統合することで、システムはリアルタイム市場データを処理し、動的にグラフを構築し、モデル推論を実行し、取引プラットフォーム内で直接潜在的な流動性ゾーンを検出できるようになりました。

このアプローチにより、トレーダーは従来のテクニカル分析を超えた高度なパターン認識機能を取引システムへ追加できます。機械学習と市場構造分析を統合することで、流動性エリアの検出を自動化し、より情報に基づいた取引判断を下すことが可能になります。自動売買戦略へ組み込むことで、このフレームワークは、構造的な取引機会への反応速度を向上させ、流動性識別における主観的な偏りを軽減し、AIによる洞察とアルゴリズムによる執行を組み合わせることで、取引システムの一貫性を高める可能性があります。

ファイル名
説明
GNN for LqZs.ipynb モデルを学習および保存するためのノートブックファイル
ONNXRuntime.mqh ONNXRuntimeを定義したインクルードファイル
Graph NN.mq5 モデルを読み込み、取引を実行するMQL5 EAファイル
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 3月 2026 において 11:53
「百聞は一見に如かず」と言います。グラフに関する記事なら、そのグラフの図を1つでも掲載してみてはいかがでしょうか。
MQL5取引ツール(第24回):3Dカーブ、パンモード、ViewCubeナビゲーションによる奥行き感の向上 MQL5取引ツール(第24回):3Dカーブ、パンモード、ViewCubeナビゲーションによる奥行き感の向上
MQL5の3D二項分布グラフ表示ツールを拡張し、確率質量関数(PMF)の奥行き感を向上させるセグメント化された3Dカーブを追加するとともに、注視点を移動できるパンモードと、素早く視点を切り替えられるインタラクティブなViewCubeを実装します。さらに、ViewCube内の面・辺・頂点に対応するクリック可能なサブゾーンを追加し、標準ビューへのカメラ遷移をアニメーションでおこなえるようにします。これらの機能を、2D/3D表示の切り替え、リアルタイム更新、各種カスタマイズ設定と組み合わせることで、トレードにおける確率分布分析を、より直感的で没入感のある分析環境を実現します。
プライスアクション分析ツールキットの開発(第64回):手動で描画したトレンドラインと自動監視の同期 プライスアクション分析ツールキットの開発(第64回):手動で描画したトレンドラインと自動監視の同期
手動で描画したトレンドラインを監視するには、常にチャートを確認し続ける必要があり、その結果、重要な価格反応を見逃す可能性があります。本記事では、MQL5 において手動で描画したトレンドラインと自動監視ロジックを同期させるトレンドライン監視エキスパートアドバイザーを開発し、価格が監視対象ラインに接近、タッチ、またはブレイクした際にアラートを生成します。
コード、涙、そしてAlgo Forge コード、涙、そしてAlgo Forge
プログラムコードや記事の添付ファイルを、より快適に公開・管理するための新しい方法として、MQL5 Algo Forgeへの移行について紹介します。従来のZIPアーカイブ形式でソースコードを添付する方法の代わりにリポジトリを使用することで、プロジェクトを常に最新の状態に保ち、素早く編集し、読者とよりプロフェッショナルな形で関わることができるようになります。MetaEditorインターフェースを通じて、開発したものをクラウド環境へ素早く移行するための推奨方法についても説明します。
ラリー・ウィリアムズの『市場の秘密』(第15回):相場環境を用いた「隠れスマッシュデー」反転戦略 ラリー・ウィリアムズの『市場の秘密』(第15回):相場環境を用いた「隠れスマッシュデー」反転戦略
ラリー・ウィリアムズの「隠れスマッシュデー」の反転を自動化するMQL5エキスパートアドバイザー(EA)を構築します。カスタムインジケータから確定済みのシグナルを読み取り、コンテキストフィルタ(Supertrendの方向一致および任意の取引日ルール)を適用します。また、スマッシュバーの構造またはATRを基準としたストップロスモデル、固定またはリスクベースのポジションサイズによってリスクを管理します。その結果、テストおよび拡張に対応した、再現可能なフレームワークを構築できます。