グラフ理論:取引における深さ優先探索(DFS)の応用
目次
はじめに
市場がどのように動くのかを理解しようとする取り組みの中で、本記事ではグラフ理論における深さ優先探索(DFS)アルゴリズムを、取引における値動きの構造へ適用します。値動きをアルゴリズム化しようとする際の大きな課題は、スイングハイ、スイングロー、そして市場の継続性といった概念が、通常、厳密で検証可能なルールではなく、定性的な説明によって表現されていることです。この問題に対応するため、市場構造をグラフとして形式化します。そこでは、確定した各スイングが1つのノードとなり、スイング間の遷移が、それらのノードを接続するエッジとなります。価格が新しいローソク足を形成し、新しいスイングが確定するにつれて、このグラフは動的に進化します。その結果、市場が時間の経過とともにどのように進行しているのかを、構造化された形で表現することが可能になります。
このフレームワークの中では、DFSによってシステムは1つの構造的な分岐を深く追跡します。たとえば、スイングローを起点として、高値切り上げ(HH)と安値切り上げ(HL)が続く構造的進行です。その後、別の構造的な可能性を検討します。アルゴリズムは、構造的条件が有効である限り、この経路に沿って進みます。そして、その経路の強さを、経路の深さ、重要な構造レベル、そしてターゲットに対する許容範囲などの客観的な基準を用いて測定します。経路が崩れる場合、たとえば重要なスイングレベルが無効化された場合、アルゴリズムはあらかじめ定めたルールに従って直前のノードまでバックトラックし、他に存在する可能性のある経路を探索します。このようにすることで、価格スイングの連続は、市場構造の中を移動可能なルートとして扱われます。その結果、主観的な値動き分析を、規律化された、コーディング可能なプロセスへ変換することができます。そしてこのプロセスは、体系的かつ自動化された取引システムに適したものになります。
システムの概要と理解
概念の対応関係:| グラフ理論 | 市場における対応概念 |
|---|---|
| ノード | スイングハイとスイングロー |
| エッジ | ブレイクまたは押し戻し(プルバック)の遷移 |
| 分岐 | 強気または弱気の継続 |
| 目標ノード | 流動性ターゲット |
| 深さ | 構造的な継続力 |
| バックトラック | 構造の無効化 |
グラフ理論において、DFSは、1つの分岐を選択し、別の可能性を検討する前に、その分岐を可能な限り深く探索することで機能します。これを市場構造に適用すると、アルゴリズムは、連続するスイングポイントを通じた価格の1つの構造的な進行を追跡します。このとき、確定した各スイングハイまたはスイングローを、グラフ内の1つのノードとして扱います。たとえば、価格は確定したスイングロー(ノードA)から始まり、スイングハイ(ノードB)へ移動し、その後リトレースしてノードCを形成し、さらに上方向へブレイクしてノードDへ進み、最終的にノードEまで拡大する可能性があります。DFSの観点では、構造的条件が有効である限り、システムはシーケンスA -> B -> C -> D -> Eに従います。
しかし、取引を実行する前に、アルゴリズムは最低限必要な構造的深さを満たす必要があります。これは、方向性のある進行を明確に示す、確定したスイングの連続を意味します。たとえば、強気条件の場合は、高値更新(HH) → 押し安値切り上げ(HL) → Break of Structure(構造ブレイク)という流れです。また、弱気条件の場合は、安値更新(LL) → 戻り高値切り下げ(LH) → 継続という流れになります。このような構造的な連鎖が検出され、検証された場合にのみ、システムは方向性バイアスの確立を検討します。

