Graph Theory: Traversal Breadth-First Search (BFS) Applied in Trading
Table of contents:
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:

Now the equity curve and 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.
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.
Market Simulation (Part 10): Sockets (IV)
Neuroboids Optimization Algorithm 2 (NOA2)
Features of Experts Advisors
Formulating Dynamic Multi-Pair EA (Part 6): Adaptive Spread Sensitivity for High-Frequency Symbol Switching
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use