Ein Expert Advisor mit GUI: Hinzufügen von Funktionen (Teil II)

Anatoli Kazharski | 17 September, 2018

Inhalt

Einführung

Im vorherigen Artikel zeigte ich, wie schnell ein grafisches Interface (GUI) für einen EA entwickelt werden kann. Jetzt werden wir das bereits entwickelte GUI mit den Funktionen des EAs verbinden. 

Erhalt von Symbol- und Indikatordaten

Zuerst müssen wir uns darum kümmern, die Symbol- und Indikatordaten zu erhalten. Lassen Sie uns die Symbole als Tabelle darstellen, die sich auf die Filterwerte im Eingabefeld Symbols filter stützt. Dies geschieht mit der Methode CProgram::GetSymbols().

Wir markieren zu Beginn der Methode mit dem Fortschrittsbalken, dass gerade die Symbole empfangen werden. Zunächst ist die Gesamtzahl der Symbole unbekannt. Wir stellen daher den Fortschrittsbalken auf 50% ein. Als nächstes geben wir den Array des Symbole frei. Wenn wir mit der Anwendung arbeiten, müssen wir möglicherweise eine weitere Symbolliste erstellen, deshalb sollten wir das jedes Mal tun, wenn die Methode CProgram::GetSymbols() aufgerufen wird.

Der Filter im Eingabefeld Symbols filter wird nur verwendet, wenn das Kontrollkästchen aktiviert ist, während das Eingabefeld einige kommagetrennte Textzeichen enthält. Wenn die Bedingungen erfüllt sind, werden diese Zeichen als separate Elemente in das Array aufgenommen, so dass sie später bei der Suche nach notwendigen Symbolen verwendet werden können. Wir entfernen die Sonderzeichen an den Enden der einzelnen Elemente, falls erforderlich.

Der nächste Schritt ist die Schleife zum Sammeln der Forex-Symbole. Sie durchläuft die vollständige Liste aller auf dem Server verfügbaren Symbole. Zu Beginn jeder Iteration erhalten wir den Namen des Symbols und entfernen ihn aus dem Fenster Market Watch. Somit stimmen die Listen in der Programmoberfläche und in diesem Fenster überein. Als nächstes prüfen wir, ob ein empfangenes Symbol zur Kategorie der Forexsymbole gehört. Wenn alle Symbole für die Arbeit benötigt werden, kommentieren wir diese Bedingung einfach aus oder löschen sie. In diesem Artikel werden wir nur mit Forexsymbolen arbeiten.

Wenn der Namensfilter aktiviert ist, überprüfen wir im Zyklus, ob der Name des bei dieser Iteration empfangenen Symbols mit Textzeichen aus dem Eingabefeld Symbols filter übereinstimmt. Wenn es keine Übereinstimmung gibt, fügen wir das Symbol zum Array hinzu.

Wenn keine Symbole gefunden werden, wird nur das aktuelle Symbol des Hauptdiagramms dem Array hinzugefügt. Danach werden alle dem Array hinzugefügten Symbole im Fenster Market Watch sichtbar. 

//+------------------------------------------------------------------+
//| Klasse zum Erstellen der Anwendung                               |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Handelssymbol
   string            m_symbols[];
   //---
private:
   //--- Symbolabfrage
   void              GetSymbols(void);
  };