取引の観点では、「深く進む」とは、測定可能なスイング関係によって市場がその方向性を確認した後にのみ、構造的バイアスを採用することを意味します。強気バイアスは、DFS経路が有効な高値切り上げ(HH)および安値切り上げ(HL)のシーケンスを特定した場合に確認されます。この場合、直近の押し安値(HL)が、重要な無効化レベルとして機能します。
同様に、弱気バイアスは、経路が安値更新(LL)と戻り高値切り下げ(LH)を形成した場合に確認されます。この場合、最新の戻り高値(LH)が構造上の境界として機能します。一度このバイアスが確認され、さらに経路の深さが最低要件を満たした場合、システムは、その経路が合理的に意味のある目標へ到達できるかどうかを評価します。その目標には、たとえば流動性プール、供給ゾーンまたは需要ゾーン、事前に定義されたリスクリワード目標のようなものがあります。 これらの条件が満たされた場合、取引は現在アクティブな経路の方向に実行されます。一方で、価格が構造的無効化レベルを突破した場合、アルゴリズムはこれを経路の失敗として扱います。そして、DFSのバックトラッキングを実行し、最後に有効だったノードまで戻り、代替となる市場方向の探索を開始します。
導入手順
//+------------------------------------------------------------------+ //| DFS.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 //+------------------------------------------------------------------+ //| Include trade class | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade TradeManager; //+------------------------------------------------------------------+ //| Input parameters | //+------------------------------------------------------------------+ input int SwingPeriod = 7; // Bars left/right for pivot input int MinDepth = 7; // Minimum structural depth input double TargetTolerance = 60; // Tolerance for target hit (in points) input bool EnableVisual = true; // Draw nodes and paths input double FixedLotSize = 0.01; // Fixed lot size (if risk management disabled) input bool UseRiskManagement = true; // Use risk-based position sizing input double RiskPercent = 1.0; // Risk percentage per trade input int StopLossPoints = 600; // Stop loss in points input int TakeProfitPoints = 800; // Take profit in points input string TradeComment = "SwingGraph"; // Trade comment input int MagicNumber = 123456; // Expert magic number input bool EnableDailyReset = true; // Reset nodes at start of each day input int MaxNodesToKeep = 100; // Maximum nodes to keep (0 = unlimited) input bool EnableMemoryOptimization = true; // Enable memory optimization //+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct SwingNode { datetime time; // Bar time double price; // Swing price int type; // 1 = high, -1 = low int index; // Node index bool visited; // For DFS };
まず、エキスパートアドバイザー(EA)の基盤を構築するために、取引機能をインポートし、システムの動作を制御する主要な設定パラメータを定義します。#include <Trade/Trade.mqh>ライブラリは、MetaTraderの取引機能へのアクセスを提供します。一方、CTrade TradeManagerオブジェクトは、買い注文および売り注文を実行するために使用されます。入力パラメータでは、ユーザーが戦略の重要な要素を設定できるようになっています。これには、スイング検出の感度(SwingPeriod)、分析に必要となる最低限の構造的深さ(MinDepth)、そして価格がターゲットに到達したと認識するための許容範囲(TargetTolerance)などが含まれます。
追加の入力項目では、可視化、ポジションサイズ、リスク管理を制御します。これにより、EAは固定ロットサイズを使用することも、指定されたリスク比率に基づいて取引サイズを動的に計算することも可能になります。ストップロス、テイクプロフィット、マジックナンバーに関するパラメータによって、取引は適切に管理され、取引口座内で識別可能な状態に保たれます。
また、EAを継続的に稼働させる際の安定性と効率性を維持するために、運用制御およびメモリ管理の設定も導入します。たとえば、EnableDailyResetは、各取引日の開始時にシステムが構造ノードを消去し、再構築できるようにします。一方、MaxNodesToKeepおよびEnableMemoryOptimizationは、アルゴリズムの処理速度を低下させる可能性がある過剰なデータの蓄積を防ぎます。
最後に、SwingNode構造体は、市場構造をモデル化するために使用される中心的なデータ単位を定義します。各ノードは、検出された1つのスイングハイまたはスイングローを表します。各ノードには、スイングが形成された時間、価格レベルとその種類(高値または安値)、固有のインデックス、DFSの探索処理で使用される訪問済みフラグが格納されます。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ SwingNode nodes[]; int nodeCount = 0; datetime lastBarTime = 0; datetime lastDayDate = 0; // Track last day for reset //--- For DFS state int currentPath[]; int pathDepth = 0; int currentDirection = 0; // 1 bullish, -1 bearish, 0 none double lastHigherLow = 0; double lastLowerHigh = 0; //--- Trade flags bool inTrade = false; ulong tradeTicket = 0; MqlTick currentTick; //--- Rate arrays for timeseries data double high[]; double low[]; datetime time[]; int barsCount = 0; //--- Diagnostic counters int tickCount = 0; datetime lastCleanupTime = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { Print("=========================================="); Print("SwingGraphTrader initialized"); Print("SwingPeriod = ", SwingPeriod); Print("MinDepth = ", MinDepth); Print("TargetTolerance = ", TargetTolerance); Print("EnableDailyReset = ", EnableDailyReset); Print("MaxNodesToKeep = ", MaxNodesToKeep); Print("=========================================="); //--- Initialize arrays with optimal size ArrayResize(nodes, 0, MaxNodesToKeep > 0 ? MaxNodesToKeep + 50 : 1000); nodeCount = 0; //--- Get initial time datetime currentTime[]; if(CopyTime(_Symbol, _Period, 0, 1, currentTime) <= 0) { Print("Failed to copy initial time. Error: ", GetLastError()); return INIT_FAILED; } lastBarTime = currentTime[0]; //--- Set initial day MqlDateTime dt; TimeToStruct(lastBarTime, dt); dt.hour = 0; dt.min = 0; dt.sec = 0; lastDayDate = StructToTime(dt); Print("Initial lastBarTime = ", TimeToString(lastBarTime)); Print("Initial lastDayDate = ", TimeToString(lastDayDate)); TradeManager.SetExpertMagicNumber(MagicNumber); //--- Pre-allocate arrays for better performance ArrayResize(high, 0, 10000); ArrayResize(low, 0, 10000); ArrayResize(time, 0, 10000); return(INIT_SUCCEEDED); }
次に、EAが稼働している間、その内部状態を維持する一連のグローバル変数を宣言します。nodes[]配列には、検出されたスイングポイントの集合が格納され、それぞれがSwingNode構造体によって表現されます。一方、nodeCountは現在存在するノードの数を追跡します。変数lastBarTimeとlastDayDateは、EAが新しいローソク足の形成を検出し、日次リセットがいつ発生すべきかを判断するために使用されます。「DFS state」とラベル付けされたセクションには、市場構造の分析に使用されるDFSのトラバーサルロジックをサポートする変数が含まれています。
たとえば、currentPath[]はアクティブな構造的経路を形成するスイングノードのシーケンスを記録し、pathDepthはトラバーサルがどの程度深く進行したかを追跡します。また、currentDirectionは、システムが現在強気経路または弱気経路のどちらを追跡しているかを示します。lastHigherLowやlastLowerHighなどの追加変数は、価格が変化する中で構造的な検証を監視するために使用されます。また、EAが現在ポジションを保有しているかどうかを追跡するために、inTradeやtradeTicketのような取引関連のフラグも含めます。さらに、スイング検出および構造分析のために、過去のローソク足データを格納するために使用されるレート配列(high[]、low[]、time[])も含まれています。
OnInit()関数は、EAが初めてチャートに接続されたとき、または再起動されたときにEAを初期化する役割を担います。この関数は、まずログに診断情報を出力することから開始します。これにより、ユーザーはSwingPeriod、MinDepth、ノード管理設定などの戦略パラメータが正しく読み込まれていることを確認できます。その後、この関数はnodes配列のサイズを変更し、ノードカウンタをリセットすることで、主要なデータ構造を準備します。これにより、システムはクリーンな構造グラフの状態から開始します。また、CopyTime()を使用して現在のバー時間を取得し、新しいローソク足を検出するための初期基準を設定します。そして、オプションの日次リセット機能に使用される開始日次タイムスタンプも計算します。
TradeManagerを通じてEAのマジックナンバーを設定した後、この関数は価格配列(high、low、time)用のメモリを事前に確保します。これは、大量の過去データを処理する際の実行時パフォーマンスを向上させるためです。すべての初期化タスクが正常に完了すると、関数はINIT_SUCCEEDEDを返します。これは、EAが市場データの分析を開始し、取引ロジックを実行する準備が整ったことを示します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(EnableVisual) ObjectsDeleteAll(0, "SwingGraph_"); //--- Free memory ArrayFree(nodes); ArrayFree(high); ArrayFree(low); ArrayFree(time); ArrayFree(currentPath); Print("SwingGraphTrader deinitialized. Reason: ", reason); } //+------------------------------------------------------------------+ //| Reset all data at day change | //+------------------------------------------------------------------+ void CheckAndResetDaily() { if(!EnableDailyReset) return; //--- Get current day MqlDateTime currentDt; TimeToStruct(TimeCurrent(), currentDt); currentDt.hour = 0; currentDt.min = 0; currentDt.sec = 0; datetime currentDay = StructToTime(currentDt); //--- Check if day changed if(currentDay > lastDayDate) { Print("Day changed from ", TimeToString(lastDayDate), " to ", TimeToString(currentDay)); Print("Resetting all nodes and state for new day"); //--- Reset all nodes ArrayResize(nodes, 0, MaxNodesToKeep > 0 ? MaxNodesToKeep + 50 : 1000); nodeCount = 0; //--- Reset DFS state currentDirection = 0; pathDepth = 0; ArrayResize(currentPath, 0); lastHigherLow = 0; lastLowerHigh = 0; //--- Clear all drawings if(EnableVisual) ObjectsDeleteAll(0, "SwingGraph_"); //--- Free timeseries arrays to release memory if(EnableMemoryOptimization) { ArrayFree(high); ArrayFree(low); ArrayFree(time); } lastDayDate = currentDay; Print("Daily reset complete. Memory freed."); } } //+------------------------------------------------------------------+ //| Optimize memory usage | //+------------------------------------------------------------------+ void OptimizeMemory() { if(!EnableMemoryOptimization) return; //--- Periodically clean up old nodes if we exceed max nodes if(MaxNodesToKeep > 0 && nodeCount > MaxNodesToKeep) { Print("Node count (", nodeCount, ") exceeds MaxNodesToKeep (", MaxNodesToKeep, "). Cleaning up old nodes."); int nodesToRemove = nodeCount - MaxNodesToKeep; //--- Shift remaining nodes to beginning of array for(int i = 0; i < MaxNodesToKeep; i++) { nodes[i] = nodes[i + nodesToRemove]; nodes[i].index = i; // Update indices } nodeCount = MaxNodesToKeep; ArrayResize(nodes, nodeCount, MaxNodesToKeep + 50); //--- Reset path if it contains removed nodes bool pathNeedsReset = false; for(int i = 0; i < pathDepth; i++) { if(currentPath[i] < nodesToRemove) { pathNeedsReset = true; break; } //--- Adjust indices currentPath[i] -= nodesToRemove; } if(pathNeedsReset) { currentDirection = 0; pathDepth = 0; ArrayResize(currentPath, 0); Print("Path reset due to node cleanup"); } Print("Node cleanup complete. New node count: ", nodeCount); } //--- Periodically free timeseries arrays if they're too large if(ArraySize(high) > 10000) { ArrayResize(high, 0, 10000); ArrayResize(low, 0, 10000); ArrayResize(time, 0, 10000); Print("Timeseries arrays reset to save memory"); } }
このコードのセクションでは、EAの終了処理および日次リセットの動作を管理します。OnDeinit()関数は、EAがチャートから削除されたとき、プラットフォームが終了したとき、またはプログラムが再コンパイルされたときに実行されます。その目的は、EAによって描画されたビジュアルオブジェクトを削除し、nodes、high、low、time、currentPathなどの配列に割り当てられたメモリを解放することで、環境を安全にクリーンアップすることです。これにより、メモリリークを防ぎ、システムが安定した状態でプラットフォームを終了できるようにします。また、この関数は、理由コードとともにSwingGraphTraderが初期化解除されたことを示すメッセージを出力します。これは、デバッグおよび監視に役立ちます。
続いて、CheckAndResetDaily()関数は、EnableDailyResetが有効になっている場合に、取引システムが新しい取引日の開始時に新しい状態から開始できるようにします。この関数は、現在の日付と保存されているlastDayDateを比較し、新しい日が検出された場合、構造ノードをリセットし、DFSトラバーサルの状態をクリアし、チャート上の描画を削除します。また、必要に応じてタイムシリーズ配列を解放してメモリを解放します。
コードの2つ目の部分では、長時間稼働する処理中のメモリ最適化とシステムの安定性に焦点を当てています。OptimizeMemory()関数は、EAが時間の経過とともに過剰なデータを蓄積しないようにします。過剰なデータの蓄積は、パフォーマンスを低下させたり、不必要なメモリを消費したりする可能性があります。保存されているスイングノードの数がMaxNodesToKeepで定義された上限を超えた場合、この関数は最も古いノードを削除し、配列内の残りのノードを前方へ移動させながら、それらのインデックスを更新します。DFSトラバーサルの経路は削除されたノードを参照している可能性があるため、この関数は現在の経路が無効になっていないかを確認し、必要に応じてリセットします。これにより、アルゴリズムが古くなった構造情報を参照しないことが保証されます。
さらに、この関数は価格履歴配列(high、low、time)のサイズも監視し、それらが大きくなりすぎた場合にはサイズを変更します。これにより、効率的なメモリ使用が維持されます。これらの仕組みによって、EAは継続的に稼働しながら、内部データ構造をクリーンで効率的な状態に保ち、最新の市場情報と整合した状態を維持することができます。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check for daily reset CheckAndResetDaily(); //--- Periodically optimize memory (every 1000 ticks or 1 hour) tickCount++; if(tickCount % 1000 == 0 || TimeCurrent() - lastCleanupTime > 3600) { OptimizeMemory(); lastCleanupTime = TimeCurrent(); } if(inTrade) { if(!PositionSelectByTicket(tradeTicket)) { Print("Trade closed externally. Resetting state."); inTrade = false; tradeTicket = 0; currentDirection = 0; pathDepth = 0; ArrayResize(currentPath,0); } } //--- Get current tick data if(!SymbolInfoTick(_Symbol, currentTick)) { Print("Failed to get current tick data. Error: ", GetLastError()); return; } //--- Check new bar datetime currentTime[]; if(CopyTime(_Symbol, _Period, 0, 1, currentTime) <= 0) { Print("Failed to copy current time. Error: ", GetLastError()); return; } //--- Process only on new bar if(currentTime[0] == lastBarTime) return; lastBarTime = currentTime[0]; Print("New bar detected at ", TimeToString(lastBarTime)); //--- Update timeseries data - reuse arrays to minimize memory allocation barsCount = Bars(_Symbol, _Period); Print("Bars count: ", barsCount); if(barsCount < SwingPeriod * 2 + 5) { Print("Not enough bars: ", barsCount, " < ", SwingPeriod * 2 + 5); return; } //--- Set series as time series ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); ArraySetAsSeries(time,true); //--- Reuse existing arrays instead of creating new ones int copiedHigh = CopyHigh(_Symbol, _Period, 0, barsCount, high); int copiedLow = CopyLow(_Symbol, _Period, 0, barsCount, low); int copiedTime = CopyTime(_Symbol, _Period, 0, barsCount, time); Print("Copied high: ", copiedHigh, ", low: ", copiedLow, ", time: ", copiedTime); if(copiedHigh <= 0 || copiedLow <= 0 || copiedTime <= 0) { Print("Failed to copy price data. Errors: ", GetLastError()); return; } //--- Update swings int oldNodeCount = nodeCount; DetectSwing(); if(nodeCount > oldNodeCount) Print("Added ", nodeCount - oldNodeCount, " new nodes. Total nodes: ", nodeCount); //--- Detect invalidation of current path if(!inTrade && currentDirection != 0) { if(IsPathInvalidated()) { Print("Path invalidated. Backtracking."); BacktrackAndSwitch(); } } //--- Run DFS to find new path if not in trade if(!inTrade) { FindBestPath(); } //--- Check if we should enter a trade if(!inTrade && currentDirection != 0 && pathDepth >= MinDepth) { Print("Trade condition satisfied: direction=", currentDirection, " depth=", pathDepth); if(currentDirection == 1) ExecuteTrade(ORDER_TYPE_BUY, TradeComment + " Bullish"); else ExecuteTrade(ORDER_TYPE_SELL, TradeComment + " Bearish"); } //--- Visualization - only if enabled if(EnableVisual) { DrawSwings(); DrawActivePath(); } } //+------------------------------------------------------------------+ //| Swing Detection | //+------------------------------------------------------------------+ void DetectSwing() { int currentBar = SwingPeriod + 1; // use previous completed bar if(currentBar >= barsCount - SwingPeriod) { Print("currentBar (", currentBar, ") >= barsCount - SwingPeriod (", barsCount - SwingPeriod, ") - returning"); return; } if(IsSwingHigh(currentBar, SwingPeriod)) { Print("Swing high detected at bar ", currentBar, ", price: ", high[currentBar]); AddNode(time[currentBar], high[currentBar], 1); } else if(IsSwingLow(currentBar, SwingPeriod)) { Print("Swing low detected at bar ", currentBar, ", price: ", low[currentBar]); AddNode(time[currentBar], low[currentBar], -1); } } //+------------------------------------------------------------------+ //| IsSwingHigh | //+------------------------------------------------------------------+ bool IsSwingHigh(int bar, int period) { double barHigh = high[bar]; for(int i = bar - period; i <= bar + period; i++) { if(i < 0 || i >= barsCount) continue; if(high[i] > barHigh) return false; } return true; } //+------------------------------------------------------------------+ //| IsSwingLow | //+------------------------------------------------------------------+ bool IsSwingLow(int bar, int period) { double barLow = low[bar]; for(int i = bar - period; i <= bar + period; i++) { if(i < 0 || i >= barsCount) continue; if(low[i] < barLow) return false; } return true; }
このコードのセクションは、EAのメイン実行サイクルを表しています。新しいティックが到着するたびに取引ロジックが処理されます。OnTick()関数は、まず日次リセットが必要かどうかを確認し、長時間の稼働中にシステムの効率を維持するため、定期的にメモリ最適化を実行します。また、既存の取引が現在も有効であるかどうかを確認し、ポジションが外部で決済されていた場合には、内部状態をリセットします。その後、EAは最新のティックデータを取得し、新しいローソク足(バー)が形成された場合にのみ処理が実行されるようにします。これにより、ティックが到着するたびに冗長な計算が実行されることを防ぎます。新しいバーが検出されると、プログラムは過去価格配列(high、low、time)を更新し、スイング分析を実行するために十分な本数のバーが利用可能であることを確認します。
続いて、DetectSwing()関数は価格データを走査し、新しいスイングハイおよびスイングローを検出します。この際、ヘルパー関数であるIsSwingHigh()およびIsSwingLow()を使用して、対象となるバーが、定義されたSwingPeriodの範囲内にある周囲のローソク足と比較して、高値または安値となっているかどうかを確認します。新たな構造ノードが検出された場合、それらはノードグラフに追加されます。これにより、システムは市場構造の変化を表すグラフを継続的に更新できます。ノードを更新した後、EAは現在のDFS経路が無効化されていないかどうかを確認し、必要に応じてバックトラックを実行します。その後、DFSのロジックを実行し、最も有望な構造的経路を探索します。有効な方向性バイアスが確立され、構造的深さが最低要件を満たした場合、EAは検出された方向へ取引を実行します。また、オプションとして、可視化のためにスイングポイントおよびアクティブな経路をチャート上に描画します。
//+------------------------------------------------------------------+ //| AddNode | //+------------------------------------------------------------------+ void AddNode(datetime nodeTime, double price, int type) { //--- Avoid duplicate at same time if(nodeCount > 0 && nodes[nodeCount-1].time == nodeTime) { Print("Duplicate node at same time, skipping."); return; } //--- Check if we need to resize with extra capacity if(nodeCount >= ArraySize(nodes)) { int newSize = nodeCount + (MaxNodesToKeep > 0 ? 50 : 100); ArrayResize(nodes, newSize, MaxNodesToKeep > 0 ? MaxNodesToKeep + 50 : 1000); } nodes[nodeCount].time = nodeTime; nodes[nodeCount].price = price; nodes[nodeCount].type = type; nodes[nodeCount].index = nodeCount; nodes[nodeCount].visited = false; nodeCount++; Print("Node added: index=", nodeCount-1, ", time=", TimeToString(nodeTime), ", price=", price, ", type=", type); } //+------------------------------------------------------------------+ //| DFS Path Finding | //+------------------------------------------------------------------+ void FindBestPath() { if(nodeCount < 2) { Print("Not enough nodes for DFS: ", nodeCount); return; } //--- Reset visited flags for(int i = 0; i < nodeCount; i++) nodes[i].visited = false; int bestDepth = 0; int bestDirection = 0; int bestPath[]; //--- Try each node as starting point for(int start = 0; start < nodeCount; start++) { //--- Bullish from a low if(nodes[start].type == -1) { int path[]; int depth = DFS_Bullish(start, nodes[start].price, -1e9, path); if(depth > bestDepth) { bestDepth = depth; bestDirection = 1; ArrayResize(bestPath, ArraySize(path)); for(int i = 0; i < ArraySize(path); i++) bestPath[i] = path[i]; } } //--- Bearish from a high if(nodes[start].type == 1) { int path[]; int depth = DFS_Bearish(start, nodes[start].price, 1e9, path); if(depth > bestDepth) { bestDepth = depth; bestDirection = -1; ArrayResize(bestPath, ArraySize(path)); for(int i = 0; i < ArraySize(path); i++) bestPath[i] = path[i]; } } } Print("Best depth found: ", bestDepth, ", direction: ", bestDirection); //--- Set current path if depth meets minimum if(bestDepth >= MinDepth) { ArrayResize(currentPath, bestDepth); for(int i = 0; i < bestDepth; i++) currentPath[i] = bestPath[i]; pathDepth = bestDepth; currentDirection = bestDirection; UpdateKeyLevels(); Print("Path set: depth=", pathDepth, ", direction=", currentDirection, ", lastHigherLow=", lastHigherLow, ", lastLowerHigh=", lastLowerHigh); } if(bestDepth < MinDepth) { currentDirection = 0; pathDepth = 0; ArrayResize(currentPath,0); } } //+------------------------------------------------------------------+ //| Bullish DFS | //+------------------------------------------------------------------+ int DFS_Bullish(int nodeIdx, double lastLow, double lastHigh, int &path[]) { //--- nodeIdx is a low nodes[nodeIdx].visited = true; //--- Initialize path with current node ArrayResize(path, 1); path[0] = nodeIdx; int maxDepth = 1; //--- Look for a high after this low for(int i = nodeIdx+1; i < nodeCount; i++) { if(nodes[i].type == 1 && !nodes[i].visited) { if(lastHigh == -1e9 || nodes[i].price > lastHigh) { //--- Look for a subsequent low after this high for(int j = i+1; j < nodeCount; j++) { if(nodes[j].type == -1 && !nodes[j].visited) { if(nodes[j].price > lastLow) { int subPath[]; int subDepth = DFS_Bullish(j, nodes[j].price, nodes[i].price, subPath); if(subDepth + 2 > maxDepth) { maxDepth = subDepth + 2; ArrayResize(path, maxDepth); path[0] = nodeIdx; // current low path[1] = i; // intermediate high for(int k = 2; k < maxDepth; k++) path[k] = subPath[k-2]; // subsequent nodes } } } } } } } nodes[nodeIdx].visited = false; return maxDepth; }
AddNode()関数は、新たに検出されたスイングポイントを、システムの構造グラフ内のノードとして保存する役割を担います。新しいノードを追加する前に、この関数は、同じタイムスタンプを持つノードがすでに存在するかどうかを確認し、重複を回避します。ノード配列が現在の容量に達した場合、この関数は配列のサイズを動的に変更し、効率性を維持するための追加のバッファ容量を確保しながら、新たな領域を確保します。領域が確保されると、この関数は、ノードの時間、価格、種類(スイングハイまたはスイングロー)、インデックス位置、および訪問済み状態を記録します。これらの情報は、後にDFSアルゴリズムがトラバーサルを実行する際に使用されます。ノードが正常に保存されると、ノード総数がインクリメントされ、診断メッセージが出力されます。これにより、システムは市場構造のグラフが時間の経過とともにどのように成長していくかを追跡できます。
FindBestPath()関数は、保存されているすべてのスイングノードを、開始点となる可能性のあるノードとして評価し、強気の構造的経路と弱気の構造的経路の両方を構築しようとします。強気の経路では、アルゴリズムはスイングローから開始し、高値切り上げ(HH)および安値切り上げ(HL)のシーケンスを探索します。一方、弱気の経路では、スイングハイから開始し、安値切り下げ(LL)および高値切り下げ(LH)のシーケンスを探索します。この探索は、DFS_Bullish()関数によって再帰的に実行されます。この関数は、構造的条件が有効である限り、ノードグラフ内を前方へトラバースしながら、経路を構築します。
トラバーサル中、アルゴリズムは、最も深い有効な経路を追跡します。この経路は、市場における最も強い構造的な進行を表します。最適な経路が特定され、その深さが最小閾値を超えた場合、EAはcurrentPathを更新し、取引方向を設定するとともに、直近のHLまたはLHなどの重要な構造レベルを記録します。必要な構造的深さを満たす経路が存在しない場合、システムはアクティブな経路をクリアし、新しい市場構造が形成されるのを待機します。
//+------------------------------------------------------------------+ //| Bearish DFS | //+------------------------------------------------------------------+ int DFS_Bearish(int nodeIdx, double lastHigh, double lastLow, int &path[]) { //--- nodeIdx is a high nodes[nodeIdx].visited = true; //--- Initialize path with current node ArrayResize(path, 1); path[0] = nodeIdx; int maxDepth = 1; for(int i = nodeIdx+1; i < nodeCount; i++) { if(nodes[i].type == -1 && !nodes[i].visited) { if(lastLow == 1e9 || nodes[i].price < lastLow) { for(int j = i+1; j < nodeCount; j++) { if(nodes[j].type == 1 && !nodes[j].visited) { if(nodes[j].price < lastHigh) { int subPath[]; int subDepth = DFS_Bearish(j, nodes[j].price, nodes[i].price, subPath); if(subDepth + 2 > maxDepth) { maxDepth = subDepth + 2; ArrayResize(path, maxDepth); path[0] = nodeIdx; // current high path[1] = i; // intermediate low for(int k = 2; k < maxDepth; k++) path[k] = subPath[k-2]; // subsequent nodes } } } } } } } nodes[nodeIdx].visited = false; return maxDepth; } //+------------------------------------------------------------------+ //| Invalidation and Backtracking | //+------------------------------------------------------------------+ bool IsPathInvalidated() { if(pathDepth == 0) return false; double price = currentTick.bid; Print("Checking invalidation: price=", price, ", lastHigherLow=", lastHigherLow, ", lastLowerHigh=", lastLowerHigh); if(currentDirection == 1) // bullish { if(price < lastHigherLow - (TargetTolerance * Point())) { Print("Bullish path invalidated: price below lastHigherLow"); return true; } } else if(currentDirection == -1) // bearish { if(price > lastLowerHigh + (TargetTolerance * Point())) { Print("Bearish path invalidated: price above lastLowerHigh"); return true; } } return false; } //+------------------------------------------------------------------+ //| BacktrackAndSwitch | //+------------------------------------------------------------------+ void BacktrackAndSwitch() { currentDirection = 0; pathDepth = 0; ArrayResize(currentPath, 0); lastHigherLow = 0; lastLowerHigh = 0; Print("Backtrack: reset all path state."); } //+------------------------------------------------------------------+ //| UpdateKeyLvls | //+------------------------------------------------------------------+ void UpdateKeyLevels() { if(pathDepth == 0) return; if(currentDirection == 1) { for(int i = pathDepth-1; i >= 0; i--) { if(nodes[currentPath[i]].type == -1) { lastHigherLow = nodes[currentPath[i]].price; break; } } } else if(currentDirection == -1) { for(int i = pathDepth-1; i >= 0; i--) { if(nodes[currentPath[i]].type == 1) { lastLowerHigh = nodes[currentPath[i]].price; break; } } } }
DFS_Bearish()関数は、ノードグラフ内で弱気の市場構造を特定するためのDFSのトラバーサルを実装します。アルゴリズムはスイングハイノードから開始し、その後続のノードを探索して、安値切り下げ(LL)および高値切り下げ(LH)から構成される有効な弱気シーケンスを構築します。この関数は、再帰処理中に同じノードを再び訪問することを防ぐため、現在のノードを訪問済みとしてマークします。続いて、開始ノードを用いて経路を初期化し、その後、ノードリストを前方へ走査します。前回の安値よりも低い有効なスイングローが見つかった場合、アルゴリズムは次に、前回の高値よりも低いスイングハイを探索し、弱気構造のルールを維持します。
このようなシーケンスが見つかった場合、この関数は再帰的に自身を呼び出し、構造のより深い分岐の探索を続けます。この過程において、アルゴリズムは最も深い有効な経路を追跡し、より長い弱気シーケンスが見つかるたびに経路配列を更新します。分岐の探索が完了すると、このノードの訪問済みマークを解除し、他のトラバーサル経路でもそのノードを評価できるようにします。そして、この関数は検出された最大深さを返します。
コードの2つ目の部分では、経路の検証、構造の無効化、およびバックトラッキングを管理します。これらは、DFSのロジックを取引環境へ適用するうえで重要な構成要素です。IsPathInvalidated()関数は、現在の構造的経路が依然として有効であるかどうかを確認します。そのために、現在の市場価格を、強気経路では直近のHL、弱気経路では直近のLHなどの重要な構造レベルと比較します。価格が、定義された許容幅を加味してこれらのレベルを突破した場合、アルゴリズムは現在の構造的経路を無効であると判断します。
このような場合、BacktrackAndSwitch()関数は、アクティブな経路をクリアし、方向性バイアスを解除し、主要な構造レベルをリセットすることで、DFSの状態をリセットします。これにより、システムは新しい構造的経路の探索を開始できるようになります。UpdateKeyLevels()関数は、この処理を補完する役割を果たします。この関数は、アクティブな経路を走査し、現在のトレンド方向に関連する最新の構造レベルを特定します。これらのレベルは、現在進行している市場の動きがアクティブなDFS経路を引き続き支持しているか、それともアルゴリズムがバックトラックして別の市場方向を探索する必要があるかを判断するための基準点として機能します。
//+------------------------------------------------------------------+ //| Target Identification | //+------------------------------------------------------------------+ bool IsTargetReached(SwingNode &node) { double targetPrice = 0; int barsToCheck = 20; if(barsCount < barsToCheck) { Print("Not enough bars for target check: ", barsCount, " < ", barsToCheck); return false; } if(currentDirection == 1) // bullish target is above { double highArray[]; int copied = CopyHigh(_Symbol, _Period, 1, barsToCheck, highArray); if(copied <= 0) { Print("Failed to copy highs for target. Error: ", GetLastError()); return false; } int highestIdx = ArrayMaximum(highArray); if(highestIdx >= 0) targetPrice = highArray[highestIdx]; } else // bearish { double lowArray[]; int copied = CopyLow(_Symbol, _Period, 1, barsToCheck, lowArray); if(copied <= 0) { Print("Failed to copy lows for target. Error: ", GetLastError()); return false; } int lowestIdx = ArrayMinimum(lowArray); if(lowestIdx >= 0) targetPrice = lowArray[lowestIdx]; } double distance = MathAbs(node.price - targetPrice); Print("Target check: node.price=", node.price, ", targetPrice=", targetPrice, ", distance=", distance, ", tolerance=", TargetTolerance); if(distance < TargetTolerance) return true; return false; } //+------------------------------------------------------------------+ //| Trade Execution | //+------------------------------------------------------------------+ void ExecuteTrade(ENUM_ORDER_TYPE orderType, string comment) { Print("Attempting to execute trade: ", (orderType==ORDER_TYPE_BUY?"BUY":"SELL"), ", comment: ", comment); //--- Calculate position size based on risk double lotSize = 0; if(UseRiskManagement) { lotSize = CalculateLotSize(orderType, StopLossPoints); if(lotSize <= 0) { Print("Failed to calculate position size, using fixed lot size: ", FixedLotSize); lotSize = FixedLotSize; } } else { lotSize = FixedLotSize; } if(lotSize <= 0) { Print("Invalid lot size. Trade not executed."); return; } Print("Lot size: ", lotSize); //--- Get current tick data if(!SymbolInfoTick(_Symbol, currentTick)) { Print("Failed to get current tick data. Error: ", GetLastError()); return; } //--- Calculate stop loss and take profit prices double stopLoss = 0.0, takeProfit = 0.0; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); if(orderType == ORDER_TYPE_BUY) { stopLoss = NormalizeDouble(currentTick.bid - (StopLossPoints * point), digits); takeProfit = NormalizeDouble(currentTick.ask + (TakeProfitPoints * point), digits); } else if(orderType == ORDER_TYPE_SELL) { stopLoss = NormalizeDouble(currentTick.ask + (StopLossPoints * point), digits); takeProfit = NormalizeDouble(currentTick.bid - (TakeProfitPoints * point), digits); } Print("SL: ", stopLoss, ", TP: ", takeProfit); //--- Validate stop levels before sending the trade if(!ValidateStopLevels(orderType, currentTick.ask, currentTick.bid, stopLoss, takeProfit)) { Print("Invalid stop levels. Trade not executed."); return; } if(PositionSelect(_Symbol)) return; //--- Execute trade bool requestSent = false; if(orderType == ORDER_TYPE_BUY) { requestSent = TradeManager.Buy(lotSize, _Symbol, 0, stopLoss, takeProfit, comment); } else if(orderType == ORDER_TYPE_SELL) { requestSent = TradeManager.Sell(lotSize, _Symbol, 0, stopLoss, takeProfit, comment); } //--- Check if the request was sent successfully if(requestSent) { //--- Check the server's return code from the trade operation uint result = TradeManager.ResultRetcode(); Print("Trade request sent. Retcode: ", result, " - ", TradeManager.ResultRetcodeDescription()); if(result == TRADE_RETCODE_DONE || result == TRADE_RETCODE_DONE_PARTIAL) { Print("Trade executed successfully. Ticket: ", TradeManager.ResultOrder()); inTrade = true; tradeTicket = TradeManager.ResultOrder(); } else if(result == TRADE_RETCODE_REQUOTE || result == TRADE_RETCODE_TIMEOUT || result == TRADE_RETCODE_PRICE_CHANGED) { Print("Trade failed due to price change. Retcode: ", TradeManager.ResultRetcodeDescription()); } else { Print("Trade execution failed. Retcode: ", TradeManager.ResultRetcodeDescription()); } } else { Print("Failed to send trade request. Last Error: ", GetLastError()); } }
IsTargetReached()関数は、構造ノードが、最近の価格の極値に基づく潜在的な市場ターゲットに到達したかどうかを判定する役割を担います。まず、この関数は、分析を実行するために十分な本数のバーが存在することを確認します。その後、現在の市場方向に応じてターゲットレベルを決定します。強気シナリオでは、アルゴリズムは直近の一定本数のローソク足から最高値を取得します。一方、弱気シナリオでは、最安値を取得します。これらの値は、価格が到達を試みる可能性のある近傍の流動性ターゲットまたはエクスパンションターゲットを表します。ターゲットレベルを決定した後、この関数は、ノードの価格と検出されたターゲットとの距離を計算します。この距離が定義されたTargetToleranceの範囲内に収まる場合、この関数はtrueを返し、その構造的な値動きが実質的にターゲット領域へ到達したことを示します。
ExecuteTrade()関数は、DFSによる構造条件が有効な取引機会を示した時点で、取引実行の一連の処理を担当します。まず、この関数は適切なポジションサイズを決定します。リスク管理が有効な場合はリスクベースの計算によってポジションサイズを算出し、リスク管理が無効な場合は固定ロットサイズを使用します。最新の市場ティックデータを取得した後、この関数は、設定されたポイント距離に基づいてストップロスおよびテイクプロフィットの水準を計算し、銘柄の価格精度に合わせてそれらを正規化します。
注文を送信する前に、システムはストップレベルがブローカーの制約に適合していることを確認し、さらに、その銘柄に対して現在ポジションが保有されていないことも確認します。すべての条件が満たされた場合、この関数はCTradeクラスを通じて買い注文または売り注文を送信し、その後、サーバーから返されたレスポンスコードを評価します。取引が正常に実行された場合、EAはチケット番号を記録し、取引状態を更新します。これにより、システムはアクティブなポジションを追跡および管理できるようになります。
//+------------------------------------------------------------------+ //| Visualization | //+------------------------------------------------------------------+ void DrawSwings() { //--- Don't draw if disabled if(!EnableVisual) return; string prefix = "SwingGraph_"; ObjectsDeleteAll(0, prefix); for(int i = 0; i < nodeCount; i++) { string name = prefix + "Node_" + IntegerToString(i); color clr = (nodes[i].type == 1) ? clrBlue : clrRed; if(!ObjectCreate(0, name, OBJ_ARROW, 0, nodes[i].time, nodes[i].price)) Print("Failed to create object ", name, ". Error: ", GetLastError()); else { ObjectSetInteger(0, name, OBJPROP_ARROWCODE, (nodes[i].type == 1) ? 241 : 242); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_WIDTH, 2); } } //--- Draw edges connecting consecutive nodes for(int i = 1; i < nodeCount; i++) { string name = prefix + "Edge_" + IntegerToString(i-1) + "_" + IntegerToString(i); if(!ObjectCreate(0, name, OBJ_TREND, 0, nodes[i-1].time, nodes[i-1].price, nodes[i].time, nodes[i].price)) Print("Failed to create edge ", name, ". Error: ", GetLastError()); else { ObjectSetInteger(0, name, OBJPROP_COLOR, clrGray); ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); ObjectSetInteger(0, name, OBJPROP_RAY, false); } } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void DrawActivePath() { //--- Don't draw if disabled if(!EnableVisual) return; if(pathDepth < 2) return; string prefix = "SwingGraph_Path_"; for(int i = 1; i < pathDepth; i++) { string name = prefix + IntegerToString(i-1) + "_" + IntegerToString(i); int from = currentPath[i-1]; int to = currentPath[i]; if(from >= 0 && from < nodeCount && to >= 0 && to < nodeCount) { if(!ObjectCreate(0, name, OBJ_TREND, 0, nodes[from].time, nodes[from].price, nodes[to].time, nodes[to].price)) Print("Failed to create path segment ", name, ". Error: ", GetLastError()); else { ObjectSetInteger(0, name, OBJPROP_COLOR, (currentDirection == 1) ? clrGreen : clrOrange); ObjectSetInteger(0, name, OBJPROP_WIDTH, 3); ObjectSetInteger(0, name, OBJPROP_RAY, false); } } } } //+------------------------------------------------------------------+
DrawSwings()関数およびDrawActivePath()関数は、グラフ構造およびアクティブなDFS経路を取引チャート上に直接可視化する役割を担います。DrawSwings()関数は、まず重複を防ぐために、「SwingGraph_」接頭辞を持つ既存の描画オブジェクトを削除します。その後、保存されているすべてのスイングノードを走査し、それぞれの時間および価格座標に矢印オブジェクトとして描画します。スイングハイは青色の矢印で表示され、スイングローは赤色の矢印で表示されるため、構造上の転換点を容易に識別できます。さらに、この関数は連続するノード間に灰色のトレンドラインエッジを描画し、検出された市場構造の視覚的なグラフを形成します。
DrawActivePath()関数は、アルゴリズムが現在追跡している、DFSによって検出された構造的経路を強調表示します。可視化が有効で、かつ経路に少なくとも2つのノードが含まれている場合、この関数はcurrentPath配列を走査し、経路内の各ノード対の間に太いトレンドラインを描画します。これらの経路セグメントは、強気構造では緑色、弱気構造ではオレンジ色で表示されます。これにより、トレーダーは、アルゴリズムが現在最も可能性が高い市場の進行として判断している方向性シーケンスを明確に確認できます。これらの可視化機能によって、基盤となるグラフベースの分析は直感的なチャート表現へと変換され、ユーザーはDFSアルゴリズムが市場構造をリアルタイムでどのように解釈しているかを理解しやすくなります。
バックテスト結果
本システムの主な目的は、主観的な値動きの構造を、決定論的かつルールベースのスイング探索へ変換することでした。この探索では、有効な構造的経路が十分な深さに到達し、方向性バイアスを確認した場合にのみ取引が実行されます。バックテストは、XAUUSDペアの15分足(M15)を対象として、2025年12月1日から2026年1月30日までの2か月間にわたって実施しました。使用した設定はデフォルト設定です。


