English
preview
Graphentheorie: Einsatz von Breadth-First Search (BFS) im Trading

Graphentheorie: Einsatz von Breadth-First Search (BFS) im Trading

MetaTrader 5Beispiele |
21 0
Hlomohang John Borotho
Hlomohang John Borotho

Inhaltsverzeichnis

  1. Einführung
  2. Systemübersicht und BFS-Grundlagen
  3. Die ersten Schritte
  4. Backtest-Ergebnisse
  5. Schlussfolgerung 


Einführung

Oberflächlich betrachtet erscheinen die Finanzmärkte oft chaotisch, doch bewegen sich die Kurse durchweg in einer Weise, die eher auf eine zugrunde liegende Logik als auf Zufälligkeit schließen lässt. Händler und Systementwickler sind daher bestrebt, den Algorithmus zu identifizieren, dem der Markt selbst zu folgen scheint, wenn er zwischen den Ebenen der Liquidität, der Struktur und des Ungleichgewichts wechselt. Dabei geht es nicht um eine isolierte Vorhersage, sondern darum, zu verstehen, wie vergangene Kursinteraktionen künftige Bewegungen beeinflussen und wie sich die Marktstruktur im Laufe der Zeit Schritt für Schritt entwickelt.

Die Graphentheorie bietet einen leistungsfähigen Rahmen, um diesem Ziel näherzukommen, da sie es ermöglicht, die Kursentwicklung als strukturiertes, regelbasiertes System zu modellieren und nicht als eine Abfolge von isolierten Bars. Durch die Anwendung von Breadth-First Search (BFS) mit Level-Order-Traversierung kann die Marktstruktur schrittweise von den jüngsten Swings zurück in den historischen Kontext erforscht werden, unter Wahrung der zeitlichen Richtung. Dieser Ansatz wandelt historische Bars in einen gerichteten Graphen von Entscheidungspunkten um, was eine systematische Interpretation von bullischen und bärischen Strukturen ermöglicht, und nähert den algorithmischen Handel der eigenen strukturellen Entscheidungslogik des Marktes an.


Systemübersicht und BFS-Grundlagen:

BFS-Konzept  Interpretation im Handelskontext 
Node. Ein Swing-Punkt (Hoch oder Tief).
Edge (directed). Zeitlicher Verlauf (älter -> neuer).
Level. Abstand in Bars oder Tagen.
Root. Historischer Anker (Startpunkt).
Traversal. Entwicklung der Marktstruktur.
Goal. Bestimmung des bullischen oder bärischen Bias.

Der BFS Market Structure EA wird die traditionelle technische Analyse durch die Anwendung von Konzepten der Graphentheorie auf das Marktgeschehen verändern. Das System beginnt mit der Ermittlung signifikanter Swing-Punkte (Höchst- und Tiefstwerte) aus historischen Kursdaten, wobei jeder Swing als Knoten in einem gerichteten Graphen behandelt wird, dessen Kanten den chronologischen Verlauf darstellen. Mithilfe der Breadth-First-Suche (BFS), die von einem konfigurierbaren historischen Ankerpunkt ausgeht, analysiert der Algorithmus die Marktstruktur ebenenweise, wobei zunächst die nächstliegenden Swings und anschließend ältere Formationen ausgewertet werden. Jeder Swing-Knoten wird auf der Grundlage vergleichbarer Kursbeziehungen als bullisch oder bärisch eingestuft, wobei neuere Strukturen bei der endgültigen Berechnung des Bias stärker gewichtet werden. Daraus ergibt sich ein kontinuierlicher Bias-Wert, der von -1 (bärisch) bis +1 (bullisch) reicht und eher die strukturelle Entwicklung des Marktes als traditionelle indikatorbasierte Signale darstellt.

