English Русский
preview
Graphentheorie: Tiefensuche (DFS) zur Traversierung von Marktstrukturen im Handel

Graphentheorie: Tiefensuche (DFS) zur Traversierung von Marktstrukturen im Handel

MetaTrader 5Beispiele |
22 1
Hlomohang John Borotho
Hlomohang John Borotho

Inhaltsverzeichnis

  1. Einführung
  2. System-Übersicht
  3. Die ersten Schritte
  4. Backtest
  5. Schlussfolgerung



Einführung

Um herauszufinden, wie sich der Markt bewegt, wenden wir in diesem Abschnitt den Algorithmus der Tiefensuche (Depth-First Search, DFS) aus der Graphentheorie auf die Struktur der Kursentwicklung im Handel an. Eine große Herausforderung bei dem Versuch, Kursbewegungen algorithmisch zu erfassen, besteht darin, dass Konzepte wie Swing-Hochs, Swing-Tiefs und Marktfortsetzung in der Regel eher qualitativ beschrieben werden als anhand strenger, überprüfbarer Regeln. Um dieses Problem zu lösen, wird die Marktstruktur als Graph formalisiert, wobei jeder bestätigte Swing zu einem Knoten wird und die Übergänge zwischen den Swings als Kanten dargestellt werden, die diese Knoten miteinander verbinden. Da sich im Kursverlauf neue Kerzen bilden und neue Swings bestätigt werden, entwickelt sich dieser Graph dynamisch weiter und liefert eine strukturierte Darstellung der zeitlichen Entwicklung des Marktes.

In diesem Rahmen ermöglicht es DFS dem System, einem strukturellen Zweig tiefgreifend zu folgen – beispielsweise einer Entwicklung von einem Swing-Tief hin zu höheren Hochs und höheren Tiefs –, bevor alternative strukturelle Möglichkeiten in Betracht gezogen werden. Der Algorithmus folgt diesem Pfad so lange, wie die strukturellen Bedingungen gültig bleiben, und misst dessen Stärke anhand objektiver Kriterien wie Pfadtiefe, wichtigen Strukturniveaus und Zieltoleranz. Wenn der Pfad unterbrochen wird – beispielsweise wenn ein kritisches Swing-Niveau invalidiert wird –, führt der Algorithmus ein Backtracking zum vorherigen Knoten durch und untersucht andere mögliche Pfade. Auf diese Weise wird die Abfolge der Kurs-Swings als begehbare Route durch die Marktstruktur betrachtet, wodurch die subjektive Analyse der Kursentwicklung in einen disziplinierten, programmierbaren Prozess umgewandelt wird, der sich für den systematischen und automatisierten Handel eignet.


Überblick und Verständnis des Systems

Begriffliche Zuordnung:
Graphentheorie   Marktäquivalent
Knoten Swing-Hoch/Swing-Tief
Kante Übergang durch Ausbruch oder Rücksetzer
Zweig Aufwärts- oder Abwärtstrendfortsetzung
Zielknoten Liquiditätsziel
Tiefe Stärke der strukturellen Fortsetzung
Backtracking Invalidierung der Struktur

In der Graphentheorie funktioniert die Tiefensuche (DFS) so, dass zunächst ein Zweig ausgewählt und dieser so tief wie möglich erkundet wird, bevor Alternativen in Betracht gezogen werden. Auf die Marktstruktur angewendet bedeutet dies, dass der Algorithmus einer einzelnen strukturellen Preisentwicklung über aufeinanderfolgende Swing-Punkte folgt und dabei jedes bestätigte Swing-Hoch oder Swing-Tief als Knoten in einem Graphen behandelt. Beispielsweise kann der Kurs bei einem bestätigten Swing-Tief (Knoten A) beginnen, sich zu einem Swing-Hoch (Knoten B) bewegen, zurücklaufen und dabei Knoten C bilden, dann nach oben durchbrechen und Knoten D erreichen und schließlich bis zu Knoten E ansteigen. In DFS-Begriffen folgt das System dieser Abfolge: A -> B -> C -> D -> E, solange die strukturellen Bedingungen weiterhin gelten.

Bevor ein Trade jedoch ausgeführt werden kann, muss der Algorithmus eine minimale strukturelle Tiefe erreichen, d. h. eine Abfolge bestätigter Kurs-Swings, die eine klare Richtungsentwicklung aufzeigen (z. B. höheres Hoch → höheres Tief → Durchbruch der Struktur bei bullischen Bedingungen oder tieferes Tief → tieferes Hoch → Fortsetzung bei bärischen Bedingungen). Erst wenn diese Strukturkette erkannt und validiert wurde, erwägt das System, eine Marktrichtungstendenz festzulegen.

Im Handelskontext bedeutet dies, einem Pfad nur dann ‚tief‘ zu folgen und sich daher erst dann auf eine strukturelle Marktrichtung festzulegen, wenn der Markt diese durch messbare Swing-Zusammenhänge bestätigt hat. Eine bullische Tendenz wird bestätigt, wenn der DFS-Pfad eine gültige Abfolge von höheren Hochs und höheren Tiefs identifiziert, wobei das jüngste höhere Tief als entscheidendes Ungültigkeitsniveau fungiert.

Ebenso bestätigt sich eine bärische Tendenz, wenn der Kursverlauf niedrigere Tiefs und niedrigere Hochs bildet, wobei das jüngste niedrigere Hoch als strukturelle Grenze dient. Sobald diese Tendenz bestätigt ist und die Pfadtiefe die Mindestanforderung erfüllt, prüft das System, ob der Pfad realistischerweise ein aussagekräftiges Ziel erreichen kann, wie beispielsweise einen Liquiditätspool, Angebots- oder Nachfragzonen oder ein vordefiniertes Chance-Risiko-Ziel. Sind diese Bedingungen erfüllt, wird ein Trade in Richtung des aktiven Pfades ausgeführt. Wenn der Kurs stattdessen das strukturelle Ungültigkeitsniveau durchbricht, wertet der Algorithmus dies als Scheitern des Pfads aus, führt ein DFS-Backtracking bis zum letzten gültigen Knoten durch und beginnt, die alternative Marktrichtung zu untersuchen.



Die ersten Schritte

//+------------------------------------------------------------------+
//|                                                          DFS.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/johnhlomohang/"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Include trade class                                              |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
CTrade TradeManager;

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input int      SwingPeriod      = 7;            // Bars left/right for pivot
input int      MinDepth         = 7;            // Minimum structural depth
input double   TargetTolerance  = 60;           // Tolerance for target hit (in points)
input bool     EnableVisual     = true;         // Draw nodes and paths
input double   FixedLotSize     = 0.01;         // Fixed lot size (if risk management disabled)
input bool     UseRiskManagement = true;        // Use risk-based position sizing
input double   RiskPercent       = 1.0;         // Risk percentage per trade
input int      StopLossPoints    = 600;         // Stop loss in points
input int      TakeProfitPoints  = 800;         // Take profit in points
input string   TradeComment      = "SwingGraph"; // Trade comment
input int      MagicNumber       = 123456;      // Expert magic number
input bool     EnableDailyReset  = true;        // Reset nodes at start of each day
input int      MaxNodesToKeep    = 100;         // Maximum nodes to keep (0 = unlimited)
input bool     EnableMemoryOptimization = true; // Enable memory optimization

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct SwingNode
  {
   datetime          time;          // Bar time
   double            price;         // Swing price
   int               type;          // 1 = high, -1 = low
   int               index;         // Node index
   bool              visited;       // For DFS
  };

