preview
Graph Theory: Traversal Breadth-First Search (BFS) Applied in Trading

Graph Theory: Traversal Breadth-First Search (BFS) Applied in Trading

MetaTrader 5Examples |
174 0
Hlomohang John Borotho
Hlomohang John Borotho

Table of contents:

  1. Introduction
  2. System Overview and BFS Fundamentals
  3. Getting Started
  4. Backtest Results
  5. Conclusion 


Introduction

Financial markets often appear chaotic on the surface, yet prices consistently move from one area to another in ways that suggest an underlying logic rather than randomness. Traders and system developers are therefore driven by the pursuit of identifying the algorithm the market itself seems to follow when transitioning between levels of liquidity, structure, and imbalance. This pursuit is not about prediction in isolation, but about understanding how past price interactions influence future movement and how market structure evolves step by step through time.

Graph theory offers a powerful framework for moving closer to this goal by allowing price action to be modeled as a structured, rule-based system rather than a sequence of isolated candles. By applying Breadth-First Search (BFS) with level-order traversal, market structure can be explored progressively from recent swings back into historical context, respecting the directional flow of time. This approach transforms historical bars into a directed graph of decision points, enabling a systematic interpretation of bullish and bearish structure and bringing algorithmic trading closer to mirroring the market’s own pathfinding logic.


System Overview and BFS Fundamentals:

BFS Concept  Trading Interpretation 
Node. A swing point (high or low).
Edge (directed). Time progression (older -> newer).
Level. Distance in bars or days.
Root. Historical anchor (start point).
Traversal. Market structure evolution.
Goal. Determine bullish or bearish bias.

The BFS Market Structure EA will transform traditional technical analysis by applying graph theory concepts to market price action. The system begins by detecting significant swing points (highs and lows) from historical price data, treating each swing as a node in a directed graph where edges represent chronological progression. Using Breadth-First Search (BFS) traversal starting from a configurable historical anchor point, the algorithm processes market structure in level-order—analyzing nearest swings first before moving to older formations. Each swing node is classified as bullish or bearish based on comparative price relationships, with more recent structures weighted more heavily in the final bias calculation. This produces a continuous bias score ranging from -1 (bearish) to +1 (bullish), representing the structural evolution of the market rather than traditional indicator-based signals.

The trading decision layer filters potential trades through this structural bias, requiring the score to exceed configurable thresholds before allowing buy or sell positions. When enabled, the EA executes trades using professional order management via the CTrade class, with optional confirmation through recent swing breakouts. The system includes three distinct context modes—previous bars for scalping, previous days for swing trading, and yesterday-only for intraday bias—allowing adaptation to different trading styles. Real-time visualization displays the swing graph with color-coded nodes and edge connections, making the structural analysis transparent and providing immediate feedback on market conditions. This approach creates a systematic, rules-based methodology that focuses on understanding market structure evolution rather than predicting future price movements.



Getting Started

//+------------------------------------------------------------------+
//|                                                       BFS-EA.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"

#include <Trade\Trade.mqh>
#include <Arrays\List.mqh>

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
enum ENUM_HISTORY_MODE
{
   MODE_PREVIOUS_BARS,     // Previous Bars
   MODE_PREVIOUS_DAYS,     // Previous Days
   MODE_YESTERDAY_ONLY     // Yesterday Only
};

input ENUM_HISTORY_MODE   HistoryMode = MODE_PREVIOUS_DAYS;  // History Mode
input int                 BarsLookback = 300;                // Bars Lookback
input int                 DaysLookback = 5;                  // Days Lookback
input int                 SwingPeriod = 3;                   // Swing Period (lookback bars)
input double              BiasThreshold = 0.3;               // Bias Threshold
input bool                EnableTrading = true;              // Enable Trading
input double              LotSize = 0.1;                     // Lot Size
input int                 TakeProfit = 100;                  // Take Profit (points)
input int                 StopLoss = 50;                     // Stop Loss (points)
input bool                VisualizeStructure = true;         // Visualize Structure
input color               BullishColor = clrGreen;           // Bullish Color
input color               BearishColor = clrRed;             // Bearish Color

We start off with the input parameter block, which defines how the Expert Advisor perceives historical market context and transforms it into structured trading logic. The ENUM_HISTORY_MODE allows the user to control whether the algorithm evaluates a fixed number of past bars, multiple previous trading days, or strictly yesterday’s price action, directly influencing how the market graph is constructed. Parameters such as BarsLookback, DaysLookback, and SwingPeriod determine the depth and granularity of swing detection, while BiasThreshold sets the minimum structural confidence required before a bullish or bearish bias is considered valid.

