Verwendung von Indikatoren zur Optimierung von Expert Advisors in Echtzeit

Dmitriy Gizlyk | 22 Oktober, 2018

Inhalt

Einführung

Jedes Mal, wenn wir einen Expert Advisor in einem Chart starten, stehen wir vor der Frage, welches sind die optimalen Parameter für eine maximale Rentabilität, um sie auszuwählen. Um solche Parameter zu finden, optimieren wir die Handelsstrategie auf Basis historischer Daten. Wie jedoch alle wissen, ist der Markt in ständiger Bewegung. Im Laufe der Zeit verlieren die ausgewählten Parameter ihre Gültigkeit.

Daher ist eine Re-Optimierung des EAs erforderlich. Dieser Zyklus ist konstant. Den Zeitpunkt der erneuten Optimierung wählt jeder Nutzer selbst. Aber ist es möglich, diesen Prozess zu automatisieren? Welche Lösungen gibt es? Möglicherweise haben Sie bereits eine Möglichkeit der Programmsteuerung des Standard-Strategietesters über Starten des Terminals mit einer benutzerdefinierten Konfigurationsdatei erwogen. Ich möchte einen unkonventionellen Ansatz anbieten und die Testfunktionen einem Indikator zuordnen.

1. Idee

Natürlich ist ein Indikator kein Strategietester. Wie kann er uns also bei der Optimierung eines EA helfen? Meine Idee ist es, die EA-Betriebslogik in einen Indikator zu implementieren und die Rentabilität von virtuellen Geschäften in Echtzeit zu verfolgen. Bei der Optimierung im Strategietester führen wir eine Reihe von Tests durch, die über bestimmte Parameter iterieren. Wir werden dasselbe tun, indem wir gleichzeitig mehrere Instanzen eines einzelnen Indikators mit unterschiedlichen Parametern starten, ähnlich wie bei Durchgängen im Strategietester. Zum Zeitpunkt der Entscheidung überprüft der Berater die laufenden Indikatoren und wählt die besten für die Ausführung aus.

Sie werden sich fragen, warum das Rad neu erfunden wurde. Lassen Sie uns die Vor- und Nachteile dieser Entscheidung analysieren. Zweifellos ist der Hauptvorteil dieses Ansatzes die Optimierung eines EA unter nahezu Echtzeitbedingungen. Der zweite Vorteil ist, dass der Test mit echten Ticks Ihres Brokers durchgeführt wird. Andererseits hat das Testen in Echtzeit einen großen Nachteil, da man warten muss, bis statistische Daten gesammelt sind. Ein weiterer Vorteil ist, dass der Testindikator beim Bewegen in der Zeit nicht die gesamte Historie neu berechnet, sondern nur den aktuellen Tick, während der Strategietester von Anfang die ganze Historie durchläuft. Dieser Ansatz ermöglicht eine schnellere Optimierung im richtigen Moment. Daher könnten wir an fast jeder Bar eine Optimierung durchführen.

Zu den Nachteilen dieses Ansatzes gehört das Fehlen einer Tick-Historie für die Prüfung anhand der Historie. Natürlich könnten wir CopyTicks oder CopyTicksRange verwenden. Das Herunterladen der Tick-Historie erfordert jedoch Zeit, und auch die Neuberechnung des großen Datenvolumens erfordert Rechenleistung und Zeit. Vergessen wir nicht, dass wir Indikatoren verwenden, und alle Indikatoren für ein einzelnes Symbol funktionieren in einem Thread in MetaTrader 5. Hier ist also eine weitere Einschränkung - zu viele Indikatoren können dazu führen, dass das Terminal verlangsamt wird.

Um die Risiken der beschriebenen Nachteile zu minimieren, lassen Sie uns die folgenden Annahmen treffen:

  1. Bei der Initialisierung des Testindikators wird die Historie durch die Preise von М1 OHLC berechnet. Bei der Berechnung von Positionsgewinnen/-verlusten wird zunächst ein Stop-Loss und dann ein Take-Profit vom High/Low (je nach Positionstyp) überprüft.
  2. Gemäß Punkt 1 werden Positionen nur beim Entstehen einer neuen Kerze geöffnet.
  3. Um die Gesamtzahl der laufenden Testindikatoren zu verringern, wenden Sie einen sinnvollen Ansatz zur Auswahl der in ihnen verwendeten Parameter an. Hier können wir eine minimale Schrittweite und Filterparameter gemäß der Indikatorlogik hinzufügen. Wenn sich beispielsweise bei der Verwendung des MACD der Parameterbereich von schnellen und langsamen MAs überschneidet, wird der Testindikator nicht für einen Parametersatz gestartet, bei dem eine langsame MA-Periode kleiner oder gleich einer schnellen MA-Periode ist, da dies der Betriebslogik des EAs widerspricht. Wir können auch einen Mindestabstand zwischen den Perioden verwenden, indem wir zunächst Optionen mit einer großen Anzahl von Fehlersignalen verwerfen.

2. Handelsstrategie

Um die Methode zu testen, verwenden wir eine einfache Strategie, die auf den drei Standardindikatoren WPR, RSI und ADX basiert. Ein Kaufsignal wird ausgelöst, wenn WPR das überverkaufte Niveau nach oben überschreitet (Niveau -80). Der RSI sollte sich nicht im überkauften Bereich (über dem Level 70) befinden. Da beide Indikatoren Oszillatoren sind, ist ihr Einsatz ine Seitwärtsbewegungen gerechtfertigt. Das Vorhandensein einer Seitwärtsbewegung wird durch einen ADX-Indikator überprüft, der den Level 40 nicht überschreiten sollte.

Kaufposition eröffnen