//+------------------------------------------------------------------+
//| Symbolabfrage                                                    |
//+------------------------------------------------------------------+
void CProgram::GetSymbols(void)
  {
   m_progress_bar.LabelText("Get symbols...");
   m_progress_bar.Update(1,2);
   ::Sleep(5);
//--- Array freigeben
   ::ArrayFree(m_symbols);
//--- Zeichenkettenarray der Elemente
   string elements[];
//--- Filtername des Symbols
   if(m_symb_filter.IsPressed())
     {
      string text=m_symb_filter.GetValue();
      if(text!="")
        {
         ushort sep=::StringGetCharacter(",",0);
         ::StringSplit(text,sep,elements);
         //---
         int elements_total=::ArraySize(elements);
         for(int e=0; e<elements_total; e++)
           {
            //--- Bereinigen der Enden
            ::StringTrimLeft(elements[e]);
            ::StringTrimRight(elements[e]);
           }
        }
     }
//--- Beschreiben des Arrays der Symbole
   int symbols_total=::SymbolsTotal(false);
   for(int i=0; i<symbols_total; i++)
     {
      //--- Abfrage der Symbole
      string symbol_name=::SymbolName(i,false);
      //--- Ausblenden des Market Watch
      ::SymbolSelect(symbol_name,false);
      //--- Wenn es kein Forexsymbol ist, weiter zum Nächsten
      if(::SymbolInfoInteger(symbol_name,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX)
         continue;
      //--- Filtername des Symbols
      if(m_symb_filter.IsPressed())
        {
         bool check=false;
         int elements_total=::ArraySize(elements);
         for(int e=0; e<elements_total; e++)
           {
            //--- Suche nach übereinstimmendem Symbolnamen
            if(::StringFind(symbol_name,elements[e])>-1)
              {
               check=true;
               break;
              }
           }
         //--- Zum Nächsten, wenn der Filter es nicht akzeptiert
         if(!check)
            continue;
        }
      //--- Sichern des Symbols im Array
      int array_size=::ArraySize(m_symbols);
      ::ArrayResize(m_symbols,array_size+1);
      m_symbols[array_size]=symbol_name;
     }
//--- Ist der Array leer, wird das aktuelle Symbol zum Standard
   int array_size=::ArraySize(m_symbols);
   if(array_size<1)
     {
      ::ArrayResize(m_symbols,array_size+1);
      m_symbols[array_size]=::Symbol();
     }
//--- Einblenden des Market Watch
   int selected_symbols_total=::ArraySize(m_symbols);
   for(int i=0; i<selected_symbols_total; i++)
      ::SymbolSelect(m_symbols[i],true);
  }

Kommen wir nun dazu, die Indikator-Handles für alle ausgewählten Symbole mit der Methode CProgram::GetHandles() zu erzeugen. Zuerst setzen wir die Größe des Handle-Arrays auf die Größe des Symbol-Arrays. Die Handles werden mit dem Zeitrahmen erstellt, der in der Combobox Timeframes angegeben ist. Da die Combobox es erlaubt, eine Zeichenkette zu übernehmen, sollte er anschließend in einen geeigneten Typ (ENUM_TIMEFRAMES) umgewandelt werden. Füllen wir nun das Handle-Array in der Schleife aus. In diesem Fall ist dies der Indikator Stochastic mit seinen Standardwerten. Wir aktualisieren die Fortschrittsanzeige bei jeder Iteration. Wir erinnern uns an den ersten Handle-Index des Graphen, der am Ende der Methode angezeigt wird.

class CProgram : public CWndEvents
  {
private:
   //--- Indikatorhandles
   int               m_handles[];
   //--- Handleindex des aktuellen Charts
   int               m_current_handle_index;
   //---
private:
   //--- Handleabfrage
   void              GetHandles(void);
  };
//+------------------------------------------------------------------+
//| Get the indicator handles for all symbols                        |
//+------------------------------------------------------------------+
void CProgram::GetHandles(void)
  {
//--- Größe des Handlearrays bestimmen
   int symbols_total=::ArraySize(m_symbols);
   ::ArrayResize(m_handles,symbols_total);
//--- Wertabfrage der Auswahlliste des Kombinationsfelds
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Über alle Symbole der Liste
   for(int i=0; i<symbols_total; i++)
     {
      //--- Abfrage des Handles des Indikators
      m_handles[i]=::iStochastic(m_symbols[i],StringToTimeframe(tf),5,3,3,MODE_SMA,STO_LOWHIGH);
      //--- Fortschrittsanzeige
      m_progress_bar.LabelText("Get handles: "+string(symbols_total)+"/"+string(i)+" ["+m_symbols[i]+"] "+((m_handles[i]!=WRONG_VALUE)? "ok" : "wrong")+"...");
      m_progress_bar.Update(i,symbols_total);
      ::Sleep(5);
     }
//--- Sichern des ersten Index der Handles der Charts
   m_current_handle_index=0;
  }

Die Werte der Indikatoren werden mit der Methode CProgram::GetIndicatorValues() abgefragt. Beschreiben wir den Algorithmus. Wir legen zunächst die Größe des Arrays für die Indikatorwerte fest, die der Größe des Handle-Arrays entspricht; gehen dann in der Hauptschleife durch das Handle-Array und machen fünf Versuche, bei jeder Iteration die Indikatorendaten zu holen. Wir überprüfen sicherheitshalber die Gültigkeit des Handles und versuchen, sie erneut zu abzufragen, wenn es nicht vorher nicht gelungen ist. Wir aktualisieren den Fortschrittsbalken am Ende der Hauptschleife, damit wir den aktuellen Programmstand sehen können.

class CProgram : public CWndEvents
  {
private:
   //--- Indikatorwerte
   double            m_values[];
   //---
private:
   //--- Abfrage der Indikatorwerte aller Symbole
   void              GetIndicatorValues(void);
  };
//+------------------------------------------------------------------+
//| Abfrage der Indikatorwerte aller Symbole                         |
//+------------------------------------------------------------------+
void CProgram::GetIndicatorValues(void)
  {
//--- Setzen der Größe
   int handles_total=::ArraySize(m_handles);
   ::ArrayResize(m_values,handles_total);
//--- Wertabfrage der Auswahlliste des Kombinationsfelds
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Abfrage der Indikatorwerte aller Symbole in der Liste
   for(int i=0; i<handles_total; i++)
     {
      //--- Mach 5 versuche die Daten abzufragen
      int attempts=0;
      int received=0;
      while(attempts<5)
        {
         //--- Wenn der Handle ungültig ist, erneuter Versuch
         if(m_handles[i]==WRONG_VALUE)
           {
            //--- Abfrage des Handles des Indikators
            m_handles[i]=::iStochastic(m_symbols[i],StringToTimeframe(tf),5,3,3,MODE_SMA,STO_LOWHIGH);
            continue;
           }
         //--- Abfrageversuch der Indikatorwerte
         double values[1];
         received=::CopyBuffer(m_handles[i],1,0,1,values);
         if(received>0)
           {
            //--- Sichern der Werte
            m_values[i]=values[0];
            break;
           }
         //--- Erhöhen des Zählers
         attempts++;
         ::Sleep(100);
        }
      //--- Fortschrittsanzeige
      m_progress_bar.LabelText("Get values: "+string(handles_total)+"/"+string(i)+" ["+m_symbols[i]+"] "+((received>0)? "ok" : "wrong")+"...");
      m_progress_bar.Update(i,handles_total);
      ::Sleep(5);
     }
  }

Nachdem die Symbolliste erstellt und die Indikatordaten empfangen wurden, fügen wir die Werte des Arrays der Tabelle auf der Registerkarte Trade hinzu. Die Methode CProgram::RebuildingTables() macht das. Die Anzahl der Symbole kann sich ändern. Daher wird die Tabelle bei jedem Aufruf dieser Methode komplett neu angeordnet.

Zuerst werden alle Zeilen, außer der Backup-Zeile, aus ihr entfernt. Anschließend werden der Tabelle wieder Zeilen entsprechend der Anzahl der Symbole hinzugefügt. Dann gehen wir sie in einer Schleife durch und addieren die zuvor in separaten Arrays empfangenen Werte. Zusätzlich zu den Werten selbst müssen wir noch den Text farblich hervorheben, um zu sehen, welche Signale sich aufgrund der Indikatorwerte bereits gebildet haben. Werte unterhalb von Stochastic werden als Kaufsignale blau hervorgehoben, während Werte oberhalb des Indikatormaximums rot als Verkaufssignale dargestellt werden. Der Fortschrittsbalken wird bei jeder Iteration aktualisiert, während das Programm arbeitet. Am Ende der Methode aktualisieren wir die Tabelle und die Bildlaufleisten.

//+------------------------------------------------------------------+
//| Neu-Anordnen der Symboltabelle                                   |
//+------------------------------------------------------------------+
void CProgram::RebuildingTables(void)
  {
//--- Entfernen aller Zeilen
   m_table_symb.DeleteAllRows();
//--- Festlegen der Zeilenzahl durch die Anzahle der Symbole
   int symbols_total=::ArraySize(m_symbols);
   for(int i=0; i<symbols_total-1; i++)
      m_table_symb.AddRow(i);
//--- Setzen der Werte der ersten Spalte
   uint rows_total=m_table_symb.RowsTotal();
   for(uint r=0; r<(uint)rows_total; r++)
     {
      //--- Setzen der Werte
      m_table_symb.SetValue(0,r,m_symbols[r]);
      m_table_symb.SetValue(1,r,::DoubleToString(m_values[r],2));
      //--- Setzen der Farben
      color clr=(m_values[r]>(double)m_up_level.GetValue())? clrRed :(m_values[r]<(double)m_down_level.GetValue())? C'85,170,255' : clrBlack;
      m_table_symb.TextColor(0,r,clr);
      m_table_symb.TextColor(1,r,clr);
      //--- Aktualisieren der Fortschrittsanzeige
      m_progress_bar.LabelText("Initialize tables: "+string(rows_total)+"/"+string(r)+"...");
      m_progress_bar.Update(r,rows_total);
      ::Sleep(5);
     }
//--- Aktualisieren der Tabelle
   m_table_symb.Update(true);
   m_table_symb.GetScrollVPointer().Update(true);
   m_table_symb.GetScrollHPointer().Update(true);
  }

Alle oben beschriebenen Methoden werden in der Methode CProgram::RequestData() aufgerufen. Sie erhält ein einziges Argument, um damit die ID des Steuerelements zu überprüfen - die Schaltfläche Request. Nach dieser Prüfung blenden wir die Tabelle vorübergehend aus und machen den Fortschrittsbalken sichtbar. Dann werden alle oben beschriebenen Methoden nacheinander aufgerufen, um die Daten zu erhalten und in die Tabelle zu schreiben. Dann blenden wir den Fortschrittsbalken aus, platzieren den Zeitrahmen aus der Combobox auf dem Diagramm und machen die letzten Änderungen sichtbar. 

//+------------------------------------------------------------------+
//| Datenabfrage                                                     |
//+------------------------------------------------------------------+
bool CProgram::RequestData(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_request.Id())
      return(false);
//--- Tabelle ausblenden
   m_table_symb.Hide();
//--- Fortschrittsanzeige
   m_progress_bar.Show();
   m_chart.Redraw();
//--- Initialisieren von Graph und Tabelle
   GetSymbols();
   GetHandles();
   GetIndicatorValues();
   RebuildingTables();
//--- Fortschrittsanzeige ausblenden
   m_progress_bar.Hide();
//--- Wertabfrage der Auswahlliste des Kombinationsfelds
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Abfrage des Pointers auf den Graphen über den Index
   m_sub_chart1.GetSubChartPointer(0).Period(StringToTimeframe(tf));
   m_sub_chart1.ResetCharts();
//--- Einblenden der Tabelle
   m_table_symb.Show();
   m_chart.Redraw();
   return(true);
  }

Erhalt von Daten über die offenen Positionen

Wenn der EA auf dem Chart gestartet wird, müssen wir sofort feststellen, ob es offene Positionen gibt, um diese Daten mit der Tabelle der Registerkarte Positionen anzuzeigen. Die Liste aller Positionen finden Sie auf dem Karteireiter Trade im Fenster Toolbox. Um eine einzelne Position durch ein Symbol zu schließen, klicken Sie auf das Kreuz in der Tabellenzelle der Spalte Profit. Wenn es mehrere Positionen desselben Symbols (hedging account) gibt und wir alle schließen müssen, benötigen wir mehrere Schritte. In der Positionstabelle der GUI sollte eine Zeile (für jedes Symbol) Gesamtdaten über das aktuelle Ergebnis, die Einlagenbelastung und den Durchschnittspreis enthalten. Außerdem können wir alle Positionen zu einem bestimmten Symbol mit einem einzigen Klick schließen. 

Zuerst betrachten wir die Methode CProgram::GetPositionsSymbols() zum Empfangen der Liste der Symbole nach offenen Positionen. Ein leeres dynamisches Array zum Empfangen von Symbolen wird an dieses übergeben. Wir gehen dann in einer Schleife durch alle offenen Positionen. Bei jeder Iteration wird der Symbolname der Position abgerufen und mit dem Trennzeichen "," zur Zeichenkettenvariablen hinzugefügt. Bevor wir einen Symbolnamen hinzufügen, überprüfen wir, ob er bereits in der Zeile vorhanden ist. 

Nachdem wir die Schleife abgeschlossen und die Symbolzeile gebildet haben, erhalten wir die Zeilenelemente in dem übergebenen Array und geben die Anzahl der empfangenen Symbole zurück.