The remaining inputs govern the execution and interpretability of our system. Trading controls such as EnableTrading, LotSize, TakeProfit, and StopLoss separate decision logic from risk management, ensuring that the structural analysis can function independently of execution rules. Visualization settings, including VisualizeStructure, BullishColor, and BearishColor, provide transparency by rendering the detected market structure directly on the chart, allowing traders to visually confirm how the algorithm classifies bullish and bearish swings based on the underlying graph traversal logic.

//+------------------------------------------------------------------+
//| Swing Node Structure                                             |
//+------------------------------------------------------------------+
struct SwingNode
{
   int               index;          // bar index
   double            price;          // high or low price
   bool              isHigh;         // true = swing high, false = swing low
   bool              bullish;        // inferred direction
   int               level;          // BFS level
   datetime          time;           // bar time
   
   // Default constructor
   SwingNode(): index(0), price(0.0), isHigh(false), bullish(false), level(-1), time(0) {}
   
   // Parameterized constructor
   SwingNode(int idx, double prc, bool high, bool bull, int lvl, datetime t):
      index(idx), price(prc), isHigh(high), bullish(bull), level(lvl), time(t) {}
};

//+------------------------------------------------------------------+
//| BFS Queue Item                                                   |
//+------------------------------------------------------------------+
struct BFSItem
{
   int               nodeIndex;      // index in nodes array
   int               level;          // BFS level
};

//+------------------------------------------------------------------+
//| Edge Structure for Graph                                         |
//+------------------------------------------------------------------+
struct Edge
{
   int from;      // Source node index
   int to;        // Destination node index
};

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
SwingNode           nodes[];         // Array of swing nodes
Edge                edges[];         // Array of edges
int                 edgeCount = 0;   // Number of edges
int                 nodeCount = 0;        // Number of nodes
double              currentBias = 0.0;    // Current market bias
bool                allowBuy = false;     // Allow buy trades
bool                allowSell = false;    // Allow sell trades
datetime            lastRecalcTime = 0;   // Last recalculation time
int                 lastBarCount = 0;     // Last bar count
CTrade              trade;                // Trading object for position management

This section of the code defines the core data structures that transform raw price action into a graph suitable for Breadth-First Search traversal. The SwingNode structure represents each significant market turning point, storing its bar index, price, time, and whether it is a swing high or low. Additional attributes such as "bullish" and "level" allow each node to carry directional meaning and BFS depth, making it possible to evaluate market structure progressively through time. By encapsulating both structural and temporal information, each swing becomes a meaningful node within a directed market graph rather than a standalone price event.

Supporting this representation, the BFSItem and Edge structures define how traversal and connectivity are handled within the graph. BFSItem enables level-order processing by tracking which node is visited at each BFS layer, while Edge explicitly models the directed relationship between older and newer swing points. The global variables then serve as the operational backbone of the EA, storing the constructed graph, maintaining traversal state, and translating structural bias into actionable trading permissions. Together, these components bridge graph theory and execution logic, allowing the EA to reason about market direction systematically before placing or filtering trades.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   // Validate inputs
   if(BarsLookback <= 0)
   {
      Print("Error: BarsLookback must be positive");
      return INIT_PARAMETERS_INCORRECT;
   }
   
   if(DaysLookback <= 0)
   {
      Print("Error: DaysLookback must be positive");
      return INIT_PARAMETERS_INCORRECT;
   }
   
   if(SwingPeriod <= 0)
   {
      Print("Error: SwingPeriod must be positive");
      return INIT_PARAMETERS_INCORRECT;
   }
   
   // Set timer for periodic updates (every 5 minutes)
   EventSetTimer(300);
   
   // Set trading parameters
   trade.SetExpertMagicNumber(12345);
   trade.SetDeviationInPoints(10);
   trade.SetTypeFilling(ORDER_FILLING_FOK);
   
   // Initial calculation
   CalculateBFSStructure();
   
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // Remove timer
   EventKillTimer();
   
   // Clean up visualization objects
   if(VisualizeStructure)
   {
      RemoveVisualization();
   }
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // Check for new bar
   if(IsNewBar())
   {
      CalculateBFSStructure();
      
      // Check for trading signals if enabled
      if(EnableTrading)
      {
         CheckTradingSignals();
      }
   }
   
   // Update visualization if enabled
   if(VisualizeStructure)
   {
      UpdateVisualization();
   }
}

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
{
   // Periodic recalculation (fallback)
   CalculateBFSStructure();
}

Here we define the lifecycle of the Expert Advisor, coordinating initialization, execution, and cleanup while ensuring the BFS-based market structure remains up to date. During initialization, input parameters are validated to prevent invalid configurations, trading settings are prepared, and an initial BFS structure calculation is performed, with a timer established to enforce periodic recalculation. The main execution logic recalculates structure only on new bars or timed intervals for efficiency, applies trading decisions when enabled, and continuously updates chart visualizations, while the deinitialization phase safely removes timers and graphical objects to maintain platform stability.

