English 日本語
preview
Automatisieren von Handelsstrategien in MQL5 (Teil 11): Entwicklung eines mehrstufigen Raster-Handelssystems

Automatisieren von Handelsstrategien in MQL5 (Teil 11): Entwicklung eines mehrstufigen Raster-Handelssystems

MetaTrader 5Handel |
149 4
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In unserem letzten Artikel (Teil 10) haben wir einen Expert Advisor entwickelt, um die Flat Momentum Strategy zu automatisieren, die eine Mischung aus gleitenden Durchschnitten und Momentum-Filtern in Language 5 (MQL5) verwendet. In Teil 11 konzentrieren wir uns nun auf den Aufbau eines mehrstufigen Raster-Handelssystems, das einen mehrstufigen Raster-Ansatz nutzt, um von Marktschwankungen zu profitieren. Wir werden den Artikel anhand der folgenden Themen strukturieren:

  1. Einführung
  2. Die Architektur eines Multi-Level-Raster-Systems verstehen
  3. Implementation in MQL5
  4. Backtests
  5. Schlussfolgerung

Am Ende dieses Artikels werden Sie über ein umfassendes Verständnis und ein voll funktionsfähiges Programm verfügen, das für den Live-Handel bereit ist. Lasst uns beginnen!


Die Architektur eines Multi-Level-Raster-Systems verstehen

Ein Multi-Level-Raster-Handelssystem ist ein strukturierter Ansatz, der die Marktvolatilität ausnutzt, indem er eine Reihe von Kauf- und Verkaufsaufträgen in vorher festgelegten Intervallen über eine Reihe von Kursniveaus erteilt. Bei der Strategie, die wir jetzt umsetzen werden, geht es nicht darum, die Richtung des Marktes vorherzusagen, sondern vielmehr darum, vom natürlichen Fluss der Preise zu profitieren und Gewinne zu erzielen, unabhängig davon, ob sich der Markt nach oben, unten oder seitwärts bewegt.

Aufbauend auf diesem Konzept wird unser Programm die mehrstufige Raster-Strategie durch einen modularen Aufbau umsetzen, der Signalerkennung, Auftragsausführung und Risikomanagement voneinander trennt. Bei der Entwicklung unseres Systems werden wir zunächst wichtige Parameter initialisieren, wie z. B. gleitende Durchschnitte zur Identifizierung von Handelssignalen, und eine Korbstruktur einrichten, die Handelsdetails wie anfängliche Losgrößen, Rasterabstände und Take-Profit-Levels enthält.

Während sich der Markt entwickelt, überwacht das Programm die Preisbewegungen, um neue Handelsgeschäfte auszulösen und bestehende Positionen zu verwalten, indem es Aufträge auf jeder Rasterebene auf der Grundlage vordefinierter Bedingungen hinzufügt und die Risikoparameter dynamisch anpasst. Die Architektur wird auch Funktionen zur Neuberechnung von Breakeven-Punkten, zur Änderung von Profit-Targets und zur Schließung von Positionen bei Erreichen von Gewinnzielen oder Risikogrenzen umfassen. Dieser strukturierte Plan gliedert das Programm nicht nur in einzelne, überschaubare Komponenten, sondern stellt auch sicher, dass jede Ebene des Rasters zu einer kohärenten, risikogeleiteten Handelsstrategie beiträgt, die für robuste Backtests und den Handelseinsatz bereit ist. Kurz gesagt, so wird die Architektur aussehen.

RASTER-ARCHITEKTUR



Implementation in MQL5

Um das Programm in MQL5 zu erstellen, öffnen Sie den MetaEditor, gehen Sie zum Navigator, suchen Sie den Ordner Indikatoren, klicken Sie auf die Registerkarte „Neu“ und folgen Sie den Anweisungen, um die Datei zu erstellen. Sobald sie erstellt ist, müssen wir in der Programmierumgebung einige Metadaten und globale Variablen deklarieren, die wir im gesamten Programm verwenden werden.

//+------------------------------------------------------------------+
//|                        Copyright 2025, Forex Algo-Trader, Allan. |
//|                                 "https://t.me/Forex_Algo_Trader" |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "This EA trades multiple signals with grid strategy using baskets"
#property strict

#include <Trade/Trade.mqh> //--- Includes the standard trading library for executing trades
CTrade obj_Trade; //--- Instantiates the CTrade object used for managing trade operations

//--- Closure Mode Enumeration and Inputs
enum ClosureMode {
   CLOSE_BY_PROFIT,      //--- Use total profit (in currency) to close positions
   CLOSE_BY_POINTS       //--- Use points threshold from breakeven to close positions
};

input group "General EA Settings"
input ClosureMode closureMode = CLOSE_BY_POINTS;
input double inpLotSize = 0.01;
input long inpMagicNo = 1234567;
input int inpTp_Points = 100;
input int inpGridSize = 100;
input double inpMultiplier = 2.0;
input int inpBreakevenPts = 50;
input int maxBaskets = 5;

input group "MA Indicator Settings" //--- Begins the input group for Moving Average indicator settings
input int inpMAPeriod = 21;                         //--- Period used for the Moving Average calculation

Hier legen wir die grundlegenden Komponenten unseres Programms fest, um eine reibungslose Handelsausführung und ein strategisches Positionsmanagement zu gewährleisten. Wir beginnen mit der Einbindung der Bibliothek „Trade/Trade.mqh“, die den Zugang zu wesentlichen Funktionen der Handelsausführung ermöglicht. Zur Erleichterung der Handelsoperationen instanziieren wir das Objekt „CTrade“ als „obj_Trade“, sodass wir Aufträge innerhalb unserer automatisierten Strategie effizient platzieren, ändern und schließen können.

Wir definieren die Enumeration „ClosureMode“, um Flexibilität bei der Verwaltung von Handelsausgängen zu gewährleisten. Das Programm kann in zwei Modi arbeiten: „CLOSE_BY_PROFIT“, das die Schließung auslöst, wenn der kumulierte Gesamtgewinn einen bestimmten Schwellenwert in der Kontowährung erreicht, und „CLOSE_BY_POINTS“, das die Positionen auf der Grundlage eines vordefinierten Abstands vom Breakeven-Level schließt. Dadurch wird sichergestellt, dass der Nutzer seine Ausstiegsstrategie dynamisch an das Marktverhalten und die Risikotoleranz anpassen kann.

Als Nächstes führen wir einen strukturierten Eingabebereich unter „Allgemeine EA-Einstellungen“ ein, um nutzerdefinierte Anpassungen der Handelsstrategie zu ermöglichen. Wir geben „inpLotSize“ an, um das anfängliche Handelsvolumen zu kontrollieren, und verwenden „inpMagicNo“, um die Handelsgeschäfte des EA eindeutig zu identifizieren und Konflikte mit anderen aktiven Strategien zu vermeiden. Bei der rasterbasierten Ausführung legen wir mit „inpTp_Points“ das Take-Profit-Niveau pro Handel fest, während „inpRasterSize“ den Abstand zwischen aufeinanderfolgenden Rasteraufträgen definiert. Der Parameter „inpMultiplier“ skaliert die Handelsgrößen progressiv und implementiert eine adaptive Rastererweiterung, um das Gewinnpotenzial zu maximieren und gleichzeitig das Risiko zu kontrollieren. Um die Risikokontrolle weiter zu verfeinern, konfigurieren wir „inpBreakevenPts“, das die Handelsgeschäfte nach einem bestimmten Schwellenwert zum Breakeven bewegt, und „maxBaskets“, das die Anzahl der unabhängigen Rasterstrukturen begrenzt, die der EA gleichzeitig verwalten kann.