//+------------------------------------------------------------------+
//| Abfrage der offenen Positionen des Symbols im Array              |
//+------------------------------------------------------------------+
int CProgram::GetPositionsSymbols(string &symbols_name[])
  {
   string symbols="";
//--- Erste Iteration zur Abfrage der Symbole der offenen Positionen
   int positions_total=::PositionsTotal();
   for(int i=0; i<positions_total; i++)
     {
      //--- Positionswahl und dessen Symbols
      string position_symbol=::PositionGetSymbol(i);
      //--- Gibt es einen Symbolnamen
      if(position_symbol=="")
         continue;
      //--- Hinzufügen einer Zeile, wenn sie fehlt
      if(::StringFind(symbols,position_symbol,0)==WRONG_VALUE)
         ::StringAdd(symbols,(symbols=="")? position_symbol : ","+position_symbol);
     }
//--- Abfrage der Elemente mittel des Separators
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(symbols,u_sep,symbols_name);
//--- Rückgabe der Nummer des Symbols
   return(symbols_total);
  }

Jetzt, da wir das Symbol-Array haben, können wir Daten über jede Position sammeln, indem wir einfach einen Symbolnamen angeben. Betrachten wir die Methoden zum Empfangen von Werten in allen Datenspalten der Positionstabelle.

Um die Anzahl der Positionen eines bestimmten Symbols zu erhalten, verwenden wir die Methode CProgram::PositionsTotal(). Es durchläuft alle Positionen in einer Schleife und zählt nur die Positionen, die dem im Methodenargument angegebenen Symbol entsprechen.

//+------------------------------------------------------------------+
//| Positionsanzahl mit den angegebenen Eigenschaften                |
//+------------------------------------------------------------------+
int CProgram::PositionsTotal(const string symbol)
  {
//--- Positionszähler
   int pos_counter=0;
//--- Existenzprüfung einer Position mit dn angegebenen Eigenschaften
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Wenn Positionsauswahl fehl schlägt, weiter zur nächsten
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Erhöhen des Zählers
      pos_counter++;
     }
//--- Rückgabe der Anzahl der Positionen
   return(pos_counter);
  }

Das Positionsvolumen kann mit der Methode CProgram::PositionsVolumeTotal() ermittelt werden. Neben dem Symbol, mit dem das Gesamtvolumen der Positionen, erhalten wird, ist es auch möglich, deren Typ an die Methode zu übergeben. Obwohl der Typ ein optionales Argument in dieser Methode ist. Standardmäßig wird der Wert WRONG_VALUE angegeben. Wenn kein Typ angegeben ist, wird die Prüfung nicht verwendet und die Methode gibt das Gesamtvolumen aller Positionen zurück. 

//+------------------------------------------------------------------+
//| Gesamtvolumen der Positionen mit den angegebenen Eigenschaften   |
//+------------------------------------------------------------------+
double CProgram::PositionsVolumeTotal(const string symbol,const ENUM_POSITION_TYPE type=WRONG_VALUE)
  {
//--- Volumenzähler
   double volume_counter=0;
//--- Existenzprüfung einer Position mit dn angegebenen Eigenschaften
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Wenn Positionsauswahl fehl schlägt, weiter zur nächsten
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Überprüfung des Typs
      if(type!=WRONG_VALUE)
        {
         //--- Wenn die Typen nicht passen, weiter zur nächsten Position
         if(type!=(ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE))
            continue;
        }
      //--- Summieren der Volumina
      volume_counter+=::PositionGetDouble(POSITION_VOLUME);
     }
//--- Rückgabe des Volumens
   return(volume_counter);
  }

Die Methode CProgram::PositionsFloatingProfitTotal() ermöglicht die Abfrage des gesamten variablen Gewinns der Positionen eines bestimmten Symbols. Der kumulierte Swap für Positionen wird bei der Berechnung berücksichtigt. Die Art der Positionen, für die wir den variablen Gewinn erhalten müssen, kann hier auch als optionales Argument angegeben werden. So wird die Methode universell einsetzbar. 

//+------------------------------------------------------------------+
//| Offener Gewinn der Positionen mit den angegebenen Eigenschaften  |
//+------------------------------------------------------------------+
double CProgram::PositionsFloatingProfitTotal(const string symbol,const ENUM_POSITION_TYPE type=WRONG_VALUE)
  {
//--- Zäher des offenen Gewinns
   double profit_counter=0.0;
//--- Existenzprüfung einer Position mit dn angegebenen Eigenschaften
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Wenn Positionsauswahl fehl schlägt, weiter zur nächsten
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Überprüfung des Typs
      if(type!=WRONG_VALUE)
        {
         //--- Wenn die Typen nicht passen, weiter zur nächsten Position
         if(type!=(ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE))
            continue;
        }
      //--- Summieren der offenen Gewinne + akkumuliertem Swap
      profit_counter+=::PositionGetDouble(POSITION_PROFIT)+::PositionGetDouble(POSITION_SWAP);
     }
//--- Rückgabe des Ergebnisses
   return(profit_counter);
  }

Der Durchschnittspreis wird mit der Methode CProgram::PositionAveragePrice() berechnet. Ermitteln wir den Preis und das Volumen für die Position jedes Symbols in der Schleife. Dann summieren wir das Produkt dieser Werte, sowie das Volumen der Positionen (separat). Nach Abschluss der Schleife teilen Sie die Summe des Produkts aus Preisen und Mengen in die Summe der Mengen, um den Durchschnittspreis der Positionen des angegebenen Symbols zu erhalten. Dies ist der Wert, der die beschriebene Methode zurückgibt.

//+------------------------------------------------------------------+
//| Durchschnittlicher Positionspreis                                |
//+------------------------------------------------------------------+
double CProgram::PositionAveragePrice(const string symbol)
  {
//--- For calculating the average price
   double sum_mult    =0.0;
   double sum_volumes =0.0;
//--- Prüfen, ob es eine Position mit den angegebenen Eigenschaften gibt properties
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Wenn Positionsauswahl fehl schlägt, weiter zur nächsten
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Abfrage von Preis und Positionsvolumen
      double pos_price  =::PositionGetDouble(POSITION_PRICE_OPEN);
      double pos_volume =::PositionGetDouble(POSITION_VOLUME);
      //--- Summieren der Zwischenwerte
      sum_mult+=(pos_price*pos_volume);
      sum_volumes+=pos_volume;
     }
//--- Division durch Null vermeiden
   if(sum_volumes<=0)
      return(0.0);
//--- Return the average price
   return(::NormalizeDouble(sum_mult/sum_volumes,(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS)));
  }

Betrachten wir den Parameter der Einlagenbelastung. Um das zu erhalten, ist die universelle Methode CProgram::DepositLoad() erforderlich. Abhängig von den übergebenen Argumenten ist es möglich, Werte in verschiedenen Darstellungen zu erhalten: in Währungseinlagen und in %. Außerdem ist es möglich, die gesamte Pfandmenge für alle offenen Positionen oder nur für ein bestimmtes Symbol zu erhalten. 

Das Verfahren hat vier Argumente. Drei von ihnen sind optional. Wenn das erste Argument false ist, gibt die Methode den Wert in einer Kontowährung zurück. Wenn true übergeben wird, wird der Wert in Prozent relativ zur freien Marge zurückgegeben

Wenn Sie die aktuelle Einzahlungslast für ein bestimmtes Symbol ermitteln müssen, wird für die Berechnung ein Bestandspreis benötigt, falls die Kontowährung von der Basiswährung des Symbols abweicht. Wenn es mehrere offene Positionen auf einem Symbol gibt, sollte ein Durchschnittspreis überschritten werden. 

//+------------------------------------------------------------------+
//| Einlagenbelastung                                                |
//+------------------------------------------------------------------+
double CProgram::DepositLoad(const bool percent_mode,const double price=0.0,const string symbol="",const double volume=0.0)
  {
//--- Berechnen der aktuellen Einlagenbelastung
   double margin=0.0;
//--- Gesamtbelastung des Kontos
   if(symbol=="" || volume==0.0)
      margin=::AccountInfoDouble(ACCOUNT_MARGIN);
//--- Belastung eines bestimmten Symbols
   else
     {
      //--- Datenabfrage zur Margenberechnung
      double leverage         =((double)::AccountInfoInteger(ACCOUNT_LEVERAGE)==0)? 1 : (double)::AccountInfoInteger(ACCOUNT_LEVERAGE);
      double contract_size    =::SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
      string account_currency =::AccountInfoString(ACCOUNT_CURRENCY);
      string base_currency    =::SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE);
      //--- Wenn die Währung des Handelskonto zur Basis des Symbols passt
      if(account_currency==base_currency)
         margin=(volume*contract_size)/leverage;
      else
         margin=(volume*contract_size)/leverage*price;
     }
