
Automatisieren von Handelsstrategien in MQL5 (Teil 6): Beherrschen der Erkennung von Auftragsblöcken für den Handel des Smart Money
Einführung
Im vorangegangenen Artikel (Teil 5 der Serie) haben wir die Strategie „Adaptive Crossover RSI Trading Suite“ entwickelt, die das Kreuzen von gleitenden Durchschnitten mit RSI-Filterung kombiniert, um Handelsmöglichkeiten mit hoher Wahrscheinlichkeit zu identifizieren. In Teil 6 konzentrieren wir uns nun auf die reine Preisaktionsanalyse mit einem automatisierten Das System zur Erkennung von Auftragsblöcken in MetaQuotes Language 5 (MQL5), einem leistungsstarken Tool, das im Smart Money Trading eingesetzt wird. Diese Strategie identifiziert wichtige institutionelle Auftragsblöcke - Zonen, in denen große Marktteilnehmer Positionen akkumulieren oder verteilen - und hilft den Händlern, potenzielle Umkehrungen und Trendfortsetzungen zu antizipieren.
Im Gegensatz zu traditionellen Indikatoren stützt sich dieser Ansatz vollständig auf die Preisstruktur und erkennt Auftragsblöcke (order blocks), die steigende oder fallende Kurse induzieren, dynamisch auf der Grundlage des historischen Preisverhaltens. Das System visualisiert diese Zonen direkt auf dem Chart und bietet Händlern einen klaren Marktkontext und potenzielle Handels-Setups. In diesem Artikel werden wir die schrittweise Entwicklung dieser Strategie behandeln, von der Definition der Auftragsblöcke bis zu ihrer Implementierung in MQL5, dem Backtest ihrer Wirksamkeit und der Analyse der Performance. Wir werden diese Diskussion in den folgenden Abschnitten strukturieren:
- Blaupause der Strategie
- Implementation in MQL5
- Backtest
- Schlussfolgerung
Am Ende des Kurses verfügen Sie über eine solide Grundlage für die Automatisierung der Erkennung von Auftragsblöcken, die es Ihnen ermöglicht, Smart-Money-Konzepte in Ihre Handelsalgorithmen zu integrieren. Fangen wir an.
Blaupause der Strategie
Wir beginnen mit der Identifizierung von Konsolidierungsbereichen, die auftreten, wenn sich der Preis innerhalb einer begrenzten Spanne ohne eine klare Trendrichtung bewegt. Zu diesem Zweck werden wir den Markt nach Bereichen absuchen, in denen die Preisentwicklung keine signifikanten Ausbrüche aufweist. Sobald wir einen Ausbruch aus diesem Bereich feststellen, werden wir prüfen, ob ein Auftragsblock gebildet werden kann. Unser Validierungsprozess umfasst die Überprüfung der drei vorangegangenen Kerzen vor dem Ausbruch. Wenn diese Kerzen eine impulsive Bewegung aufweisen, stufen wir den Auftragsblock je nach Ausbruchsrichtung als „bullish“ oder „bearish“ ein. Ein „bullish“ Auftragsblock wird identifiziert, wenn der Ausbruch nach oben erfolgt, während ein „bearish“ Auftragsblock markiert wird, wenn der Ausbruch nach unten erfolgt. Nach der Validierung wird der Auftragsblock im Chart als Referenz dargestellt. Hier ist ein Beispiel.
Wenn die drei vorangegangenen Kerzen keine impulsive Bewegung aufweisen, wird ein Auftragsblock nicht validiert. Stattdessen werden wir nur den Konsolidierungsbereich einzeichnen und sicherstellen, dass wir keine schwachen oder unbedeutenden Zonen markieren. Nachdem wir die gültigen Auftragsblöcke markiert haben, werden wir die Preisentwicklung kontinuierlich beobachten. Wenn der Kurs zu einem zuvor bestätigten Auftragsblock zurückkehrt, führen wir Handelsgeschäfte in Übereinstimmung mit der ursprünglichen Ausbruchsrichtung aus, in der Erwartung, dass sich der Trend fortsetzt. Wenn ein Auftragsblock jedoch über den letzten signifikanten Preispunkt hinausgeht, wird er aus der Liste der gültigen Auftragsblöcke entfernt, um sicherzustellen, dass wir nur relevante und frische Zonen handeln. Dieser strukturierte Ansatz wird uns helfen, uns auf wahrscheinliche Setups zu konzentrieren, schwache Ausbrüche herauszufiltern und sicherzustellen, dass unsere Handelsgeschäfte mit den Bewegungen des Smart Money übereinstimmen.
Implementation in MQL5
Um die Identifizierung der Auftragsblöcke in MQL5 zu implementieren, müssen wir einige globale Variablen definieren, die während des gesamten Prozesses erforderlich sind.
#include <Trade/Trade.mqh> CTrade obj_Trade; // Struct to hold both the price and the index of the high or low struct PriceIndex { double price; int index; }; // Global variables to track the range and breakout state PriceIndex highestHigh = {0, 0}; // Stores the highest high of the range PriceIndex lowestLow = {0, 0}; // Stores the lowest low of the range bool breakoutDetected = false; // Tracks if a breakout has occurred double impulseLow = 0.0; double impulseHigh = 0.0; int breakoutBarIndex = -1; // To track the bar at which breakout occurred datetime breakoutTime = 0; // To store the breakout time string totalOBs_names[]; datetime totalOBs_dates[]; bool totalOBs_is_signals[]; #define OB_Prefix "OB REC " #define CLR_UP clrLime #define CLR_DOWN clrRed bool is_OB_UP = false; bool is_OB_DOWN = false;
Wir beginnen damit, die Bibliothek „Trade.mqh“ einzubinden und ein „CTrade“-Objekt, „obj_Trade“, zu erstellen, um die Ausführung des Handels zu steuern. Wir definieren die Struktur „PriceIndex“, um sowohl das Preisniveau als auch den entsprechenden Index zu speichern, der uns hilft, den höchsten Höchststand und den niedrigsten Tiefststand innerhalb des Konsolidierungsbereichs zu verfolgen. Die globalen Variablen „highestHigh“ und „lowestLow“ speichern diese Schlüsselwerte, während das Flag „breakoutDetected“ anzeigt, ob ein Ausbruch stattgefunden hat.
Um impulsive Bewegungen zu validieren, führen wir „impulseLow“ und „impulseHigh“ ein, die helfen, die Stärke des Ausbruchs zu bestimmen. Die Variable „breakoutBarIndex“ erfasst den genauen Balken, an dem der Ausbruch stattfand, und „breakoutTime“ speichert den entsprechenden Zeitstempel. Für die Verwaltung der Auftragsblocks werden drei globale Arrays geführt: „totalOBs_names“, „totalOBs_dates“ und „totalOBs_is_signals“. Diese Arrays speichern die Namen der Auftragsblöcke, ihre jeweiligen Zeitstempel und ob es sich um gültige Handelssignale handelt.
Wir definieren das Präfix des Auftragsblocks als „OB_Prefix“ und weisen den Auftragsblöcke „bullish“ und „bearish“ Farben zu, „CLR_UP“ (lime) für „bullish“ und „CLR_DOWN“ (rot) für „bearish“. Schließlich helfen uns die booleschen Flags „is_OB_UP“ und „is_OB_DOWN“ dabei festzustellen, ob der letzte erkannte Auftragsblock „bullish“ oder „bearish“ ist. Wir brauchen die Auftragsblöcke bei der Initialisierung des Programms nicht zu verfolgen, da wir mit einem sauberen Neustart beginnen wollen. Daher werden wir die Steuerlogik direkt in OnTick implementieren.
//+------------------------------------------------------------------+ //| Expert ontick function | //+------------------------------------------------------------------+ void OnTick() { static bool isNewBar = false; int currBars = iBars(_Symbol, _Period); static int prevBars = currBars; // Detect a new bar if (prevBars == currBars) { isNewBar = false; } else if (prevBars != currBars) { isNewBar = true; prevBars = currBars; } if (!isNewBar) return; // Process only on a new bar int rangeCandles = 7; // Initial number of candles to check double maxDeviation = 50; // Max deviation between highs and lows in points int startingIndex = 1; // Starting index for the scan int waitBars = 3; //--- }
In der Ereignisbehandlung von OnTick wird zunächst die Bildung eines neuen Balkens anhand von „currBars“ und „prevBars“ erkannt. Wir setzen „isNewBar“ auf „true“, wenn ein neuer Balken erscheint, und kehren vorzeitig zurück, wenn kein neuer Balken erkannt wird. Dann definieren wir „rangeCandles“ als „7“, was die Mindestanzahl von Kerzen darstellt, die wir analysieren, um eine Konsolidierung zu erkennen. Die Variable „maxDeviation“ wird auf „50“ Punkte gesetzt, wodurch die zulässige Differenz zwischen dem höchsten und dem niedrigsten Preis innerhalb der Spanne begrenzt wird. Der „startingIndex“ wird auf „1“ initialisiert, um sicherzustellen, dass wir mit dem Scannen beim letzten abgeschlossenen Takt beginnen. Zusätzlich setzen wir „waitBars“ auf „3“, um festzulegen, wie viele Bars vergehen sollen, bevor ein Auftragsblock validiert wird. Als Nächstes müssen wir nach Konsolidierungsbereichen suchen und die Preise für die weitere Bestimmung gültiger Auftragsblöcke ermitteln.
// Check for consolidation or extend the range if (!breakoutDetected) { if (highestHigh.price == 0 && lowestLow.price == 0) { // If range is not yet established, look for consolidation if (IsConsolidationEqualHighsAndLows(rangeCandles, maxDeviation, startingIndex)) { GetHighestHigh(rangeCandles, startingIndex, highestHigh); GetLowestLow(rangeCandles, startingIndex, lowestLow); Print("Consolidation range established: Highest High = ", highestHigh.price, " at index ", highestHigh.index, " and Lowest Low = ", lowestLow.price, " at index ", lowestLow.index); } } else { // Extend the range if the current bar's prices remain within the range ExtendRangeIfWithinLimits(); } }
Bei jedem neuen Balken, der sich bildet, prüfen wir, ob es zu einer Konsolidierung kommt oder ob der bestehende Bereich erweitert wird, wenn kein Ausbruch festgestellt wurde. Wenn „highestHigh.price“ und „lowestLow.price“ beide null sind, bedeutet dies, dass noch kein Konsolidierungsbereich festgelegt wurde. Anschließend rufen wir die Funktion „IsConsolidationEqualHighsAndLows“ auf, um zu prüfen, ob die letzten „rangeCandles“ eine Konsolidierung innerhalb der zulässigen „maxDeviation“ bilden. Falls bestätigt, verwenden wir die Funktionen „GetHighestHigh“ und „GetLowestLow“, um die exakten Höchst- und Tiefstpreise innerhalb der Spanne zu ermitteln und ihre Werte zusammen mit den jeweiligen Bar-Indizes zu speichern.
Wenn bereits ein Bereich festgelegt wurde, stellen wir sicher, dass der aktuelle Balken innerhalb der festgelegten Grenzen bleibt, indem wir die Funktion „ExtendRangeIfWithinLimits“ aufrufen. Diese Funktion hilft, den Bereich dynamisch anzupassen, solange kein Ausbruch erfolgt. Hier ist das Codefragment der Implementierung der nutzerdefinierten Funktionen.
// Function to detect consolidation where both highs and lows are nearly equal bool IsConsolidationEqualHighsAndLows(int rangeCandles, double maxDeviation, int startingIndex) { // Loop through the last `rangeCandles` to check if highs and lows are nearly equal for (int i = startingIndex; i < startingIndex + rangeCandles - 1; i++) { // Compare the high of the current candle with the next one if (MathAbs(high(i) - high(i + 1)) > maxDeviation * Point()) { return false; // If the high difference is greater than allowed, it's not a consolidation } // Compare the low of the current candle with the next one if (MathAbs(low(i) - low(i + 1)) > maxDeviation * Point()) { return false; // If the low difference is greater than allowed, it's not a consolidation } } // If both highs and lows are nearly equal, it's a consolidation range return true; }
Wir definieren die boolesche Funktion „IsConsolidationEqualHighsAndLows“, die für die Erkennung von Konsolidierungen zuständig ist, indem sie prüft, ob die Hochs und Tiefs der letzten „rangeCandles“ innerhalb einer bestimmten „maxDeviation“ nahezu gleich sind. Wir erreichen dies, indem wir über jeden Balken iterieren, beginnend mit „startingIndex“, und die Höchst- und Tiefstwerte aufeinander folgender Kerzen vergleichen.
Innerhalb der for-Schleife verwenden wir die Funktion MathAbs, um die absolute Differenz zwischen dem Hoch des aktuellen Balkens („high(i)“) und dem nächsten Hoch zu berechnen. Wenn diese Differenz die in Punkte, Point, umgewandelte, maximale Abweichung überschreitet, gibt die Funktion sofort false zurück, was bedeutet, dass die Höchstwerte nicht gleich genug sind, um als Konsolidierung zu gelten. In ähnlicher Weise wird die Funktion MathAbs erneut angewandt, um die Tiefstwerte aufeinanderfolgender Balken („low(i)“ und „low(i + 1)“) zu vergleichen und sicherzustellen, dass die Tiefstwerte ebenfalls innerhalb der zulässigen Abweichung liegen. Wenn eine Prüfung fehlschlägt, wird die Funktion vorzeitig mit false beendet. Wenn alle Höchst- und Tiefstwerte innerhalb der akzeptablen Abweichung bleiben, wird true zurückgegeben, was eine gültige Konsolidierungsspanne bestätigt. Die nächsten Funktionen, die wir definieren, sind diejenigen, die für das Abrufen der höchsten und niedrigsten Barpreise verantwortlich sind.
// Function to get the highest high and its index in the last `rangeCandles` candles, starting from `startingIndex` void GetHighestHigh(int rangeCandles, int startingIndex, PriceIndex &highestHighRef) { highestHighRef.price = high(startingIndex); // Start by assuming the first candle's high is the highest highestHighRef.index = startingIndex; // The index of the highest high (starting with the `startingIndex`) // Loop through the candles and find the highest high and its index for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) { if (high(i) > highestHighRef.price) { highestHighRef.price = high(i); // Update highest high highestHighRef.index = i; // Update index of highest high } } } // Function to get the lowest low and its index in the last `rangeCandles` candles, starting from `startingIndex` void GetLowestLow(int rangeCandles, int startingIndex, PriceIndex &lowestLowRef) { lowestLowRef.price = low(startingIndex); // Start by assuming the first candle's low is the lowest lowestLowRef.index = startingIndex; // The index of the lowest low (starting with the `startingIndex`) // Loop through the candles and find the lowest low and its index for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) { if (low(i) < lowestLowRef.price) { lowestLowRef.price = low(i); // Update lowest low lowestLowRef.index = i; // Update index of lowest low } } }
Die Funktion „GetHighestHigh“ hat die Aufgabe, das höchste Hoch und den entsprechenden Index innerhalb der letzten „rangeCandles“-Balken, ausgehend vom „startingIndex“, zu ermitteln. Wir initialisieren „highestHighRef.price“ mit dem Hoch der ersten Kerze im Bereich („high(startingIndex)“) und setzen „highestHighRef.index“ auf „startingIndex“. Anschließend werden die verbleibenden Kerzen im angegebenen Bereich durchlaufen und geprüft, ob eine von ihnen einen höheren Preis als den aktuellen „highestHighRef.price“ aufweist. Wenn ein neuer Höchststand gefunden wird, aktualisieren wir sowohl „highestHighRef.price“ als auch „highestHighRef.index“. Diese Funktion hilft uns, die obere Grenze eines Konsolidierungskreises zu bestimmen.
In ähnlicher Weise findet die Funktion „GetLowestLow“ den niedrigsten Wert und seinen Index innerhalb desselben Bereichs. Wir initialisieren „lowestLowRef.price“ mit „low(startingIndex)“ und „lowestLowRef.index“ mit „startingIndex“. Beim Durchlaufen der Kerzen wird geprüft, ob eine Kerze einen niedrigeren Preis als den aktuellen „lowestLowRef.price“ hat. Wenn ja, aktualisieren wir sowohl „lowestLowRef.price“ als auch „lowestLowRef.index“. Diese Funktion ermittelt die untere Grenze eines Konsolidierungskreises. Schließlich haben wir die Funktion, die den Bereich erweitern wird.
// Function to extend the range if the latest bar remains within the range limits void ExtendRangeIfWithinLimits() { double currentHigh = high(1); // Get the high of the latest closed bar double currentLow = low(1); // Get the low of the latest closed bar if (currentHigh <= highestHigh.price && currentLow >= lowestLow.price) { // Extend the range if the current bar is within the established range Print("Range extended: Including candle with High = ", currentHigh, " and Low = ", currentLow); } else { Print("No extension possible. The current bar is outside the range."); } }
Hier sorgt die Funktion „ExtendRangeIfWithinLimits“ dafür, dass der zuvor ermittelte Konsolidierungsbereich gültig bleibt, wenn neue Balken weiterhin in seine Grenzen fallen. Zunächst werden mit den Funktionen „high(1)“ und „low(1)“ der Höchst- und Tiefststand der zuletzt geschlossenen Kerze ermittelt. Dann wird geprüft, ob der „currentHigh“ kleiner oder gleich dem „highestHigh.price“ und der „currentLow“ größer oder gleich dem „lowestLow.price“ ist. Wenn beide Bedingungen erfüllt sind, wird der Spanne erweitert, und es wird eine Bestätigungsmeldung gedruckt, die angibt, dass die neue Kerze im bestehenden Spanne enthalten ist.
Bewegt sich die neue Kerze außerhalb des festgelegten Bereichs, erfolgt keine Erweiterung, und es wird eine Meldung ausgegeben, dass der Bereich nicht erweitert werden kann. Diese Funktion spielt eine Schlüsselrolle bei der Aufrechterhaltung gültiger Konsolidierungszonen und verhindert eine unnötige Ausbruchserkennung, wenn der Markt innerhalb der vordefinierten Spanne bleibt.
Wir haben auch vordefinierte Funktionen verwendet, die für das Abrufen von Barpreisdaten zuständig sind. Hier sind ihre Codefragmente.
//--- One-line functions to access price data double high(int index) { return iHigh(_Symbol, _Period, index); } double low(int index) { return iLow(_Symbol, _Period, index); } double open(int index) { return iOpen(_Symbol, _Period, index); } double close(int index) { return iClose(_Symbol, _Period, index); } datetime time(int index) { return iTime(_Symbol, _Period, index); }
Diese einzeiligen Funktionen „high“, „low“, „open“, „close“ und „time“ dienen als einfache Wrapper zum Abrufen von Preis- und Zeitdaten historischer Balken. Jede Funktion ruft die entsprechende integrierte MQL5-Funktion - iHigh, iLow, iOpen, iClose und iTime - auf, um den angeforderten Wert für einen bestimmten „Index“ abzurufen. Die Funktion „high“ gibt den Höchstkurs eines bestimmten Balkens zurück, während die Funktion „low“ den Tiefstkurs zurückgibt. In ähnlicher Weise wird mit „open“ der Eröffnungskurs und mit „close“ der Schlusskurs abgefragt. Die Funktion „time“ gibt den Zeitstempel des Balkens zurück. Wir verwenden sie, um die Lesbarkeit des Codes zu verbessern und einen saubereren, besser strukturierten Zugriff auf historische Daten in unserem Programm zu ermöglichen.
Mit diesen Funktionen können wir nun mit dem folgenden Codefragment auf Ausbrüche prüfen, wenn sich ein Konsolidierungsbereich gebildet hat.
// Check for breakout if a consolidation range is established if (highestHigh.price > 0 && lowestLow.price > 0) { breakoutDetected = CheckRangeBreak(highestHigh, lowestLow); }
Wenn ein Konsolidierungsbereich festgelegt wurde, prüfen wir mit einer nutzerdefinierten Funktion namens „CheckRangeBreak“, ob ein Bereich durchbrochen wurde, und speichern das Ergebnis in der Variablen „breakoutDetected“. Die Funktion ist wie folgt implementiert.
// Function to check for range breaks bool CheckRangeBreak(PriceIndex &highestHighRef, PriceIndex &lowestLowRef) { double closingPrice = close(1); // Get the closing price of the current candle if (closingPrice > highestHighRef.price) { Print("Range break upwards detected. Closing price ", closingPrice, " is above the highest high: ", highestHighRef.price); return true; // Breakout detected } else if (closingPrice < lowestLowRef.price) { Print("Range break downwards detected. Closing price ", closingPrice, " is below the lowest low: ", lowestLowRef.price); return true; // Breakout detected } return false; // No breakout }
Für die boolesche Funktion „CheckRangeBreak“ vergleichen wir den „closingPrice“ der aktuellen Kerze mit dem „highestHighRef.price“ und dem „lowestLowRef.price“. Wenn der „closingPrice“ höher ist als der „highestHighRef.price“, erkennen wir einen Ausbruch nach oben. Wenn er niedriger ist als der „lowestLowRef.price“, erkennen wir einen Ausbruch nach unten. In beiden Fällen geben wir „true“ zurück und drucken die Ausbruchsrichtung aus. Wenn keine der beiden Bedingungen erfüllt ist, wird „false“ zurückgegeben.
Wir können die Variable nun verwenden, um einen Ausbruch zu erkennen, bei dem wir den Bereichsstatus zurücksetzen müssen, um uns auf den nächsten möglichen Konsolidierungsbereich vorzubereiten, wie folgt.
// Reset state after breakout if (breakoutDetected) { Print("Breakout detected. Resetting for the next range."); breakoutBarIndex = 1; // Use the current bar's index (index 1 refers to the most recent completed bar) breakoutTime = TimeCurrent(); impulseHigh = highestHigh.price; impulseLow = lowestLow.price; breakoutDetected = false; highestHigh.price = 0; highestHigh.index = 0; lowestLow.price = 0; lowestLow.index = 0; }
Nachdem ein Ausbruch festgestellt wurde, wird der Status für den nächsten Bereich zurückgesetzt. Wir setzen „breakoutBarIndex“ auf 1, was sich auf den letzten abgeschlossenen Balken bezieht. Außerdem wird „breakoutTime“ mit der Funktion „TimeCurrent“ mit der aktuellen Zeit aktualisiert. Die Werte „impulseHigh“ und „impulseLow“ werden auf die Werte „highestHigh.price“ und „lowestLow.price“ des vorherigen Bereichs gesetzt. Anschließend markieren wir „breakoutDetected“ als „false“ und setzen sowohl „highestHigh“ als auch „lowestLow“ Preise und Indizes auf 0 zurück, um die nächste Bereichserkennung vorzubereiten. Wir können nun auf der Grundlage der impulsiven Bewegung nach gültigen Auftragsblöcken suchen.
if (breakoutBarIndex >= 0 && TimeCurrent() > breakoutTime + waitBars * PeriodSeconds()) { DetectImpulsiveMovement(impulseHigh,impulseLow,waitBars,1); bool is_OB_Valid = is_OB_DOWN || is_OB_UP; datetime time1 = iTime(_Symbol,_Period,rangeCandles+waitBars+1); double price1 = impulseHigh; int visibleBars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS); datetime time2 = is_OB_Valid ? time1 + (visibleBars/1)*PeriodSeconds() : time(waitBars+1); double price2 = impulseLow; string obNAME = OB_Prefix+"("+TimeToString(time1)+")"; color obClr = clrBlack; if (is_OB_Valid){obClr = is_OB_UP ? CLR_UP : CLR_DOWN;} else if (!is_OB_Valid){obClr = clrBlue;} string obText = ""; if (is_OB_Valid){obText = is_OB_UP ? "Bullish Order Block"+ShortToString(0x2BED) : "Bearish Order Block"+ShortToString(0x2BEF);} else if (!is_OB_Valid){obText = "Range";} //--- }
Hier wird zunächst geprüft, ob der „breakoutBarIndex“ größer oder gleich 0 ist und ob die aktuelle Zeit größer ist als die „breakoutTime“ plus einer Wartezeit, die durch Multiplikation von „waitBars“ mit der Zeit in Sekunden (mit der Funktion PeriodSeconds) berechnet wird. Wenn diese Bedingung erfüllt ist, rufen wir die Funktion „DetectImpulsiveMovement“ auf, um impulsive Marktbewegungen zu erkennen, wobei wir die Werte „impulseHigh“, „impulseLow“, „waitBars“ und einen festen Parameter von 1 übergeben.
Anschließend wird der Auftragsblock validiert, indem geprüft wird, ob entweder „is_OB_DOWN“ oder „is_OB_UP“ wahr ist, und das Ergebnis in „is_OB_Valid“ gespeichert. Wir rufen den Zeitstempel des Balkens mit iTime ab, der die Zeit eines bestimmten Balkens für das Symbol und den Zeitraum angibt, und speichern ihn in „time1“. Der Preis dieses Balkens wird in „impulseHigh“ gespeichert, das wir für weitere Berechnungen verwenden. Als Nächstes ermitteln wir die Anzahl der sichtbaren Balken im Chart mithilfe der Funktion ChartGetInteger mit dem Parameter CHART_VISIBLE_BARS, der die Anzahl der sichtbaren Balken im Chart angibt. Anschließend berechnen wir „time2“, die davon abhängt, ob die Auftragssperre gültig ist. Wenn „is_OB_Valid“ wahr ist, wird die Zeit angepasst, indem die sichtbaren Balken zu „time1“ addiert werden, multipliziert mit dem Zeitraum in Sekunden. Andernfalls wird der Zeitpunkt des nächsten Balkens verwendet, der durch „time(waitBars+1)“ bestimmt wird. Wir bestimmen dies mit Hilfe eines ternären Operators.
Der „Preis2“ wird auf „impulseLow“ gesetzt. Dann generieren wir den Namen des Auftragsblocks mit „OB_Prefix“ zusammen mit der formatierten Zeit mit der Funktion TimeToString. Die Farbe für den Auftragsblock wird über die Variable „obClr“ festgelegt, die standardmäßig schwarz ist. Wenn der Auftragsblock gültig ist, setzen wir die Farbe entweder auf „CLR_UP“ (für einen Aufwärtsauftragsblock) oder „CLR_DOWN“ (für einen Abwärtsauftragsblock). Wenn der Auftragsblock ungültig ist, wird die Farbe auf blau gesetzt.
Der Text des Auftragsblocks, der in „obText“ gespeichert ist, wird entsprechend der Richtung des Auftragsblocks gesetzt. Wenn der Auftragsblock gültig ist, wird „bullisch Order Block“ oder „Bearish Order Block“ mit eindeutigen Unicode-Zeichencodes (0x2BED für „bullish“ , 0x2BEF für „bearish“) angezeigt, die wir mit der Funktion „ShortToString“ umwandeln. Wenn nicht, wird es als „Bereich“ bezeichnet. Diese Unicode-Symbole sind wie folgt.
Die Funktion zur Erkennung impulsiver Bewegungen ist wie folgt.
// Function to detect impulsive movement after breakout void DetectImpulsiveMovement(double breakoutHigh, double breakoutLow, int impulseBars, double impulseThreshold) { double range = breakoutHigh - breakoutLow; // Calculate the breakout range double impulseThresholdPrice = range * impulseThreshold; // Threshold for impulsive move // Check for the price movement in the next `impulseBars` bars after breakout for (int i = 1; i <= impulseBars; i++) { double closePrice = close(i); // Get the close price of the bar // Check if the price moves significantly beyond the breakout high if (closePrice >= breakoutHigh + impulseThresholdPrice) { is_OB_UP = true; Print("Impulsive upward movement detected: Close Price = ", closePrice, ", Threshold = ", breakoutHigh + impulseThresholdPrice); return; } // Check if the price moves significantly below the breakout low else if (closePrice <= breakoutLow - impulseThresholdPrice) { is_OB_DOWN = true; Print("Impulsive downward movement detected: Close Price = ", closePrice, ", Threshold = ", breakoutLow - impulseThresholdPrice); return; } } // If no impulsive movement is detected is_OB_UP = false; is_OB_DOWN = false; Print("No impulsive movement detected after breakout."); }
Um zu erkennen, ob sich der Kurs nach einem Ausbruch impulsiv bewegt, berechnen wir in der Funktion zunächst die „range“, indem wir den „breakoutLow“ vom „breakoutHigh“ abziehen. Der „impulseThresholdPrice“ wird durch Multiplikation der Spanne mit dem „impulseThreshold“-Wert bestimmt, der angibt, wie weit sich der Preis bewegen muss, um als impulsiv zu gelten. Anschließend überprüfen wir die Kursbewegung in den nächsten „impulseBars“-Balken mit Hilfe einer for-Schleife.
Für jeden Balken wird der „closePrice“ mit Hilfe der Funktion „close(i)“ ermittelt, die den Schlusskurs des i-ten Balkens abruft. Wenn der Schlusskurs den „breakoutHigh“ um mindestens den „impulseThresholdPrice“ übersteigt, betrachten wir dies als eine impulsive Aufwärtsbewegung, setzen „is_OB_UP“ auf true und drucken die erkannte Bewegung. Ähnlich verhält es sich, wenn der Schlusskurs mindestens um den „impulseThresholdPrice“ unter den „breakoutLow“ fällt: Wir erkennen eine impulsive Abwärtsbewegung, setzen „is_OB_DOWN“ auf true und drucken das Ergebnis.
Wenn nach der Überprüfung aller Balken keine signifikante Kursbewegung festgestellt wird, werden sowohl „is_OB_UP“ als auch „is_OB_DOWN“ auf „false“ gesetzt, und wir drucken aus, dass keine impulsive Bewegung festgestellt wurde. Jetzt können wir die Bereiche im Chart und die Auftragsblöcke wie folgt darstellen.
if (!is_OB_Valid){ if (ObjectFind(0,obNAME) < 0){ CreateRec(obNAME,time1,price1,time2,price2,obClr,obText); } } else if (is_OB_Valid){ if (ObjectFind(0,obNAME) < 0){ CreateRec(obNAME,time1,price1,time2,price2,obClr,obText); Print("Old ArraySize = ",ArraySize(totalOBs_names)); ArrayResize(totalOBs_names,ArraySize(totalOBs_names)+1); Print("New ArraySize = ",ArraySize(totalOBs_names)); totalOBs_names[ArraySize(totalOBs_names)-1] = obNAME; ArrayPrint(totalOBs_names); Print("Old ArraySize = ",ArraySize(totalOBs_dates)); ArrayResize(totalOBs_dates,ArraySize(totalOBs_dates)+1); Print("New ArraySize = ",ArraySize(totalOBs_dates)); totalOBs_dates[ArraySize(totalOBs_dates)-1] = time2; ArrayPrint(totalOBs_dates); Print("Old ArraySize = ",ArraySize(totalOBs_is_signals)); ArrayResize(totalOBs_is_signals,ArraySize(totalOBs_is_signals)+1); Print("New ArraySize = ",ArraySize(totalOBs_is_signals)); totalOBs_is_signals[ArraySize(totalOBs_is_signals)-1] = false; ArrayPrint(totalOBs_is_signals); } } breakoutBarIndex = -1; // Use the current bar's index (index 1 refers to the most recent completed bar) breakoutTime = 0; impulseHigh = 0; impulseLow = 0; is_OB_UP = false; is_OB_DOWN = false;
Hier wird geprüft, ob die Auftragssperre („is_OB_Valid“) gültig ist. Wenn er nicht gültig ist, wird mit der Funktion ObjectFind ermittelt, ob ein Objekt mit dem Namen „obNAME“ bereits im Chart vorhanden ist. Wenn das Objekt nicht gefunden wird (die Funktion gibt einen negativen Wert zurück), rufen wir „CreateRec“ auf, um den Auftragsblock im Chart unter Verwendung der angegebenen Parameter wie Zeit, Preis, Farbe und Text zu erstellen.
Wenn die Auftragssperre gültig ist, wird erneut geprüft, ob das Objekt existiert. Ist dies nicht der Fall, erstellen wir es und verwalten dann die Auftragsblockdaten, indem wir die Größe mit der Funktion ArrayResize ändern und unsere drei Arrays aktualisieren: „totalOBs_names“ zum Speichern der Auftragsblocknamen, „totalOBs_dates“ für die Zeitstempel und „totalOBs_is_signals“ zum Speichern, ob jeder Auftragsblock ein gültiges Signal ist (anfangs auf false gesetzt). Nach der Größenänderung der Arrays werden die alten und neuen Arraygrößen mit ArraySize ausgedruckt und die Arrayinhalte mit der Funktion ArrayPrint angezeigt. Schließlich setzen wir den Breakout-Status zurück, indem wir „breakoutBarIndex“ auf -1 setzen, „breakoutTime“, „impulseHigh“ und „impulseLow“ auf 0 zurücksetzen und die Auftragsblock-Richtungsflags „is_OB_UP“ und „is_OB_DOWN“ auf false setzen.
Um die Rechtecke mit Text zu erstellen, haben wir eine nutzerdefinierte Funktion „CreateRec“ wie folgt verwendet.
void CreateRec(string objName,datetime time1,double price1, datetime time2,double price2,color clr,string txt){ if (ObjectFind(0,objName) < 0){ ObjectCreate(0,objName,OBJ_RECTANGLE,0,time1,price1,time2,price2); Print("SUCCESS CREATING OBJECT >",objName,"< WITH"," T1: ",time1,", P1: ",price1, ", T2: ",time2,", P2: ",price2); ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1); ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1); ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2); ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2); ObjectSetInteger(0,objName,OBJPROP_FILL,true); ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); ObjectSetInteger(0,objName,OBJPROP_BACK,false); // Calculate the center position of the rectangle datetime midTime = time1 + (time2 - time1) / 2; double midPrice = (price1 + price2) / 2; // Create a descriptive text label centered in the rectangle string description = txt; string textObjName = objName + description; // Unique name for the text object if (ObjectFind(0, textObjName) < 0) { ObjectCreate(0, textObjName, OBJ_TEXT, 0, midTime, midPrice); ObjectSetString(0, textObjName, OBJPROP_TEXT, description); ObjectSetInteger(0, textObjName, OBJPROP_COLOR, clrBlack); ObjectSetInteger(0, textObjName, OBJPROP_FONTSIZE, 15); ObjectSetInteger(0, textObjName, OBJPROP_ANCHOR, ANCHOR_CENTER); Print("SUCCESS CREATING LABEL >", textObjName, "< WITH TEXT: ", description); } ChartRedraw(0); } }
In der Funktion „CreateRec“, die wir definieren, prüfen wir mit der Funktion ObjectFind, ob das Objekt „objName“ existiert. Ist dies nicht der Fall, wird mit der Funktion ObjectCreate, die durch OBJ_RECTANGLE definiert ist, ein Rechteck mit den angegebenen Zeit- und Preispunkten erstellt und seine Eigenschaften (z. B. Farbe, Füllung, Sichtbarkeit) mit ObjectSetInteger und ObjectSetDouble festgelegt. Wir berechnen die mittlere Position des Rechtecks und erstellen eine Beschriftung in der Mitte mit ObjectCreate für Text, definiert durch OBJ_TEXT, wobei wir seine Eigenschaften (Text, Farbe, Größe, Anker) festlegen. Schließlich rufen wir die Funktion ChartRedraw auf, um das Chart zu aktualisieren. Wenn das Objekt oder Etikett bereits existiert, wird keine Aktion durchgeführt.
Mit den aufgezeichneten Auftragsblöcken können wir nun bestimmen, ob wir sie erneut testen und Positionen eröffnen, wenn der Preis in die Bereiche eintritt und sie durchbricht.
for (int j=ArraySize(totalOBs_names)-1; j>=0; j--){ string obNAME = totalOBs_names[j]; bool obExist = false; //Print("name = ",fvgNAME," >",ArraySize(totalFVGs)," >",j); //ArrayPrint(totalFVGs); //ArrayPrint(barTIMES); double obHigh = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,0); double obLow = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,1); datetime objTime1 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,0); datetime objTime2 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,1); color obColor = (color)ObjectGetInteger(0,obNAME,OBJPROP_COLOR); if (time(1) < objTime2){ //Print("FOUND: ",obNAME," @ bar ",j,", H: ",obHigh,", L: ",obLow); obExist = true; } double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); if (obColor == CLR_UP && Ask > obHigh && close(1) > obHigh && open(1) < obHigh && !totalOBs_is_signals[j]){ Print("BUY SIGNAL For (",obNAME,") Now @ ",Ask); double sl = Bid - 1500*_Point; double tp = Bid + 1500*_Point; obj_Trade.Buy(0.01,_Symbol,Ask,sl,tp); totalOBs_is_signals[j] = true; ArrayPrint(totalOBs_names,_Digits," [< >] "); ArrayPrint(totalOBs_is_signals,_Digits," [< >] "); } else if (obColor == CLR_DOWN && Bid < obLow && close(1) < obLow && open(1) > obLow && !totalOBs_is_signals[j]){ Print("SELL SIGNAL For (",obNAME,") Now @ ",Bid); double sl = Ask + 1500*_Point; double tp = Ask - 1500*_Point; obj_Trade.Sell(0.01,_Symbol,Bid,sl,tp); totalOBs_is_signals[j] = true; ArrayPrint(totalOBs_names,_Digits," [< >] "); ArrayPrint(totalOBs_is_signals,_Digits," [< >] "); } if (obExist == false){ bool removeName = ArrayRemove(totalOBs_names,0,1); bool removeTime = ArrayRemove(totalOBs_dates,0,1); bool remove_isSignal = ArrayRemove(totalOBs_is_signals,0,1); if (removeName && removeTime && remove_isSignal){ Print("Success removing the OB DATA from arrays. New Data as below:"); Print("Total Sizes => OBs: ",ArraySize(totalOBs_names),", TIMEs: ",ArraySize(totalOBs_dates),", SIGNALs: ",ArraySize(totalOBs_is_signals)); ArrayPrint(totalOBs_names); ArrayPrint(totalOBs_dates); ArrayPrint(totalOBs_is_signals); } } }
Hier wird in einer Schleife das Array „totalOBs_names“ durchlaufen, um jeden Auftragsblock („obNAME“) zu verarbeiten. Mit den Funktionen ObjectGetDouble und ObjectGetInteger werden der Höchst- und Tiefstpreis, der Zeitstempel und die Farbe des Auftragsblocks abgerufen. Anschließend wird geprüft, ob die aktuelle Uhrzeit vor der Endzeit des Auftragsblocks liegt. Wenn die Zeitbedingung erfüllt ist, wird auf der Grundlage der Farbe des Auftragsblocks und der Preisbedingungen nach Kauf- oder Verkaufssignalen gesucht. Wenn die Bedingungen erfüllt sind, führen wir einen Kauf- oder Verkaufshandel mit den Funktionen „obj_Trade.Buy“ oder „obj_Trade.Sell“ aus und aktualisieren das Array „totalOBs_is_signals“, um den Auftragsblock als ein Signal auslösend zu kennzeichnen, damit wir ihn nicht erneut handeln, falls der Preis zurückgeht.
Wenn ein Auftragsblock die Zeitbedingung nicht erfüllt, wird er mit der Funktion ArrayRemove aus den Arrays „totalOBs_names“, „totalOBs_dates“ und „totalOBs_is_signals“ entfernt. Wenn die Entfernung erfolgreich war, werden die aktualisierten Größen und Inhalte der Felder ausgedruckt. Hier ist der aktuelle Meilenstein, den wir erreicht haben.
Aus dem Bild können wir ersehen, dass die Auftragsblöcke erkannt und gehandelt werden, womit unser Ziel erreicht ist. Nun müssen wir das Programm noch backtesten und seine Leistung analysieren. Dies wird im nächsten Abschnitt behandelt.
Backtest und Optimierung
Nach einem gründlichen Backtest haben wir die folgenden Ergebnisse.
Backtest-Grafik:
Backtest-Bericht:
Hier ist auch ein Video, das den gesamten Backtest der Strategie über einen Zeitraum von 1 Jahr, 2024, zeigt.
Schlussfolgerung
Abschließend haben wir den Prozess der Entwicklung eines hochentwickelten MQL5 Expert Advisors (EA) demonstriert, der das Erkennen von Auftragsblöcken für Handelsstrategien, die Smart-Money handeln, nutzt. Durch die Einbeziehung von Hilfsmitteln wie der dynamischen Bereichsanalyse, der Preisaktion und der Erkennung von Ausbrüchen in Echtzeit haben wir ein Programm entwickelt, das wichtige Unterstützungs- und Widerstandsniveaus identifizieren, umsetzbare Handelssignale erzeugen und Aufträge mit hoher Präzision verwalten kann.
Haftungsausschluss: Dieser Artikel ist nur für Bildungszwecke bestimmt. Der Handel birgt ein erhebliches finanzielles Risiko, und das Marktverhalten kann sehr unvorhersehbar sein. Die in diesem Artikel vorgestellten Strategien bieten einen strukturierten Ansatz, sind aber keine Garantie für künftige Rentabilität. Ordnungsgemäße Tests und Risikomanagement sind vor dem Live-Handel unerlässlich.
Durch die Anwendung dieser Methoden können Sie effektivere Handelssysteme aufbauen, Ihren Ansatz zur Marktanalyse verfeinern und Ihren algorithmischen Handel auf die nächste Stufe heben. Viel Glück auf Ihrer Handelsreise!
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17135





- 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.