Um das Handelsfiltern zu verbessern, haben wir unter „MA-Indikatoreinstellungen“ einen Mechanismus für gleitende Durchschnitte integriert. Hier definieren wir „inpMAPeriod“, das die Anzahl der Perioden bestimmt, die zur Berechnung des gleitenden Durchschnitts verwendet werden. Dies trägt dazu bei, den Rasterhandel an den vorherrschenden Markttrends auszurichten, ungünstige Bedingungen herauszufiltern und sicherzustellen, dass die Handelseinträge mit der allgemeinen Marktdynamik übereinstimmen. Da wir viele Signalinstanzen verarbeiten müssen, können wir als nächstes eine Korbstruktur (basket) definieren.

//--- Basket Structure
struct BasketInfo {
   int basketId;            //--- Unique basket identifier (e.g., 1, 2, 3...)
   long magic;              //--- Unique magic number for this basket to differentiate its trades
   int direction;           //--- Direction of the basket: POSITION_TYPE_BUY or POSITION_TYPE_SELL
   double initialLotSize;   //--- The initial lot size assigned to the basket
   double currentLotSize;   //--- The current lot size for subsequent grid trades
   double gridSize;         //--- The next grid level price for the basket
   double takeProfit;       //--- The current take-profit price for the basket
   datetime signalTime;     //--- Timestamp of the signal to avoid duplicate trade entries
};

Hier definieren wir die Struktur „BasketInfo“, um jeden Rasterkorb unabhängig zu organisieren und zu verwalten. Wir vergeben eine eindeutige „basketId“, um jeden Korb zu kontrollieren, und verwenden „Magie“, um sicherzustellen, dass unsere Handelsgeschäfte von anderen unterscheidbar bleiben. Mit „Richtung“ legen wir die Handelsrichtung fest und entscheiden, ob wir eine Kauf- oder Verkaufsstrategie verfolgen.

Wir legen „initialLotSize“ für den ersten Handel im Korb fest, während „currentLotSize“ sich dynamisch für nachfolgende Handelsgeschäfte anpasst. Wir verwenden „gridSize“, um den Abstand zwischen den Handelsgeschäfte festzulegen und „takeProfit“, um unser Gewinnziel zu definieren. Um doppelte Eröffnungen zu vermeiden, verfolgen wir das Timing des Signals mit „signalTime“. Dann können wir ein Speicherfeld deklarieren, das die definierte Struktur und einige anfängliche globale Variablen verwendet.

BasketInfo baskets[];       //--- Dynamic array to store active basket information
int nextBasketId = 1;       //--- Counter for assigning unique IDs to new baskets
long baseMagic = inpMagicNo;//--- Base magic number obtained from user input
double takeProfitPts = inpTp_Points * _Point; //--- Convert take profit points into price units
double gridSize_Spacing = inpGridSize * _Point; //--- Convert grid size spacing from points into price units
double profitTotal_inCurrency = 100; //--- Target profit in account currency for closing positions

//--- Global Variables
int totalBars = 0;          //--- Stores the total number of bars processed so far
int handle;                 //--- Handle for the Moving Average indicator
double maData[];            //--- Array to store Moving Average indicator data

Wir verwenden das dynamische Array „baskets[]“, um Informationen über aktive Körbe zu speichern, damit wir mehrere Positionen effizient verfolgen können. Die Variable „nextBasketId“ weist jedem neuen Korb eine eindeutige Kennung zu, während „baseMagic“ sicherstellt, dass alle Handelsgeschäfte innerhalb des Systems anhand der nutzerdefinierten magischen Zahl unterscheidbar sind. Wir konvertieren Nutzereingaben in Preiseinheiten, indem wir „inpTp_Points“ und „inpRasterSize“ mit „_Point“ multiplizieren, was eine genaue Kontrolle über „takeProfitPts“ und „gridSize_Spacing“ ermöglicht. Die Variable „profitTotal_inCurrency“ definiert die Gewinnschwelle, die erforderlich ist, um alle Positionen zu schließen, wenn ein währungsbasierter Schließungsmodus verwendet wird.

Für die technische Analyse initialisieren wir „totalBars“, um die Anzahl der verarbeiteten Preisbalken zu verfolgen, „handle“, um das Handle des Moving Average Indikators zu speichern, und „maData[]“ als Array zum Speichern der berechneten Moving Average Werte. Damit können wir einige beliebige Funktionsprototypen definieren, die wir bei Bedarf im gesamten Programm verwenden werden.

//--- Function Prototypes
void InitializeBaskets(); //--- Prototype for basket initialization function (if used)
void CheckAndCloseProfitTargets(); //--- Prototype to check and close positions if profit target is reached
void CheckForNewSignal(double ask, double bid); //--- Prototype to check for new trading signals based on price
bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction); //--- Prototype to execute the initial trade for a basket
void ManageGridPositions(int basketIdx, double ask, double bid); //--- Prototype to manage and add grid positions for an active basket
void UpdateMovingAverage(); //--- Prototype to update the Moving Average indicator data
bool IsNewBar(); //--- Prototype to check whether a new bar has formed
double CalculateBreakevenPrice(int basketId); //--- Prototype to calculate the weighted breakeven price for a basket
void CheckBreakevenClose(int basketIdx, double ask, double bid); //--- Prototype to check and close positions based on breakeven criteria
void CloseBasketPositions(int basketId); //--- Prototype to close all positions within a basket
string GetPositionComment(int basketId, bool isInitial); //--- Prototype to generate a comment for a position based on basket and trade type
int CountBasketPositions(int basketId); //--- Prototype to count the number of open positions in a basket

Hier definieren wir Funktionsprototypen, die die Kernoperationen unseres mehrstufigen Raster-Handelssystems umreißen. Diese Funktionen gewährleisten Modularität und ermöglichen es uns, Handelsausführung, Positionsmanagement und Risikohandling effizient zu strukturieren. Wir beginnen mit „InitializeBaskets()“, das das System auf die Erfassung aktiver Körbe vorbereitet. Die Funktion „CheckAndCloseProfitTargets()“ sorgt dafür, dass Positionen geschlossen werden, sobald vordefinierte Gewinnbedingungen erfüllt sind. Um Handelsmöglichkeiten zu erkennen, wertet „CheckForNewSignal()“ die Kursniveaus aus, um festzustellen, ob ein neues Handelssignal ausgeführt werden sollte.

Die Funktion „ExecuteInitialTrade()“ verwaltet den ersten Handel innerhalb eines Korbes, während „ManageRasterPositions()“ dafür sorgt, dass die Rasterebenen systematisch erweitert werden, wenn sich der Markt bewegt. „UpdateMovingAverage()“ ruft die Daten des Indikators Gleitender Durchschnitt ab und verarbeitet sie, um die Signalerzeugung zu unterstützen. Für das Handelsmanagement trägt „IsNewBar()“ zur Optimierung der Ausführung bei, indem sichergestellt wird, dass Aktionen nur auf Basis aktueller Kursdaten durchgeführt werden. „CalculateBreakevenPrice()“ berechnet den gewichteten Breakeven-Preis für einen Korb, während „CheckBreakevenClose()“ feststellt, ob die Bedingungen erfüllt sind, um Positionen auf der Grundlage der Breakeven-Kriterien zu schließen.