//--- Abfrage des aktuellen Saldos
   double equity=(::AccountInfoDouble(ACCOUNT_EQUITY)==0)? 1 : ::AccountInfoDouble(ACCOUNT_EQUITY);
//--- Rückgabe der aktuellen Einlagenbelastung
   return((!percent_mode)? margin : (margin/equity)*100);
  }

Alle Methoden zum Empfangen von Parametern werden der Tabelle durch Aufruf der Methode CProgram::SetValuesToPositionsTable() hinzugefügt. Die Anordnung der notwendigen Symbole sollte an die Methode übergeben werden. Wir stellen zunächst sicher, dass das übergebene Array nicht kleiner als die Anzahl der Tabellenzeilen ist. Anschließend übergeben wir alle Tabellenzeilen, die Parameter empfangen, und füllen diese nacheinander in die Tabellenzellen ein. Zusätzlich zu den Werten sollten wir auch die Textfarbe einstellen: grün für ein positives Ergebnis, rot - für ein negatives und grau - für Null. Bitte beachten Sie, dass pro Symbol, getrennt durch "/" in Geld und prozentualen Begriffen, eine Einzahlungsladung angezeigt wird. 

//+------------------------------------------------------------------+
//| Einschreiben der Werte in die Positionstabelle                   |
//+------------------------------------------------------------------+
void CProgram::SetValuesToPositionsTable(string &symbols_name[])
  {
//--- Prüfen auf Überlauf
   uint symbols_total =::ArraySize(symbols_name);
   uint rows_total    =m_table_positions.RowsTotal();
   if(symbols_total<rows_total)
      return;
//--- Hinzufügen der Parameter zur Tabelle
   for(uint r=0; r<rows_total; r++)
     {
      int    positions_total =PositionsTotal(symbols_name[r]);
      double pos_volume      =PositionsVolumeTotal(symbols_name[r]);
      double buy_volume      =PositionsVolumeTotal(symbols_name[r],POSITION_TYPE_BUY);
      double sell_volume     =PositionsVolumeTotal(symbols_name[r],POSITION_TYPE_SELL);
      double pos_profit      =PositionsFloatingProfitTotal(symbols_name[r]);
      double buy_profit      =PositionsFloatingProfitTotal(symbols_name[r],POSITION_TYPE_BUY);
      double sell_profit     =PositionsFloatingProfitTotal(symbols_name[r],POSITION_TYPE_SELL);
      double average_price   =PositionAveragePrice(symbols_name[r]);
      string deposit_load    =::DoubleToString(DepositLoad(false,average_price,symbols_name[r],pos_volume),2)+"/"+
                              ::DoubleToString(DepositLoad(true,average_price,symbols_name[r],pos_volume),2)+"%";
      //--- Setzen der Werte
      m_table_positions.SetValue(0,r,symbols_name[r]);
      m_table_positions.SetValue(1,r,(string)positions_total);
      m_table_positions.SetValue(2,r,::DoubleToString(pos_volume,2));
      m_table_positions.SetValue(3,r,::DoubleToString(buy_volume,2));
      m_table_positions.SetValue(4,r,::DoubleToString(sell_volume,2));
      m_table_positions.SetValue(5,r,::DoubleToString(pos_profit,2));
      m_table_positions.SetValue(6,r,::DoubleToString(buy_profit,2));
      m_table_positions.SetValue(7,r,::DoubleToString(sell_profit,2));
      m_table_positions.SetValue(8,r,deposit_load);
      m_table_positions.SetValue(9,r,::DoubleToString(average_price,(int)::SymbolInfoInteger(symbols_name[r],SYMBOL_DIGITS)));
      //--- Setzen der Farbe
      m_table_positions.TextColor(3,r,(buy_volume>0)? clrBlack : clrLightGray);
      m_table_positions.TextColor(4,r,(sell_volume>0)? clrBlack : clrLightGray);
      m_table_positions.TextColor(5,r,(pos_profit!=0)? (pos_profit>0)? clrGreen : clrRed : clrLightGray);
      m_table_positions.TextColor(6,r,(buy_profit!=0)? (buy_profit>0)? clrGreen : clrRed : clrLightGray);
      m_table_positions.TextColor(7,r,(sell_profit!=0)?(sell_profit>0)? clrGreen : clrRed : clrLightGray);
     }
  }

Für die Aktualisierung der Tabelle nach der Implementierung der Änderungen sollten wir eine eigene Methode bereitstellen, da sie im Programm mehrfach aufgerufen werden soll.

//+------------------------------------------------------------------+
//| Update der Positionstabelle                                      |
//+------------------------------------------------------------------+
void CProgram::UpdatePositionsTable(void)
  {
//--- Aktualisieren der Tabelle
   m_table_positions.Update(true);
   m_table_positions.GetScrollVPointer().Update(true);
   m_table_positions.GetScrollHPointer().Update(true);
  }

Die Initialisierung der Positionstabelle erfolgt in der Methode CProgram::InitializePositionsTable(). Alle in diesem Abschnitt beschriebenen Methoden werden darin aufgerufen. Zuerst erhalten wir Symbole der offenen Positionen im Array. Dann bereiten wir die Tabelle vor — entfernen alle Zeilen und fügen so viele neue hinzu, wie der Array der Symbole enthält. Wenn offene Positionen vorhanden sind, müssen wir zunächst die ersten Spaltenzellen als Buttons bezeichnen. Dazu setzen wir den entsprechenden Typ (CELL_BUTTON) und fügen ein Bild ein. Danach werden die Werte in den Zellen gesetzt und die Tabelle aktualisiert.

//+------------------------------------------------------------------+
//| Initialisieren der Positionstabelle                              |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Controls\\close_black.bmp"
//---
void CProgram::InitializePositionsTable(void)
  {
//--- Symbolabfrage der offenen Positionen
   string symbols_name[];
   int symbols_total=GetPositionsSymbols(symbols_name);
//--- Entfernen aller Zeilen
   m_table_positions.DeleteAllRows();
//--- Festlegen der Zeilenzahl durch die Anzahle der Symbole
   for(int i=0; i<symbols_total-1; i++)
      m_table_positions.AddRow(i);
//--- Wenn es die Positionen gibt
   if(symbols_total>0)
     {
      //--- Array der Bilder der Schaltflächen
      string button_images[1]={"Images\\EasyAndFastGUI\\Controls\\close_black.bmp"};
      //--- Wert der dritten Spalte setzen
      for(uint r=0; r<(uint)symbols_total; r++)
        {
         //--- Setzen von Typ und Bild
         m_table_positions.CellType(0,r,CELL_BUTTON);
         m_table_positions.SetImages(0,r,button_images);
        }
      //--- Setzen der Werte in der Tabelle
      SetValuesToPositionsTable(symbols_name);
     }
//--- Aktualisieren der Tabelle
   UpdatePositionsTable();
  }

Initialisieren der Tabellen mit den Daten

Die Symbol- und Positionstabellen sollten unmittelbar nach der Erstellung der Programmoberfläche initialisiert werden. Das Ereignis ON_END_CREATE_GUI der Ereignisbehandlung zeigt an, dass seine Bildung abgeschlossen ist. Um die Symboltabelle zu initialisieren, rufen wir die Methode CProgram::RequestData() auf, die wir oben bereits beschrieben haben. Für die erfolgreiche Arbeit der Methode übergeben wir ihr die ID des Button-Elements Request.

//+------------------------------------------------------------------+
//| Ereignisbehandlung                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Ereignis einer GUI-Erstellung
   if(id==CHARTEVENT_CUSTOM+ON_END_CREATE_GUI)
     {
      //--- Datenabfrage
      RequestData(m_request.Id());
      //--- Initialisieren der Positionstabelle
      InitializePositionsTable();
      return;
     }
  }

Jetzt schaut die Tabelle der Symbole nach dem Start auf dem Chart so aus:

 Abb. 1. Initialisierte Symboltabelle

Abb. 1. Initialisierte Symboltabelle

Wenn es auf dem Konto bei Programmstart bereits offenen Positionen gibt, dann schaut die Tabelle der Positionen so aus:

 Abb. 2. Initialisierte Positionstabelle

Abb. 2. Initialisierte Positionstabelle

Aktualisieren der Tabellen in Echtzeit