Zu Beginn legen wir die Grundlage für den Expert Advisor, indem wir die Handelsfunktionalität importieren und die zentralen Konfigurationsparameter festlegen, die das Verhalten des Systems steuern. Die Bibliothek #include <Trade/Trade.mqh> ermöglicht den Zugriff auf die Handelsfunktionen von MetaTrader, während das Objekt TradeManager der Klasse CTrade zur Ausführung von Kauf- und Verkaufsaufträgen verwendet wird. Über die Eingabeparameter kann der Benutzer wichtige Aspekte der Strategie konfigurieren, wie beispielsweise die Empfindlichkeit der Swing-Erkennung (SwingPeriod), die für die Analyse erforderliche minimale Strukturtiefe (MinDepth) und die Toleranz bei der Erkennung, wann der Kurs ein Ziel erreicht hat (TargetTolerance).

Zusätzliche Eingabewerte steuern die Visualisierung, die Positionsgröße und das Risikomanagement, sodass der EA entweder eine feste Lotgröße verwenden oder die Handelsgröße dynamisch auf der Grundlage eines festgelegten Risikoprozentsatzes berechnen kann. Parameter für Stop-Loss, Take-Profit und die MagicNumber stellen sicher, dass Trades ordnungsgemäß verwaltet und innerhalb des Handelskontos eindeutig identifizierbar sind.

Außerdem führen wir Betriebskontrollen und Einstellungen für die Speicherverwaltung ein, die dazu beitragen, die Stabilität und Effizienz des EA während der kontinuierlichen Ausführung aufrechtzuerhalten. So ermöglicht beispielsweise EnableDailyReset dem System, seine Strukturknoten zu Beginn jedes Handelstags zu löschen und neu aufzubauen, während MaxNodesToKeep und EnableMemoryOptimization die Anhäufung übermäßiger Datenmengen verhindern, die den Algorithmus verlangsamen könnten.

Schließlich definiert die Struktur SwingNode die zentrale Dateneinheit zur Modellierung der Marktstruktur, wobei jeder Knoten ein erkanntes Swing-Hoch oder Swing-Tief darstellt. Jeder Knoten speichert den Zeitpunkt des Swings, das Kursniveau, seinen Typ (Hoch oder Tief), einen eindeutigen Index sowie ein Besucht-Flag, das beim Durchlaufverfahren der Tiefensuche (DFS) verwendet wird.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
SwingNode nodes[];
int nodeCount = 0;
datetime lastBarTime = 0;
datetime lastDayDate = 0;    // Track last day for reset

//--- For DFS state
int currentPath[];
int pathDepth = 0;
int currentDirection = 0;    // 1 bullish, -1 bearish, 0 none
double lastHigherLow = 0;
double lastLowerHigh = 0;

//--- Trade flags
bool inTrade = false;
ulong tradeTicket = 0;
MqlTick currentTick;

//--- Rate arrays for timeseries data
double high[];
double low[];
datetime time[];
int barsCount = 0;

//--- Diagnostic counters
int tickCount = 0;
datetime lastCleanupTime = 0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("==========================================");
   Print("SwingGraphTrader initialized");
   Print("SwingPeriod = ", SwingPeriod);
   Print("MinDepth = ", MinDepth);
   Print("TargetTolerance = ", TargetTolerance);
   Print("EnableDailyReset = ", EnableDailyReset);
   Print("MaxNodesToKeep = ", MaxNodesToKeep);
   Print("==========================================");

   //--- Initialize arrays with optimal size
   ArrayResize(nodes, 0, MaxNodesToKeep > 0 ? MaxNodesToKeep + 50 : 1000);
   nodeCount = 0;

   //--- Get initial time
   datetime currentTime[];
   if(CopyTime(_Symbol, _Period, 0, 1, currentTime) <= 0)
     {
      Print("Failed to copy initial time. Error: ", GetLastError());
      return INIT_FAILED;
     }
   lastBarTime = currentTime[0];

   //--- Set initial day
   MqlDateTime dt;
   TimeToStruct(lastBarTime, dt);
   dt.hour = 0;
   dt.min = 0;
   dt.sec = 0;
   lastDayDate = StructToTime(dt);

   Print("Initial lastBarTime = ", TimeToString(lastBarTime));
   Print("Initial lastDayDate = ", TimeToString(lastDayDate));

   TradeManager.SetExpertMagicNumber(MagicNumber);

   //--- Pre-allocate arrays for better performance
   ArrayResize(high, 0, 10000);
   ArrayResize(low, 0, 10000);
   ArrayResize(time, 0, 10000);

   return(INIT_SUCCEEDED);
  }

Anschließend deklarieren wir eine Reihe globaler Variablen, die den internen Zustand des Expert Advisors während seiner Ausführung verwalten. Das Array nodes[] speichert die Sammlung der erkannten Swing-Punkte, die jeweils durch die Struktur SwingNode dargestellt werden, während nodeCount die Anzahl der derzeit vorhandenen Knoten erfasst. Die Variablen lastBarTime und lastDayDate helfen dem EA dabei, die Bildung neuer Kerzen zu erkennen und festzustellen, wann ein täglicher Reset erfolgen sollte. Der Abschnitt mit der Bezeichnung DFS-Zustand enthält Variablen, die die Durchsuchungslogik der Tiefensuche (DFS) unterstützen, die zur Analyse der Marktstruktur verwendet wird.

Beispielsweise erfasst currentPath[] die Abfolge der Swing-Knoten, die den aktiven Strukturpfad bilden, pathDepth verfolgt, wie tief die Durchquerung bereits fortgeschritten ist, und currentDirection gibt an, ob das System derzeit einem bullischen oder bärischen Pfad folgt. Zusätzliche Variablen wie lastHigherLow und lastLowerHigh helfen dabei, die strukturelle Validierung im Verlauf der Kursentwicklung zu überwachen. Außerdem deklarieren wir handelsbezogene Flags wie inTrade und tradeTicket, um zu verfolgen, ob der EA derzeit eine Position hält, sowie Kurs-Arrays (high[], low[], time[]), die zur Speicherung historischer Kerzendaten für die Swing-Erkennung und die Strukturanalyse dienen.

Die Funktion OnInit() ist für die Initialisierung des EAs zuständig, wenn dieser zum ersten Mal an das Chart angehängt oder neu gestartet wird. Zunächst werden Diagnoseinformationen in das Protokoll geschrieben, damit der Benutzer überprüfen kann, ob die Strategieparameter – wie beispielsweise SwingPeriod, MinDepth und die Einstellungen für die Knotenverwaltung – korrekt geladen wurden. Die Funktion bereitet anschließend die zentralen Datenstrukturen vor, indem sie die Größe des Knoten-Arrays anpasst und den Knotenzähler zurücksetzt, um sicherzustellen, dass das System mit einem leeren Strukturgraphen startet. Die Funktion ruft mithilfe von CopyTime() die aktuelle Barzeit ab, um den Ausgangswert für die Erkennung neuer Kerzen festzulegen, und berechnet zudem den täglichen Startzeitstempel, der für die optionale tägliche Rücksetzfunktion verwendet wird.

Nachdem die MagicNumber des Experten über den TradeManager festgelegt wurde, reserviert die Funktion im Voraus Speicherplatz für die Kurs-Arrays (Höchst-, Tiefst- und Zeitwerte), um die Laufzeitleistung bei der Verarbeitung großer Mengen historischer Daten zu verbessern. Sobald alle Initialisierungsaufgaben erfolgreich abgeschlossen sind, gibt die Funktion den Wert INIT_SUCCEEDED zurück, was bedeutet, dass der EA bereit ist, mit der Analyse der Marktdaten und der Ausführung seiner Handelslogik zu beginnen.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(EnableVisual)
      ObjectsDeleteAll(0, "SwingGraph_");

   //--- Free memory
   ArrayFree(nodes);
   ArrayFree(high);
   ArrayFree(low);
   ArrayFree(time);
   ArrayFree(currentPath);

   Print("SwingGraphTrader deinitialized. Reason: ", reason);
  }