Zur Verwaltung von Korbpositionen erleichtert „CloseBasketPositions()“ kontrollierte Ausstiege und stellt sicher, dass alle Positionen innerhalb eines Korbes bei Bedarf geschlossen werden. „GetPositionComment()“ liefert strukturierte Handelskommentare, die die Nachverfolgung von Handelsgeschäften verbessern, und CountBasketPositions()“ hilft bei der Überwachung der Anzahl aktiver Positionen innerhalb eines Baskets und stellt sicher, dass das System innerhalb definierter Risikolimits arbeitet.

Wir können nun damit beginnen, den gleitenden Durchschnitt zu initialisieren, da wir ihn ausschließlich zur Signalerzeugung verwenden werden.

//+------------------------------------------------------------------+
//--- Expert initialization function
//+------------------------------------------------------------------+
int OnInit() {
   handle = iMA(_Symbol, _Period, inpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); //--- Initialize the Moving Average indicator with specified period and parameters
   if(handle == INVALID_HANDLE) {
      Print("ERROR: Unable to initialize Moving Average indicator!"); //--- Log error if indicator initialization fails
      return(INIT_FAILED); //--- Terminate initialization with a failure code
   }
   ArraySetAsSeries(maData, true); //--- Set the moving average data array as a time series (newest data at index 0)
   ArrayResize(baskets, 0); //--- Initialize the baskets array as empty at startup
   obj_Trade.SetExpertMagicNumber(baseMagic); //--- Set the default magic number for trade operations
   return(INIT_SUCCEEDED); //--- Signal that initialization completed successfully
}

In OnInit beginnen wir mit der Initialisierung des gleitenden Durchschnittsindikators mithilfe der Funktion iMA(), wobei wir den angegebenen Zeitraum und die Parameter anwenden, um trendbasierte Daten abzurufen. Wenn das Handle ungültig ist (INVALID_HANDLE), wird eine Fehlermeldung protokolliert und der Initialisierungsprozess mit INIT_FAILED abgebrochen, um zu verhindern, dass der EA mit fehlenden Daten läuft.

Als Nächstes konfigurieren wir das Array der gleitenden Durchschnittsdaten mit der Funktion ArraySetAsSeries, wobei wir sicherstellen, dass die jüngsten Werte für einen effizienten Zugriff bei Index 0 gespeichert werden. Anschließend wird die Größe des Arrays „baskets“ auf Null gesetzt, um es für die dynamische Zuweisung bei der Eröffnung neuer Handelsgeschäfte vorzubereiten. Schließlich weisen wir dem Handelsobjekt mit der Methode „SetExpertMagicNumber()“ die magische Zahl zu, sodass der EA die Handelsgeschäfte mit einer eindeutigen Kennung verfolgen und verwalten kann. Wenn alle Komponenten erfolgreich initialisiert sind, geben wir INIT_SUCCEEDED zurück, um zu bestätigen, dass der EA bereit ist, mit der Ausführung zu beginnen.

Da wir Daten gespeichert haben, können wir die Ressourcen freigeben, wenn wir das Programm in OnDeinit nicht mehr benötigen, indem wir die Funktion IndicatorRelease aufrufen.

//+------------------------------------------------------------------+
//--- Expert deinitialization function
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   IndicatorRelease(handle); //--- Release the indicator handle to free up resources when the EA is removed
}

Anschließend können wir die Daten bei jedem Tick mit OnTick verarbeiten. Da wir das Programm jedoch einmal pro Balken ausführen wollen, müssen wir dafür eine Funktion definieren.

//+------------------------------------------------------------------+
//--- Expert tick function
//+------------------------------------------------------------------+
void OnTick() {
   if(IsNewBar()) { //--- Execute logic only when a new bar is detected

   }
}

Der Prototyp der Funktion sieht wie folgt aus.

//+------------------------------------------------------------------+
//--- Check for New Bar
//+------------------------------------------------------------------+
bool IsNewBar() {
   int bars = iBars(_Symbol, _Period); //--- Get the current number of bars on the chart for the symbol and period
   if(bars > totalBars) { //--- Compare the current number of bars with the previously stored total
      totalBars = bars; //--- Update the stored bar count to the new value
      return true; //--- Return true to indicate a new bar has formed
   }
   return false; //--- Return false if no new bar has been detected
}

Hier definieren wir die Funktion „IsNewBar()“, die prüft, ob sich ein neuer Balken auf dem Chart gebildet hat. Dies ist wichtig, um sicherzustellen, dass unser EA neue Preisdaten nur dann verarbeitet, wenn ein neuer Balken erscheint, und so unnötige Neuberechnungen verhindert. Zunächst wird die aktuelle Anzahl der Balken im Chart mit der Funktion iBars ermittelt, die die Gesamtzahl der historischen Balken für das aktive Symbol und den Zeitrahmen angibt. Anschließend wird dieser Wert mit der Variablen „totalBars“ verglichen, in der die Anzahl der zuvor aufgezeichneten Balken gespeichert ist.

Wenn die aktuelle Balkenanzahl größer ist als der in der Variablen „totalBars“ gespeicherte Wert, bedeutet dies, dass ein neuer Balken erschienen ist. In diesem Fall aktualisieren wir die Variable „totalBars“ mit der neuen Anzahl und geben „true“ zurück, um zu signalisieren, dass der EA mit balken-basierten Berechnungen oder Handelslogik fortfahren soll. Wird kein neuer Balken erkannt, gibt die Funktion „false“ zurück, um sicherzustellen, dass der EA keine redundanten Operationen auf demselben Balken durchführt.

Sobald wir einen neuen Balken erkennen, müssen wir die Daten des gleitenden Durchschnitts für die weitere Verarbeitung abrufen. Hierfür verwenden wir eine Funktion.

//+------------------------------------------------------------------+
//--- Update Moving Average
//+------------------------------------------------------------------+
void UpdateMovingAverage() {
   if(CopyBuffer(handle, 0, 1, 3, maData) < 0) { //--- Copy the latest 3 values from the Moving Average indicator buffer into the maData array
      Print("Error: Unable to update Moving Average data."); //--- Log an error if copying the indicator data fails
   }
}

Für die Funktion „UpdateMovingAverage()“, die sicherstellt, dass unser EA die neuesten Werte aus dem gleitenden Durchschnittsindikator abruft, verwenden wir die Funktion CopyBuffer(), um die letzten drei Werte aus dem Puffer des gleitenden Durchschnittsindikators zu extrahieren und sie im Array „maData“ zu speichern. Die Parameter geben den Indikator-Handle („handle“), den Pufferindex (0 für die Hauptzeile), die Startposition (1, um den aktuellen Formierungsbalken zu überspringen), die Anzahl der Werte (3) und das Ziel-Array („maData“) an.

Wenn es uns nicht gelingt, die Daten abzurufen, protokollieren wir eine Fehlermeldung mit Print(), um uns auf mögliche Probleme beim Abrufen der Indikatordaten hinzuweisen, den EA vor unvollständigen oder fehlenden gleitenden Durchschnittswerten zu schützen und die Zuverlässigkeit der Entscheidungsfindung zu gewährleisten. Wir können dann die Funktion aufrufen und die abgerufenen Daten zur Signalerzeugung verwenden.

UpdateMovingAverage(); //--- Update the Moving Average data for the current bar
double ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get and normalize the current ask price
double bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get and normalize the current bid price

//--- Check for new signals and create baskets accordingly
CheckForNewSignal(ask, bid);