Die Handelsentscheidungsebene filtert potenzielle Trades durch diesen strukturellen Bias, indem sie verlangt, dass der Bias-Wert die konfigurierbaren Schwellenwerte überschreitet, bevor sie Kauf- oder Verkaufspositionen zulässt. Wenn diese Funktion aktiviert ist, führt der EA Trades mit professionellem Ordermanagement über die CTrade-Klasse aus, mit optionaler Bestätigung durch aktuelle Swing-Breakouts. Das System umfasst drei verschiedene Kontextmodi – vorangegangene Bars für Scalping, vorangegangene Tage für Swing-Trading und nur den Vortag zur Bestimmung des Intraday-Bias –, die eine Anpassung an verschiedene Handelsstile ermöglichen. Die Echtzeit-Visualisierung zeigt das Swing-Diagramm mit farbcodierten Knoten und Kantenverbindungen an, wodurch die Strukturanalyse transparent wird und unmittelbar Rückmeldung zu den Marktbedingungen liefert. Dieser Ansatz schafft eine systematische, regelbasierte Methodik, die sich auf das Verständnis der Entwicklung der Marktstruktur und nicht auf die Vorhersage künftiger Kursbewegungen konzentriert.



Die ersten Schritte

//+------------------------------------------------------------------+
//|                                                       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

Wir beginnen mit dem Input-Parameter-Block, der festlegt, wie der Expert Advisor den historischen Marktkontext wahrnimmt und ihn in eine strukturierte Handelslogik umwandelt. Mit dem ENUM_HISTORY_MODE kann der Benutzer steuern, ob der Algorithmus eine feste Anzahl vergangener Bars, mehrere vorangegangene Handelstage oder ausschließlich das Kursverhalten des Vortages auswertet, was sich direkt auf die Konstruktion des Charts des Marktes auswirkt. Parameter wie BarsLookback, DaysLookback und SwingPeriod bestimmen die Tiefe und Granularität der Swing-Erkennung, während BiasThreshold die erforderliche strukturelle Mindestbestätigung festlegt, bevor ein bullischer oder bärischer Marktbias (bias) als gültig angesehen wird.

Die übrigen Eingaben bestimmen die Ausführung und Interpretierbarkeit unseres Systems. Handelskontrollen wie EnableTrading, LotSize, TakeProfit und StopLoss trennen die Entscheidungslogik vom Risikomanagement und stellen sicher, dass die Strukturanalyse unabhängig von den Ausführungsregeln funktionieren kann. Visualisierungseinstellungen wie VisualizeStructure, BullishColor und BearishColor sorgen für Transparenz, indem sie die erkannte Marktstruktur direkt im Diagramm darstellen, sodass Händler visuell nachvollziehen können, wie der Algorithmus bullische und bärische Swings auf Grundlage der zugrunde liegenden Traversierungslogik des Graphen klassifiziert.

//+------------------------------------------------------------------+
//| 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

In diesem Abschnitt des Codes werden die Kerndatenstrukturen definiert, die das rohe Kursverhalten in einen Graphen umwandeln, der für die Breadth-First-Search-Traversierung geeignet ist. Die SwingNode-Struktur stellt jeden bedeutenden Marktumkehrpunkt dar und speichert dessen Bar-Index, Kurs, Zeit und ob es sich um ein Swing-Hoch oder -Tief handelt. Zusätzliche Attribute wie „bullish“ und „level“ ermöglichen es, jedem Knoten eine richtungsweisende Bedeutung und eine BFS-Tiefe zuzuordnen, sodass die Marktstruktur schrittweise über die Zeit bewertet werden kann. Indem sowohl strukturelle als auch zeitliche Informationen gekapselt werden, wird jeder Swing zu einem aussagekräftigen Knoten innerhalb eines gerichteten Marktgraphen, statt nur ein isoliertes Kursereignis zu bleiben.

Zur Unterstützung dieser Darstellung definieren die Strukturen BFSItem und Edge, wie Traversierung und Konnektivität innerhalb des Graphen gehandhabt werden. BFSItem ermöglicht die Verarbeitung in der Reihenfolge der Ebenen, indem es verfolgt, welcher Knoten auf jeder BFS-Ebene besucht wird, während Edge explizit die gerichtete Beziehung zwischen älteren und neueren Swing-Punkten modelliert. Die globalen Variablen dienen dann als operatives Rückgrat des EA, indem sie den konstruierten Graphen speichern, den Traversierungstatus aufrechterhalten und strukturelles Marktbias in umsetzbare Handelssignale umwandeln. Zusammen bilden diese Komponenten eine Brücke zwischen Graphentheorie und Ausführungslogik, die es dem EA ermöglicht, systematisch Rückschlüsse auf die Marktrichtung zu ziehen, bevor Trades platziert oder gefiltert werden.

//+------------------------------------------------------------------+
//| 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();
}

