Graph Theory: Traversal Depth-First Search (DFS) Applied in Trading
Table of contents:
Introduction
In the pursuit of trying to figure out how the market moves, in this part we apply the Depth-First Search (DFS) algorithm from graph theory to the structure of price action in trading. A major challenge when attempting to algorithmize price action is that concepts such as swing highs, swing lows, and market continuation are usually described qualitatively rather than with strict, testable rules. To address this, market structure is formalized as a graph, where each confirmed swing becomes a node and the transitions between swings become edges connecting those nodes. As price forms new candles and new swings are confirmed, this graph evolves dynamically, creating a structured representation of how the market progresses through time.
Within this framework, DFS allows the system to follow one structural branch deeply, such as a progression from a swing low to higher highs and higher lows, before considering alternative structural possibilities. The algorithm commits to this path as long as the structural conditions remain valid, measuring its strength through objective criteria such as path depth, key structural levels, and target tolerance. When the path breaks—such as when a critical swing level is invalidated—the algorithm deterministically backtracks to the previous node and explores other potential paths. By doing this, the sequence of price swings is treated as a traversable route through the market’s structure, transforming subjective price action analysis into a disciplined, codable process suitable for systematic and automated trading.
System Overview and Understanding
Concept mapping:| Graph Theory | Market Equivalent |
|---|---|
| Node | Swing high/Swing low |
| Edge | Break or pullback transition |
| Branch | Bullish or Bearish continuation |
| Goal node | Liquidity target |
| Depth | Structural continuation strength |
| Backtrack | Structure invalidation |
In graph theory, Depth-First Search (DFS) works by selecting a branch and exploring it as deeply as possible before considering alternatives. Applied to market structure, this means the algorithm follows a single structural progression of price through consecutive swing points, treating each confirmed swing high or swing low as a node in a graph. For example, price may begin at a confirmed swing low (Node A), move to a Swing High (Node B), retrace to form Node C, then break higher to Node D, and finally expand to Node E. In DFS terms, the system follows this sequence: A -> B -> C -> D -> E, as long as the structural conditions remain valid.
However, before a trade can be executed, the algorithm must obtain a minimum structural depth, meaning a sequence of confirmed swings that clearly demonstrates directional progression (e.g., Higher High -> Higher Low -> Break of Structure for bullish conditions, or Lower Low -> Lower High -> Continuation for bearish conditions). Only when this structural chain is detected and validated does the system consider establishing a directional bias.