//+------------------------------------------------------------------+
//| Reset all data at day change                                     |
//+------------------------------------------------------------------+
void CheckAndResetDaily()
  {
   if(!EnableDailyReset)
      return;

   //--- Get current day
   MqlDateTime currentDt;
   TimeToStruct(TimeCurrent(), currentDt);
   currentDt.hour = 0;
   currentDt.min = 0;
   currentDt.sec = 0;
   datetime currentDay = StructToTime(currentDt);

   //--- Check if day changed
   if(currentDay > lastDayDate)
     {
      Print("Day changed from ", TimeToString(lastDayDate), " to ", TimeToString(currentDay));
      Print("Resetting all nodes and state for new day");

      //--- Reset all nodes
      ArrayResize(nodes, 0, MaxNodesToKeep > 0 ? MaxNodesToKeep + 50 : 1000);
      nodeCount = 0;

      //--- Reset DFS state
      currentDirection = 0;
      pathDepth = 0;
      ArrayResize(currentPath, 0);
      lastHigherLow = 0;
      lastLowerHigh = 0;

      //--- Clear all drawings
      if(EnableVisual)
         ObjectsDeleteAll(0, "SwingGraph_");

      //--- Free timeseries arrays to release memory
      if(EnableMemoryOptimization)
        {
         ArrayFree(high);
         ArrayFree(low);
         ArrayFree(time);
        }

      lastDayDate = currentDay;
      Print("Daily reset complete. Memory freed.");
     }
  }

//+------------------------------------------------------------------+
//| Optimize memory usage                                            |
//+------------------------------------------------------------------+
void OptimizeMemory()
  {
   if(!EnableMemoryOptimization)
      return;

   //--- Periodically clean up old nodes if we exceed max nodes
   if(MaxNodesToKeep > 0 && nodeCount > MaxNodesToKeep)
     {
      Print("Node count (", nodeCount, ") exceeds MaxNodesToKeep (", MaxNodesToKeep, "). Cleaning up old nodes.");

      int nodesToRemove = nodeCount - MaxNodesToKeep;

      //--- Shift remaining nodes to beginning of array
      for(int i = 0; i < MaxNodesToKeep; i++)
        {
         nodes[i] = nodes[i + nodesToRemove];
         nodes[i].index = i; // Update indices
        }

      nodeCount = MaxNodesToKeep;
      ArrayResize(nodes, nodeCount, MaxNodesToKeep + 50);

      //--- Reset path if it contains removed nodes
      bool pathNeedsReset = false;
      for(int i = 0; i < pathDepth; i++)
        {
         if(currentPath[i] < nodesToRemove)
           {
            pathNeedsReset = true;
            break;
           }
         //--- Adjust indices
         currentPath[i] -= nodesToRemove;
        }

      if(pathNeedsReset)
        {
         currentDirection = 0;
         pathDepth = 0;
         ArrayResize(currentPath, 0);
         Print("Path reset due to node cleanup");
        }

      Print("Node cleanup complete. New node count: ", nodeCount);
     }

   //--- Periodically free timeseries arrays if they're too large
   if(ArraySize(high) > 10000)
     {
      ArrayResize(high, 0, 10000);
      ArrayResize(low, 0, 10000);
      ArrayResize(time, 0, 10000);
      Print("Timeseries arrays reset to save memory");
     }
  }

Dieser Abschnitt des Codes regelt das Verhalten des Expert Advisors beim Herunterfahren und beim täglichen Zurücksetzen. Die Funktion OnDeinit() wird ausgeführt, wenn der EA aus dem Chart entfernt wird, die Plattform geschlossen wird oder das Programm neu kompiliert wird. Ihr Zweck besteht darin, die Umgebung sicher zu bereinigen, indem alle von der EA gezeichneten visuellen Objekte entfernt und der für Arrays wie nodes, high, low, time und currentPath zugewiesene Speicher freigegeben wird. Dadurch werden Speicherlecks verhindert und sichergestellt, dass das System die Plattform in einem stabilen Zustand verlässt. Die Funktion gibt außerdem eine Meldung aus, die darauf hinweist, dass der SwingGraphTrader deaktiviert wurde, zusammen mit dem angegebenen Grund, was bei der Fehlersuche und Überwachung hilfreich ist.

Anschließend sorgt die Funktion CheckAndResetDaily() dafür, dass das Handelssystem an jedem neuen Handelstag von vorne beginnt, sofern EnableDailyReset aktiviert ist. Es vergleicht das aktuelle Datum mit dem gespeicherten lastDayDate und setzt, falls ein neuer Tag erkannt wird, die Strukturknoten zurück, löscht den DFS-Durchlaufstatus, löscht Zeichnungen im Chart und gibt optional Zeitreihen-Arrays frei, um Speicherplatz freizugeben.

Der zweite Teil des Codes konzentriert sich auf die Speicheroptimierung und die Systemstabilität bei lang andauernden Vorgängen. Die Funktion OptimizeMemory() sorgt dafür, dass der EA im Laufe der Zeit keine übermäßigen Datenmengen ansammelt, die die Leistung beeinträchtigen oder unnötig Speicherplatz beanspruchen könnten. Wenn die Anzahl der gespeicherten Swing-Knoten den durch MaxNodesToKeep festgelegten Grenzwert überschreitet, entfernt die Funktion die ältesten Knoten und verschiebt die verbleibenden Knoten im Array nach vorne, wobei ihre Indizes aktualisiert werden. Da der DFS-Durchlaufpfad möglicherweise auf Knoten verweist, die entfernt wurden, prüft die Funktion, ob der aktuelle Pfad ungültig geworden ist, und setzt ihn gegebenenfalls zurück, um sicherzustellen, dass der Algorithmus nicht auf veraltete Strukturinformationen zurückgreift.

Zudem überwacht die Funktion die Größe der Arrays für den Kursverlauf (Höchst-, Tiefst- und Zeitwerte) und passt deren Größe an, falls sie zu groß werden, um so eine effiziente Speichernutzung zu gewährleisten. Zusammen ermöglichen diese Mechanismen einen kontinuierlichen Betrieb des EA, wobei dessen interne Datenstrukturen übersichtlich, effizient und stets auf dem neuesten Stand der Marktinformationen bleiben.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Check for daily reset
   CheckAndResetDaily();

   //--- Periodically optimize memory (every 1000 ticks or 1 hour)
   tickCount++;
   if(tickCount % 1000 == 0 || TimeCurrent() - lastCleanupTime > 3600)
     {
      OptimizeMemory();
      lastCleanupTime = TimeCurrent();
     }

   if(inTrade)
     {
      if(!PositionSelectByTicket(tradeTicket))
        {
         Print("Trade closed externally. Resetting state.");
         inTrade = false;
         tradeTicket = 0;
         currentDirection = 0;
         pathDepth = 0;
         ArrayResize(currentPath,0);
        }
     }

   //--- Get current tick data
   if(!SymbolInfoTick(_Symbol, currentTick))
     {
      Print("Failed to get current tick data. Error: ", GetLastError());
      return;
     }

   //--- Check new bar
   datetime currentTime[];
   if(CopyTime(_Symbol, _Period, 0, 1, currentTime) <= 0)
     {
      Print("Failed to copy current time. Error: ", GetLastError());
      return;
     }

   //--- Process only on new bar
   if(currentTime[0] == lastBarTime)
      return;

   lastBarTime = currentTime[0];
   Print("New bar detected at ", TimeToString(lastBarTime));

   //--- Update timeseries data - reuse arrays to minimize memory allocation
   barsCount = Bars(_Symbol, _Period);
   Print("Bars count: ", barsCount);

   if(barsCount < SwingPeriod * 2 + 5)
     {
      Print("Not enough bars: ", barsCount, " < ", SwingPeriod * 2 + 5);
      return;
     }

   //--- Set series as time series
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(time,true);

   //--- Reuse existing arrays instead of creating new ones
   int copiedHigh = CopyHigh(_Symbol, _Period, 0, barsCount, high);
   int copiedLow = CopyLow(_Symbol, _Period, 0, barsCount, low);
   int copiedTime = CopyTime(_Symbol, _Period, 0, barsCount, time);

   Print("Copied high: ", copiedHigh, ", low: ", copiedLow, ", time: ", copiedTime);
   if(copiedHigh <= 0 || copiedLow <= 0 || copiedTime <= 0)
     {
      Print("Failed to copy price data. Errors: ", GetLastError());
      return;
     }

   //--- Update swings
   int oldNodeCount = nodeCount;
   DetectSwing();
   if(nodeCount > oldNodeCount)
      Print("Added ", nodeCount - oldNodeCount, " new nodes. Total nodes: ", nodeCount);

   //--- Detect invalidation of current path
   if(!inTrade && currentDirection != 0)
     {
      if(IsPathInvalidated())
        {
         Print("Path invalidated. Backtracking.");
         BacktrackAndSwitch();
        }
     }

   //--- Run DFS to find new path if not in trade
   if(!inTrade)
     {
      FindBestPath();
     }

   //--- Check if we should enter a trade
   if(!inTrade && currentDirection != 0 && pathDepth >= MinDepth)
     {
      Print("Trade condition satisfied: direction=", currentDirection, " depth=", pathDepth);

      if(currentDirection == 1)
         ExecuteTrade(ORDER_TYPE_BUY, TradeComment + " Bullish");
      else
         ExecuteTrade(ORDER_TYPE_SELL, TradeComment + " Bearish");
     }

   //--- Visualization - only if enabled
   if(EnableVisual)
     {
      DrawSwings();
      DrawActivePath();
     }
  }