Nach dem Abrufen der Indikatordaten werden die aktuellen Geld- und Briefkurse mit der Funktion SymbolInfoDouble() und den Konstanten SYMBOL_ASK bzw. SYMBOL_BID ermittelt. Da Preise oft mehrere Dezimalstellen haben, verwenden wir die Funktion NormalizeDouble mit dem Parameter _Digits, um sicherzustellen, dass sie entsprechend der Preisgenauigkeit des Symbols korrekt formatiert werden.

Schließlich rufen wir die Funktion „CheckForNewSignal()“ auf und übergeben die normalisierten Geld- und Briefkurse. Hier ist der Codeausschnitt der Funktion.

//+------------------------------------------------------------------+
//--- Check for New Crossover Signal
//+------------------------------------------------------------------+
void CheckForNewSignal(double ask, double bid) {
   double close1 = iClose(_Symbol, _Period, 1); //--- Retrieve the close price of the previous bar
   double close2 = iClose(_Symbol, _Period, 2); //--- Retrieve the close price of the bar before the previous one
   datetime currentBarTime = iTime(_Symbol, _Period, 1); //--- Get the time of the current bar

   if(ArraySize(baskets) >= maxBaskets) return; //--- Exit if the maximum allowed baskets are already active

   //--- Buy signal: current bar closes above the MA while the previous closed below it
   if(close1 > maData[1] && close2 < maData[1]) {
      //--- Check if this signal was already processed by comparing signal times in existing baskets
      for(int i = 0; i < ArraySize(baskets); i++) {
         if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function
      }
      int basketIdx = ArraySize(baskets); //--- Index for the new basket equals the current array size
      ArrayResize(baskets, basketIdx + 1); //--- Increase the size of the baskets array to add a new basket
      if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_BUY)){
         baskets[basketIdx].signalTime = currentBarTime; //--- Record the time of the signal after a successful trade
      }
   }
   //--- Sell signal: current bar closes below the MA while the previous closed above it
   else if(close1 < maData[1] && close2 > maData[1]) {
      //--- Check for duplicate signals by verifying the signal time in active baskets
      for(int i = 0; i < ArraySize(baskets); i++) {
         if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function
      }
      int basketIdx = ArraySize(baskets); //--- Determine the index for the new basket
      ArrayResize(baskets, basketIdx + 1); //--- Resize the baskets array to accommodate the new basket
      if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_SELL)){
         baskets[basketIdx].signalTime = currentBarTime; //--- Record the signal time for the new sell basket
      }
   }
}

Für die Funktion „CheckForNewSignal()“ werden zunächst die Schlusskurse der beiden vorangegangenen Balken mit der Funktion iClose() abgefragt. Auf diese Weise können wir feststellen, ob ein Crossover stattgefunden hat. Wir verwenden auch die Funktion iTime(), um den Zeitstempel des letzten Balkens zu erhalten, um sicherzustellen, dass wir das gleiche Signal nicht mehrmals verarbeiten.

Bevor wir fortfahren, prüfen wir, ob die Anzahl der aktiven Warenkörbe den Grenzwert „maxBaskets“ erreicht hat. Ist dies der Fall, kehrt die Funktion zurück, um ein übermäßiges Stapeln von Handelsgeschäften zu verhindern. Bei Kaufsignalen prüfen wir, ob der letzte Schlusskurs über dem Gleitenden Durchschnitt liegt, während der vorherige Schlusskurs darunter lag. Wenn diese Kreuzungs-Bedingung erfüllt ist, werden die vorhandenen Körbe durchlaufen, um sicherzustellen, dass das gleiche Signal nicht bereits verarbeitet wurde. Wenn das Signal neu ist, vergrößern wir das Array „baskets“, speichern den neuen Korb am nächsten verfügbaren Index und rufen die Funktion „ExecuteInitialTrade()“ mit einer POSITION_TYPE_BUY-Order auf. Wenn der Handel erfolgreich ausgeführt wird, zeichnen wir die Signalzeit auf, um doppelte Eingaben zu vermeiden.

Ebenso wird bei Verkaufssignalen geprüft, ob der letzte Schlusskurs unter dem gleitenden Durchschnitt liegt, während der vorherige Schlusskurs darüber lag. Wenn diese Bedingung erfüllt ist und kein doppeltes Signal gefunden wird, erweitern wir das Array „baskets“, führen einen ersten Verkauf mit der Funktion „ExecuteInitialTrade()“ mit dem Auftrag POSITION_TYPE_SELL aus und speichern die Signalzeit, um die Einzigartigkeit zu wahren. Die Funktion zur Ausführung der Handelsgeschäfte lautet wie folgt.

//+------------------------------------------------------------------+
//--- Execute Initial Trade
//+------------------------------------------------------------------+
bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction) {
   baskets[basketIdx].basketId = nextBasketId++; //--- Assign a unique basket ID and increment the counter
   baskets[basketIdx].magic = baseMagic + baskets[basketIdx].basketId * 10000; //--- Calculate a unique magic number for the basket
   baskets[basketIdx].initialLotSize = inpLotSize; //--- Set the initial lot size for the basket from input
   baskets[basketIdx].currentLotSize = inpLotSize; //--- Initialize current lot size to the same as the initial lot size
   baskets[basketIdx].direction = direction; //--- Set the trade direction (buy or sell) for the basket
   bool isTradeExecuted = false; //--- Initialize flag to track if the trade was successfully executed
   string comment = GetPositionComment(baskets[basketIdx].basketId, true); //--- Generate a comment string indicating an initial trade
   obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Set the trade object's magic number to the basket's unique value

   if(direction == POSITION_TYPE_BUY) {
      baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Set the grid level for subsequent buy orders below the current ask price
      baskets[basketIdx].takeProfit = ask + takeProfitPts; //--- Calculate the take profit level for the buy order
      if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY at ", ask, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful buy order details
         isTradeExecuted = true; //--- Mark the trade as executed successfully
      } else {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY failed, error: ", GetLastError()); //--- Log the error if the buy order fails
         ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails
      }
   } else if(direction == POSITION_TYPE_SELL) {
      baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Set the grid level for subsequent sell orders above the current bid price
      baskets[basketIdx].takeProfit = bid - takeProfitPts; //--- Calculate the take profit level for the sell order
      if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL at ", bid, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful sell order details
         isTradeExecuted = true; //--- Mark the trade as executed successfully
      } else {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL failed, error: ", GetLastError()); //--- Log the error if the sell order fails
         ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails
      }
   }
   return (isTradeExecuted); //--- Return the status of the trade execution
}

Wir definieren die Funktion „ExecuteInitialTrade()“, um sicherzustellen, dass jeder Korb eine eindeutige Kennung hat, weisen ihm eine eindeutige magische Zahl zu und initialisieren die wichtigsten Handelsparameter, bevor der Auftrag erteilt wird. Zunächst weisen wir eine „basketId“ zu, indem wir die Variable „nextBasketId“ inkrementieren. Wir generieren dann eine eindeutige magische Zahl für den Korb, indem wir einen skalierten Offset zum Wert von „baseMagic“ hinzufügen, um sicherzustellen, dass jeder Korb unabhängig funktioniert. Die ursprüngliche und die aktuelle Losgröße werden beide auf „inpLotSize“ gesetzt, um die Basishandelsgröße für diesen Korb festzulegen. Die „Richtung“ wird gespeichert, um zwischen Kauf- und Verkaufskörben zu unterscheiden.

Um sicherzustellen, dass die Handelsgeschäfte identifizierbar sind, rufen wir die Funktion „GetPositionComment()“ auf, um einen beschreibenden Kommentar zu generieren, und wenden die magische Zahl des Korbes mit der Methode „SetExpertMagicNumber()“ auf das Handelsobjekt an. Die Funktion ist wie folgt definiert, wobei wir die Funktion StringFormat verwenden, um den Kommentar über einen ternären Operator zu erhalten.