//+------------------------------------------------------------------+
//| Calculate BFS Market Structure                                   |
//+------------------------------------------------------------------+
void CalculateBFSStructure()
{
   // Reset values
   currentBias = 0.0;
   allowBuy = false;
   allowSell = false;
   
   // Step 1: Detect swing points
   if(!DetectSwingNodes())
   {
      Print("Failed to detect swing nodes");
      return;
   }
   
   // Step 2: Build directed graph (edges)
   BuildDirectedGraph();
   
   // Step 3: Select BFS root based on mode
   int rootIndex = SelectBFSRoot();
   if(rootIndex < 0)
   {
      Print("No valid root found");
      return;
   }
   
   // Step 4: Perform BFS traversal
   PerformBFSTraversal(rootIndex);
   
   // Step 5: Calculate market bias
   CalculateMarketBias();
   
   // Step 6: Update trading permissions
   UpdateTradingPermissions();
   
   // Update last calculation time
   lastRecalcTime = TimeCurrent();
}

//+------------------------------------------------------------------+
//| Detect Swing Nodes                                               |
//+------------------------------------------------------------------+
bool DetectSwingNodes()
{
   // Determine how many bars to analyze
   int barsToAnalyze = 0;
   datetime startTime = 0;
   
   switch(HistoryMode)
   {
      case MODE_PREVIOUS_BARS:
         barsToAnalyze = BarsLookback + SwingPeriod * 2;
         break;
         
      case MODE_PREVIOUS_DAYS:
         startTime = TimeCurrent() - (DaysLookback * 86400);
         barsToAnalyze = iBars(_Symbol, _Period);
         break;
         
      case MODE_YESTERDAY_ONLY:
         {MqlDateTime yesterday;
         TimeToStruct(TimeCurrent() - 86400, yesterday);
         yesterday.hour = 0;
         yesterday.min = 0;
         yesterday.sec = 0;
         startTime = StructToTime(yesterday);
         barsToAnalyze = iBars(_Symbol, _Period);
         break;}
   }
   
   // Get historical data
   int totalBars = MathMin(barsToAnalyze, iBars(_Symbol, _Period));
   if(totalBars < SwingPeriod * 2 + 10)
   {
      Print("Not enough bars for swing detection");
      return false;
   }
   
   // Prepare arrays for highs and lows
   double highs[], lows[];
   datetime times[];
   
   ArrayResize(highs, totalBars);
   ArrayResize(lows, totalBars);
   ArrayResize(times, totalBars);
   
   // Copy data
   if(CopyHigh(_Symbol, _Period, 0, totalBars, highs) != totalBars ||
      CopyLow(_Symbol, _Period, 0, totalBars, lows) != totalBars ||
      CopyTime(_Symbol, _Period, 0, totalBars, times) != totalBars)
   {
      Print("Failed to copy historical data");
      return false;
   }
   
   // Detect swing highs and lows
   ArrayResize(nodes, 0);
   nodeCount = 0;
   
   for(int i = SwingPeriod; i < totalBars - SwingPeriod; i++)
   {
      bool isSwingHigh = true;
      bool isSwingLow = true;
      
      // Check for swing high
      for(int j = 1; j <= SwingPeriod; j++)
      {
         if(highs[i] <= highs[i-j] || highs[i] <= highs[i+j])
         {
            isSwingHigh = false;
            break;
         }
      }
      
      // Check for swing low
      for(int j = 1; j <= SwingPeriod; j++)
      {
         if(lows[i] >= lows[i-j] || lows[i] >= lows[i+j])
         {
            isSwingLow = false;
            break;
         }
      }
      
      // Add swing node if found
      if(isSwingHigh || isSwingLow)
      {
         SwingNode node;
         node.index = i;
         node.time = times[i];
         node.isHigh = isSwingHigh;
         
         if(isSwingHigh)
            node.price = highs[i];
         else
            node.price = lows[i];
            
         node.bullish = false;  // Will be determined during BFS
         node.level = -1;        // Not assigned yet
         
         ArrayResize(nodes, nodeCount + 1);
         nodes[nodeCount] = node;
         nodeCount++;
      }
   }
   
   Print("Detected ", nodeCount, " swing nodes");
   return nodeCount > 0;
}