Der Preis bewegt sich ständig, daher sollten die Daten in den Tabellen während einer Handelssitzung ständig neu berechnet werden. Die Tabelle ist in bestimmten Abständen zu aktualisieren, die im Timer des Programms eingestellt sind. Um die Aktualisierung von Elementen mit unterschiedlichen Intervallen zu ermöglichen, können wir CTimeCounter Typ Objekte verwenden. Diese Klasse befindet sich in der Bibliothek EasyAndFast. Um es im Projekt zu verwenden, einfach die Datei mit ihrem Inhalt einbinden:

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
...
#include <EasyAndFastGUI\TimeCounter.mqh>
...

Unser EA benötigt drei Zeitzähler für die Aktualisierung der Daten, im Fortschrittsbalken und in den Tabellen. 

Um die Zähler einzustellen, deklarieren wir einfach Objekte vom Typ CTimeCounter und setzen deren Parameter im Konstruktor (siehe Auflistung unten). Der erste Parameter ist eine Timerfrequenz, während der zweite ein Zeitintervall ist. Die Methode CTimeCounter::CheckTimeCounter() gibt nach der Übergabe 'true' zurück. Danach setzt sich der Zähler zurück und beginnt erneut zu zählen.

//+------------------------------------------------------------------+
//| Klasse zum Erstellen der Anwendung                               |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
...
   //--- Zeitzähler
   CTimeCounter      m_counter1;
   CTimeCounter      m_counter2;
   CTimeCounter      m_counter3;
...
  };
//+------------------------------------------------------------------+
//| Konstruktor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
//--- Setzen der Parameter des Zeitzählers
   m_counter1.SetParameters(16,500);
   m_counter2.SetParameters(16,5000);
   m_counter3.SetParameters(16,1000);
...
  }

Wir fügen den unten angezeigten Code hinzu, um die Statusleiste im ersten Block des Zählers des Programmtimers zu aktualisieren. Um die implementierten Änderungen anzuzeigen, dürfen wir nicht vergessen, jeden Punkt separat zu aktualisieren.

//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void CProgram::OnTimerEvent(void)
  {
...
//--- Update der Punkte in der Statusleiste
   if(m_counter1.CheckTimeCounter())
     {
      //--- Setzen der Werte
      m_status_bar.SetValue(1,"Deposit load: "+::DoubleToString(DepositLoad(false),2)+"/"+::DoubleToString(DepositLoad(true),2)+"%");
      m_status_bar.SetValue(2,::TimeToString(::TimeTradeServer(),TIME_DATE|TIME_SECONDS));
      //--- Update der Punkte
      m_status_bar.GetItemPointer(1).Update(true);
      m_status_bar.GetItemPointer(2).Update(true);
     }
...
  }

Um die Aktualisierung der Tabelle zu beschleunigen, in der nur die Indikatorwerte aktualisiert werden sollen, verwenden wir die separate Methode — CProgram::UpdateSymbolsTable(). Bevor wir sie aufrufen, sollten wir zuerst das Array der Indikatorwerte aktualisieren. Die Methode CProgram::UpdateSymbolsTable() wird anschließend aufgerufen. Die Überprüfung auf einen Arrayüberlauf wird hier bei jeder Iteration durchgeführt. Wenn die Prüfung bestanden ist, aktualisieren wir die Zellen der zweiten Tabellenspalte und passen die Textfarbe an. Der Empfang von Daten und die Initialisierung der Tabellen werden durch den Fortschrittsbalken angezeigt.

//+------------------------------------------------------------------+
//| Update der Symboltabelle                                         |
//+------------------------------------------------------------------+
void CProgram::UpdateSymbolsTable(void)
  {
   uint values_total=::ArraySize(m_values);
//--- Einschreiben der Werte in die Symboltabelle
   uint rows_total=m_table_symb.RowsTotal();
   for(uint r=0; r<(uint)rows_total; r++)
     {
      //--- Schleife bei einem Arrayüberlauf beenden
      if(r>values_total-1 || values_total<1)
         break;
      //--- Setzen der Werte
      m_table_symb.SetValue(1,r,::DoubleToString(m_values[r],2));
      //--- Setzen der Farben
      color clr=(m_values[r]>(double)m_up_level.GetValue())? clrRed :(m_values[r]<(double)m_down_level.GetValue())? C'85,170,255' : clrBlack;
      m_table_symb.TextColor(0,r,clr,true);
      m_table_symb.TextColor(1,r,clr,true);
      //--- Aktualisieren der Fortschrittsanzeige
      m_progress_bar.LabelText("Initialize tables: "+string(rows_total)+"/"+string(r)+"...");
      m_progress_bar.Update(r,rows_total);
      ::Sleep(5);
     }
//--- Aktualisieren der Tabelle
   m_table_symb.Update();
  }

Der Block des zweiten Zeitzählers zur Aktualisierung der Tabelle ist unten dargestellt. So erhält das Programm für alle Symbole die aktuellen Werte der Anzeige und aktualisiert die Tabelle alle 5 Sekunden.

void CProgram::OnTimerEvent(void)
  {
...
//--- Update der Symboltabelle
   if(m_counter2.CheckTimeCounter())
     {
      //--- Fortschrittsanzeige
      m_progress_bar.Show();
      m_chart.Redraw();
      //--- Update der Tabellenwerte
      GetIndicatorValues();
      UpdateSymbolsTable();
      //--- Fortschrittsanzeige ausblenden
      m_progress_bar.Hide();
      m_chart.Redraw();
     }
...
  }

Um die Positionstabelle im Timer zu aktualisieren, erhalten wir zunächst die Symbole der offenen Positionen. Dann aktualisieren wir die Tabelle mit den entsprechenden Daten. Wir sortieren nach der gleichen Spalte und Richtung wie vor dem Update. Das wenden wir auf die Tabelle an, um die implementierten Änderungen anzuzeigen.

void CProgram::OnTimerEvent(void)
  {
...
//--- Update der Positionstabelle
   if(m_counter3.CheckTimeCounter())
     {
      //--- Symbolabfrage der offenen Positionen
      string symbols_name[];
      int symbols_total=GetPositionsSymbols(symbols_name);
      //--- Update der Werte in der Tabelle
      SetValuesToPositionsTable(symbols_name);
      //--- Sortiert bereits durch den Nutzer vor dem Update
      m_table_positions.SortData((uint)m_table_positions.IsSortedColumnIndex(),m_table_positions.IsSortDirection());
      //--- Aktualisieren der Tabelle
      UpdatePositionsTable();
     }
  }

Verarbeiten der Ereignisse des Steuerelements

In diesem Abschnitt werden wir die Methoden zur Behandlung von Ereignissen betrachten, die bei der Interaktion mit der grafischen Oberfläche unseres EA erzeugt werden. Wir haben bereits die Methode CProgram::RequestData() zur Abfrage der Symbol- und Indikatordaten besprochen. Wenn dies nicht die erste Initialisierung ist, wird die Methode beim Anklicken der Schaltfläche Request jederzeit während der Programmausführung aufgerufen. Klickt man auf die Schaltfläche, wird ein Benutzerdefiniertes Ereignis mit der ID ON_CLICK_BUTTON generiert.

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Ereignis eines Tastendrucks
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Datenabfrage
      if(RequestData(lparam))
         return;
      //---
      return;
     }
...
  }

Das gif-Bild unten zeigt Folgendes. Die Tabelle enthält die Liste der Devisensymbole, die USD enthalten. Dann erstellen wir schnell die Liste der Symbole, die EUR enthalten. Dazu geben wir im Eingabefeld Symbols filter "EUR" ein und klicken auf Request. Wenn wir alle Symbole mit USD und EUR auf dem Server verfügbar machen möchten, sollten wir diese durch Komma trennen: "USD,EUR".

 Abb. 3. Bilden der Liste der Forex-Symbole

Abb. 3. Bilden der Liste der Forex-Symbole

Das Bilden der Liste der Forex-Symbole und das Erhalten der Indikator-Handles erfolgt für den in der Combobox Timeframes angegebenen Zeitrahmen. Wenn wir einen anderen Zeitraum in der Auswahlliste auswählen, sollten wir die neuen Handles erhalten und die Tabellenwerte aktualisieren. Um dies zu erreichen, benötigen wir die Methode CProgram::ChangePeriod(). Wenn die ID der Combobox angekommen ist, aktualisieren wir zunächst den Zeitrahmen im Objektdiagramm. Dann holen wir uns die Handles und Indikatorendaten für alle Tabellensymbole. Danach wird die Tabelle aktualisiert, um die vorgenommenen Änderungen anzuzeigen. 