//+------------------------------------------------------------------+
//--- Generate Position Comment
//+------------------------------------------------------------------+
string GetPositionComment(int basketId, bool isInitial) {
   return StringFormat("Basket_%d_%s", basketId, isInitial ? "Initial" : "Grid"); //--- Generate a standardized comment string for a position indicating basket ID and trade type
}

Wenn die Richtung POSITION_TYPE_BUY ist, berechnen wir die Raster-Ebene, indem wir „gridSize_Spacing“ vom Briefkurs abziehen und Take-Profit bestimmen, indem wir „takeProfitPts“ zum Briefkurs addieren. Anschließend verwenden wir die Funktion „Buy()“ der Klasse „CTrade“, um den Auftrag zu erteilen. Im Erfolgsfall protokollieren wir die Details des Handelsgeschäfts mit der Funktion Print() und kennzeichnen das Handelsgeschäft als ausgeführt. Wenn der Handel fehlschlägt, protokollieren wir den Fehler mit der Funktion GetLastError() und verwenden die Funktion ArrayResize(), um die Größe des Arrays „baskets“ zu verringern und den fehlgeschlagenen Korb zu entfernen.

Bei einem Verkauf (POSITION_TYPE_SELL) berechnen wir die Raster-Ebene, indem wir „gridSize_Spacing“ zum Geldkurs addieren und das Take-Profit-Level bestimmen, indem wir „takeProfitPts“ vom Geldkurs abziehen. Der Handel wird mit der Funktion „Sell()“ ausgeführt. Wie bei den Käufen wird die erfolgreiche Ausführung mit der Funktion „Print()“ protokolliert, und ein Fehlschlag führt zu einem Fehlerprotokoll mit GetLastError, gefolgt von einer Größenänderung des Arrays „baskets“ mit „ArrayResize()“, um den fehlgeschlagenen Korb zu entfernen.

Bevor ein Handel ausgeführt wird, stellt die Funktion sicher, dass das Array genügend Platz hat, indem sie „ArrayResize()“ aufruft, um es zu vergrößern. Schließlich gibt die Funktion „true“ zurück, wenn der Handel erfolgreich ausgeführt wurde, und „false“, wenn nicht. Nach Ausführung des Programms erhalten wir folgendes Ergebnis.

BESTÄTIGTE ANFANGSPOSITIONEN IN KÖRBEN

Aus dem Bild können wir ersehen, dass wir die Anfangspositionen gemäß den realisierten Körben oder Signalen bestätigt haben. Wir müssen dann dazu übergehen, diese Positionen zu verwalten, indem wir jeden Korb einzeln verwalten. Um dies zu erreichen, verwenden wir eine for-Schleife für die Iteration.

//--- Loop through all active baskets to manage grid positions and potential closures
for(int i = 0; i < ArraySize(baskets); i++) {
   ManageGridPositions(i, ask, bid); //--- Manage grid trading for the current basket
}

Hier werden alle aktiven Warenkörbe in einer for-Schleife durchlaufen, um sicherzustellen, dass jeder Korb entsprechend verwaltet wird. Die Funktion ArraySize bestimmt die aktuelle Anzahl der Körbe im Array „baskets“ und bildet damit die Obergrenze der Schleife. Dadurch wird sichergestellt, dass alle vorhandenen Körbe verarbeitet werden, ohne die Array-Grenzen zu überschreiten. Für jeden Korb rufen wir die Funktion „ManageRasterPositions()“ auf, wobei wir den Index des Korbes zusammen mit den normalisierten „Ask“- und „Bid“-Preisen übergeben. Die Funktion ist wie folgt.

//+------------------------------------------------------------------+
//--- Manage Grid Positions
//+------------------------------------------------------------------+
void ManageGridPositions(int basketIdx, double ask, double bid) {
   bool newPositionOpened = false; //--- Flag to track if a new grid position has been opened
   string comment = GetPositionComment(baskets[basketIdx].basketId, false); //--- Generate a comment for grid trades in this basket
   obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Ensure the trade object uses the basket's unique magic number

   if(baskets[basketIdx].direction == POSITION_TYPE_BUY) {
      if(ask <= baskets[basketIdx].gridSize) { //--- Check if the ask price has reached the grid level for a buy order
         baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the defined multiplier
         if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) {
            newPositionOpened = true; //--- Set flag if the grid buy order is successfully executed
            Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY at ", ask); //--- Log the grid buy execution details
            baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Adjust the grid level for the next potential buy order
         } else {
            Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY failed, error: ", GetLastError()); //--- Log an error if the grid buy order fails
         }
      }
   } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) {
      if(bid >= baskets[basketIdx].gridSize) { //--- Check if the bid price has reached the grid level for a sell order
         baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the multiplier for grid orders
         if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) {
            newPositionOpened = true; //--- Set flag if the grid sell order is successfully executed
            Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL at ", bid); //--- Log the grid sell execution details
            baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Adjust the grid level for the next potential sell order
         } else {
            Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL failed, error: ", GetLastError()); //--- Log an error if the grid sell order fails
         }
      }
   }

   //--- If a new grid position was opened and there are multiple positions, adjust the take profit to breakeven
   if(newPositionOpened && CountBasketPositions(baskets[basketIdx].basketId) > 1) {
      double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the weighted breakeven price for the basket
      double newTP = (baskets[basketIdx].direction == POSITION_TYPE_BUY) ?
                     breakevenPrice + (inpBreakevenPts * _Point) : //--- Set new TP for buy positions
                     breakevenPrice - (inpBreakevenPts * _Point);  //--- Set new TP for sell positions
      baskets[basketIdx].takeProfit = newTP; //--- Update the basket's take profit level with the new value
      for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to update TP where necessary
         ulong ticket = PositionGetTicket(j); //--- Get the ticket number for the current position
         if(PositionSelectByTicket(ticket) && 
            PositionGetString(POSITION_SYMBOL) == _Symbol && 
            PositionGetInteger(POSITION_MAGIC) == baskets[basketIdx].magic) { //--- Identify positions that belong to the current basket
            if(!obj_Trade.PositionModify(ticket, 0, newTP)) { //--- Attempt to modify the position's take profit level
               Print("Basket ", baskets[basketIdx].basketId, ": Failed to modify TP for ticket ", ticket); //--- Log error if modifying TP fails
            }
         }
      }
      Print("Basket ", baskets[basketIdx].basketId, ": Breakeven = ", breakevenPrice, ", New TP = ", newTP); //--- Log the new breakeven and take profit levels
   }
}

Hier implementieren wir die Funktion „ManageRasterPositions()“, um den rasterbasierten Handel innerhalb jedes aktiven Korbes dynamisch zu verwalten. Wir stellen sicher, dass neue Netzpositionen zu den richtigen Kursen ausgeführt werden und dass bei Bedarf Gewinnanpassungen vorgenommen werden. Wir beginnen mit der Initialisierung des Flags „newPositionOpened“, um festzustellen, ob ein neuer Rasterhandel ausgeführt wurde. Mit der Funktion „GetPositionComment()“ erzeugen wir einen Kommentar, der für den Handelstyp (Initial oder Raster) spezifisch ist. Anschließend rufen wir die Funktion „SetExpertMagicNumber()“ auf, um dem Korb eine eindeutige magische Nummer zuzuweisen, die sicherstellt, dass alle Handelsgeschäfte innerhalb des Korbs ordnungsgemäß nachverfolgt werden.