//+------------------------------------------------------------------+
//| Build Directed Graph                                             |
//+------------------------------------------------------------------+
void BuildDirectedGraph()
{
   // Clear existing edges
   ArrayResize(edges, 0);
   edgeCount = 0;
   
   // Create edges from older to newer nodes (time progression)
   for(int i = 0; i < nodeCount; i++)
   {
      // Find the next valid swing to connect to
      int nextIndex = -1;
      
      // Look for the next swing of alternating type
      for(int j = i + 1; j < nodeCount; j++)
      {
         // Check if swing types alternate (high-low-high-low pattern)
         if(nodes[j].isHigh != nodes[i].isHigh)
         {
            // Check if there are no other valid swings between i and j
            bool validConnection = true;
            
            // Skip checking intermediate nodes for simplicity
            // In a more sophisticated version, we could check for 
            // price action patterns between swings
            
            if(validConnection)
            {
               nextIndex = j;
               break;
            }
         }
      }
      
      // Add edge if found
      if(nextIndex > i)
      {
         Edge edge;
         edge.from = i;
         edge.to = nextIndex;
         
         ArrayResize(edges, edgeCount + 1);
         edges[edgeCount] = edge;
         edgeCount++;
      }
   }
   
   Print("Created ", edgeCount, " edges in the graph");
}

//+------------------------------------------------------------------+
//| Select BFS Root Based on Mode                                    |
//+------------------------------------------------------------------+
int SelectBFSRoot()
{
   int rootIndex = -1;
   datetime currentTime = TimeCurrent();
   
   switch(HistoryMode)
   {
      case MODE_PREVIOUS_BARS:
         // Find the oldest swing within BarsLookback
         for(int i = 0; i < nodeCount; i++)
         {
            if(nodes[i].index >= BarsLookback)
            {
               rootIndex = i;
               break;
            }
         }
         break;
         
      case MODE_PREVIOUS_DAYS:
      {
         datetime startTime = currentTime - (DaysLookback * 86400);
         // Find first swing after startTime
         for(int i = 0; i < nodeCount; i++)
         {
            if(nodes[i].time >= startTime)
            {
               rootIndex = i;
               break;
            }
         }
         break;
      }
         
      case MODE_YESTERDAY_ONLY:
      {
         MqlDateTime yesterday;
         TimeToStruct(currentTime - 86400, yesterday);
         yesterday.hour = 0;
         yesterday.min = 0;
         yesterday.sec = 0;
         datetime yesterdayStart = StructToTime(yesterday);
         
         datetime todayStart = yesterdayStart + 86400;
         
         // Find first swing of yesterday
         for(int i = 0; i < nodeCount; i++)
         {
            if(nodes[i].time >= yesterdayStart && nodes[i].time < todayStart)
            {
               rootIndex = i;
               break;
            }
         }
         break;
      }
   }
   
   // If no root found with mode criteria, use the oldest node
   if(rootIndex == -1 && nodeCount > 0)
   {
      rootIndex = 0;
   }
   
   return rootIndex;
}

This section orchestrates the complete market-structure evaluation pipeline by coordinating swing detection, graph construction, traversal, and bias computation. The CalculateBFSStructure function acts as the central controller, first resetting all directional and permission states before sequentially executing each analytical stage. By separating detection, graph building, traversal, and decision updates into clear steps, the EA ensures that each recalculation of market structure is consistent, deterministic, and easy to debug, while also preventing partial or stale results from influencing trading decisions.

The DetectSwingNodes function is responsible for converting raw historical price data into meaningful structural nodes. Based on the selected history mode, it dynamically determines how much historical data should be analyzed, whether that is a fixed number of bars, multiple days, or only yesterday’s session. It then scans price highs and lows using the defined swing period to identify valid turning points, ensuring that each detected swing is isolated from surrounding noise. These swing points become the foundational nodes of the graph, each carrying precise time, price, and structural identity.

Once swing nodes are established, BuildDirectedGraph transforms them into a directed market graph by linking older swings to newer ones in a time-consistent sequence. Edges are created only between alternating swing types, enforcing a realistic high–low progression that mirrors natural market structure. This directional connectivity ensures the graph respects temporal flow and avoids backward or cyclic relationships, making it suitable for level-order traversal and preventing repainting or logical contradictions in structure analysis.

The selectBFSRoot determines where traversal begins, anchoring the analysis within the user-defined historical context. Depending on the chosen mode, the root may represent the earliest relevant swing within a bar window, a multi-day range, or a single trading session. If no node satisfies the criteria, the algorithm safely defaults to the oldest available swing, guaranteeing continuity. This root selection is critical, as it defines the perspective from which BFS unfolds, directly influencing how past structure is layered, evaluated, and ultimately translated into a current market bias.