Das Signal für eine Verkaufsposition wird gespiegelt. Der WPR-Indikator überschreitet den überkauften Bereich -20 nach unten, der RSI sollte über dem überverkauften Bereich von 30 liegen. ADX kontrolliert die Existenz einer Seitwärtsbewegung, genauso wie beim Kauf.

Verkaufsposition eröffnen

Wie bereits erwähnt, erfolgt der Markteintritt bei der neuen Kerze nach dem Signal. Das Schließen der Positionen erfolgt durch einen festen Stop-Loss oder Take-Profit.

Um mögliche Verluste zu begrenzen, gibt es nicht mehr als eine offene Position im Markt.

3. Vorbereitung der Testindikatoren

3.1. Klasse der virtuellen Deals

Nach der Definition einer Handelsstrategie ist es an der Zeit, den Testindikator zu entwickeln. Zunächst müssen wir die virtuellen Positionen vorbereiten, die im Indikator verfolgt werden sollen. In diesem Artikel[1] wurde bereits eine virtuelle Positionsklasse beschrieben. Wir können diese Arbeit mit einer kleinen Ergänzung nutzen. Die zuvor beschriebene Klasse hat die Tick-Methode, die den Zeitpunkt des Schließens einer Position anhand der aktuellen Geld- und Briefkurse (Bid und Ask) überprüft. Dieser Ansatz ist nur bei der Arbeit in Echtzeit anwendbar und nicht bei der Überprüfung historischer Daten. Ändern wir die erwähnte Funktion etwas ab, indem wir einen Preis und einen Spread zu den Parametern hinzufügen. Nach der Ausführung von Operationen gibt die Methode den Positionsstatus zurück. Als Ergebnis dieser Ergänzung wird das Verfahren die folgende Form annehmen.

bool CDeal::Tick(double price, int spread)
  {
   if(d_ClosePrice>0)
      return true;
//---
   switch(e_Direct)
     {
      case POSITION_TYPE_BUY:
        if(d_SL_Price>0 && d_SL_Price>=price)
          {
           d_ClosePrice=price;
           i_Profit=(int)((d_ClosePrice-d_OpenPrice)/d_Point);
          }
        else
          {
           if(d_TP_Price>0 && d_TP_Price<=price)
             {
              d_ClosePrice=price;
              i_Profit=(int)((d_ClosePrice-d_OpenPrice)/d_Point);
             }
          }
        break;
      case POSITION_TYPE_SELL:
        price+=spread*d_Point;
        if(d_SL_Price>0 && d_SL_Price<=price)
          {
           d_ClosePrice=price;
           i_Profit=(int)((d_OpenPrice-d_ClosePrice)/d_Point);
          }
        else
          {
           if(d_TP_Price>0 && d_TP_Price>=price)
             {
              d_ClosePrice=price;
              i_Profit=(int)((d_OpenPrice-d_ClosePrice)/d_Point);
             }
          }
        break;
     }
   return IsClosed();
  }

Der gesamte Code der Klasse befindet sich im Anhang.

3.2. Programmieren der Indikatoren

Als nächstes kodieren wir den Indikator selbst. Da unser Testindikator in gewisser Weise die Rolle eines EA spielt, ähneln seine Eingaben den EA-Parametern. Wir legen zunächst den Testzeitraum sowie den Stop-Loss und Take-Profit über die Indikatorparametern. Als nächstes geben wir die Parameter der verwendeten Indikatoren an. Schließlich geben wir eine Handelsrichtung und die Glättungslänge der statistischen Daten an. Weitere Einzelheiten über die Verwendung der einzelnen Parameter sind anzugeben, wo sie im Indikatorcode verwendet werden.

input int                  HistoryDepth      =  500;           //Zeitraum der Historie (Bars)
input int                  StopLoss          =  200;           //Stop Loss(points)
input int                  TakeProfit        =  600;           //Take Profit(points)
//--- RSI Indikatorparameter
input int                  RSIPeriod         =  28;            //RSI Period
input double               RSITradeZone      =  30;            //Überkauft-/Überverkauftzone
//--- Indikatorparameter vom WPR
input int                  WPRPeriod         =  7;             //Period WPR
input double               WPRTradeZone      =  30;            //Überkauft-/Überverkauftzone
//--- ADX Indikatorparameter
input int                  ADXPeriod         =  11;            //ADX Period
input int                  ADXLevel          =  40;            // Seitwärtsebene ADX
//---
input int                  Direction         =  -1;            //Handelsrichtung "-1"-Alle, "0"-Kauf , "1"-Verkauf 
//---
input int                  AveragePeriod     =  10;            //Glättungslänge

Für die Berechnung und dem Austausch der Daten mit dem Indikator erstellen wir neun Indikatorpuffer mit folgenden Daten:

1. Wahrscheinlichkeit einer profitablen Positionen.

double      Buffer_Probability[];

2. Profit-Faktor des getesteten Zeitraums.

double      Buffer_ProfitFactor[];

3. Stop-Loss und Take-Profit. Diese beiden Puffer können ausgeschlossen werden, indem man das Array erstellt, das dem Handle des Indikators und den spezifizierten Niveaus im EA entspricht, oder indem man die Indikatorparameter über dessen Handle anfordert, wenn man eine Position eröffnet wird. Mir allerdings erscheint die gegenwärtige Lösung als die beste.

double      Buffer_TakeProfit[];
double      Buffer_StopLoss[];

4. Die Puffer für die Berechnung der Gesamtzahl der Positionen innerhalb des Testzeitraums und die Zahl derer mit Gewinn.

double      Buffer_ProfitCount[];
double      Buffer_DealsCount[];

5. Die folgenden beiden Puffer dienen hilfsweise der Berechnung vorheriger Werte und enthalten ähnliche Daten nur der aktuellen Bar.

