Graphentheorie: Einsatz von Breadth-First Search (BFS) im Trading
Inhaltsverzeichnis
- Einführung
- Systemübersicht und BFS-Grundlagen
- Die ersten Schritte
- Backtest-Ergebnisse
- 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:

Nachfolgend die Equity-Kurve und der 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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Vom Einsteiger zum Experten: Erstellung eines Liquiditätszonenindikators
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Automatisierung von Handelsstrategien in MQL5 (Teil 47): Nick Rypock Trailing Reverse (NRTR) mit Hedging-Funktionen
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.