//+------------------------------------------------------------------+
//| Perform BFS Traversal                                            |
//+------------------------------------------------------------------+
void PerformBFSTraversal(int rootIndex)
{
   if(rootIndex < 0 || rootIndex >= nodeCount)
      return;
   
   // Reset node levels
   for(int i = 0; i < nodeCount; i++)
   {
      nodes[i].level = -1;
      nodes[i].bullish = false;
   }
   
   // Initialize queue for BFS
   BFSItem queue[];
   int queueSize = nodeCount;
   ArrayResize(queue, queueSize);
   
   // Enqueue root
   queue[0].nodeIndex = rootIndex;
   queue[0].level = 0;
   nodes[rootIndex].level = 0;
   int queueStart = 0;
   int queueEnd = 1;
   
   // BFS traversal
   while(queueStart < queueEnd)
   {
      // Dequeue
      BFSItem current = queue[queueStart];
      queueStart++;
      
      int currentIdx = current.nodeIndex;
      int currentLevel = current.level;
      
      // Classify node direction
      ClassifyNodeDirection(currentIdx);
      
      // Find and enqueue children (nodes reachable from current node)
      for(int e = 0; e < edgeCount; e++)
      {
         if(edges[e].from == currentIdx)
         {
            int childIdx = edges[e].to;
            if(nodes[childIdx].level == -1)  // Not visited yet
            {
               nodes[childIdx].level = currentLevel + 1;
               
               // Enqueue child
               if(queueEnd < queueSize)
               {
                  queue[queueEnd].nodeIndex = childIdx;
                  queue[queueEnd].level = currentLevel + 1;
                  queueEnd++;
               }
            }
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Classify Node Direction                                          |
//+------------------------------------------------------------------+
void ClassifyNodeDirection(int nodeIndex)
{
   if(nodeIndex <= 0)
   {
      // First node, classify based on recent price action
      double currentPrice = iClose(_Symbol, _Period, 0);
      if(nodes[nodeIndex].isHigh)
      {
         nodes[nodeIndex].bullish = (currentPrice > nodes[nodeIndex].price);
      }
      else
      {
         nodes[nodeIndex].bullish = (currentPrice < nodes[nodeIndex].price);
      }
      return;
   }
   
   // Find previous node of the same type
   int prevSameType = -1;
   for(int i = nodeIndex - 1; i >= 0; i--)
   {
      if(nodes[i].isHigh == nodes[nodeIndex].isHigh)
      {
         prevSameType = i;
         break;
      }
   }
   
   if(prevSameType >= 0)
   {
      // Compare with previous node of same type
      if(nodes[nodeIndex].isHigh)
      {
         // Swing high: higher high = bullish, lower high = bearish
         nodes[nodeIndex].bullish = (nodes[nodeIndex].price > nodes[prevSameType].price);
      }
      else
      {
         // Swing low: higher low = bullish, lower low = bearish
         nodes[nodeIndex].bullish = (nodes[nodeIndex].price > nodes[prevSameType].price);
      }
   }
   else
   {
      // No previous node of same type, use simple logic
      double currentPrice = iClose(_Symbol, _Period, 0);
      if(nodes[nodeIndex].isHigh)
      {
         nodes[nodeIndex].bullish = (currentPrice > nodes[nodeIndex].price);
      }
      else
      {
         nodes[nodeIndex].bullish = (currentPrice < nodes[nodeIndex].price);
      }
   }
}

//+------------------------------------------------------------------+
//| Calculate Market Bias                                            |
//+------------------------------------------------------------------+
void CalculateMarketBias()
{
   double totalWeight = 0.0;
   double weightedSum = 0.0;
   
   for(int i = 0; i < nodeCount; i++)
   {
      if(nodes[i].level >= 0)  // Only consider visited nodes
      {
         // Calculate weight based on level (higher weight for lower levels)
         double weight = 1.0 / (1.0 + nodes[i].level * 0.2);
         
         // Add to weighted sum
         if(nodes[i].bullish)
            weightedSum += weight;
         else
            weightedSum -= weight;
            
         totalWeight += weight;
      }
   }
   
   if(totalWeight > 0)
   {
      currentBias = weightedSum / totalWeight;
   }
   else
   {
      currentBias = 0.0;
   }
   
   Print("Market Bias: ", DoubleToString(currentBias, 3));
}

//+------------------------------------------------------------------+
//| Update Trading Permissions                                       |
//+------------------------------------------------------------------+
void UpdateTradingPermissions()
{
   allowBuy = (currentBias > BiasThreshold);
   allowSell = (currentBias < -BiasThreshold);
   
   Print("Trading Permissions - Buy: ", allowBuy, ", Sell: ", allowSell);
}

//+------------------------------------------------------------------+
//| Check Trading Signals                                            |
//+------------------------------------------------------------------+
void CheckTradingSignals()
{
   // Check for existing positions
   if(PositionsTotal() > 0)
   {
      // Manage existing positions
      ManagePositions();
      return;
   }
   
   // Check for new signals
   if(allowBuy)
   {
      // Buy signal logic
      double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double sl = askPrice - StopLoss * _Point;
      double tp = askPrice + TakeProfit * _Point;
      
      // Optional: Add confirmation from recent swing break
      if(CheckRecentSwingBreak(true))
      {
         ExecuteBuyOrder(askPrice, sl, tp);
      }
   }
   else if(allowSell)
   {
      // Sell signal logic
      double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double sl = bidPrice + StopLoss * _Point;
      double tp = bidPrice - TakeProfit * _Point;
      
      // Optional: Add confirmation from recent swing break
      if(CheckRecentSwingBreak(false))
      {
         ExecuteSellOrder(bidPrice, sl, tp);
      }
   }
}

//+------------------------------------------------------------------+
//| Check Recent Swing Break                                         |
//+------------------------------------------------------------------+
bool CheckRecentSwingBreak(bool forBuy)
{
   if(nodeCount < 2)
      return true;  // No recent swings, allow trade
   
   // Find the most recent swing
   int recentSwingIndex = -1;
   for(int i = nodeCount - 1; i >= 0; i--)
   {
      if(nodes[i].index < 50)  // Within recent 50 bars
      {
         recentSwingIndex = i;
         break;
      }
   }
   
   if(recentSwingIndex < 0)
      return true;
   
   double currentPrice = iClose(_Symbol, _Period, 0);
   
   if(forBuy)
   {
      // For buy: check if price has broken above a recent swing high
      if(nodes[recentSwingIndex].isHigh)
      {
         return (currentPrice > nodes[recentSwingIndex].price);
      }
   }
   else
   {
      // For sell: check if price has broken below a recent swing low
      if(!nodes[recentSwingIndex].isHigh)
      {
         return (currentPrice < nodes[recentSwingIndex].price);
      }
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| Execute Buy Order using CTrade                                   |
//+------------------------------------------------------------------+
void ExecuteBuyOrder(double price, double sl, double tp)
{
   // Check if we already have a buy position
   if(trade.Buy(LotSize, _Symbol, price, sl, tp, "BFS Buy Signal"))
   {
      Print("Buy order executed successfully");
   }
   else
   {
      Print("Buy order failed. Error: ", GetLastError(), " - ", trade.ResultRetcodeDescription());
   }
}

//+------------------------------------------------------------------+
//| Execute Sell Order using CTrade                                  |
//+------------------------------------------------------------------+
void ExecuteSellOrder(double price, double sl, double tp)
{
   // Check if we already have a sell position
   if(trade.Sell(LotSize, _Symbol, price, sl, tp, "BFS Sell Signal"))
   {
      Print("Sell order executed successfully");
   }
   else
   {
      Print("Sell order failed. Error: ", GetLastError(), " - ", trade.ResultRetcodeDescription());
   }
}

//+------------------------------------------------------------------+
//| Manage Positions using CTrade                                    |
//+------------------------------------------------------------------+
void ManagePositions()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
      {
         string symbol = PositionGetString(POSITION_SYMBOL);
         long magic = PositionGetInteger(POSITION_MAGIC);
         
         if(symbol == _Symbol && magic == 12345)
         {
            // Check if we should close based on bias change
            if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && currentBias < -BiasThreshold) ||
               (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && currentBias > BiasThreshold))
            {
               // Close position due to bias reversal
               if(trade.PositionClose(ticket))
               {
                  Print("Position closed due to bias reversal. Ticket: ", ticket);
               }
               else
               {
                  Print("Failed to close position. Error: ", GetLastError(), " - ", trade.ResultRetcodeDescription());
               }
            }
         }
      }
   }
}

In this section, we implement the core reasoning engine of the Expert Advisor by traversing the market-structure graph using Breadth-First Search in a level-order manner. The PerformBFSTraversal function initializes traversal from the selected root swing and progressively visits connected swings in chronological order, assigning each node a depth level that reflects its structural distance from the root. As each node is visited, it is classified as bullish or bearish based on its relationship to prior swings, ensuring that directional interpretation is grounded in evolving market structure rather than isolated price points.

The ClassifyNodeDirection logic determines whether a swing represents a bullish or bearish structure by comparing it with the most recent swing of the same type. Higher highs and higher lows are interpreted as bullish continuation, while lower highs and lower lows signal bearish intent. When no comparable prior swing exists, the algorithm falls back to current price interaction with the swing level, preserving directional context even at structural boundaries. This layered classification approach allows each BFS level to carry meaningful directional information that reflects how structure unfolds over time.

Finally, the directional signals produced during traversal are aggregated into a single, normalized market bias. The CalculateMarketBias function assigns greater weight to lower BFS levels, ensuring that recent structure has a stronger influence than distant history. This bias is then translated into actionable permissions through UpdateTradingPermissions, which gate trade execution rather than forcing it. Subsequent functions handle confirmation, order execution, and position management, ensuring that trades align with the prevailing structural bias and are exited promptly if the underlying market direction reverses.

//+------------------------------------------------------------------+
//| Update Visualization                                             |
//+------------------------------------------------------------------+
void UpdateVisualization()
{
   RemoveVisualization();
   
   // Draw swing nodes
   for(int i = 0; i < nodeCount; i++)
   {
      if(nodes[i].level >= 0 && nodes[i].index < 200)  // Only show recent 200 bars
      {
         string objName = "SwingNode_" + IntegerToString(i);
         
         // Create object based on swing type
         if(nodes[i].isHigh)
         {
            // Draw swing high as downward arrow
            ObjectCreate(0, objName, OBJ_ARROW_DOWN, 0, nodes[i].time, nodes[i].price);
            ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_TOP);
         }
         else
         {
            // Draw swing low as upward arrow
            ObjectCreate(0, objName, OBJ_ARROW_UP, 0, nodes[i].time, nodes[i].price);
            ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_BOTTOM);
         }
         
         // Set color based on bullish/bearish classification
         if(nodes[i].bullish)
            ObjectSetInteger(0, objName, OBJPROP_COLOR, BullishColor);
         else
            ObjectSetInteger(0, objName, OBJPROP_COLOR, BearishColor);
         
         // Set size
         ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2);
         
         // Add level annotation
         string labelName = "Label_" + IntegerToString(i);
         ObjectCreate(0, labelName, OBJ_TEXT, 0, nodes[i].time, nodes[i].price);
         ObjectSetString(0, labelName, OBJPROP_TEXT, "L" + IntegerToString(nodes[i].level));
         ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrWhite);
         ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8);
         ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_UPPER);
         
         // Adjust label position based on swing type
         if(nodes[i].isHigh)
            ObjectSetDouble(0, labelName, OBJPROP_PRICE, nodes[i].price + 10 * _Point);
         else
            ObjectSetDouble(0, labelName, OBJPROP_PRICE, nodes[i].price - 10 * _Point);
      }
   }
   
   // Draw edges between nodes
   for(int e = 0; e < edgeCount; e++)
   {
      int fromIdx = edges[e].from;
      int toIdx = edges[e].to;
      
      // Only draw edges for visible nodes
      if(fromIdx < nodeCount && toIdx < nodeCount && 
         nodes[fromIdx].index < 200 && nodes[toIdx].index < 200)
      {
         string edgeName = "Edge_" + IntegerToString(e);
         ObjectCreate(0, edgeName, OBJ_TREND, 0, nodes[fromIdx].time, nodes[fromIdx].price, 
                     nodes[toIdx].time, nodes[toIdx].price);
         ObjectSetInteger(0, edgeName, OBJPROP_COLOR, clrGray);
         ObjectSetInteger(0, edgeName, OBJPROP_WIDTH, 1);
         ObjectSetInteger(0, edgeName, OBJPROP_STYLE, STYLE_DASH);
      }
   }
   
   // Draw bias indicator
   string biasObjName = "BiasIndicator";
   ObjectCreate(0, biasObjName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, biasObjName, OBJPROP_XDISTANCE, 10);
   ObjectSetInteger(0, biasObjName, OBJPROP_YDISTANCE, 10);
   ObjectSetInteger(0, biasObjName, OBJPROP_XSIZE, 150);
   ObjectSetInteger(0, biasObjName, OBJPROP_YSIZE, 60);
   ObjectSetInteger(0, biasObjName, OBJPROP_BGCOLOR, clrBlack);
   ObjectSetInteger(0, biasObjName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, biasObjName, OBJPROP_BORDER_COLOR, clrGray);
   
   // Bias value
   string biasValueName = "BiasValue";
   ObjectCreate(0, biasValueName, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, biasValueName, OBJPROP_XDISTANCE, 20);
   ObjectSetInteger(0, biasValueName, OBJPROP_YDISTANCE, 20);
   ObjectSetString(0, biasValueName, OBJPROP_TEXT, "Bias: " + DoubleToString(currentBias, 3));
   ObjectSetInteger(0, biasValueName, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(0, biasValueName, OBJPROP_FONTSIZE, 12);
   
   // Trading status
   string statusName = "TradingStatus";
   ObjectCreate(0, statusName, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, statusName, OBJPROP_XDISTANCE, 20);
   ObjectSetInteger(0, statusName, OBJPROP_YDISTANCE, 40);
   
   string statusText = "Status: ";
   if(allowBuy)
      statusText += "BUY";
   else if(allowSell)
      statusText += "SELL";
   else
      statusText += "NEUTRAL";
   
   ObjectSetString(0, statusName, OBJPROP_TEXT, statusText);
   ObjectSetInteger(0, statusName, OBJPROP_COLOR, clrYellow);
   ObjectSetInteger(0, statusName, OBJPROP_FONTSIZE, 10);
}