double      Buffer_ProfitCountCurrent[];
double      Buffer_DealsCountCurrent[];

6. Und zuletzt der Puffer, über den das Signal an den EA für die Positionseröffnung gesendet wird.

double      Buffer_TradeSignal[];

Wir deklarieren zusätzlich zu den angegebenen Puffern ein Array zum Speichern offener Positionen, eine Variable zum Aufzeichnen der Zeit der letzten Position, Variablen zum Speichern der Indikator-Handles sowie Arrays zum Erhalten von Informationen aus Indikatoren im globalen Variablenblock.

CArrayObj   Deals;
datetime    last_deal;
int         wpr_handle,rsi_handle,adx_handle;
double      rsi[],adx[],wpr[];

Die Initialisierung der Indikatoren zu Beginn der Funktion OnInit().

int OnInit()
  {
//--- Holen des Indikatorhandles des RSI
   rsi_handle=iRSI(Symbol(),PERIOD_CURRENT,RSIPeriod,PRICE_CLOSE);
   if(rsi_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get RSI handle");
      Print("Handle = ",rsi_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }
//--- Holen des Indikatorhandles des WPR
   wpr_handle=iWPR(Symbol(),PERIOD_CURRENT,WPRPeriod);

   if(wpr_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get WPR handle");
      Print("Handle = ",wpr_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }
//--- Holen des Indikatorhandles des ADX
   adx_handle=iADX(Symbol(),PERIOD_CURRENT,ADXPeriod);
   if(adx_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get ADX handle");
      Print("Handle = ",adx_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }

Jetzt verbinden wir die Indikatorpuffer mit den dynamischen Arrays.

//--- Zuweisung der Puffer des Indikators
   SetIndexBuffer(0,Buffer_Probability,INDICATOR_CALCULATIONS);
   SetIndexBuffer(1,Buffer_DealsCount,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,Buffer_TradeSignal,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,Buffer_ProfitFactor,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,Buffer_ProfitCount,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,Buffer_TakeProfit,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,Buffer_StopLoss,INDICATOR_CALCULATIONS);
   SetIndexBuffer(7,Buffer_DealsCountCurrent,INDICATOR_CALCULATIONS);
   SetIndexBuffer(8,Buffer_ProfitCountCurrent,INDICATOR_CALCULATIONS);

Zuweisung der Eigenschaften der Zeitreihen zu allen Arrays.

   ArraySetAsSeries(Buffer_Probability,true);
   ArraySetAsSeries(Buffer_ProfitFactor,true);
   ArraySetAsSeries(Buffer_TradeSignal,true);
   ArraySetAsSeries(Buffer_DealsCount,true);
   ArraySetAsSeries(Buffer_ProfitCount,true);
   ArraySetAsSeries(Buffer_TakeProfit,true);
   ArraySetAsSeries(Buffer_StopLoss,true);
   ArraySetAsSeries(Buffer_DealsCountCurrent,true);
   ArraySetAsSeries(Buffer_ProfitCountCurrent,true);
//--- 
   ArraySetAsSeries(rsi,true);
   ArraySetAsSeries(wpr,true);
   ArraySetAsSeries(adx,true);

Am Ende der Funktion wird das Array der Positionen und das Datum der letzten Position zurückgesetzt, sowie den Namen unseres Indikators festgelegt.

   Deals.Clear();
   last_deal=0;
//---
   IndicatorSetString(INDICATOR_SHORTNAME,"Test Indicator");
//---
   return(INIT_SUCCEEDED);
  }

Die aktuellen Daten der Indikatoren werden mit der Funktion GetIndValue heruntergeladen. Die angegebene Funktion holt sich die erforderliche Tiefe der geladenen Datenhistorie, und gibt die Anzahl der geladenen Elemente zurück. Die Daten der Indikatoren werden in den global deklarierten Arrays gespeichert.

int GetIndValue(int depth)
  {
   if(CopyBuffer(wpr_handle,MAIN_LINE,0,depth,wpr)<=0 || CopyBuffer(adx_handle,MAIN_LINE,0,depth,adx)<=0 || CopyBuffer(rsi_handle,MAIN_LINE,0,depth,rsi)<=0)
      return -1;
   depth=MathMin(ArraySize(rsi),MathMin(ArraySize(wpr),ArraySize(adx)));
//---
   return depth;
  }

Um die Eröffnungssignale zu überprüfen, erstellen wir die Funktionen BuySignal und SellSignal. Den Code der Funktionen befindet sich im Anhang.

Wie bei jedem Indikator liegen die wichtigen Arbeitsschritte in der Funktion OnCalculate. Die Funktionsoperationen können logisch in zwei Abläufe unterteilt werden:

  1. Beim Neuberechnen von mehr als einer Bar (erster Start nach der Initialisierung oder Öffnen eines neuen Balkens). In diesem Ablauf werden wir die Signale für eine Positionseröffnung für jede nicht berechnete Bar überprüfen und die Stop-Orders der offenen Positionen basierend auf den historischen Daten des M1-Zeitrahmens verarbeiten.
  2. Während eine neue Bar noch nicht gebildet ist, überprüfen wir bei jedem neuen Tick, ob die Stopp-Orders der offenen Positionen aktiviert sind.

Überprüfen wir zu Beginn der Funktion die Anzahl der neuen Bars seit dem letzten Start der Funktion. Wenn dies der erste Funktionsaufruf nach der Initialisierung des Indikators ist, stellen wir das Ausmaß der Neuberechnung des Indikators auf nicht mehr als die erforderliche Prüftiefe ein und bringen die Indikatorpuffer in den Ausgangszustand.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   int total=rates_total-prev_calculated;
   if(prev_calculated<=0)
     {
      total=fmin(total,HistoryDepth);
//---
      ArrayInitialize(Buffer_Probability,0);
      ArrayInitialize(Buffer_ProfitFactor,0);
      ArrayInitialize(Buffer_TradeSignal,0);
      ArrayInitialize(Buffer_DealsCount,0);
      ArrayInitialize(Buffer_ProfitCount,0);
      ArrayInitialize(Buffer_TakeProfit,TakeProfit*_Point);
      ArrayInitialize(Buffer_StopLoss,StopLoss*_Point);
      ArrayInitialize(Buffer_DealsCountCurrent,0);
      ArrayInitialize(Buffer_ProfitCountCurrent,0);
     }

Als nächstes kommen die Operationen des ersten Logikflusses, wenn sich eine neue Bar öffnet. Wir laden zunächst die aktuellen Daten der angewandten Indikatoren herunter. Im Falle eines Datenübertragungsfehlers verlassen Sie die Funktion bis zum nächsten Tick und warten so mit der Neuberechnung der Indikatoren.

   if(total>0)
     {
      total=MathMin(GetIndValue(total+2),rates_total);
      if(total<=0)
         return prev_calculated;

Dann weisen wir die Eigenschaften der Zeitreihen den Arrays der hereinkommenden Preisen zu.

      if(!ArraySetAsSeries(open,true) || !ArraySetAsSeries(high,true) || !ArraySetAsSeries(low,true) || !ArraySetAsSeries(close,true)
         || !ArraySetAsSeries(time,true) || !ArraySetAsSeries(spread,true))
         return prev_calculated;

Die Hauptschleife zur Neuberechnung jeder Bar kommt als nächstes. Zu Beginn der Schleife initialisieren wir die Indikatorpuffer für den neu berechneten Bars.

      for(int i=total-3;i>=0;i--)
        {
         Buffer_TakeProfit[i]=TakeProfit*_Point;
         Buffer_StopLoss[i]=StopLoss*_Point;
         Buffer_DealsCount[i]=Buffer_DealsCountCurrent[i]=0;
         Buffer_ProfitCount[i]=Buffer_ProfitCountCurrent[i]=0;

Danach wollen wir prüfen, ob wir noch eine weitere Position für eine berechnete Bar eröffnet haben. Wenn nicht, überprüfen wir die Eröffnungssignale der Indikator, indem wir die zuvor erstellte Funktionen aufrufen. Wenn es ein Eröffnungssignal gibt, erstellen wir eine virtuelle Position und schreiben das entsprechende Signal in den Signalpuffer.

         if(last_deal<time[i])
           {
            if(BuySignal(i))
              {
               double open_price=open[i]+spread[i]*_Point;
               double sl=open_price-StopLoss*_Point;
               double tp=open_price+TakeProfit*_Point;
               CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_BUY,time[i],open_price,sl,tp);
               if(temp!=NULL)
                  Deals.Add(temp);
               Buffer_TradeSignal[i]=1;
              }
            else /*BuySignal*/
            if(SellSignal(i))
              {
               double open_price=open[i];
               double sl=open_price+StopLoss*_Point;
               double tp=open_price-TakeProfit*_Point;
               CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_SELL,time[i],open_price,sl,tp);
               if(temp!=NULL)
                  Deals.Add(temp);
               Buffer_TradeSignal[i]=-1;
              }
            else /*SellSignal*/
               Buffer_TradeSignal[i]=0;
           }

Jetzt können wir zu den offenen Positionen kommen. Zuerst prüfen wir den aktuellen Zeitrahmen. Wenn der Indikator mit M1 arbeitet, wird mit den Daten der Zeitreihen, die der Funktion OnCalculate() übergeben wurden, geprüft, ob die Stop-Orders ausgelöst wurden. Andernfalls müssen wir die Daten des Zeitrahmens M1 laden.

         if(Deals.Total()>0)
           {
            if(PeriodSeconds()!=60)
              {
               MqlRates rates[];
               int rat=CopyRates(_Symbol,PERIOD_M1,time[i],(i>0 ? time[i-1] : TimeCurrent()),rates);

Nach dem Laden der Kurse prüfen wir in der Schleife auf jeder M1-Bar, ob die Stopp-Orders ausgelöst wurden. Wir summieren die geschlossenen und die profitable Positionen in geeigneten Puffern der Indikatoren für eine Neuberechnung der Bar. Das Array wird in der Funktion CheckDeals verarbeitet. Die Daten einer geprüften Minutenkerze werden mittels der Funktionsparameter übergeben. Der Algorithmus der Funktion ist im Folgenden dargestellt.

               int closed=0, profit=0;
               for(int r=0;(r<rat && Deals.Total()>0);r++)
                 {
                  CheckDeals(rates[r].open,rates[r].high,rates[r].low,rates[r].close,rates[r].spread,rates[r].time,closed,profit);
                  if(closed>0)
                    {
                     Buffer_DealsCountCurrent[i]+=closed;
                     Buffer_ProfitCountCurrent[i]+=profit;
                    }
                 }

Ähnliche, andere Blöcke kommen als nächstes. Sie überprüfen die Positionen anhand der aktuellen Zeitrahmendaten, falls das Laden der Kurse oder der Indikatorberechnung im M1-Zeitrahmen fehlschlug.

               if(rat<0)
                 {
                  CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
                  Buffer_DealsCountCurrent[i]+=closed;
                  Buffer_ProfitCountCurrent[i]+=profit;
                 }
              }
            else /* PeriodSeconds()!=60 */
              {
               int closed=0, profit=0;
               CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
               Buffer_DealsCountCurrent[i]+=closed;
               Buffer_ProfitCountCurrent[i]+=profit;
              }
           } /* Deals.Total()>0 */

Zum Schluss analysieren wir die Statistik unsere Strategie. Berechnen der Zahl der Positionen während der Testperiode und wieviele von ihnen einem Gewinn erwirtschafteten.

         Buffer_DealsCount[i+1]=NormalizeDouble(Buffer_DealsCount[i+2]+Buffer_DealsCountCurrent[i+1]-((i+HistoryDepth+1)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth+1] : 0),0);
         Buffer_ProfitCount[i+1]=NormalizeDouble(Buffer_ProfitCount[i+2]+Buffer_ProfitCountCurrent[i+1]-((i+HistoryDepth+1)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth+1] : 0),0);
         Buffer_DealsCount[i]=NormalizeDouble(Buffer_DealsCount[i+1]+Buffer_DealsCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth] : 0),0);
         Buffer_ProfitCount[i]=NormalizeDouble(Buffer_ProfitCount[i+1]+Buffer_ProfitCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth] : 0),0);