In trading terms, “going deep” therefore means committing to a structural bias only after the market confirms it through measurable swing relationships. A bullish bias is confirmed when the DFS path identifies a valid sequence of higher highs and higher lows, with the most recent higher low acting as the key invalidation level.
Similarly, a bearish bias is confirmed when the path produces lower lows and lower highs, with the latest lower high serving as the structural boundary. Once this bias is confirmed and the path depth satisfies the minimum requirement, the system evaluates whether the path can reasonably reach a meaningful objective, such as a liquidity pool, supply or demand zone, or predefined risk-reward target. If these conditions are satisfied, a trade is executed in the direction of the active path. If the price breaks the structural invalidation level instead, the algorithm treats this as a path failure, performs DFS backtracking to the last valid node, and begins exploring the alternative market direction.
Getting Started
//+------------------------------------------------------------------+ //| DFS.mq5 | //| GIT under Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/en/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "GIT under Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/en/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 };
Getting started, we establish the foundation of the Expert Advisor by importing the trading functionality and defining the core configuration parameters that control how the system behaves. The #include <Trade/Trade.mqh> library provides access to MetaTrader’s trading functions, while the CTrade TradeManager object is used to execute buy and sell orders. The input parameters allow the user to configure important aspects of the strategy, such as the swing detection sensitivity (SwingPeriod), the minimum structural depth required for analysis (MinDepth), and the tolerance for recognizing when price has reached a target (TargetTolerance).
Additional inputs control visualization, position sizing, and risk management, enabling the EA to either use a fixed lot size or calculate trade size dynamically based on a specified risk percentage. Parameters for stop loss, take profit, and magic numbers ensure that trades are properly managed and identifiable within the trading account.
We also introduce operational controls and memory management settings that help maintain the stability and efficiency of the EA during continuous execution. For example, EnableDailyReset allows the system to clear and rebuild its structural nodes at the beginning of each trading day, while MaxNodesToKeep and EnableMemoryOptimization prevent the accumulation of excessive data that could slow down the algorithm.
Finally, the SwingNode structure defines the core data unit used to model market structure, where each node represents a detected swing high or swing low. Each node stores the time of the swing, the price level, its type (high or low), a unique index, and a visited flag used by the Depth-First Search (DFS) traversal process.
//+------------------------------------------------------------------+ //| 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); }
We then declare a set of global variables that maintain the internal state of the Expert Advisor while it is running. The nodes[] array stores the collection of detected swing points, each represented by the SwingNode structure, while nodeCount keeps track of how many nodes currently exist. The variables lastBarTime and lastDayDate help the EA detect the formation of new candles and determine when a daily reset should occur. The section labeled DFS state contains variables that support the Depth-First Search traversal logic used to analyze market structure.
For example, currentPath[] records the sequence of swing nodes forming the active structural path, pathDepth tracks how deep the traversal has progressed, and currentDirection indicates whether the system is currently following a bullish or bearish path. Additional variables such as lastHigherLow and lastLowerHigh help monitor structural validation as price evolves. We also include trade-related flags, like inTrade and tradeTicket, to track whether the EA currently holds a position, along with rate arrays (high[], low[], time[]) used to store historical candle data for swing detection and structural analysis.
The OnInit() function is responsible for initializing the EA when it is first attached to the chart or when it is restarted. It begins by printing diagnostic information to the log so the user can confirm that the strategy parameters—such as SwingPeriod, MinDepth, and node management settings—have been loaded correctly. The function then prepares the core data structures by resizing the nodes array and resetting the node counter, ensuring that the system starts with a clean structural graph. It retrieves the current bar time using CopyTime() to establish the initial reference for detecting new candles and also calculates the starting daily timestamp used for the optional daily reset feature.
After setting the expert’s magic number through the TradeManager, the function pre-allocates memory for the price arrays (high, low, and time) to improve runtime performance when processing large amounts of historical data. Once all initialization tasks are completed successfully, the function returns INIT_SUCCEEDED, indicating that the EA is ready to begin analyzing market data and executing its trading logic.
//+------------------------------------------------------------------+ //| 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"); } }
This section of the code manages the shutdown and daily reset behavior of the Expert Advisor. The OnDeinit() function runs when the EA is removed from the chart, the platform is closed, or the program is recompiled. Its purpose is to safely clean up the environment by removing any visual objects drawn by the EA and freeing memory allocated to arrays such as nodes, high, low, time, and currentPath. This prevents memory leaks and ensures that the system leaves the platform in a stable state. The function also prints a message indicating that the SwingGraphTrader has been deinitialized along with the reason code, which helps with debugging and monitoring.
Following this, the CheckAndResetDaily() function ensures that the trading system starts fresh each new trading day when EnableDailyReset is active. It compares the current date with the stored lastDayDate, and if a new day is detected, it resets the structural nodes, clears the DFS traversal state, removes chart drawings, and optionally frees time series arrays to release memory.
The second part of the code focuses on memory optimization and system stability during long-running operations. The OptimizeMemory() function ensures that the EA does not accumulate excessive data over time, which could slow down performance or consume unnecessary memory. If the number of stored swing nodes exceeds the limit defined by MaxNodesToKeep, the function removes the oldest nodes and shifts the remaining nodes forward in the array while updating their indices. Because the DFS traversal path may reference nodes that were removed, the function checks whether the current path has become invalid and resets it if necessary, ensuring the algorithm does not reference outdated structural information.
Additionally, the function monitors the size of the price history arrays (high, low, and time) and resizes them if they grow too large, helping maintain efficient memory usage. Together, these mechanisms allow the EA to run continuously while keeping its internal data structures clean, efficient, and aligned with the most recent market information.
//+------------------------------------------------------------------+ //| 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; }
This section of the code represents the main execution cycle of the Expert Advisor, where the trading logic is processed every time a new tick arrives. The OnTick() function first checks whether a daily reset is required and periodically runs memory optimization to keep the system efficient during long operation. It also verifies whether an existing trade is still active and resets the internal state if the position was closed externally. The EA then retrieves the latest tick data and ensures that processing only occurs when a new candle (bar) forms, which prevents redundant calculations on every tick. Once a new bar is detected, the program updates the historical price arrays (high, low, and time), ensuring there are enough bars available to perform swing analysis.
The DetectSwing() function then scans the price data to identify new swing highs and swing lows, using the helper functions IsSwingHigh() and IsSwingLow() to confirm whether a bar is higher or lower than surrounding candles within the defined SwingPeriod. If new structural nodes are detected, they are added to the node graph, allowing the system to maintain an evolving representation of market structure. After updating the nodes, the EA checks whether the current DFS path has been invalidated and backtracks if necessary, then runs the Depth-First Search logic to find the most promising structural path. When a valid directional bias is established and the structural depth meets the minimum requirement, the EA executes a trade in the detected direction, while optionally drawing swing points and the active path on the chart for visualization.
//+------------------------------------------------------------------+ //| 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; }
The AddNode() function is responsible for storing newly detected swing points as nodes in the system’s structural graph. Before adding a new node, the function checks whether a node with the same timestamp already exists to avoid duplicates. If the node array reaches its current capacity, it dynamically resizes the array to create additional space while maintaining extra buffer capacity for efficiency. Once space is available, the function records the node’s time, price, type (swing high or swing low), index position, and visited state, which will later be used by the DFS algorithm during traversal. After the node is successfully stored, the total node count is incremented and a diagnostic message is printed, allowing the system to track how the graph of market structure grows over time.
The FindBestPath() function evaluates all stored swing nodes as possible starting points and attempts to construct both bullish and bearish structural paths. For bullish paths, the algorithm begins from a swing low and looks for sequences of higher highs and higher lows, while bearish paths begin from swing highs and search for lower lows and lower highs. This exploration is performed recursively by the function DFS_Bullish(), which traverses forward through the node graph, building a path whenever structural conditions remain valid.
During traversal, the algorithm tracks the deepest valid path, representing the strongest structural progression in the market. Once the best path is identified and its depth exceeds the minimum threshold, the EA updates the currentPath, sets the trading direction, and records key structural levels such as the last higher low or lower high. If no path satisfies the required structural depth, the system clears the active path and waits for new market structure to form.
//+------------------------------------------------------------------+ //| 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; } } } }
The DFS_Bearish() function implements the Depth-First Search traversal for identifying bearish market structure within the node graph. Starting from a swing high node, the algorithm explores subsequent nodes to build a valid bearish sequence of lower lows and lower highs. The function marks the current node as visited to prevent revisiting the same node during recursion, initializes the path with the starting node, and then scans forward through the node list. When a valid swing low is found that is lower than the previous low, the algorithm then searches for a following swing high that is lower than the previous high, maintaining the bearish structural rule.
If such a sequence is found, the function recursively calls itself to continue exploring deeper branches of the structure. During this process, it keeps track of the deepest valid path, updating the path array whenever a longer bearish sequence is discovered. Once exploration for the branch is complete, the node is unmarked as visited so that other traversal paths can evaluate it, and the function returns the maximum depth found.
The second part of the code manages path validation, structural invalidation, and backtracking, which are critical components of applying DFS logic to a trading environment. The IsPathInvalidated() function checks whether the current structural path remains valid by comparing the current market price to key structural levels, such as the most recent higher low for bullish paths or lower high for bearish paths. If the price breaks beyond these levels within the defined tolerance, the algorithm considers the current structural path invalid.
When this occurs, the BacktrackAndSwitch() function resets the DFS state by clearing the active path, removing directional bias, and resetting key levels, allowing the system to search for a new structural path. The UpdateKeyLevels() function complements this process by scanning the active path to identify the most recent structural level relevant to the current trend direction. These levels serve as reference points for determining whether the ongoing market movement continues to support the active DFS path or requires the algorithm to backtrack and explore alternative market directions.
//+------------------------------------------------------------------+ //| 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()); } }
The IsTargetReached() function is responsible for identifying whether a structural node has reached a potential market target based on recent price extremes. First, the function ensures that there are enough bars available to perform the analysis, then determines the target level depending on the current market direction. For a bullish scenario, the algorithm retrieves the highest high from the most recent set of candles, while for a bearish scenario, it retrieves the lowest low. These values represent nearby liquidity or expansion targets that price may attempt to reach. After determining the target level, the function calculates the distance between the node’s price and the detected target. If this distance falls within the defined TargetTolerance, the function returns true, indicating that the structural move has effectively reached its target area.
The ExecuteTrade() function handles the complete trade execution process once the DFS structural conditions signal a valid opportunity. It begins by determining the appropriate position size, either through a risk-based calculation or by using a fixed lot size if risk management is disabled. After retrieving the latest market tick data, the function calculates the stop loss and take profit levels based on the configured point distances and normalizes them according to the symbol’s precision.
Before sending the order, the system validates the stop levels to ensure they comply with broker constraints and also checks that no position is currently open for the symbol. If all conditions are satisfied, the function sends a buy or sell request through the CTrade class and then evaluates the server’s response code. If the trade is successfully executed, the EA records the ticket number and updates the trading state, allowing the system to track and manage the active position.
//+------------------------------------------------------------------+ //| 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); } } } } //+------------------------------------------------------------------+
The DrawSwings() and DrawActivePath() functions are responsible for visualizing the graph structure and the active DFS path directly on the trading chart. The DrawSwings() function begins by clearing previously drawn objects with the "SwingGraph_" prefix to prevent duplication, then iterates through all stored swing nodes and plots them as arrow objects at their respective time and price coordinates. Swing highs are displayed using blue arrows, while swing lows are shown in red, making it easy to distinguish structural turning points. In addition, the function draws gray trendline edges between consecutive nodes, effectively forming a visual graph of the detected market structure.
The DrawActivePath() function highlights the current DFS-discovered structural path that the algorithm is following. If visualization is enabled and the path contains at least two nodes, the function iterates through the currentPath array and draws thicker trendlines between each pair of nodes in the path. These path segments are colored green for bullish structures and orange for bearish structures, allowing traders to clearly see which directional sequence the algorithm currently considers the most probable market progression. Together, these visualization tools transform the underlying graph-based analysis into an intuitive chart representation that helps users understand how the DFS algorithm is interpreting market structure in real time.
Back Test Results
The primary goal of the system was to convert subjective price-action structure into a deterministic, rule-based traversal of swings, where trades are executed only after a valid structural path reaches sufficient depth and confirms directional bias. The back-testing was conducted on the XAUUSD pair, on the M15 timeframe across a 2-month testing window (01 December 2025 to 30 January 2026), with the default settings:


The results indicate that this objective is largely achieved, which suggests that the DFS approach successfully filters trades by requiring confirmed structural sequences before entry.
Conclusion
In summary, we applied the Depth-First Search (DFS) concept from graph theory to trading by converting market structure into a graph-based model of interconnected swing points, where each swing high or swing low becomes a node and the relationships between them form edges. Within this framework, the algorithm explores potential price paths by moving deeply along one structural branch—such as a sequence of higher highs and higher lows—before considering alternative directions. To make this operational, we implemented a working MQL5 Expert Advisor that includes swing detection, node storage through the SwingNode structure, DFS traversal for bullish and bearish paths, and path selection based on structural depth using the MinDepth criterion. The system also extracts key invalidation levels (lastHigherLow and lastLowerHigh) with a configurable tolerance (TargetTolerance), executes trades with defined SL/TP and optional risk-based sizing, and visualizes the resulting nodes and active structural path directly on the chart.
In conclusion, integrating DFS into trading transforms raw price action into a structured and navigable search space that traders can analyze and test programmatically. With this framework, you can model market structure as a graph, trace potential movement paths between swing levels, and objectively determine when directional bias is confirmed or invalidated. The resulting EA template provides a reproducible and testable workflow, allowing you to compile the system, run it in the strategy tester, and experiment with parameters such as SwingPeriod, MinDepth, and risk settings while observing how pathDepth, currentDirection, and structural levels evolve. By formalizing swing logic into deterministic rules, this approach converts subjective chart interpretation into a verifiable search problem, enabling systematic experimentation, refinement of trading logic, and the development of more advanced algorithmic strategies.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Unified Validation Pipeline Against Backtest Overfitting
Engineering Trading Discipline into Code (Part 3): Enforcing Symbol-Level Trading Boundaries with a Whitelist System in MQL5
Low-Frequency Quantitative Strategies in Metatrader 5: (Part 1) Setting Up An OLAP-Friendly Data Store
MQL5 Trading Tools (Part 23): Camera-Controlled, DirectX-Enabled 3D Graphs for Distribution Insights
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
It doesn't look like a graph - just a lookup for 2 types of extremums in a chronologically assembled array. Reminds me a zigzag, inspected with nested loops.
It would be more graph-oriented to generalize on history and find statistically meaningful weights for edges between most probable swing sequences when calculate probablity of future routes.