MQL5とデータ処理パッケージの統合(第8回):グラフニューラルネットワークによる流動性ゾーンの検出
目次
はじめに
トレーダーが継続的に直面する課題の一つは、価格が反応する前に流動性ゾーンを特定することです。多くの場合、スイングハイ、スイングロー、あるいは保ち合いレンジ付近に集まるストップ注文などの流動性プールは、市場がすでに動いた後になって初めて認識されます。従来のインジケータは、多くの場合、価格を厳密に逐次的な形で扱い、各バーを独立して分析するため、市場に存在する異なる価格ポイント間のより深い構造的な関係を捉えることができません。アルゴリズム戦略を構築するシステムトレーダーにとって、この制約により、構造的な価格水準周辺で流動性がどのように形成されるかをプログラムで検出することが難しくなり、結果としてエントリーの遅れや機会損失、あるいは流動性がすでに消費された後の取引につながる可能性があります。
この制約に対処するため、本記事では市場構造をグラフとして表現します。このグラフでは、スイングハイ、スイングロー、および構造的な転換点をノードとし、それらの関係を価格が市場内をどのように推移するかを表すエッジとして表現します。この構造に対して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
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5取引ツール(第24回):3Dカーブ、パンモード、ViewCubeナビゲーションによる奥行き感の向上
プライスアクション分析ツールキットの開発(第64回):手動で描画したトレンドラインと自動監視の同期
コード、涙、そしてAlgo Forge
ラリー・ウィリアムズの『市場の秘密』(第15回):相場環境を用いた「隠れスマッシュデー」反転戦略
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索