//+------------------------------------------------------------------+
//| Wechsel des Zeitrahmens                                          |
//+------------------------------------------------------------------+
bool CProgram::ChangePeriod(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_timeframes.Id())
      return(false);
//--- Wertabfrage der Auswahlliste des Kombinationsfelds
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Abfrage des Pointers des Charts über den Index
   m_sub_chart1.GetSubChartPointer(0).Period(StringToTimeframe(tf));
   m_sub_chart1.ResetCharts();
//--- Fortschrittsanzeige
   m_progress_bar.Show();
   m_chart.Redraw();
//--- Abfrage der Handles und der Indikatorwerte
   GetHandles();
   GetIndicatorValues();
//--- Aktualisieren der Tabelle
   UpdateSymbolsTable();
//--- Fortschrittsanzeige ausblenden
   m_progress_bar.Hide();
   m_chart.Redraw();
   return(true);
  }

Bei der Auswahl eines Elementes aus der Auswahlliste wird ein Nutzerereignis mit der ID ON_CLICK_COMBOBOX_ITEM erzeugt:

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Ereignis einer Elementwahl im Kombinationsfeld
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
     {
      //--- Wechsel des Zeitrahmens
      if(ChangePeriod(lparam))
         return;
      //---
      return;
     }
...
  }

Und so schaut es aus, nach dem Ändern des Zeitrahmens und dem Erhalt der neuen Daten:

 Abb. 4. Ändern des Zeitrahmens

Abb. 4. Ändern des Zeitrahmens

Überlegen wir nun, wie man schnell ein Symbol in einem Objektdiagramm ändern kann. Die Symbolnamen stehen bereits in der ersten Spalte der Symboltabelle. Daher ist es möglich, zwischen ihnen zu wechseln, indem man einfach die Tabellenzeilen markiert. Die Methode CProgram::ChangeSymbol() wird durch Anklicken einer benötigten Zeile aufgerufen. Hier wird zunächst die ID der Symboltabelle überprüft. Nun, Überprüfen wir, ob die Tabellenzeile markiert ist, da die Zeilenhervorhebung durch einen erneuten Klick deaktiviert wird. Wenn Prüfungen bestanden werden, speichern wir den markierten Zeilenindex als Handle. Mit ihm kann der Indikator dann in einem Diagramm platziert werden (siehe unten).

Nachdem wir ein Symbol aus der ersten Tabellenspalte mit einem hervorgehobenen Zeilenindex erhalten haben, übertragen wir es in das Objektdiagramm. Die vollständige Symbolbeschreibung wird im ersten Abschnitt der Statusleiste als Zusatzinformation angezeigt. Wenn wir die Markierung der Tabellenzeile deaktivieren, wird der Text für den Standardtext geändert.

//+------------------------------------------------------------------+
//| Symbolwechsel                                                    |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_table_symb.Id())
      return(false);
//--- Verlassen, wenn die Zeile nicht hervorgehoben ist
   if(m_table_symb.SelectedItem()==WRONG_VALUE)
     {
      //--- Anzeige der gesamten Symbolbeschreibung in der Statusleiste
      m_status_bar.SetValue(0,"For Help, press F1");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- Sichern des Handleindex
   m_current_handle_index=m_table_symb.SelectedItem();
//--- Symbolabfrage
   string symbol=m_table_symb.GetValue(0,m_current_handle_index);
//--- Update des Charts
   m_sub_chart1.GetSubChartPointer(0).Symbol(symbol);
   m_sub_chart1.ResetCharts();
//--- Anzeige der gesamten Symbolbeschreibung in der Statusleiste
   m_status_bar.SetValue(0,::SymbolInfoString(symbol,SYMBOL_DESCRIPTION));
   m_status_bar.GetItemPointer(0).Update(true);
   m_chart.Redraw();
   return(true);
  }

Beim Hervorheben einer Tabellenzeile wird ein benutzerdefiniertes Ereignis mit der ID ON_CLICK_LIST_ITEM erzeugt. Die Symbole können auch mit den Tasten «Up», «Down», «Home» und «End». geändert werden. In diesem Fall wird das Ereignis CHARTEVENT_KEYDOWN erzeugt. Seine Handhabungsmethode wurde im vorherigen Artikel besprochen, so dass es keinen Sinn macht, hier darauf einzugehen.

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Ereignis einer Auswahl aus der Liste/Tabelle
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      //--- Symbolwechsel
      if(ChangeSymbol(lparam))
         return;
      //---
      return;
     }
//--- Klick auf der Schaltfläche
   if(id==CHARTEVENT_KEYDOWN)
     {
      //--- Ergebnisauswahl mittels Tasten
      if(SelectingResultsUsingKeys(lparam))
         return;
      //---
      return;
     }
...
  }

Das Ergebnis dieser Ereignisbehandlung sieht dann so aus:

 Abb. 5. Symbolwechsel

Abb. 5. Symbolwechsel

Manchmal müssen wir den Indikator sehen, der verwendet wird, um Signale zu erhalten. Mit dem Kontrollkästchen Show indicator können wir das aktivieren. Die Methode CProgram::ShowIndicator() ist für die Interaktion verantwortlich. Überprüfungen der Element-ID und eines Arrayüberlaufs des Handle-Arrays sollten auch hier übergeben werden. Eine Diagramm-ID wird benötigt, um den Indikator dem entsprechenden Chart hinzuzufügen oder zu entfernen. Wenn das Kontrollkästchen aktiviert ist, fügen wir den Indikator dem Chart hinzu. Da wir immer nur einen einzigen Indikator verwenden, wird der Subfensterindex auf 1 gesetzt. Für komplexere Fälle sollte die Anzahl der Indikatoren in einem Chart definiert werden. 

//+------------------------------------------------------------------+
//| Sichtbarkeit des Indikators                                      |
//+------------------------------------------------------------------+
bool CProgram::ShowIndicator(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_show_indicator.Id())
      return(false);
//--- Prüfen auf Überlauf des Arrays
   int handles_total=::ArraySize(m_handles);
   if(m_current_handle_index<0 || m_current_handle_index>handles_total-1)
      return(true);
//--- Abfrage der Chart-ID
   long sub_chart_id=m_sub_chart1.GetSubChartPointer(0).GetInteger(OBJPROP_CHART_ID);
//--- Index des Unterfensters des Indikators
   int subwindow =1;
//--- Abfrage des Pointers des Charts über den Index
   if(m_show_indicator.IsPressed())
     {
      //--- Hinzufügen des Indikators zum Chart
      ::ChartIndicatorAdd(sub_chart_id,subwindow,m_handles[m_current_handle_index]);
     }
   else
     {
      //--- Entfernen des Indikators vom Chart
      ::ChartIndicatorDelete(sub_chart_id,subwindow,ChartIndicatorName(sub_chart_id,subwindow,0));
     }
//--- Update des Charts
   m_chart.Redraw();
   return(true);
  }

Beim Ändern des Kontrollkästchens wird das Nutzerereignis ON_CLICK_CHECKBOX erzeugt:

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Ereignis eine Klicks auf das Element "Check box"
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_CHECKBOX)
     {
      //--- Klick auf das Kontrollkästchen "Show indicator"
      if(ShowIndicator(lparam))
         return;
      //---
      return;
     }
  }

Und so schaut die Verwendung aus:

 Abb. 6. Darstellung des Indikators

Abb. 6. Darstellung des Indikators

Zwei weitere Steuerelemente in der EA-GUI beziehen sich ebenfalls auf den Indikator. Dies sind digitale Eingabefelder für den Indikator Stochastisch: Aufwärtsebene und Abwärtsebene. Standardmäßig sind sie auf 80 und 20 eingestellt. Überschreiten die Indikatorwerte irgendeines Symbols diese Grenzen nach oben und unten, wird der Text in den Zellen der Symboltabelle von schwarz auf blau für die obere Ebene und auf rot - für die untere Ebene - geändert. Wenn wir die Werte in diesen Eingabefeldern ändern, ändert sich beim nächsten Update auch die Farbanzeige (alle fünf Sekunden). 

So funktioniert es, wenn Sie Werte von 80/20 auf 90/10 und zurück ändern:

 Abb. 7. Ändern der Ebenen des Indikators

Abb. 7. Ändern der Ebenen des Indikators

Mehrere Steuerelemente sind für die Arbeit mit den Chart-Eigenschaften gedacht. Das sind:

Die Methoden zur Behandlung von Ereignissen aus den Kontrollkästchen Date scale und Price scale sind sehr ähnlich. In beiden Fällen wird eine entsprechende Chart-Eigenschaft aktiviert oder deaktiviert, abhängig vom Status des Kontrollkästchens. Die Methode CStandardChart::ResetCharts() verschiebt das Diagramm an das Ende.

//+------------------------------------------------------------------+
//| Zeitskalen Sichtbarkeit                                          |
//+------------------------------------------------------------------+
bool CProgram::DateScale(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_date_scale.Id())
      return(false);
//--- Abfrage des Pointers des Charts über den Index
   m_sub_chart1.GetSubChartPointer(0).DateScale(m_date_scale.IsPressed());
   m_sub_chart1.ResetCharts();
//--- Update des Charts
   m_chart.Redraw();
   return(true);
  }
//+------------------------------------------------------------------+
//| Preisskalen Sichtbarkeit                                         |
//+------------------------------------------------------------------+
bool CProgram::PriceScale(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_price_scale.Id())
      return(false);
//--- Abfrage des Pointers des Charts über den Index
   m_sub_chart1.GetSubChartPointer(0).PriceScale(m_price_scale.IsPressed());
   m_sub_chart1.ResetCharts();
//--- Update des Charts
   m_chart.Redraw();
   return(true);
  }

Die Methode CProgram::ChartScale() wird verwendet, um den Maßstab des Charts zu verwalten. Hier wird der Wert, wenn er im Eingabefeld geändert wurde, dem Chart zugeordnet.

//+------------------------------------------------------------------+
//| Chartskala                                                       |
//+------------------------------------------------------------------+
bool CProgram::ChartScale(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_chart_scale.Id())
      return(false);
//--- Skala setzen
   if((int)m_chart_scale.GetValue()!=m_sub_chart1.GetSubChartPointer(0).Scale())
      m_sub_chart1.GetSubChartPointer(0).Scale((int)m_chart_scale.GetValue());
//--- Update
   m_chart.Redraw();
   return(true);
  }

Eine Änderung des Wertes im Eingabefeld Chart scale wird beim Eintreffen der Nutzerereignisse mit den IDs ON_CLICK_BUTTON und ON_END_EDIT veranlasst.

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Klicks auf Tasten
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Chartskala
      if(ChartScale(lparam))
         return;
      //---
      return;
     }
//--- Ereignis des Beendens des Änderns eine Eingabefelds
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      //--- Chartskala
      if(ChartScale(lparam))
         return;
      //---
      return;
     }
  }

Der Code der Methode CProgram::ChartShift() zum Aktivieren des rechten Abstands im Chart ist unten dargestellt. Nach der Überprüfung der ID des Elements holen wir uns die ID des Charts, und verwenden sie als Zugriffsschlüssel , um den Abstand (CHART_SHIFT) festzulegen.

//+------------------------------------------------------------------+
//| Chartverschub                                                    |
//+------------------------------------------------------------------+
bool CProgram::ChartShift(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_chart_shift.Id())
      return(false);
//--- Abfrage der Chart-ID
   long sub_chart_id=m_sub_chart1.GetSubChartPointer(0).GetInteger(OBJPROP_CHART_ID);
//--- Setzen des rechten Abstands des Charts
   ::ChartSetInteger(sub_chart_id,CHART_SHIFT,true);
   m_sub_chart1.ResetCharts();
   return(true);
  }

Hier ist, wie es aussieht:

 Abb. 8. Verändern der Charteigenschaften

Abb. 8. Verändern der Charteigenschaften

Methoden zur Durchführung von Handelsoperationen

Ich werde Ihnen anhand von Beispielen zeigen, wie wir die Handelsmethoden schnell mit dem EA-GUI verbinden können. Unser EA wird nicht nur Daten visualisieren, sondern auch Handelsoperationen durchführen. Es ist bequemer zu arbeiten, wenn alles an einem Ort ist, und Sie können schnell die Charts wechseln und bei Bedarf handeln. Als Beispiel werden wir die Standardfunktionen der Bibliothek für den Handel verwenden. Auch andere Handelsbibliotheken können einbezogen werden. 

Include the Trade.mqh file with the CTrade class to the project and declare the instance of the class:

//--- Klasse der Handelsoperationen
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| Klasse zum Erstellen von Anwendungen                             |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Handelsoperationen
   CTrade            m_trade;
  };

Setzen wir den asynchronen Handelsmodus, so dass das Programm nicht auf das Ergebnis jeder Handelsoperation warten muss. Außerdem legen wir den maximal zulässigen Schlupf fest. Das bedeutet, dass die Positionen innerhalb der Abweichung von dem in einem Handelsoperation festgelegten Preis ausgeführt werden.

//+------------------------------------------------------------------+
//| Konstruktor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
...
   m_trade.SetAsyncMode(true);
   m_trade.SetDeviationInPoints(INT_MAX);
  }

Ein Klick auf die Schaltflächen Buy und Sell sind mit ähnlichen Methoden zu bedienen - CProgramm::OnBuy() und CProgramm::OnSell(). Das Positionsvolumen wird aus dem Eingabefeld Lot empfangen. Das gehandelte Symbol verwendet das des Objektcharts. Dies ist die Mindestanzahl von Maßnahmen, die erforderlich sind, um eine Handelsoperation abzuschließen. Die Klasse CTrade bietet die Methoden CTrade::Buy() und CTrade::Sell(). Beim Aufruf der Methoden können nur zwei dieser Argumente übergeben werden. 

//+------------------------------------------------------------------+
//| Kaufen                                                           |
//+------------------------------------------------------------------+
bool CProgram::OnBuy(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_buy.Id())
      return(false);
//--- Volumen und Symbol zur Positionseröffnung
   double lot    =::NormalizeDouble((double)m_lot.GetValue(),2);
   string symbol =m_sub_chart1.GetSubChartPointer(0).Symbol();
//--- Positionseröffnung
   m_trade.Buy(lot,symbol);
   return(true);
  }
//+------------------------------------------------------------------+
//| Verkaufen                                                        |
//+------------------------------------------------------------------+
bool CProgram::OnSell(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_sell.Id())
      return(false);
//--- Volumen und Symbol zur Positionseröffnung
   double lot    =::NormalizeDouble((double)m_lot.GetValue(),2);
   string symbol =m_sub_chart1.GetSubChartPointer(0).Symbol();
//--- Positionseröffnung
   m_trade.Sell(lot,symbol);
   return(true);
  }

Es sollte eine separate Methode implementiert werden, um alle Positionen gleichzeitig oder die eines bestimmten Symbols zu schließen, da die Klasse CTrade die nicht hat. Wird ein Symbol (optionaler Parameter) an die Methode übergeben, werden nur die Positionen dieses Symbols geschlossen. Wird kein Symbol angegeben, werden alle Positionen geschlossen.

//+------------------------------------------------------------------+
//| Schließen aller Positionen                                       |
//+------------------------------------------------------------------+
bool CProgram::CloseAllPosition(const string symbol="")
  {
//--- Existenzprüfung einer Position mit dn angegebenen Eigenschaften
   int total=::PositionsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- Positionsauswahl
      string pos_symbol=::PositionGetSymbol(i);
      //--- Schließen von Symbol
      if(symbol!="")
         if(symbol!=pos_symbol)
            continue;
      //--- Abfrage der Ticketnummer
      ulong position_ticket=::PositionGetInteger(POSITION_TICKET);
      //--- Rücksetzen der Fehlervariablen
      ::ResetLastError();
      //--- Wurde die Position nicht geschlossen, informieren
      if(!m_trade.PositionClose(position_ticket))
         ::Print(__FUNCTION__,": > An error occurred when closing a position: ",::GetLastError());
     }
//---
   return(true);
  }

Das Schließen aller Positionen ist an die Schaltfläche "Close all positions" gebunden. Deren Klick ruft die Methode CProgram::OnCloseAllPositions() auf. Um versehentliche Klicks auf diese Schaltfläche zu vermeiden, öffnet sich ein Bestätigungsfenster.

//+------------------------------------------------------------------+
//| Schließen aller Positionen                                       |
//+------------------------------------------------------------------+
bool CProgram::OnCloseAllPositions(const long id)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_close_all.Id())
      return(false);
//--- Dialogfenster
   int mb_id=::MessageBox("Are you sure you want to close \nall positions?","Close positions",MB_YESNO|MB_ICONWARNING);