Wenn es offene Positionen gibt, berechnen wir die Wahrscheinlichkeit, dass die Positionen einen Gewinn erzielen und Profit-Faktor der Strategie innerhalb des getesteten Zeitraums. Um plötzliche Änderungen der Gewinnwahrscheinlichkeit zu vermeiden, wird dieser Parameter gemäß der exponentiellen Durchschnittsgleichung unter Verwendung der in den Indikatorparametern eingestellten Glättungslänge ermittelt.

         if(Buffer_DealsCount[i]>0)
           {
            double pr=2.0/(AveragePeriod-1.0);
            Buffer_Probability[i]=((i+1)<rates_total && Buffer_Probability[i+1]>0 && Buffer_DealsCount[i+1]>=AveragePeriod ? Buffer_ProfitCount[i]/Buffer_DealsCount[i]*100*pr+Buffer_Probability[i+1]*(1-pr) : Buffer_ProfitCount[i]/Buffer_DealsCount[i]*100);
            if(Buffer_DealsCount[i]>Buffer_ProfitCount[i])
              {
               double temp=(Buffer_ProfitCount[i]*TakeProfit)/(StopLoss*(Buffer_DealsCount[i]-Buffer_ProfitCount[i]));
               Buffer_ProfitFactor[i]=((i+1)<rates_total && Buffer_ProfitFactor[i+1]>0 ? temp*pr+Buffer_ProfitFactor[i+1]*(1-pr) : temp);
              }
            else
               Buffer_ProfitFactor[i]=TakeProfit*Buffer_ProfitCount[i];
           }
        }
     }