Bei Kaufkörben wird geprüft, ob der Angebotspreis auf oder unter den Schwellenwert „gridSize“ gefallen ist. Wenn diese Bedingung erfüllt ist, wird die Losgröße durch Multiplikation von „currentLotSize“ mit dem Eingabeparameter „inpMultiplier“ angepasst. Als Nächstes versuchen wir, mit der Methode „Buy()“ des Handelsobjekts „obj_Trade“ einen Kaufauftrag zu erteilen. Wenn der Handel erfolgreich ausgeführt wurde, wird „gridSize“ durch Subtraktion von „gridSize_Spacing“ aktualisiert, um sicherzustellen, dass der nächste Kaufhandel korrekt positioniert wird. Wir protokollieren auch die erfolgreiche Ausführung mit der Funktion Print(). Wenn der Kaufauftrag fehlschlägt, wird der Fehler mit der Funktion GetLastError() ermittelt und protokolliert.

Bei Verkaufskörben gehen wir ähnlich vor, prüfen aber stattdessen, ob der Angebotspreis auf oder über den Schwellenwert von RasterSize gestiegen ist. Wenn diese Bedingung erfüllt ist, wird die Losgröße angepasst, indem der „inpMultiplier“ auf „currentLotSize“ angewendet wird. Anschließend führen wir einen Verkaufsauftrag mit der Funktion „Sell()“ aus und aktualisieren die RasterSize durch Hinzufügen von „gridSize_Spacing“, um die nächste Verkaufsstufe zu definieren. Wenn der Auftrag erfolgreich ist, protokollieren wir die Details mit „Print()“, und wenn sie fehlschlägt, protokollieren wir den Fehler mit „GetLastError()“.

Wenn eine neue Rasterposition eröffnet wird und der Korb nun mehrere Handelsgeschäfte enthält, passen wir Take Profit auf ein Breakeven-Niveau an. Zunächst ermitteln wir den Breakeven-Preis, indem wir die Funktion „CalculateBreakevenPrice()“ aufrufen. Anschließend berechnen wir ein neues Take-Profit-Niveau auf der Grundlage der Richtung des Korbs:

  • Bei Kaufkörben wird der Take Profit durch Addition von „inpBreakevenPts“ (umgerechnet in Preispunkte) zum Breakeven-Preis festgelegt.
  • Bei Verkaufskörben wird der Take Profit angepasst, indem „inpBreakevenPts“ vom Breakeven-Preis abgezogen wird.

Als Nächstes durchlaufen wir mit der Funktion PositionsTotal() eine Schleife durch alle offenen Positionen und rufen die Ticketnummer jeder Position mit PositionGetTicket() ab. Wir verwenden PositionSelectByTicket(), um die Position auszuwählen und ihr Symbol mit der Funktion „PositionGetString“ zu überprüfen. Wir stellen auch sicher, dass die Position zum richtigen Korb gehört, indem wir ihre magische Zahl mit dem Parameter „POSITION_MAGIC“ überprüfen. Nach der Überprüfung versuchen wir, den Take Profit mit der Methode „PositionModify()“ zu ändern. Wenn diese Änderung fehlschlägt, wird der Fehler protokolliert.

Schließlich protokollieren wir den neu berechneten Breakeven-Preis und das aktualisierte Take-Profit-Level mit der Funktion Print(). Dadurch wird sichergestellt, dass sich die Raster-Handelsstrategie dynamisch anpasst und gleichzeitig effiziente Ausstiegspunkte beibehält. Die Funktion, die für die Berechnung des Durchschnittspreises zuständig ist, lautet wie folgt.

//+------------------------------------------------------------------+
//--- Calculate Weighted Breakeven Price for a Basket
//+------------------------------------------------------------------+
double CalculateBreakevenPrice(int basketId) {
   double weightedSum = 0.0; //--- Initialize sum for weighted prices
   double totalLots = 0.0;   //--- Initialize sum for total lot sizes
   for(int i = 0; i < PositionsTotal(); i++) { //--- Loop over all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position
      if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket
         double lot = PositionGetDouble(POSITION_VOLUME); //--- Get the lot size of the position
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the open price of the position
         weightedSum += openPrice * lot; //--- Add the weighted price to the sum
         totalLots += lot; //--- Add the lot size to the total lots
      }
   }
   return (totalLots > 0) ? (weightedSum / totalLots) : 0; //--- Return the weighted average price (breakeven) or 0 if no positions found
}

Wir implementieren die Funktion „CalculateBreakevenPrice()“, um den gewichteten Breakeven-Preis für einen gegebenen Handelskorb zu ermitteln und so sicherzustellen, dass Take-Profit dynamisch auf der Grundlage der volumengewichteten Einstiegspreise aller offenen Positionen innerhalb des Korbs angepasst werden kann. Wir beginnen mit der Initialisierung von „weightedSum“, um die Summe der gewichteten Preise zu speichern, und „totalLots“, um die Gesamtlosgröße aller Positionen im Korb zu erfassen. Dann gehen wir alle offenen Positionen durch.

Für jede Position wird die Ticketnummer mit PositionGetTicket() abgefragt und die Position mit PositionSelectByTicket() ausgewählt. Wir überprüfen, ob die Position zum aktuellen Handelssymbol gehört. Außerdem wird geprüft, ob die Position zum angegebenen Korb gehört, indem mit der Funktion StringFind() nach der Korb-ID im Kommentarstring gesucht wird. Der Kommentar muss „Basket_“ + IntegerToString(basketId) enthalten, um demselben Korb zugeordnet zu werden.

Sobald die Position verifiziert ist, extrahieren wir ihre Losgröße mit „PositionGetDouble(POSITION_VOLUME)“ und ihren Eröffnungspreis mit POSITION_PRICE_OPEN. Wir multiplizieren dann den Eröffnungspreis mit der Losgröße und addieren das Ergebnis zu „weightedSum“, um sicherzustellen, dass größere Losgrößen einen größeren Einfluss auf den endgültigen Breakeven-Preis haben. Gleichzeitig wird die gesamte Losgröße in „totalLots“ akkumuliert.

Nachdem wir alle Positionen durchlaufen haben, berechnen wir den gewichteten durchschnittlichen Breakeven-Preis, indem wir „weightedSum“ durch „totalLots“ dividieren. Wenn keine Positionen im Korb vorhanden sind („totalLots“ == 0), wird 0 zurückgegeben, um den Fehler einer Teilung durch Null zu vermeiden. Nach Ausführung des Programms erhalten wir folgendes Ergebnis.

ERÖFFNETS RASTER BILD 1

Aus dem Bild ist ersichtlich, dass die Warenkörbe unabhängig voneinander verwaltet werden, indem Raster geöffnet und die Preise gemittelt werden. Korb 2 hat zum Beispiel das gleiche Take-Profit von 0,68074. Wir können dies in dem unten abgebildeten Journal bestätigen.

RASTERPOSITIONEN JOURNAL

Aus dem Bild können wir ersehen, dass wir, sobald wir die Raster-Kaufposition für Korb 4 eröffnen, auch den Take Profit ändern. Jetzt müssen wir die Positionen, die auf den Modi basieren, aus Sicherheitsgründen schließen, obwohl dies nicht notwendig ist, da wir die Ebenen bereits wie folgt geändert haben.