//--- Positionen schließen
   if(mb_id==IDYES)
      CloseAllPosition();
//---
   return(true);
  }

Das schaut dann so aus:

 Abb. 9. Alle Positionen schließen

Abb. 9. Alle Positionen schließen

Positionen eines bestimmten Symbols können auf der Registerkarte Positionen geschlossen werden. Es wurden Schaltflächen mit Kreuzen in den Zellen der ersten Spalte der Positionstabelle hinzugefügt. Mit ihnen können wir alle Positionen eines Symbols, das in der entsprechenden Zeile angezeigt wird, gleichzeitig schließen. Durch Anklicken der Schaltflächen wird ein Nutzerereignis mit der ID ON_CLICK_BUTTON erzeugt. Aber das Element vom Typ CTable hat Scrollbalken, und ihre Schaltflächen erzeugen die gleichen Ereignisse, während die ID des Elements die gleiche ist. Das bedeutet, dass wir den Parameter der Zeichenkette (sparam) eines Ereignisses kontrollieren müssen, um eine versehentliche Behandlung eines Klicks auf andere Schaltflächen des Elements zu vermeiden. Im Parameter der Zeichenkette definieren wir den Typ des Elements, auf das ein Klick erfolgt ist. Für Bildlaufleisten ist dies der Wert "scroll". Trifft ein Ereignis mit einem solchen Wert ein, beendet das Programm die Methode. Danach müssen wir prüfen, ob noch offene Positionen vorhanden sind.

Wenn alle Prüfungen bestanden sind, müssen wir den Zeilenindex, über den das Symbol in der ersten Tabellenspalte definiert ist, aus der Beschreibung des Parameters der Zeichenkette extrahieren. Um versehentliche Klicks auf die Schaltflächen zu vermeiden, wird zunächst ein Bestätigungsfenster der Aktionen geöffnet. Klicken von Yes schließt Positionen nur für ein bestimmtes Symbol

//+------------------------------------------------------------------+
//| Schließen aller Position eines angegebenen Symbols               |
//+------------------------------------------------------------------+
bool CProgram::OnCloseSymbolPositions(const long id,const string desc)
  {
//--- Prüfen der Elemente-ID
   if(id!=m_table_positions.Id())
      return(false);
//--- Verlassen, wenn es ein Klick auf der Taste der Bildlaufleiste war
   if(::StringFind(desc,"scroll",0)!=WRONG_VALUE)
      return(false);
//--- Verlassen, wenn es keine Positionen gibt
   if(::PositionsTotal()<1)
      return(true);
//--- Daten aus der Zeichenkette extrahieren
   string str_elements[];
   ushort sep=::StringGetCharacter("_",0);
   ::StringSplit(desc,sep,str_elements);
//--- Abfrage des Index des Symbols
   int    row_index =(int)str_elements[1];
   string symbol    =m_table_positions.GetValue(0,row_index);
//--- Dialogfenster
   int mb_id=::MessageBox("Are you sure you want to close \nall positions on symbol "+symbol+"?","Close positions",MB_YESNO|MB_ICONWARNING);
//--- Schließen aller Positionen des angegebenen Symbols
   if(mb_id==IDYES)
      CloseAllPosition(symbol);
//---
   return(true);
  }

Hier ist, wie es aussieht:

 Abb. 10. Schließen aller Positionen des angegebenen Symbols

Abb. 10. Schließen aller Positionen des angegebenen Symbols

Alle oben beschriebenen Handelsoperationen werden beim Auftreten des Ereignisses ON_CLICK_BUTTON bearbeitet:

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Tasten der Klickereignisse
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      ...
      //--- Kaufen
      if(OnBuy(lparam))
         return;
      //--- Verkaufen
      if(OnSell(lparam))

         return;
      //--- Schließen aller Positionen
      if(OnCloseAllPositions(lparam))
         return;
      //--- Schließen aller Positionen des angegebenen Symbols
      if(OnCloseSymbolPositions(lparam,sparam))
         return;
      //---
      return;
     }
...
  }

Jede Handelsoperation sollte der Positionstabelle hinzugefügt werden. Um dies zu erreichen, müssen wir die Handelsgeschehnisse und die Handelsgeschichte des Kontos verfolgen. Wenn sich die Anzahl der Positionen geändert hat, sollte die Tabelle neu gebildet werden. Mit der Methode CProgram::IsLastDealTicket() wird überprüft, ob sich die Historie geändert hat. Zeit und letztes Deal-Ticket sollten nach jeder Überprüfung gespeichert werden. Wir sparen die Zeit, um nicht ständig die gesamte Handelshistorie abzufragen. Das Ticket ermöglicht es uns zu überprüfen, ob sich die Anzahl der Deals in der Historie geändert hat. Da das Geschäft mehrere Handelsgeschehnisse auslöst, gibt diese Methode wahr nur einmal zurück.

class CProgram : public CWndEvents
  {
private:
   //--- Zeit und Ticketnummer des zuletzt geprüften Deals
   datetime          m_last_deal_time;
   ulong             m_last_deal_ticket;
   //---
private:
   //--- Prüfen des neuen Deals in der Historie
   bool              IsLastDealTicket(void);
  };
//+------------------------------------------------------------------+
//| Konstruktor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  {
...
  }
//+------------------------------------------------------------------+
//| Prüfen des neuen Deals in der Historie                           |
//+------------------------------------------------------------------+
bool CProgram::IsLastDealTicket(void)
  {
//--- Verlassen, wenn die Historie nicht abgerufen werden kann
   if(!::HistorySelect(m_last_deal_time,UINT_MAX))
      return(false);
//--- Abfrage der Dealnummer der erhaltenen Liste
   int total_deals=::HistoryDealsTotal();
//--- Schleife über alle Deals der erhaltenen Liste vom vom letzten zum ersten Deal
   for(int i=total_deals-1; i>=0; i--)
     {
      //--- Abfrage der Ticketnummer
      ulong deal_ticket=::HistoryDealGetTicket(i);
      //--- Verlassen, wenn die Ticketnummern passen
      if(deal_ticket==m_last_deal_ticket)
         return(false);
      //--- Wenn die Ticketnummern nicht passen, informieren
      else
        {
         datetime deal_time=(datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         //--- Sichern von Zeit und Ticketnummer des letzten Deals
         m_last_deal_time   =deal_time;
         m_last_deal_ticket =deal_ticket;
         return(true);
        }
     }
//--- Ticketnummer eines anderen Symbols
   return(false);
  }

Die Methode CProgram::IsLastDealTicket() wird von der Ereignisbehandlung des Handels aufgerufen. Wird die Historie geändert, wird die Tabelle der Positionen erneuert:

//+------------------------------------------------------------------+
//| Ereignis einer Handelsoperation                                  |
//+------------------------------------------------------------------+
void CProgram::OnTradeEvent(void)
  {
//--- Im Falle eines neuen Deals
   if(IsLastDealTicket())
     {
      //--- Initialisieren der Positionstabelle
      InitializePositionsTable();
     }
  }

Hier ist, wie es aussieht:

 Abb. 11. Änderungen der Tabelle beim Schließen der Positionen eines Symbols

Abb. 11. Änderungen der Tabelle beim Schließen der Positionen eines Symbols

Schlussfolgerung

Wir haben diskutiert, wie man GUIs für Programme beliebiger Komplexität ohne großen Aufwand entwickelt. Sie können dieses Programm weiter entwickeln und für Ihre eigenen Zwecke nutzen. Die Idee kann weiter verbessert werden, wenn nutzerdefinierte Indikatoren und Ergebnisberechnungen hinzugefügt werden.

Im Market gibt es bereits das fertige Programm Trading Exposure für diejenigen, die nicht den Code ändern und ihn kompilieren wollen.

Beigefügt sind Dateien zum Testen und weiterführender Untersuchungen des Codes aus diesem Artikel.

Dateiname Kommentar
Nachfolgend können die Dateien zum Testen und detaillierten Studium des im Artikel enthaltenen Codes heruntergeladen werden. Der EA für ein manuelles Handeln mit einer GUI
MQL5\Experts\TradePanel\Program.mqh Programmklasse
MQL5\Experts\TradePanel\CreateGUI.mqh Methoden für die Entwicklung des GUI aus der Programmklasse aus Program.mqh
MQL5\Include\EasyAndFastGUI\Controls\Table.mqh Aktualisierte Klasse CTable
MQL5\Include\EasyAndFastGUI\Keys.mqh Aktualisierte Klasse CKeys