//+------------------------------------------------------------------+
//| Swing Detection                                                  |
//+------------------------------------------------------------------+
void DetectSwing()
  {
   int currentBar =  SwingPeriod + 1; // use previous completed bar
   if(currentBar >= barsCount - SwingPeriod)
     {
      Print("currentBar (", currentBar, ") >= barsCount - SwingPeriod (", barsCount - SwingPeriod, ") - returning");
      return;
     }

   if(IsSwingHigh(currentBar, SwingPeriod))
     {
      Print("Swing high detected at bar ", currentBar, ", price: ", high[currentBar]);
      AddNode(time[currentBar], high[currentBar], 1);
     }
   else
      if(IsSwingLow(currentBar, SwingPeriod))
        {
         Print("Swing low detected at bar ", currentBar, ", price: ", low[currentBar]);
         AddNode(time[currentBar], low[currentBar], -1);
        }
  }

//+------------------------------------------------------------------+
//| IsSwingHigh                                                      |
//+------------------------------------------------------------------+
bool IsSwingHigh(int bar, int period)
  {
   double barHigh = high[bar];
   for(int i = bar - period; i <= bar + period; i++)
     {
      if(i < 0 || i >= barsCount)
         continue;
      if(high[i] > barHigh)
         return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//|  IsSwingLow                                                      |
//+------------------------------------------------------------------+
bool IsSwingLow(int bar, int period)
  {
   double barLow = low[bar];
   for(int i = bar - period; i <= bar + period; i++)
     {
      if(i < 0 || i >= barsCount)
         continue;
      if(low[i] < barLow)
         return false;
     }
   return true;
  }

Dieser Abschnitt des Codes stellt den Hauptausführungszyklus des Expert Advisors dar, in dem die Handelslogik bei jedem Eintreffen eines neuen Ticks abgearbeitet wird. Die Funktion OnTick() prüft zunächst, ob ein täglicher Reset erforderlich ist, und führt in regelmäßigen Abständen eine Speicheroptimierung durch, um die Effizienz des Systems auch bei längerem Betrieb aufrechtzuerhalten. Außerdem wird überprüft, ob ein bestehender Trade noch aktiv ist, und der interne Status wird zurückgesetzt, falls die Position extern geschlossen wurde. Der EA ruft dann die aktuellsten Tick-Daten ab und stellt sicher, dass die Verarbeitung erst dann erfolgt, wenn sich eine neue Kerze (Bar) bildet, wodurch überflüssige Berechnungen bei jedem Tick vermieden werden. Sobald eine neue Bar erkannt wird, aktualisiert das Programm die Arrays mit den historischen Kurswerten (Höchst-, Tiefst- und Zeitangaben) und stellt so sicher, dass genügend Bars für die Swing-Analyse zur Verfügung stehen.

Die Funktion DetectSwing() durchsucht anschließend die Kursdaten, um neue Swing-Hochs und Swing-Tiefs zu identifizieren. Dabei werden die Hilfsfunktionen IsSwingHigh() und IsSwingLow() verwendet, um zu überprüfen, ob eine Bar höher oder niedriger ist als die umgebenden Bars innerhalb des definierten SwingPeriod-Zeitraums. Werden neue Strukturknoten erkannt, werden diese dem Knotengrafen hinzugefügt, wodurch das System eine sich ständig weiterentwickelnde Darstellung der Marktstruktur aufrechterhalten kann. Nach der Aktualisierung der Knoten prüft der EA, ob der aktuelle DFS-Pfad ungültig geworden ist, und führt ein Backtracking durch; anschließend wendet er die Logik der Tiefensuche an, um den vielversprechendsten Strukturpfad zu finden. Sobald eine gültige Marktrichtungstendenz festgestellt wurde und die strukturelle Tiefe die Mindestanforderung erfüllt, führt der EA einen Trade in der erkannten Richtung aus und zeichnet dabei optional Swing-Punkte und den aktiven Pfad zur Veranschaulichung in das Chart ein.

//+------------------------------------------------------------------+
//|  AddNode                                                         |
//+------------------------------------------------------------------+
void AddNode(datetime nodeTime, double price, int type)
  {
   //--- Avoid duplicate at same time
   if(nodeCount > 0 && nodes[nodeCount-1].time == nodeTime)
     {
      Print("Duplicate node at same time, skipping.");
      return;
     }

   //--- Check if we need to resize with extra capacity
   if(nodeCount >= ArraySize(nodes))
     {
      int newSize = nodeCount + (MaxNodesToKeep > 0 ? 50 : 100);
      ArrayResize(nodes, newSize, MaxNodesToKeep > 0 ? MaxNodesToKeep + 50 : 1000);
     }

   nodes[nodeCount].time = nodeTime;
   nodes[nodeCount].price = price;
   nodes[nodeCount].type = type;
   nodes[nodeCount].index = nodeCount;
   nodes[nodeCount].visited = false;
   nodeCount++;
   Print("Node added: index=", nodeCount-1, ", time=", TimeToString(nodeTime), ", price=", price, ", type=", type);
  }

//+------------------------------------------------------------------+
//| DFS Path Finding                                                 |
//+------------------------------------------------------------------+
void FindBestPath()
  {
   if(nodeCount < 2)
     {
      Print("Not enough nodes for DFS: ", nodeCount);
      return;
     }

   //--- Reset visited flags
   for(int i = 0; i < nodeCount; i++)
      nodes[i].visited = false;

   int bestDepth = 0;
   int bestDirection = 0;
   int bestPath[];

   //--- Try each node as starting point
   for(int start = 0; start < nodeCount; start++)
     {
      //--- Bullish from a low
      if(nodes[start].type == -1)
        {
         int path[];
         int depth = DFS_Bullish(start, nodes[start].price, -1e9, path);
         if(depth > bestDepth)
           {
            bestDepth = depth;
            bestDirection = 1;
            ArrayResize(bestPath, ArraySize(path));
            for(int i = 0; i < ArraySize(path); i++)
               bestPath[i] = path[i];
           }
        }
      //--- Bearish from a high
      if(nodes[start].type == 1)
        {
         int path[];
         int depth = DFS_Bearish(start, nodes[start].price, 1e9, path);
         if(depth > bestDepth)
           {
            bestDepth = depth;
            bestDirection = -1;
            ArrayResize(bestPath, ArraySize(path));
            for(int i = 0; i < ArraySize(path); i++)
               bestPath[i] = path[i];
           }
        }
     }

   Print("Best depth found: ", bestDepth, ", direction: ", bestDirection);

   //--- Set current path if depth meets minimum
   if(bestDepth >= MinDepth)
     {
      ArrayResize(currentPath, bestDepth);
      for(int i = 0; i < bestDepth; i++)
         currentPath[i] = bestPath[i];
      pathDepth = bestDepth;
      currentDirection = bestDirection;
      UpdateKeyLevels();
      Print("Path set: depth=", pathDepth, ", direction=", currentDirection, ", lastHigherLow=", lastHigherLow, ", lastLowerHigh=", lastLowerHigh);
     }

   if(bestDepth < MinDepth)
     {
      currentDirection = 0;
      pathDepth = 0;
      ArrayResize(currentPath,0);
     }

  }

//+------------------------------------------------------------------+
//|  Bullish DFS                                                     |
//+------------------------------------------------------------------+
int DFS_Bullish(int nodeIdx, double lastLow, double lastHigh, int &path[])
  {
   //--- nodeIdx is a low
   nodes[nodeIdx].visited = true;

   //--- Initialize path with current node
   ArrayResize(path, 1);
   path[0] = nodeIdx;
   int maxDepth = 1;

   //--- Look for a high after this low
   for(int i = nodeIdx+1; i < nodeCount; i++)
     {
      if(nodes[i].type == 1 && !nodes[i].visited)
        {
         if(lastHigh == -1e9 || nodes[i].price > lastHigh)
           {
            //--- Look for a subsequent low after this high
            for(int j = i+1; j < nodeCount; j++)
              {
               if(nodes[j].type == -1 && !nodes[j].visited)
                 {
                  if(nodes[j].price > lastLow)
                    {
                     int subPath[];
                     int subDepth = DFS_Bullish(j, nodes[j].price, nodes[i].price, subPath);

                     if(subDepth + 2 > maxDepth)
                       {
                        maxDepth = subDepth + 2;
                        ArrayResize(path, maxDepth);
                        path[0] = nodeIdx;        // current low
                        path[1] = i;              // intermediate high
                        for(int k = 2; k < maxDepth; k++)
                           path[k] = subPath[k-2]; // subsequent nodes
                       }
                    }
                 }
              }
           }
        }
     }
   nodes[nodeIdx].visited = false;
   return maxDepth;
  }

Die Funktion AddNode() ist dafür zuständig, neu erkannte Swing-Punkte als Knoten im Strukturgraphen des Systems zu speichern. Bevor ein neuer Knoten hinzugefügt wird, prüft die Funktion, ob bereits ein Knoten mit demselben Zeitstempel vorhanden ist, um Duplikate zu vermeiden. Wenn das Knoten-Array seine aktuelle Kapazität erreicht, wird die Größe des Arrays dynamisch angepasst, um zusätzlichen Speicherplatz zu schaffen, wobei aus Effizienzgründen eine zusätzliche Pufferkapazität beibehalten wird. Sobald Speicherplatz verfügbar ist, speichert die Funktion den Zeitpunkt, den Kurs, den Typ (Swing-Hoch oder Swing-Tief), die Indexposition und das Status-Flag für besuchte Knoten, die später vom DFS-Algorithmus während der Durchquerung verwendet werden. Nachdem der Knoten erfolgreich gespeichert wurde, wird die Gesamtanzahl der Knoten erhöht und eine Diagnosemeldung ausgegeben, sodass das System verfolgen kann, wie sich der Graph der Marktstruktur im Laufe der Zeit entwickelt.

Die Funktion FindBestPath() wertet alle gespeicherten Swing-Knoten als mögliche Startpunkte aus und versucht, sowohl bullische als auch bärische Strukturverläufe zu konstruieren. Bei Aufwärtstrends beginnt der Algorithmus an einem Swing-Tief und sucht nach Sequenzen aus höheren Hochs und höheren Tiefs, während Abwärtstrends an Swing-Hochs beginnen und nach niedrigeren Tiefs und niedrigeren Hochs suchen. Diese Durchsuchung wird rekursiv durch die entsprechenden Funktionen, insbesondere DFSBullish() bzw. DFSBearish(), durchgeführt, die den Knotengrafen vorwärts durchläuft und einen Pfad aufbaut, solange die strukturellen Bedingungen gültig bleiben.

Während der Durchquerung verfolgt der Algorithmus den tiefsten gültigen Pfad, der den stärksten strukturellen Verlauf auf dem Markt widerspiegelt. Sobald der beste Pfad ermittelt wurde und dessen Tiefe den Mindestschwellenwert überschreitet, aktualisiert der EA den currentPath, legt die Handelsrichtung fest und erfasst wichtige strukturelle Niveaus wie das letzte höhere Tief oder das letzte niedrigere Hoch. Wenn kein Pfad die erforderliche strukturelle Tiefe erfüllt, löscht das System den aktiven Pfad und wartet darauf, dass sich eine neue Marktstruktur bildet.

//+------------------------------------------------------------------+
//|  Bearish DFS                                                     |
//+------------------------------------------------------------------+
int DFS_Bearish(int nodeIdx, double lastHigh, double lastLow, int &path[])
  {
   //--- nodeIdx is a high
   nodes[nodeIdx].visited = true;

   //--- Initialize path with current node
   ArrayResize(path, 1);
   path[0] = nodeIdx;
   int maxDepth = 1;

   for(int i = nodeIdx+1; i < nodeCount; i++)
     {
      if(nodes[i].type == -1 && !nodes[i].visited)
        {
         if(lastLow == 1e9 || nodes[i].price < lastLow)
           {
            for(int j = i+1; j < nodeCount; j++)
              {
               if(nodes[j].type == 1 && !nodes[j].visited)
                 {
                  if(nodes[j].price < lastHigh)
                    {
                     int subPath[];
                     int subDepth = DFS_Bearish(j, nodes[j].price, nodes[i].price, subPath);

                     if(subDepth + 2 > maxDepth)
                       {
                        maxDepth = subDepth + 2;
                        ArrayResize(path, maxDepth);
                        path[0] = nodeIdx;        // current high
                        path[1] = i;              // intermediate low
                        for(int k = 2; k < maxDepth; k++)
                           path[k] = subPath[k-2]; // subsequent nodes
                       }
                    }
                 }
              }
           }
        }
     }
   nodes[nodeIdx].visited = false;
   return maxDepth;
  }

//+------------------------------------------------------------------+
//| Invalidation and Backtracking                                    |
//+------------------------------------------------------------------+
bool IsPathInvalidated()
  {
   if(pathDepth == 0)
      return false;

   double price = currentTick.bid;
   Print("Checking invalidation: price=", price, ", lastHigherLow=", lastHigherLow, ", lastLowerHigh=", lastLowerHigh);

   if(currentDirection == 1) // bullish
     {
      if(price < lastHigherLow - (TargetTolerance * Point()))
        {
         Print("Bullish path invalidated: price below lastHigherLow");
         return true;
        }
     }
   else
      if(currentDirection == -1) // bearish
        {
         if(price > lastLowerHigh + (TargetTolerance * Point()))
           {
            Print("Bearish path invalidated: price above lastLowerHigh");
            return true;
           }
        }
   return false;
  }

//+------------------------------------------------------------------+
//| BacktrackAndSwitch                                               |
//+------------------------------------------------------------------+
void BacktrackAndSwitch()
  {
   currentDirection = 0;
   pathDepth = 0;
   ArrayResize(currentPath, 0);
   lastHigherLow = 0;
   lastLowerHigh = 0;
   Print("Backtrack: reset all path state.");
  }

//+------------------------------------------------------------------+
//|  UpdateKeyLvls                                                   |
//+------------------------------------------------------------------+
void UpdateKeyLevels()
  {
   if(pathDepth == 0)
      return;

   if(currentDirection == 1)
     {
      for(int i = pathDepth-1; i >= 0; i--)
        {
         if(nodes[currentPath[i]].type == -1)
           {
            lastHigherLow = nodes[currentPath[i]].price;
            break;
           }
        }
     }
   else
      if(currentDirection == -1)
        {
         for(int i = pathDepth-1; i >= 0; i--)
           {
            if(nodes[currentPath[i]].type == 1)
              {
               lastLowerHigh = nodes[currentPath[i]].price;
               break;
              }
           }
        }
  }

Die Funktion DFS_Bearish() implementiert die Tiefensuche, um eine bärische Marktstruktur innerhalb des Knotengrafen zu identifizieren. Ausgehend von einem Swing-Hoch-Knoten untersucht der Algorithmus nachfolgende Knoten, um eine gültige bärische Sequenz aus niedrigeren Tiefs und niedrigeren Hochs zu bilden. Die Funktion markiert den aktuellen Knoten als besucht, um zu verhindern, dass derselbe Knoten während der Rekursion erneut aufgerufen wird, initialisiert den Pfad mit dem Startknoten und durchläuft dann die Knotenliste in Vorwärtsrichtung. Wird ein gültiges Swing-Tief gefunden, das unter dem vorherigen Tief liegt, sucht der Algorithmus anschließend nach einem darauf folgenden Swing-Hoch, das unter dem vorherigen Hoch liegt, wodurch die bärische Strukturregel beibehalten wird.

Wird eine solche Sequenz gefunden, ruft die Funktion sich selbst rekursiv auf, um die tieferen Verzweigungen der Struktur weiter zu untersuchen. Während dieses Vorgangs verfolgt das Programm den tiefsten gültigen Pfad und aktualisiert das Pfad-Array, sobald eine längere Abwärtssequenz entdeckt wird. Sobald die Erkundung des Zweigs abgeschlossen ist, wird die Markierung als besucht vom Knoten entfernt, damit andere Durchlaufpfade ihn auswerten können, und die Funktion gibt die gefundene maximale Tiefe zurück.

Der zweite Teil des Codes befasst sich mit der Pfadvalidierung, der strukturellen Ungültigkeitsprüfung und dem Backtracking – allesamt entscheidende Komponenten bei der Anwendung der DFS-Logik in einer Handelsumgebung. Die Funktion IsPathInvalidated() prüft, ob der aktuelle Strukturpfad weiterhin gültig ist, indem sie den aktuellen Marktkurs mit wichtigen Strukturniveaus vergleicht, wie beispielsweise dem jüngsten höheren Tiefpunkt bei bullischen Pfaden oder dem niedrigeren Hochpunkt bei bärischen Pfaden. Wenn der Kurs diese Niveaus innerhalb der festgelegten Toleranzgrenze überschreitet, betrachtet der Algorithmus den aktuellen strukturellen Verlauf als ungültig.

In diesem Fall setzt die Funktion BacktrackAndSwitch() den DFS-Zustand zurück, indem sie den aktiven Pfad löscht, die Richtungsvoreinstellung aufhebt und die Schlüsselebenen zurücksetzt, sodass das System nach einem neuen Strukturpfad suchen kann. Die Funktion UpdateKeyLevels() ergänzt diesen Vorgang, indem sie den aktiven Pfad durchsucht, um die jüngste Strukturebene zu ermitteln, die für die aktuelle Trendrichtung relevant ist. Diese Niveaus dienen als Anhaltspunkte, um festzustellen, ob die aktuelle Marktentwicklung den aktiven DFS-Pfad weiterhin stützt oder ob der Algorithmus ein Backtracking durchführt und alternative Marktrichtungen untersuchen muss.

//+------------------------------------------------------------------+
//| Target Identification                                            |
//+------------------------------------------------------------------+
bool IsTargetReached(SwingNode &node)
  {
   double targetPrice = 0;
   int barsToCheck = 20;

   if(barsCount < barsToCheck)
     {
      Print("Not enough bars for target check: ", barsCount, " < ", barsToCheck);
      return false;
     }

   if(currentDirection == 1) // bullish target is above
     {
      double highArray[];
      int copied = CopyHigh(_Symbol, _Period, 1, barsToCheck, highArray);
      if(copied <= 0)
        {
         Print("Failed to copy highs for target. Error: ", GetLastError());
         return false;
        }

      int highestIdx = ArrayMaximum(highArray);
      if(highestIdx >= 0)
         targetPrice = highArray[highestIdx];
     }
   else // bearish
     {
      double lowArray[];
      int copied = CopyLow(_Symbol, _Period, 1, barsToCheck, lowArray);
      if(copied <= 0)
        {
         Print("Failed to copy lows for target. Error: ", GetLastError());
         return false;
        }

      int lowestIdx = ArrayMinimum(lowArray);
      if(lowestIdx >= 0)
         targetPrice = lowArray[lowestIdx];
     }

   double distance = MathAbs(node.price - targetPrice);
   Print("Target check: node.price=", node.price, ", targetPrice=", targetPrice, ", distance=", distance, ", tolerance=", TargetTolerance);

   if(distance < TargetTolerance)
      return true;
   return false;
  }

//+------------------------------------------------------------------+
//| Trade Execution                                                  |
//+------------------------------------------------------------------+
void ExecuteTrade(ENUM_ORDER_TYPE orderType, string comment)
  {
   Print("Attempting to execute trade: ", (orderType==ORDER_TYPE_BUY?"BUY":"SELL"), ", comment: ", comment);

   //--- Calculate position size based on risk
   double lotSize = 0;

   if(UseRiskManagement)
     {
      lotSize = CalculateLotSize(orderType, StopLossPoints);
      if(lotSize <= 0)
        {
         Print("Failed to calculate position size, using fixed lot size: ", FixedLotSize);
         lotSize = FixedLotSize;
        }
     }
   else
     {
      lotSize = FixedLotSize;
     }

   if(lotSize <= 0)
     {
      Print("Invalid lot size. Trade not executed.");
      return;
     }
   Print("Lot size: ", lotSize);

   //--- Get current tick data
   if(!SymbolInfoTick(_Symbol, currentTick))
     {
      Print("Failed to get current tick data. Error: ", GetLastError());
      return;
     }

   //--- Calculate stop loss and take profit prices
   double stopLoss = 0.0, takeProfit = 0.0;
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

   if(orderType == ORDER_TYPE_BUY)
     {
      stopLoss = NormalizeDouble(currentTick.bid - (StopLossPoints * point), digits);
      takeProfit = NormalizeDouble(currentTick.ask + (TakeProfitPoints * point), digits);
     }
   else
      if(orderType == ORDER_TYPE_SELL)
        {
         stopLoss = NormalizeDouble(currentTick.ask + (StopLossPoints * point), digits);
         takeProfit = NormalizeDouble(currentTick.bid - (TakeProfitPoints * point), digits);
        }
   Print("SL: ", stopLoss, ", TP: ", takeProfit);

   //--- Validate stop levels before sending the trade
   if(!ValidateStopLevels(orderType, currentTick.ask, currentTick.bid, stopLoss, takeProfit))
     {
      Print("Invalid stop levels. Trade not executed.");
      return;
     }

   if(PositionSelect(_Symbol))
      return;

   //--- Execute trade
   bool requestSent = false;

   if(orderType == ORDER_TYPE_BUY)
     {
      requestSent = TradeManager.Buy(lotSize, _Symbol, 0, stopLoss, takeProfit, comment);
     }
   else
      if(orderType == ORDER_TYPE_SELL)
        {
         requestSent = TradeManager.Sell(lotSize, _Symbol, 0, stopLoss, takeProfit, comment);
        }

   //--- Check if the request was sent successfully
   if(requestSent)
     {
      //--- Check the server's return code from the trade operation
      uint result = TradeManager.ResultRetcode();
      Print("Trade request sent. Retcode: ", result, " - ", TradeManager.ResultRetcodeDescription());
      if(result == TRADE_RETCODE_DONE || result == TRADE_RETCODE_DONE_PARTIAL)
        {
         Print("Trade executed successfully. Ticket: ", TradeManager.ResultOrder());
         inTrade = true;
         tradeTicket = TradeManager.ResultOrder();
        }
      else
         if(result == TRADE_RETCODE_REQUOTE || result == TRADE_RETCODE_TIMEOUT || result == TRADE_RETCODE_PRICE_CHANGED)
           {
            Print("Trade failed due to price change. Retcode: ", TradeManager.ResultRetcodeDescription());
           }
         else
           {
            Print("Trade execution failed. Retcode: ", TradeManager.ResultRetcodeDescription());
           }
     }
   else
     {
      Print("Failed to send trade request. Last Error: ", GetLastError());
     }
  }

Die Funktion IsTargetReached() dient dazu, anhand der jüngsten Kurshochs und -tiefs festzustellen, ob ein Strukturknoten ein potenzielles Marktziel erreicht hat. Zunächst stellt die Funktion sicher, dass genügend Bars für die Analyse zur Verfügung stehen, und ermittelt dann das Zielniveau in Abhängigkeit von der aktuellen Marktrichtung. Bei einem bullischen Szenario ermittelt der Algorithmus das höchste Hoch aus den zuletzt gebildeten Bars, während er bei einem bärischen Szenario das tiefste Tief ermittelt. Diese Werte stellen nahegelegene Liquiditäts- oder Ausdehnungsziele dar, die der Kurs möglicherweise anzustreben versucht. Nach der Ermittlung des Zielniveaus berechnet die Funktion die Differenz zwischen dem Kurs des Knotens und dem ermittelten Ziel. Liegt dieser Abstand innerhalb der definierten TargetTolerance, gibt die Funktion true zurück, was bedeutet, dass die strukturelle Bewegung ihren Zielbereich tatsächlich erreicht hat.

Die Funktion ExecuteTrade() übernimmt den gesamten Prozess der Handelsausführung, sobald die strukturellen Bedingungen des DFS eine gültige Gelegenheit signalisieren. Zunächst wird die angemessene Positionsgröße ermittelt, entweder durch eine risikobasierte Berechnung oder – falls das Risikomanagement deaktiviert ist – durch Verwendung einer festen Lotgröße. Nach dem Abruf der aktuellen Markt-Tick-Daten berechnet die Funktion die Stop-Loss- und Take-Profit-Niveaus auf der Grundlage der konfigurierten Punktabstände und normiert diese entsprechend der Genauigkeit des Symbols.

Vor dem Absenden der Order überprüft das System die Stop-Levels, um sicherzustellen, dass sie den Vorgaben des Brokers entsprechen, und stellt zudem sicher, dass für das Symbol derzeit keine Position offen ist. Sind alle Bedingungen erfüllt, sendet die Funktion über die Klasse CTrade einen Kauf- oder Verkaufsauftrag und wertet anschließend den Antwortcode des Servers aus. Wird der Trade erfolgreich ausgeführt, erfasst der EA die Ticketnummer und aktualisiert den Handelsstatus, sodass das System die aktive Position verfolgen und verwalten kann.

//+------------------------------------------------------------------+
//| Visualization                                                    |
//+------------------------------------------------------------------+
void DrawSwings()
  {
   //--- Don't draw if disabled
   if(!EnableVisual)
      return;

   string prefix = "SwingGraph_";
   ObjectsDeleteAll(0, prefix);

   for(int i = 0; i < nodeCount; i++)
     {
      string name = prefix + "Node_" + IntegerToString(i);
      color clr = (nodes[i].type == 1) ? clrBlue : clrRed;

      if(!ObjectCreate(0, name, OBJ_ARROW, 0, nodes[i].time, nodes[i].price))
         Print("Failed to create object ", name, ". Error: ", GetLastError());
      else
        {
         ObjectSetInteger(0, name, OBJPROP_ARROWCODE, (nodes[i].type == 1) ? 241 : 242);
         ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
         ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);
        }
     }

   //--- Draw edges connecting consecutive nodes
   for(int i = 1; i < nodeCount; i++)
     {
      string name = prefix + "Edge_" + IntegerToString(i-1) + "_" + IntegerToString(i);
      if(!ObjectCreate(0, name, OBJ_TREND, 0, nodes[i-1].time, nodes[i-1].price, nodes[i].time, nodes[i].price))
         Print("Failed to create edge ", name, ". Error: ", GetLastError());
      else
        {
         ObjectSetInteger(0, name, OBJPROP_COLOR, clrGray);
         ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
         ObjectSetInteger(0, name, OBJPROP_RAY, false);
        }
     }
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void DrawActivePath()
  {
   //--- Don't draw if disabled
   if(!EnableVisual)
      return;
   if(pathDepth < 2)
      return;

   string prefix = "SwingGraph_Path_";
   for(int i = 1; i < pathDepth; i++)
     {
      string name = prefix + IntegerToString(i-1) + "_" + IntegerToString(i);
      int from = currentPath[i-1];
      int to = currentPath[i];
      if(from >= 0 && from < nodeCount && to >= 0 && to < nodeCount)
        {
         if(!ObjectCreate(0, name, OBJ_TREND, 0, nodes[from].time, nodes[from].price, nodes[to].time, nodes[to].price))
            Print("Failed to create path segment ", name, ". Error: ", GetLastError());
         else
           {
            ObjectSetInteger(0, name, OBJPROP_COLOR, (currentDirection == 1) ? clrGreen : clrOrange);
            ObjectSetInteger(0, name, OBJPROP_WIDTH, 3);
            ObjectSetInteger(0, name, OBJPROP_RAY, false);
           }
        }
     }
  }
//+------------------------------------------------------------------+

Die Funktionen DrawSwings() und DrawActivePath() dienen dazu, die Graphstruktur und den aktiven DFS-Pfad direkt im Handelschart darzustellen. Die Funktion DrawSwings() löscht zunächst alle zuvor gezeichneten Objekte mit dem Präfix SwingGraph_, um Doppelzeichnungen zu vermeiden, und durchläuft anschließend alle gespeicherten Swing-Knoten, um diese als Pfeilobjekte an ihren jeweiligen Zeit- und Kurskoordinaten darzustellen. Swing-Hochs werden durch blaue Pfeile angezeigt, während Swing-Tiefs rot dargestellt werden, wodurch sich strukturelle Wendepunkte leicht erkennen lassen. Zudem zeichnet die Funktion graue Trendlinien zwischen aufeinanderfolgenden Knoten und erstellt so eine visuelle Darstellung der erkannten Marktstruktur.

Die Funktion DrawActivePath() hebt den aktuellen, durch DFS ermittelten Strukturpfad hervor, dem der Algorithmus folgt. Wenn die Visualisierung aktiviert ist und der Pfad mindestens zwei Knoten enthält, durchläuft die Funktion das Array currentPath und zeichnet dickere Trendlinien zwischen jedem Knotenpaar im Pfad. Diese Pfadsegmente sind bei bullischen Strukturen grün und bei bärischen Strukturen orange eingefärbt, sodass Händler klar erkennen können, welche Richtungsfolge der Algorithmus derzeit als die wahrscheinlichste Marktentwicklung ansieht. Gemeinsam wandeln diese Visualisierungstools die zugrunde liegende graphbasierte Analyse in eine intuitive Chartdarstellung um, die den Nutzern hilft, zu verstehen, wie der DFS-Algorithmus die Marktstruktur in Echtzeit interpretiert.



Backtest-Ergebnisse

Das vorrangige Ziel des Systems bestand darin, die subjektive Struktur der Kursentwicklung in eine deterministische, regelbasierte Abfolge von Kurs-Swings umzuwandeln, bei der Trades erst dann ausgeführt werden, wenn ein gültiger struktureller Verlauf eine ausreichende Tiefe erreicht und die Marktrichtungstendenz bestätigt. Das Backtesting wurde für das Währungspaar XAUUSD im M15-Zeitrahmen über einen Testzeitraum von zwei Monaten (1. Dezember 2025 bis 30. Januar 2026) mit den Standardeinstellungen durchgeführt:

Die Ergebnisse deuten darauf hin, dass dieses Ziel weitgehend erreicht wurde, was darauf hindeutet, dass der DFS-Ansatz Trades erfolgreich filtert, indem er vor dem Einstieg bestätigte strukturelle Sequenzen verlangt.


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir das Konzept der Tiefensuche aus der Graphentheorie auf den Handel übertragen haben, indem wir die Marktstruktur in ein graphbasiertes Modell miteinander verbundener Swing-Punkte umgewandelt haben, bei dem jedes Swing-Hoch bzw. jedes Swing-Tief einen Knoten darstellt und die Beziehungen zwischen ihnen Kanten bilden. In diesem Rahmen untersucht der Algorithmus mögliche Kursverläufe, indem er zunächst einem strukturellen Zweig – beispielsweise einer Abfolge von höheren Hochs und höheren Tiefs – tief folgt, bevor er alternative Richtungen in Betracht zieht. Um dies in die Praxis umzusetzen, haben wir einen funktionsfähigen MQL5-Expert Advisor implementiert, der eine Swing-Erkennung, die Speicherung von Knoten über die SwingNode-Struktur, eine DFS-Durchquerung für bullische und bärische Pfade sowie eine Pfadauswahl auf Basis der strukturellen Tiefe unter Verwendung des MinDepth-Kriteriums umfasst. Das System ermittelt zudem Schlüsselwerte für die Ungültigkeitsstufen (lastHigherLow und lastLowerHigh) mit einer konfigurierbaren Toleranz (TargetTolerance), führt Trades mit definierten SL/TP-Werten und optionaler risikobasierter Positionsgröße aus und visualisiert die resultierenden Knoten sowie den aktiven Strukturpfad direkt im Chart.

Zusammenfassend lässt sich sagen, dass die Integration von DFS in den Handel die ungefilterten Kursbewegungen in einen strukturierten und navigierbaren Suchraum verwandelt, den Händler programmgesteuert analysieren und testen können. Mit diesem Framework können Sie die Marktstruktur als Graph modellieren, mögliche Bewegungspfade zwischen Swing-Niveaus nachverfolgen und objektiv feststellen, wann sich eine Tendenz bestätigt oder widerlegt. Die daraus resultierende EA-Vorlage bietet einen reproduzierbaren und testbaren Arbeitsablauf, mit dem Sie das System kompilieren, im Strategietester ausführen und mit Parametern wie SwingPeriod, MinDepth und den Risikoeinstellungen experimentieren können, während Sie beobachten, wie sich pathDepth, currentDirection und die strukturellen Ebenen entwickeln. Durch die Formalisierung der Swing-Logik in deterministische Regeln wandelt dieser Ansatz die subjektive Chartinterpretation in ein überprüfbares Suchproblem um und ermöglicht so systematisches Experimentieren, die Verfeinerung der Handelslogik sowie die Entwicklung fortgeschrittener algorithmischer Strategien.

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

Beigefügte Dateien |
DFS.mq5 (33.12 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (1)
Stanislav Korotky
Stanislav Korotky | 13 März 2026 in 15:24

Das sieht nicht nach einem Graphen aus – sondern lediglich nach einer Suche nach zwei Arten von Extremwerten in einem chronologisch angeordneten Array. Das erinnert mich an einen Zickzack, der mit verschachtelten Schleifen untersucht wird.

Es wäre graforientierter, auf der Historie aufzubauen und statistisch aussagekräftige Gewichte für Kanten zwischen den wahrscheinlichsten Schwingungssequenzen zu ermitteln, wenn die Wahrscheinlichkeit zukünftiger Verläufe berechnet wird.

MetaTrader 5 – Leitfaden für maschinelles Lernen (Teil 8): Bayes’sche Hyperparameter-Optimierung mit Purged Cross-Validation und Trial Pruning MetaTrader 5 – Leitfaden für maschinelles Lernen (Teil 8): Bayes’sche Hyperparameter-Optimierung mit Purged Cross-Validation und Trial Pruning
GridSearchCV und RandomizedSearchCV teilen eine grundlegende Einschränkung im finanziellen ML: Jeder Versuch ist unabhängig, sodass sich die Suchqualität durch zusätzliche Rechenleistung nicht verbessert. Dieser Artikel integriert Optuna – unter Verwendung des Tree-structured Parzen Estimator – mit PurgedKFold-Kreuzvalidierung, HyperbandPruner-Frühstopp und einer Dual-Weight-Konvention, die Trainingsgewichte von Bewertungsgewichten trennt. Das Ergebnis ist ein System aus fünf Komponenten: eine Zielfunktion mit Fold-Level-Pruning, eine Vorschlagsebene, die das Gewichtungsschema gemeinsam mit den Modell-Hyperparametern optimiert, ein finanziell kalibrierter Pruner, ein fortsetzbarer, SQLite-basierter Orchestrator und ein Konverter für das scikit-learn cv_results_-Format. Der Artikel zieht zudem die Grenze – basierend auf Timothy Masters – zwischen statistischen Zielen, bei denen eine gezielte Suche vorteilhaft ist, und finanziellen Zielen, bei denen sie schädlich ist.
Paketbasierter Ansatz mit KnitPkg für die MQL5-Entwicklung Paketbasierter Ansatz mit KnitPkg für die MQL5-Entwicklung
Um maximale Zuverlässigkeit und Produktivität bei mit MQL erstellten MetaTrader-Produkten zu gewährleisten, empfiehlt dieser Artikel einen Entwicklungsansatz, der auf wiederverwendbaren Paketen basiert, die von KnitPkg, einem Projektmanager für MQL5/MQL4, verwaltet werden. Ein Paket kann als Baustein für andere Pakete oder als Grundlage für fertige Artefakte dienen, die direkt auf der MetaTrader-Plattform ausgeführt werden, wie beispielsweise EAs, Indikatoren und vieles mehr.
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.
Entwicklung eines dynamischen Multi-Pair-EA (Teil 7): Paarübergreifende Korrelationsabbildung für die Handelsfilterung in Echtzeit Entwicklung eines dynamischen Multi-Pair-EA (Teil 7): Paarübergreifende Korrelationsabbildung für die Handelsfilterung in Echtzeit
In diesem Abschnitt werden wir eine Echtzeit-Korrelationsmatrix in einen Expert Advisor für mehrere Symbole integrieren, um redundante oder risikobehaftete Trades zu vermeiden. Durch die dynamische Messung der Wechselbeziehungen zwischen Symbolen filtert der EA Einstiegssignale heraus, die im Widerspruch zur bestehenden Exposition stehen, wodurch die Ausgewogenheit des Portfolios verbessert, das systemische Risiko verringert und die Qualität der Trades insgesamt verbessert wird.