Hier definieren wir den Lebenszyklus des Expert Advisors, der die Initialisierung, Ausführung und Bereinigung koordiniert und gleichzeitig sicherstellt, dass die BFS-basierte Marktstruktur auf dem neuesten Stand bleibt. Während der Initialisierung werden die Eingabeparameter validiert, um ungültige Konfigurationen zu verhindern, die Handelseinstellungen werden vorbereitet und eine erste Berechnung der BFS-Struktur wird durchgeführt, wobei ein Timer eingerichtet wird, um eine regelmäßige Neuberechnung sicherzustellen. Die Hauptausführungslogik berechnet die Struktur aus Effizienzgründen nur bei neuen Bars oder Zeitintervallen neu, wendet Handelsentscheidungen an, wenn sie aktiviert sind, und aktualisiert kontinuierlich die Chart-Visualisierung, während die Deinitialisierungsphase Zeitgeber und grafische Objekte sicher entfernt, um die Stabilität der Plattform zu gewährleisten.

//+------------------------------------------------------------------+
//| 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;
}

Dieser Abschnitt koordiniert die gesamte Pipeline zur Bewertung der Marktstruktur, indem er die Erkennung von Swings, den Aufbau des Graphen, das Traversieren und die Berechnung des Marktbias koordiniert. Die Funktion CalculateBFSStructure fungiert als zentraler Controller, der zunächst alle Richtungs- und Erlaubniszustände zurücksetzt, bevor die einzelnen Analyseschritte nacheinander ausgeführt werden. Durch die Trennung von Erkennung, Graphenerstellung, Traversierung und Entscheidungsaktualisierung in klare Schritte stellt der EA sicher, dass jede Neuberechnung der Marktstruktur konsistent, deterministisch und einfach zu debuggen ist, und verhindert gleichzeitig, dass unvollständige oder veraltete Ergebnisse die Handelsentscheidungen beeinflussen.

Die Funktion DetectSwingNodes ist für die Umwandlung roher historischer Kursdaten in aussagekräftige Strukturknoten verantwortlich. Auf der Grundlage des gewählten Historienmodus wird dynamisch festgelegt, wie viele historische Daten analysiert werden sollen, sei es eine feste Anzahl von Bars, mehrere Tage oder nur die Sitzung des Vortags. Anschließend werden die Höchst- und Tiefststände der Kurse unter Verwendung der definierten Swing-Periode gescannt, um gültige Wendepunkte zu identifizieren, wobei sichergestellt wird, dass jeder erkannte Swing von den umgebenden Störungen isoliert ist. Diese Swing-Punkte werden zu den grundlegenden Knotenpunkten des Graphen, die jeweils eine genaue Zeit-, Kurs- und Strukturidentität aufweisen.

Sobald die Swing-Knoten festgelegt sind, wandelt BuildDirectedGraph sie in einen gerichteten Marktgraphen um, indem ältere Swings mit neueren in einer zeitlich konsistenten Reihenfolge verknüpft werden. Kanten werden nur zwischen abwechselnden Swing-Typen erzeugt, wodurch ein realistischer Hoch-Tief-Verlauf erzwungen wird, der die natürliche Marktstruktur widerspiegelt. Diese gerichtete Konnektivität stellt sicher, dass der Marktgraph den zeitlichen Fluss respektiert und rückwärts gerichtete oder zyklische Beziehungen vermeidet, wodurch er sich für die Traversierung von Ebenen eignet und ein Übermalen oder logische Widersprüche bei der Strukturanalyse verhindert.

Der selectBFSRoot bestimmt, wo die Traversierung beginnt, und verankert die Analyse im benutzerdefinierten historischen Kontext. Je nach gewähltem Modus kann die Wurzel den frühesten relevanten Swing innerhalb eines Bar-Fensters, einen mehrtägigen Bereich oder eine einzelne Handelssitzung darstellen. Wenn kein Knoten die Kriterien erfüllt, greift der Algorithmus auf den ältesten verfügbaren Swing zurück, um die Kontinuität zu gewährleisten. Diese Auswahl der Wurzeln ist von entscheidender Bedeutung, da sie die Perspektive definiert, aus der sich die BFS entfaltet, und sich direkt darauf auswirkt, wie die frühere Struktur schichtweise eingeordnet, bewertet und schließlich in einen aktuellen Marktbias übersetzt wird.