if(closureMode == CLOSE_BY_PROFIT) CheckAndCloseProfitTargets(); //--- If using profit target closure mode, check for profit conditions
if(closureMode == CLOSE_BY_POINTS && CountBasketPositions(baskets[i].basketId) > 1) {
   CheckBreakevenClose(i, ask, bid); //--- If using points-based closure and multiple positions exist, check breakeven conditions
}

Hier verwalten wir Handelsabschlüsse auf der Grundlage des ausgewählten „closureMode“. Bei der Einstellung „CLOSE_BY_PROFIT“ wird „CheckAndCloseProfitTargets()“ aufgerufen, um Körbe zu schließen, die ihre Gewinnziele erreicht haben. Wenn auf „CLOSE_BY_POINTS“ gesetzt, stellen wir sicher, dass der Basket mehrere Positionen hat, indem wir „CountBasketPositions()“ verwenden, bevor wir „CheckBreakevenClose()“ aufrufen, um Handelsgeschäfte zum Breakeven zu schließen, wenn die Bedingungen erfüllt sind. Die Funktionen sind wie folgt.

//+------------------------------------------------------------------+
//--- Check and Close Profit Targets (for CLOSE_BY_PROFIT mode)
//+------------------------------------------------------------------+
void CheckAndCloseProfitTargets() {
   for(int i = 0; i < ArraySize(baskets); i++) { //--- Loop through each active basket
      int posCount = CountBasketPositions(baskets[i].basketId); //--- Count how many positions belong to the current basket
      if(posCount <= 1) continue; //--- Skip baskets with only one position as profit target checks apply to multiple positions
      double totalProfit = 0; //--- Initialize the total profit accumulator for the basket
      for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to sum their profits
         ulong ticket = PositionGetTicket(j); //--- Get the ticket for the current position
         if(PositionSelectByTicket(ticket) && 
            StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(baskets[i].basketId)) >= 0) { //--- Check if the position is part of the current basket
            totalProfit += PositionGetDouble(POSITION_PROFIT); //--- Add the position's profit to the basket's total profit
         }
      }
      if(totalProfit >= profitTotal_inCurrency) { //--- Check if the accumulated profit meets or exceeds the profit target
         Print("Basket ", baskets[i].basketId, ": Profit target reached (", totalProfit, ")"); //--- Log that the profit target has been reached for the basket
         CloseBasketPositions(baskets[i].basketId); //--- Close all positions in the basket to secure the profits
      }
   }
}

Hier prüfen wir die Körbe und schließen sie, wenn sie das Gewinnziel im Modus „CLOSE_BY_PROFIT“ erreichen. Wir durchlaufen die „baskets“ und verwenden „CountBasketPositions()“, um sicherzustellen, dass mehrere Positionen vorhanden sind. Dann summieren wir die Gewinne mit „PositionGetDouble(POSITION_PROFIT)“ für alle Positionen im Korb. Wenn der Gesamtgewinn „profitTotal_inCurrency“ erreicht oder übersteigt, protokollieren wir das Ereignis und rufen „CloseBasketPositions()“ auf, um die Gewinne zu sichern. Die Funktion „CountBasketPositions“ ist wie folgt definiert.

//+------------------------------------------------------------------+
//--- Count Positions in a Basket
//+------------------------------------------------------------------+
int CountBasketPositions(int basketId) {
   int count = 0; //--- Initialize the counter for positions in the basket
   for(int i = 0; i < PositionsTotal(); i++) { //--- Loop through all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position
      if(PositionSelectByTicket(ticket) && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket
         count++; //--- Increment the counter if a matching position is found
      }
   }
   return count; //--- Return the total number of positions in the basket
}

Wir verwenden die Funktion „CountBasketPositions()“, um die Positionen in einem bestimmten Korb zu zählen. Wir durchlaufen eine Schleife durch alle Positionen, rufen jedes „ticket“ mit der Funktion PositionGetTicket() ab und prüfen, ob der POSITION_COMMENT die Basket-ID enthält. Wenn eine Übereinstimmung gefunden wird, wird „count“ erhöht. Schließlich geben wir die Gesamtzahl der Positionen im Korb zurück. Die Definition der Funktion „CloseBasketPositions()“ lautet ebenfalls wie folgt.

//+------------------------------------------------------------------+
//--- Close All Positions in a Basket
//+------------------------------------------------------------------+
void CloseBasketPositions(int basketId) {
   for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop backwards through all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket of the current position
      if(PositionSelectByTicket(ticket) && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Identify if the position belongs to the specified basket
         if(obj_Trade.PositionClose(ticket)) { //--- Attempt to close the position
            Print("Basket ", basketId, ": Closed position ticket ", ticket); //--- Log the successful closure of the position
         }
      }
   }
}

Wir verwenden dieselbe Logik, um alle Positionen zu durchlaufen, sie zu überprüfen und sie mit der Methode „PositionClose“ zu schließen. Schließlich gibt es noch die Funktion, die die Schließung von Positionen erzwingt, wenn diese die festgelegten Zielwerte überschreiten.