結果は、この目的がおおむね達成されたことを示しています。これは、DFSアプローチが、エントリー前に確認済みの構造シーケンスを要求することによって、取引を効果的にフィルタリングしていることを示唆しています。
結論
本記事では、グラフ理論におけるDFSの概念を取引へ適用しました。そのために、市場構造を相互に接続されたスイングポイントから成るグラフベースのモデルへ変換し、各スイングハイおよびスイングローをノードとし、それらの関係をエッジとして表現しました。このフレームワークの中で、アルゴリズムは、高値切り上げと安値切り上げのシーケンスなどの1つの構造的分岐を深く探索した後、別の方向性を検討します。これを実現するために、スイング検出、SwingNode構造体によるノードの保存、強気経路および弱気経路に対するDFSトラバーサル、ならびにMinDepth基準を用いた構造的深さに基づく経路選択を備えた、実際に動作するMQL5 EAを実装しました。また、本システムは、設定可能な許容範囲(TargetTolerance)とともに主要な無効化レベル(lastHigherLowおよびlastLowerHigh)を抽出し、定義されたストップロスおよびテイクプロフィット、ならびにオプションのリスクベースのポジションサイズ計算を用いて取引を実行します。さらに、生成されたノードおよびアクティブな構造的経路をチャート上に直接可視化します。
結論として、DFSを取引へ統合することにより、生の値動きは、プログラムによって分析および検証可能な、構造化された探索空間へと変換されます。このフレームワークを用いることで、市場構造をグラフとしてモデル化し、スイングレベル間の潜在的な値動き経路を追跡し、方向性バイアスがいつ確認されたか、あるいは無効化されたかを客観的に判断できます。その結果として得られるEAテンプレートは、再現可能かつ検証可能なワークフローを提供します。これにより、システムをコンパイルし、ストラテジーテスターで実行するとともに、SwingPeriod、MinDepth、およびリスク設定などのパラメータを変更しながら、pathDepth、currentDirection、および構造レベルがどのように変化するかを観察できます。スイングロジックを決定論的なルールとして形式化することにより、このアプローチは主観的なチャート分析を検証可能な探索問題へと変換し、体系的な実験、取引ロジックの改善、そしてより高度なアルゴリズム取引戦略の開発を可能にします。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21590
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5経済指標カレンダーを用いたニュースフィルタリング(第2回):ニュースリリース中に管理ポジションを停止する
MQL5における切断ニュートン共役勾配(TNC)アルゴリズムの実装
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
MQL5取引ツール(第23回):カメラ制御対応DirectX 3Dグラフによる二項分布分析
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
グラフというよりは、時系列順に並べられた配列の中から2種類の極値を探すだけの処理のように見えます。ネストしたループで検査するジグザグを彷彿とさせます。
将来の経路の確率を計算する際、履歴を一般化し、最も確率の高い変動シーケンス間のエッジに対して統計的に有意な重みを割り当てる方が、よりグラフ指向的なアプローチとなるでしょう。