//+------------------------------------------------------------------+
//| Remove Visualization                                             |
//+------------------------------------------------------------------+
void RemoveVisualization()
{
   // Remove all objects created by this EA
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
   {
      string name = ObjectName(0, i);
      if(StringFind(name, "SwingNode_") == 0 ||
         StringFind(name, "Label_") == 0 ||
         StringFind(name, "Edge_") == 0 ||
         StringFind(name, "Bias") == 0 ||
         StringFind(name, "TradingStatus") == 0)
      {
         ObjectDelete(0, name);
      }
   }
}

The UpdateVisualization function is responsible for translating the internal graph-based market structure into clear, real-time visual elements on the chart. It begins by removing any previously drawn objects to prevent overlap or stale information, ensuring the display always reflects the most recent analysis. Each detected swing node is then rendered as an arrow, with swing highs drawn as downward arrows and swing lows as upward arrows, limited to the most recent 200 bars to maintain clarity and performance. Color coding is applied based on bullish or bearish classification, while a small level label is positioned near each swing to indicate its BFS depth, visually conveying how far each node is from the root in the structural hierarchy.

Beyond the individual nodes, the function also visualizes the relationships between them by drawing dashed trend lines that represent edges in the graph, effectively mapping how price transitions from one swing to the next over time. In addition, a compact on-chart dashboard is created to display the current market bias and trading permission state, such as BUY, SELL, or NEUTRAL, allowing traders to immediately interpret the algorithm’s structural conclusions. The complementary RemoveVisualization function acts as a cleanup mechanism, selectively deleting only objects created by the EA based on naming conventions, which keeps the chart organized and ensures that each visualization update is both accurate and non-intrusive.