//+------------------------------------------------------------------+
//| 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 diesem Abschnitt implementieren wir den Kern des Expert Advisors, indem wir den Marktstrukturgraphen mithilfe von Breadth-First Search in einer Level-Order durchlaufen. Die Funktion PerformBFSTraversal startet die Traversierung ausgehend von der ausgewählten Swing-Wurzel und besucht nach und nach die verbundenen Swings in chronologischer Reihenfolge, wobei jedem Knoten eine Tiefenstufe zugewiesen wird, die den strukturellen Abstand des Knotens von der Swing-Wurzel widerspiegelt. Jeder besuchte Knotenpunkt wird auf der Grundlage seiner Beziehung zu früheren Swings als bullisch oder bärisch eingestuft, um sicherzustellen, dass die Richtungsinterpretation auf der sich entwickelnden Marktstruktur und nicht auf isolierten Kurspunkten beruht.

Die ClassifyNodeDirection-Logik bestimmt, ob ein Swing eine bullische oder bärische Struktur darstellt, indem sie ihn mit dem letzten Swing desselben Typs vergleicht. Höhere Hochs und höhere Tiefs werden als bullische Fortsetzung interpretiert, während niedrigere Hochs und niedrigere Tiefs bärische Absichten signalisieren. Wenn kein vergleichbarer früherer Swing existiert, greift der Algorithmus auf die Interaktion des aktuellen Kurses mit dem Swing-Level zurück, um den Richtungskontext auch an strukturellen Grenzen zu erhalten. Dieser mehrstufige Klassifizierungsansatz ermöglicht es, dass jede BFS-Ebene aussagekräftige Richtungsinformationen enthält, die widerspiegeln, wie sich die Struktur im Laufe der Zeit entwickelt.

Schließlich werden die Richtungssignale, die während der Traversierung erzeugt werden, zu einer einzigen, normalisierten Marktbias aggregiert. Mit der Funktion CalculateMarketBias werden niedrigere BFS-Stufen stärker gewichtet, um sicherzustellen, dass die jüngste Struktur einen stärkeren Einfluss hat als die weit zurückliegende Vergangenheit. Dieser Marktbias wird dann durch UpdateTradingPermissions in umsetzbare Handelsfreigaben umgesetzt, die die Ausführung von Trades ermöglichen, anstatt sie zu erzwingen. Nachfolgende Funktionen sorgen für die Bestätigung, die Ausführung von Aufträgen und das Positionsmanagement, um sicherzustellen, dass die Trades mit der vorherrschenden strukturellen Bias übereinstimmen und sofort beendet werden, wenn sich die zugrunde liegende Marktrichtung umkehrt.

//+------------------------------------------------------------------+
//| 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);
      }
   }
}

Die Funktion UpdateVisualization ist für die Umsetzung der internen grafbasierten Marktstruktur in klare visuelle Echtzeitelemente auf dem Chart verantwortlich. Zunächst werden alle zuvor gezeichneten Objekte entfernt, um Überschneidungen oder veraltete Informationen zu vermeiden und sicherzustellen, dass die Anzeige immer die aktuellste Analyse widerspiegelt. Jeder erkannte Swing-Knoten wird dann als Pfeil dargestellt, wobei Swing-Hochs als abwärts gerichtete Pfeile und Swing-Tiefs als aufwärts gerichtete Pfeile gezeichnet werden, wobei die Darstellung auf die letzten 200 Bars beschränkt ist, um die Übersichtlichkeit und Leistung zu erhalten. Die Farbcodierung basiert auf der Einstufung als bullisch oder bärisch, während ein kleines Level-Label neben jedem Swing die BFS-Tiefe anzeigt und damit visuell vermittelt, wie weit jeder Knoten von der Wurzel in der strukturellen Hierarchie entfernt ist.