Der Ablauf der Verarbeitung von jedem Tick hat eine ähnliche Logik, so dass es keinen Sinn macht, hier die vollständige Beschreibung anzugeben. Den vollständigen Code aller Indikatorfunktionen finden Sie im Anhang.

Ich habe bereits erwähnt, dass die Überprüfung der Auslösung der Stopp-Orders bestehender Positionen in der Funktion CheckDeals durchgeführt wird. Schauen wir den Algorithmus an. In den Parametern erhält die Funktion die erhaltenen Kurse der analysierten Bar und zwei Links zu den Variablen für die Rückgabe der Anzahl der geschlossenen und profitablen Positionen.

Zu Beginn der Funktion setzen wir zurückgegebene Variablen zurück und deklarieren die resultierende logische Variable.

bool CheckDeals(double open,double high,double low,double close,int spread,datetime time,int &closed, int &profit)
  {
   closed=0;
   profit=0;
   bool result=true;

Weiterhin ist in der Funktion die Schleife zur Iteration über alle Positionen im Array angeordnet. Zeiger, um Objekte zu behandeln, werden nacheinander in der Schleife erhalten. Im Falle eines fehlerhaften Zeigers auf das Objekt, löschen wir diese Position aus dem Array und fahren mit dem nächsten fort. Wenn ein Fehler bei der Ausführung von Operationen auftritt, setzen Sie die resultierende Variable auf "false".

   for(int i=0;i<Deals.Total();i++)
     {
      CDeal *deal=Deals.At(i);
      if(CheckPointer(deal)==POINTER_INVALID)
        {
         if(Deals.Delete(i))
            i--;
         else
            result=false;
         continue;
        }

Als nächstes überprüfen wir, ob Positionen beim Entstehen einer neuen Kerze eröffnet wurde. Wenn nicht, fahren wir mit der nächsten Position fort.

      if(deal.GetTime()>time)
         continue;

Schließlich überprüfen wir die Auslösung der Stop Orders der Positionen mit den Preisen von Open, High, Low und Close, indem wir die Methode Tick einer geprüften Position für jeden Preis aufrufen. Der Algorithmus der Methode wurde am Anfang des aktuellen Abschnitts beschrieben. Vergessen wir nicht, dass die Prüfreihenfolge für Kauf- und Verkaufspositionen unterschiedlich ist. Zuerst wird die Aktivierung der Stop-Losses überprüft, gefolgt von den Take-Profits. Dieser Ansatz mag die Handelsergebnisse teilweise unterschätzen, reduziert aber die Verluste im zukünftigen Handel. Wenn eine der Stop-Orders ausgelöst wurde, steigt die Anzahl der abgeschlossenen Positionen, und im Falle eines Gewinns steigt auch die Anzahl der profitablen Positionen. Nachdem ein Position geschlossen wurde, wird es aus dem Array entfernt, um eine Neuberechnung zu vermeiden.

      if(deal.Tick(open,spread))
        {
         closed++;
         if(deal.GetProfit()>0)
            profit++;
         if(Deals.Delete(i))
            i--;
         if(CheckPointer(deal)!=POINTER_INVALID)
            delete deal;
         continue;
        }
      switch(deal.Type())
        {
         case POSITION_TYPE_BUY:
            if(deal.Tick(low,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
            if(deal.Tick(high,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
           break;
         case POSITION_TYPE_SELL:
            if(deal.Tick(high,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
            if(deal.Tick(low,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
           break;
        }
     }
//---
   return result;
  }

Der gesamte Code des Indikators und alle seiner Funktionen befindet sich in der Anlage.

4. Erstellen des EAs

Nachdem wir den Tester-Indikator erstellt haben, ist es nun an der Zeit, unseren EA zu entwickeln. In den EA-Parametern legen wir die Anzahl der statischen Variablen fest (gemeinsam für alle Durchläufe) und definieren, ähnlich wie beim Strategie-Tester, Anfangs- und Endwerte der geänderten Parameter sowie den Schritt der Wertänderung. Außerdem legen wir in den EA-Parametern auch Kriterien für die Auswahl der Eröffnungssignale fest - dies ist die minimale Gewinnwahrscheinlichkeit und der minimale Gewinnfaktor für den Testzeitraum. Um die Objektivität der erhaltenen statistischen Daten zu erhalten, geben wir außerdem die Mindestanzahl der Geschäfte für den Testzeitraum an.

input double               Lot                     =  0.01;
input int                  HistoryDepth            =  500;           // HistoryDepth      =  500;           //Zeitraum der Historie (Bars)
//--- RSI Indikatorparameter
input int                  RSIPeriod_Start         =  5;             //RSI Period
input int                  RSIPeriod_Stop          =  30;            //RSI Period
input int                  RSIPeriod_Step          =  5;             //RSI Period
//---
input double               RSITradeZone_Start      =  30;            //Überkauft-/Überverkauftzone Start
input double               RSITradeZone_Stop       =  30;            //Überkauft-/Überverkauftzone Stop
input double               RSITradeZone_Step       =  5;             //Überkauft-/Überverkauftzone Schrittweite
//--- Indikatorparameter vom WPR
input int                  WPRPeriod_Start         =  5;             //Period WPR Start
input int                  WPRPeriod_Stop          =  30;            //Period WPR Stop
input int                  WPRPeriod_Step          =  5;             //Period WPR Schrittweite
//---
input double               WPRTradeZone_Start      =  20;            //Überkauft-/Überverkauftzone Start
input double               WPRTradeZone_Stop       =  20;            //Überkauft-/Überverkauftzone Stop
input double               WPRTradeZone_Step       =  5;             //Überkauft-/Überverkauftzone Schrittweite
//--- ADX Indikatorparameter
input int                  ADXPeriod_Start         =  5;             //ADX Period Start
input int                  ADXPeriod_Stop          =  30;            //ADX Period Stop
input int                  ADXPeriod_Step          =  5;             //ADX Period Step
//---
input int                  ADXTradeZone_Start      =  40;            //Seitwärtsebene ADX Start
input int                  ADXTradeZone_Stop       =  40;            //Seitwärtsebene ADX Stop
input int                  ADXTradeZone_Step       =  10;            //Seitwärtsebene ADX Schrittweite
//--- Positionseinstellungen
input int                  TakeProfit_Start        =  600;           //TakeProfit Start
input int                  TakeProfit_Stop         =  600;           //TakeProfit Stop
input int                  TakeProfit_Step         =  100;           //TakeProfit Schrittweite
//---
input int                  StopLoss_Start          =  200;           //StopLoss Start
input int                  StopLoss_Stop           =  200;           //StopLoss Stop
input int                  StopLoss_Step           =  100;           //StopLoss Schrittweite
//---
input double               MinProbability          =  60.0;          //Mindestgewinn
input double               MinProfitFactor         =  1.6;           //Minimaler Profitfactor
input int                  MinOrders               =  10;            //Mindestanzahl von Positionen in der Historie

Deklarieren wir in den globalen Variablen eine Instanz der Klasse der Handelsoperationen und das Array zum Speichern von Handles der Testindikatoren.

CArrayInt   ar_Handles;
CTrade      Trade;

Ordnen wir in der EA-Funktion OnInit eine Reihe von verschachtelten Schleifen an, um über alle Optionen der getesteten Parameter zu iterieren, und fügen einen separaten Test von Kauf- und Verkaufspositionen hinzu. Dieser Ansatz ermöglicht es, die Auswirkungen globaler Trends zu berücksichtigen, die nicht durch eine getestete Strategie verfolgt werden. Die Testindikatoren werden innerhalb der Schleifen initialisiert. Wenn das Laden des Indikators fehlschlägt, verlassen wir die Funktion mit dem Ergebnis INIT_FAILED. Wenn der Indikator erfolgreich geladen wurde, fügen wir dessen Handle dem Array hinzu.

int OnInit()
  {
//---
   for(int rsi=RSIPeriod_Start;rsi<=RSIPeriod_Stop;rsi+=RSIPeriod_Step)
      for(double rsi_tz=RSITradeZone_Start;rsi_tz<=RSITradeZone_Stop;rsi_tz+=RSITradeZone_Step)
         for(int wpr=WPRPeriod_Start;wpr<=WPRPeriod_Stop;wpr+=WPRPeriod_Step)
            for(double wpr_tz=WPRTradeZone_Start;wpr_tz<=WPRTradeZone_Stop;wpr_tz+=WPRTradeZone_Step)
               for(int adx=ADXPeriod_Start;adx<=ADXPeriod_Stop;adx+=ADXPeriod_Step)
                  for(double adx_tz=ADXTradeZone_Start;adx_tz<=ADXTradeZone_Stop;adx_tz+=ADXTradeZone_Step)
                     for(int tp=TakeProfit_Start;tp<=TakeProfit_Stop;tp+=TakeProfit_Step)
                        for(int sl=StopLoss_Start;sl<=StopLoss_Stop;sl+=StopLoss_Step)
                          for(int dir=0;dir<2;dir++)
                             {
                              int handle=iCustom(_Symbol,PERIOD_CURRENT,"::Indicators\\TestIndicator\\TestIndicator.ex5",HistoryDepth,
                                                                                                                        sl,
                                                                                                                        tp,
                                                                                                                        rsi,
                                                                                                                        rsi_tz,
                                                                                                                        wpr,
                                                                                                                        wpr_tz,
                                                                                                                        adx, 
                                                                                                                        adx_tz,
                                                                                                                        dir);
                              if(handle==INVALID_HANDLE)
                                 return INIT_FAILED;
                              ar_Handles.Add(handle);
                             }

Nach dem erfolgreichen Start aller Testindikatoren initialisieren wir die Klasse der Handelsoperationen und beenden die Ausführung der Funktion.

   Trade.SetAsyncMode(false);
   if(!Trade.SetTypeFillingBySymbol(_Symbol))
      return INIT_FAILED;
   Trade.SetMarginMode();
//---
   return(INIT_SUCCEEDED);
  }

In der Funktion OnTick werden die Handelssignale sortiert und die Handelsoperationen durchgeführt. Da wir uns bisher entschieden haben, Positionen nur beim Öffnen einer Bar zu öffnen, werden wir das Eintreten dieses Ereignisses zu Beginn der Funktion überprüfen.

void OnTick()
  {
//---
   static datetime last_bar=0;
   datetime cur_bar=(datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE);
   if(cur_bar==last_bar)
      return;

Unsere zweite Einschränkung bedeutet, dass nicht mehr als eine offene Position existiert. Daher beendet die Funktion die Ausführung, wenn es eine offene Position gibt.

   if(PositionSelect(_Symbol))
     {
      last_bar=cur_bar;
      return;
     }

Nach der Überprüfung der Kontrollpunkte fahren wir mit der Hauptschleife der Iteration über alle Indikatoren auf der Suche nach Signalen fort. Zu Beginn der Schleife versuchen wir, den Signalpuffer der Anzeige zu laden. Wenn der Indikator noch nicht neu berechnet wurde oder kein Handelssignal vorliegt, fahren wir mit dem nächsten Indikator fort.

   int signal=0;
   double probability=0;
   double profit_factor=0;
   double tp=0,sl=0;
   bool ind_caclulated=false;
   double temp[];
   for(int i=0;i<ar_Handles.Total();i++)
     {
      if(CopyBuffer(ar_Handles.At(i),2,1,1,temp)<=0)
         continue;
      ind_caclulated=true;
      if(temp[0]==0)
         continue;

Im nächsten Schritt wird geprüft, ob das empfangene Signal nicht im Widerspruch zu den zuvor empfangenen Signalen anderer Indikatoren steht. Das Vorhandensein widersprüchlicher Signale erhöht die Wahrscheinlichkeit eines Fehlers, so dass wir die Funktion verlassen, bevor die Bildung der nächsten Kerze beginnt.

      if(signal!=0 && temp[0]!=signal)
        {
         last_bar=cur_bar;
         return;
        }
      signal=(int)temp[0];

Überprüfen wir dann das Vorhandensein der minimal erforderlichen Anzahl von Positionen innerhalb des Testzeitraums. Wenn die Stichprobe nicht ausreicht, gehen wir zum nächsten Indikator.

      if(CopyBuffer(ar_Handles.At(i),1,1,1,temp)<=0 || temp[0]<MinOrders)
         continue;

Weiter wird die ausreichende Wahrscheinlichkeit der Profitabilität der Positionen auf einfache Weise verifiziert.

      if(CopyBuffer(ar_Handles.At(i),0,1,1,temp)<=0 || temp[0]<MathMax(probability,MinProbability))
         continue;

Wenn die Diskrepanzen in der Wahrscheinlichkeit einer profitablen Position nach dem analysierten Indikator und den zuvor geprüften weniger als 1 Prozent betragen, wird der beste aus den beiden Durchgängen anhand des Gewinnfaktors und der Gewinn-/Risikoverhältnisse ausgewählt. Die besten Daten der Durchläufe werden für die weitere Arbeit gespeichert.

      if(MathAbs(temp[0]-probability)<=1)
        {
         double ind_probability=temp[0];
//---
         if(CopyBuffer(ar_Handles.At(i),3,1,1,temp)<=0 || temp[0]<MathMax(profit_factor,MinProfitFactor))
            continue;
         double ind_profit_factor=temp[0];
         if(CopyBuffer(ar_Handles.At(i),5,1,1,temp)<=0)
            continue;
         double ind_tp=temp[0];
         if(CopyBuffer(ar_Handles.At(i),6,1,1,temp)<=0)
            continue;
         double ind_sl=temp[0];
         if(MathAbs(ind_profit_factor-profit_factor)<=0.01)
           {
            if(sl<=0 || tp/sl>=ind_tp/ind_sl)
               continue;
           }
//---
         probability=ind_probability;
         profit_factor=ind_profit_factor;
         tp=ind_tp;
         sl=ind_sl;
        }

Ist die Wahrscheinlichkeit für eine profitable Position deutlich höher, wird die Anforderung an den Gewinnfaktor für den Durchlauf überprüft. Wenn alle Voraussetzungen erfüllt sind, werden die Daten der Durchlauf für die weitere Arbeit gespeichert.

      else /* MathAbs(temp[0]-probability)<=1 */
        {
         double ind_probability=temp[0];
//---
         if(CopyBuffer(ar_Handles.At(i),3,1,1,temp)<=0 || temp[0]<MinProfitFactor)
            continue;
         double ind_profit_factor=temp[0];
         if(CopyBuffer(ar_Handles.At(i),5,1,1,temp)<=0)
            continue;
         double ind_tp=temp[0];
         if(CopyBuffer(ar_Handles.At(i),6,1,1,temp)<=0)
            continue;
         double ind_sl=temp[0];
         probability=ind_probability;
         profit_factor=ind_profit_factor;
         tp=ind_tp;
         sl=ind_sl;
        }
     }

Wenn nicht ein einziger Testindikator nach Überprüfung aller Indikatoren neu berechnet wird, verlassen wir die Funktion bis zum nächsten Tick und warten darauf, dass die Indikatoren neu berechnet werden.

   if(!ind_caclulated)
      return;

Nachdem die Indikatoren erfolgreich überprüft wurden und es gibt kein aktives Handelssignal gibt, verlassen wir die Funktion bevor eine neue Bar entsteht.

   last_bar=cur_bar;
//---
   if(signal==0 || probability==0 || profit_factor==0 || tp<=0 || sl<=0)
      return;

Am Ende der Funktion senden wir einen Auftrag gemäß des des besten Durchlaufs falls es ein Eröffnungssignal gibt.

   if(signal==1)
     {
      double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      tp+=price;
      sl=price-sl;
      Trade.Buy(Lot,_Symbol,price,sl,tp,"Real Time Optimizator");
     }
   else
      if(signal==-1)
        {
         double price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
         tp=price-tp;
         sl+=price;
         Trade.Sell(Lot,_Symbol,price,sl,tp,"Real Time Optimizator");
        }
  }

Der gesamte Code des EAs befindet sich im Anhang.

5. Testen dieses Ansatzes

Um die Methode zu demonstrieren, wurde der erstellte EA zusammen mit einer parallelen Optimierung eines Standard-EA mit Vorwärts-Test unter Verwendung ähnlicher Bereiche von einstellbaren Parametern und unter Beibehaltung des gemeinsamen Testzeitintervalls getestet. Um die Bedingungen gleich zu halten, haben wir den EA geschaffen, der nur einen Testindikator startet und Positionen mit allen seinen Signalen öffnet, ohne nach statistischen Daten zu filtern. Die Struktur seiner Konstruktion ist ähnlich dem oben erstellten EA, mit Ausnahme des Blocks zur Filterung der Signalkonsistenz. Der vollständige Code des EAs befindet sich im Anhang (ClassicExpert.mq5).

Es wurden Tests am H1 für 7 Monate des Jahres 2018 durchgeführt. 1/3 eines Testzeitraums wurde für einen Forward-Test des Standard EA gelassen.

Testparameter

Die Glättungsperioden der Indikatoren wurden für die Optimierung ausgewählt. Die einzelnen Wertebereiche reichen von 5 bis 30 mit einer Schrittweite von 5 für alle Indikatoren.

Testparameter

Die Optimierungsergebnisse zeigten die Inkonsistenz der vorgeschlagenen Strategie. Parameterwerte, die während der Optimierung einen kleinen Gewinn zeigten, erwiesen sich bei einem Vorwärts-Test als verlustbringend. Insgesamt zeigte keiner der Durchgänge einen Gewinn innerhalb des analysierten Zeitraums.

Optimierungsergebnisse

Die Ergebnisse der grafischen Analyse der Optimierung und des Vorwärts-Test zeigten eine Veränderung der Struktur der Preisbewegung, die zur Verschiebung der Gewinnzone um die Glättungsperiode des WPR-Indikators führte.

WPR-OptimierungsdiagrammWPR Diagramm des Vorwärts-Tests

Um den nach einer vorgeschlagenen Methode entwickelte EA zu testen, spezifizierten wir ähnliche Testparameter bei gleichem Analysezeitraum. Um die Eröffnungssignale zu sortieren, haben wir die minimale Gewinnwahrscheinlichkeit der Positionen von 60% und den minimalen Gewinnfaktor innerhalb des Testzeitraums von 2 festgelegt. Die Prüfungslänge beträgt 500 Kerzen.

Testen der vorgeschlagenen Methode

Während des Tests zeigte der EA einen Gewinn mit einem tatsächlichen Gewinnfaktor von 1,66 innerhalb des analysierten Zeitraums. Während des Tests im visuellen Modus belegte der Testagent 1250 MB RAM.

Schlussfolgerung

Der Artikel schlug die Methode der Entwicklung von EAs mit Optimierung in Echtzeit vor. Die Tests haben gezeigt, dass der Ansatz für den realen Handel tragfähig ist. Der durch die vorgeschlagene Methode geschaffene Experte hat die Möglichkeit gezeigt, einen Gewinn in der Periode der Rentabilität der Strategie zu machen und den Handel während der Periode seines Verlustes auszusetzen. Andererseits gibt anspruchsvolle Anforderungen der vorgeschlagenen Methode für die technischen Eigenschaften des verwendeten Computers. Die Prozessorgeschwindigkeit sollte in der Lage sein, alle geladenen Indikatoren neu zu berechnen, und der zur Verfügung stehende RAM-Speicher sollte alle verwendeten Indikatoren aufnehmen können.

Referenzen

  1. Das Erstellen einer neuen Handelsstrategie und sich die Positionseröffnungen durch Indikatoren bestimmen lassen

Die dieses Artikels

#
Name
Typ
Beschreibung
1 Deal.mqh Klassenbibliothek Klasse der virtuellen Deals
2 TestIndicator.mq5 Indikator Tester indicator
3 RealTimeOptimization.mq5 Expert Advisor De EA mit den vorgeschlagenen Methoden
4 ClassicExpert.mq5 Expert Advisor Der EA mit den Standardmethoden für eine Vergleichsberechnung