//+------------------------------------------------------------------+
//--- Check Breakeven Close
//+------------------------------------------------------------------+
void CheckBreakevenClose(int basketIdx, double ask, double bid) {
   double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the breakeven price for the basket
   if(baskets[basketIdx].direction == POSITION_TYPE_BUY) {
      if(bid >= breakevenPrice + (inpBreakevenPts * _Point)) { //--- Check if the bid price exceeds breakeven plus threshold for buy positions
         Print("Basket ", baskets[basketIdx].basketId, ": Closing BUY positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions
         CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket
      }
   } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) {
      if(ask <= breakevenPrice - (inpBreakevenPts * _Point)) { //--- Check if the ask price is below breakeven minus threshold for sell positions
         Print("Basket ", baskets[basketIdx].basketId, ": Closing SELL positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions
         CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket
      }
   }
}

Hier implementieren wir breakeven-basierte Abschlüsse mit „CheckBreakevenClose()“. Wir ermitteln zunächst den Breakeven-Preis mit „CalculateBreakevenPrice()“. Befindet sich der Korb in einer KAUF-Richtung und der Geldkurs übersteigt den Breakeven plus den definierten Schwellenwert („inpBreakevenPts * _Point“), protokollieren wir das Ereignis und führen „CloseBasketPositions()“ aus, um die Gewinne zu sichern. In ähnlicher Weise prüfen wir bei VERKAUFS-Körben, ob der Briefkurs unter den Breakeven abzüglich des Schwellenwerts fällt, was die Schließung auslöst. Dadurch wird sichergestellt, dass die Positionen abgesichert sind, sobald die Preisbewegung den Breakeven-Bedingungen entspricht.

Da wir die Positionen zunächst mit Take-Profit schließen, bedeutet dies, dass wir leere „Shells“ oder Positionskörbe haben, die das System verunreinigen. Um also eine Bereinigung zu gewährleisten, müssen wir die leeren Körbe, die keine Elemente enthalten, identifizieren und sie entfernen. Wir implementieren die folgende Logik.

//--- Remove inactive baskets that no longer have any open positions
for(int i = ArraySize(baskets) - 1; i >= 0; i--) {
   if(CountBasketPositions(baskets[i].basketId) == 0) {
      Print("Removing inactive basket ID: ", baskets[i].basketId); //--- Log the removal of an inactive basket
      for(int j = i; j < ArraySize(baskets) - 1; j++) {
         baskets[j] = baskets[j + 1]; //--- Shift basket elements down to fill the gap
      }
      ArrayResize(baskets, ArraySize(baskets) - 1); //--- Resize the baskets array to remove the empty slot
   }
}

Hier stellen wir sicher, dass inaktive Körbe, die keine offenen Positionen mehr enthalten, effizient entfernt werden. Wir durchlaufen das Array „baskets“ in umgekehrter Reihenfolge, um Indexverschiebungen beim Entfernen zu vermeiden. Mit „CountBasketPositions()“ prüfen wir, ob ein Korb keine verbleibenden Handelsgeschäfte hat. Wenn es leer ist, protokollieren wir seine Entfernung und verschieben die nachfolgenden Elemente nach unten, um die Array-Struktur zu erhalten. Schließlich rufen wir ArrayResize() auf, um die Größe des Arrays anzupassen, um unnötigen Speicherverbrauch zu vermeiden und sicherzustellen, dass nur aktive Körbe erfasst werden. Dieser Ansatz sorgt für eine effiziente Verwaltung der Warenkörbe und verhindert ein Durcheinander im System. Nach der Ausführung erhalten wir folgendes Ergebnis.

AUFRÄUMEN

Anhand des Bildes können wir sehen, dass wir die Entfernung von Unordnung effizient handhaben und die Rasterpositionen verwalten können, wodurch wir unser Ziel erreichen. Bleiben nur noch die Backtests des Programms, und das wird im nächsten Abschnitt behandelt.


Backtests

Nach gründlichen Backtests für 1 Jahr, 2023, mit den Standardeinstellungen haben wir die folgenden Ergebnisse.

Grafik des Backtests:

GRAPH

Bericht des Backtest:

BERICHT


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir einen MQL5 Multi-Level Raster Trading Expert Advisor entwickelt haben, der effizient mehrstufige Handelseinträge, dynamische Raster-Anpassungen und eine strukturierte Wiederherstellung verwaltet. Durch die Integration skalierbarer Rasterabstände, kontrollierter Lotprogression und Breakeven-Exits passt sich das System an Marktschwankungen an und optimiert gleichzeitig Risiko und Ertrag.

Haftungsausschluss: Dieser Artikel ist nur für Bildungszwecke gedacht. Der Handel ist mit erheblichen finanziellen Risiken verbunden, und die Marktbedingungen können unvorhersehbar sein. Ordnungsgemäße Backtests und ein Risikomanagement sind vor dem Live-Einsatz unerlässlich.

Durch die Anwendung dieser Techniken können Sie Ihre Fähigkeiten im algorithmischen Handel verbessern und Ihre rasterbasierte Strategie verfeinern. Testen und optimieren Sie weiter, um langfristig erfolgreich zu sein. Viel Glück!

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

Letzte Kommentare | Zur Diskussion im Händlerforum (4)
johnsteed
johnsteed | 12 März 2025 in 11:49

Ein sehr guter Code und sehr schneller EA!

Leider gibt es ein Problem mit der Losgrößenberechnung - Multiplikatoren mit einer Dezimalzahl (wie 1,3, 1,5 usw.) können Probleme mit MQL-Order-Funktionen verursachen, da die Losgröße manchmal Fehlercodes 4756 gibt, wenn der Multiplikator nicht 1 oder 2 ist.

Es wäre zu schön, wenn die Losgrößenberechnung geringfügig geändert werden könnte, um sicherzustellen, dass die Losgrößen für die Eingabe in die Auftragsfunktionen für alle Multiplikatorwerte angemessen berechnet werden.

Allan Munene Mutiiria
Allan Munene Mutiiria | 17 März 2025 in 19:19
johnsteed Fehlercodes 4756 gibt, wenn der Multiplikator nicht 1 oder 2 ist.

Es wäre zu schön, wenn die Losgrößenberechnung geringfügig geändert werden könnte, um sicherzustellen, dass die Losgrößen für die Eingabe in die Auftragsfunktionen für alle Multiplikatorwerte ordnungsgemäß berechnet werden.

Vielen Dank für das freundliche Feedback. Gern geschehen.

cbkiri
cbkiri | 6 Juni 2025 in 03:18

Hallo,

Nach der Lektüre des Artikels, fand es nützlich und wird auf jeden Fall testen es aus. Allerdings scheine ich nicht sehen, oder vielleicht habe ich aus dem Artikel auf die Trennung der ersten Position TP, die ich glaube, es ist auch nützlich und nachhaltig für die Handelsstrategie verpasst.


Ich danke Ihnen.

Allan Munene Mutiiria
Allan Munene Mutiiria | 6 Juni 2025 in 11:19
cbkiri trading-Strategie.


Ich danke Ihnen.

Sicher. Danke.

Datenwissenschaft und ML (Teil 34): Zeitreihenzerlegung, den Aktienmarkt auf den Kern herunterbrechen. Datenwissenschaft und ML (Teil 34): Zeitreihenzerlegung, den Aktienmarkt auf den Kern herunterbrechen.
In einer Welt, die von verrauschten und unvorhersehbaren Daten überschwemmt wird, kann es schwierig sein, aussagekräftige Muster zu erkennen. In diesem Artikel befassen wir uns mit der saisonalen Dekomposition, einer leistungsstarken Analysetechnik, die dabei hilft, Daten in ihre Hauptkomponenten zu zerlegen: Trend, saisonale Muster und Rauschen. Wenn wir die Daten auf diese Weise aufschlüsseln, können wir verborgene Erkenntnisse aufdecken und mit klareren, besser interpretierbaren Informationen arbeiten.
Der Kalman-Filter für Forex-Strategien der Rückkehr zur Mitte Der Kalman-Filter für Forex-Strategien der Rückkehr zur Mitte
Der Kalman-Filter ist ein rekursiver Algorithmus, der im algorithmischen Handel verwendet wird, um den wahren Zustand einer Finanzzeitreihe durch Herausfiltern von Rauschen aus den Preisbewegungen zu schätzen. Er aktualisiert die Vorhersagen dynamisch auf der Grundlage neuer Marktdaten, was ihn für adaptive Strategien wie Mean Reversion wertvoll macht. In diesem Artikel wird zunächst der Kalman-Filter vorgestellt und seine Berechnung und Anwendung erläutert. Als nächstes wenden wir den Filter auf eine klassische Devisenstrategie, der Rückkehr zur Mitte, als Beispiel an. Schließlich führen wir verschiedene statistische Analysen durch, indem wir den Filter mit einem gleitenden Durchschnitt für verschiedene Devisenpaare vergleichen.
Einführung in MQL5 (Teil 13): Ein Anfängerleitfaden zur Erstellung nutzerdefinierter Indikatoren (II) Einführung in MQL5 (Teil 13): Ein Anfängerleitfaden zur Erstellung nutzerdefinierter Indikatoren (II)
Dieser Artikel führt Sie durch die Erstellung eines nutzerdefinierten Heikin Ashi-Indikators von Grund auf und zeigt Ihnen, wie Sie Ihre nutzerdefinierte Indikatoren in einen EA integrieren können. Es umfasst Indikatorberechnungen, Handelsausführungslogik und Risikomanagementtechniken zur Verbesserung automatisierter Handelsstrategien.
Installation von MetaTrader 5 und anderen MetaQuotes-Anwendungen auf HarmonyOS NEXT Installation von MetaTrader 5 und anderen MetaQuotes-Anwendungen auf HarmonyOS NEXT
Eine einfache Installation des MetaTrader 5 und andere MetaQuotes-Applikationen auf Geräten mit HarmonyOS NEXT mit DroiTong. Eine detaillierte Schritt-für-Schritt-Anleitung für Ihr Handy oder Ihren Laptop.