Über die einzelnen Knoten hinaus visualisiert die Funktion auch die Beziehungen zwischen ihnen, indem sie gestrichelte Trendlinien zeichnet, die Kanten im Diagramm darstellen und effektiv abbilden, wie der Kurs im Laufe der Zeit von einem Swing zum nächsten übergeht. Darüber hinaus wird ein kompaktes Dashboard auf dem Chart erstellt, das das aktuelle Marktbias und den Status der Handelserlaubnis anzeigt, z. B. Kauf, Verkauf oder Neutral, sodass die Händler die strukturellen Schlussfolgerungen des Algorithmus sofort interpretieren können. Die ergänzende Funktion RemoveVisualization fungiert als Bereinigungsmechanismus, der selektiv nur die vom EA erstellten Objekte auf der Grundlage von Namenskonventionen löscht. So bleibt der Chart übersichtlich und es wird sichergestellt, dass jede Aktualisierung der Visualisierung sowohl genau als auch unaufdringlich ist.


Backtest-Ergebnisse

Das Backtesting wurde für den Zeitrahmen H1 über ein zweimonatiges Testfenster (01. Oktober 2025 bis 01. Dezember 2025) mit den folgenden Einstellungen bewertet:

Eingabe-Parameter

Nachfolgend die Equity-Kurve und der Backtest:

Equity-Kurve

Backtest


Schlussfolgerung

Zusammenfassend kann gesagt werden, dass wir die Breadth-First Search (BFS) entwickelt und in das Handelssystem integriert haben, indem wir zunächst die Marktstruktur als einen Graphen neu definiert haben, der aus Swing-Highs und Swing-Lows besteht, die als Knoten fungieren, wobei zeitkonsistente Übergänge als gerichtete Kanten dargestellt werden. Die Swings wurden aus den Rohdaten der Kurse ermittelt, in Knoten mit kontextbezogenen Attributen strukturiert und dann verknüpft, um einen realistischen Hoch-Tief-Verlauf widerzuspiegeln. Ausgehend von einer sorgfältig ausgewählten Wurzel im historischen Kontext wurde eine BFS-Traversierung angewandt, die es dem System ermöglicht, die Marktstruktur Ebene für Ebene zu schichten. Diese Traversierung erzeugte einen messbaren strukturellen Bias, der dann in klare Handelserlaubnisse übersetzt wurde, während eine spezielle Visualisierungsschicht Knoten, Kanten und Biasinformationen direkt auf dem Diagramm für Transparenz und Validierung darstellte.

Zusammenfassend kann gesagt werden, dass die Integration des BFS in den Handel den Händlern eine systematische und objektive Methode zur Interpretation der Marktstruktur bietet, indem die subjektive Swing-Analyse durch einen mathematisch fundierten Rahmen ersetzt wird. Durch die schrittweise Verarbeitung des Kursgeschehens auf strukturellen Ebenen erhalten Händler einen klareren Einblick in die Trendstärke, das Bias und die gültigen Handelsbedingungen. Insgesamt wird dieser Ansatz den Händlern helfen, mit Struktur, Konsistenz und Klarheit zu handeln und die Ausführung auf den tatsächlichen hierarchischen Fluss des Marktes und nicht auf isolierte Kursbewegungen auszurichten. 

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20856

Beigefügte Dateien |
BFS-EA.mq5 (30.2 KB)
Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Vom Einsteiger zum Experten: Erstellung eines Liquiditätszonenindikators Vom Einsteiger zum Experten: Erstellung eines Liquiditätszonenindikators
Das Ausmaß der Liquiditätszonen und das Ausmaß der Ausbruchsbewegung sind Schlüsselvariablen, die die Wahrscheinlichkeit eines Retests erheblich beeinflussen. In diesem Beitrag zeigen wir den vollständigen Entwicklungsprozess eines Indikators, der diese Verhältnisse berücksichtigt.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Automatisierung von Handelsstrategien in MQL5 (Teil 47): Nick Rypock Trailing Reverse (NRTR) mit Hedging-Funktionen Automatisierung von Handelsstrategien in MQL5 (Teil 47): Nick Rypock Trailing Reverse (NRTR) mit Hedging-Funktionen
In diesem Artikel entwickeln wir ein Nick Rypock Trailing Reverse (NRTR) Handelssystem in MQL5, das Channel-Indikatoren für Umkehrsignale verwendet und trendfolgende Einstiege mit Hedging-Unterstützung für Long- und Short-Positionen ermöglicht. Wir integrieren Risikomanagement-Funktionen wie automatische Berechnung der Lot-Größen auf der Basis von Kontoeigenkapital (equity) oder Kontostand (balance), feste oder dynamische Stop-Loss- und Take-Profit-Niveaus unter Verwendung von ATR-Multiplikatoren und Positionslimits.