Back Test Results

The back-testing was evaluated on the H1 timeframe across a 2-month testing window (01 October 2025 to 01 December 2025), with the following settings:

Input parameters

Now the equity curve and back test:

Equity Curve

Back Test


Conclusion

In summary, we developed and integrated Breadth-First Search (BFS) into the trading system by first redefining market structure as a graph composed of swing highs and swing lows acting as nodes, with time-consistent transitions represented as directed edges. Swings were detected from raw price data, structured into nodes with contextual attributes, and then linked to reflect realistic high–low progression. A BFS traversal was applied starting from a carefully selected root within the historical context, allowing the system to layer market structure level by level. This traversal produced a measurable structural bias that was then translated into clear trading permissions, while a dedicated visualization layer rendered nodes, edges, and bias information directly on the chart for transparency and validation.

In conclusion, integrating BFS into trading provides traders with a systematic and objective way to interpret market structure, replacing subjective swing analysis with a mathematically grounded framework. By processing price action progressively through structural levels, traders gain clearer insight into trend strength, directional bias, and valid trade conditions. Overall, this approach will help traders trade with structure, consistency, and clarity, aligning execution with the true hierarchical flow of the market rather than isolated price movements. 

Attached files |
BFS-EA.mq5 (30.2 KB)
Market Simulation (Part 10): Sockets (IV) Market Simulation (Part 10): Sockets (IV)
In this article, we'll look at what you need to do to start using Excel to manage MetaTrader 5, but in a very interesting way. To do this, we will use an Excel add-in to avoid using built-in VBA. If you don't know what add-in is meant, read this article and learn how to program in Python directly in Excel.
Neuroboids Optimization Algorithm 2 (NOA2) Neuroboids Optimization Algorithm 2 (NOA2)
The new proprietary optimization algorithm NOA2 (Neuroboids Optimization Algorithm 2) combines the principles of swarm intelligence with neural control. NOA2 combines the mechanics of a neuroboid swarm with an adaptive neural system that allows agents to self-correct their behavior while searching for the optimum. The algorithm is under active development and demonstrates potential for solving complex optimization problems.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Formulating Dynamic Multi-Pair EA (Part 6): Adaptive Spread Sensitivity for High-Frequency Symbol Switching Formulating Dynamic Multi-Pair EA (Part 6): Adaptive Spread Sensitivity for High-Frequency Symbol Switching
In this part, we will focus on designing an intelligent execution layer that continuously monitors and evaluates real-time spread conditions across multiple symbols. The EA dynamically adapts its symbol selection by enabling or disabling trading based on spread efficiency rather than fixed rules. This approach allows high-frequency multi-pair systems to prioritize cost-effective symbols.