English Русский 中文 Español 日本語 Português
Trianguläre Arbitrage

Trianguläre Arbitrage

MetaTrader 5Handel | 4 Dezember 2017, 09:21
1 952 0
Alexey Oreshkin
Alexey Oreshkin

Die Allgemeine Idee

Themen, die der Triangulären Arbitrage gewidmet sind, erscheinen in Foren mit ungebrochener Regelmäßigkeit. Also, was ist es genau?

"Arbitrage" impliziert eine gewisse Neutralität gegenüber dem Markt. "Triangulär" bedeutet, dass das drei Handelsinstrumente verwendet werden.

Nehmen wir das bekannteste Beispiel: die großen Drei "EUR - GBP - USD". Bezogen auf die Währungspaare kann man sie wie folgt beschreiben: EURUSD + GBPUSD + EURGBP. Die geforderte Neutralität besteht in dem Versuch, dieselben Handelsinstrumente gleichzeitig zu kaufen und zu verkaufen und trotzdem Gewinne zu erwirtschaften.  

Dies sieht wie folgt aus. Jedes Paar aus diesem Beispiel wird durch die anderen beiden repräsentiert:

EURUSD=GBPUSD*EURGBP,

oder GBPUSD=EURUSD/EURGBP,

oder EURGBP=EURUSD/GBPUSD.

Alle diese Varianten sind identisch, und die Auswahl einer dieser Varianten wird im Folgenden näher erläutert. In der Zwischenzeit wenden wir uns der ersten Möglichkeit zu.

Zuerst schauen wir auf die Preise Bid und Ask. Die Vorgehensweise ist wie folgt:

  1. Wir kaufen EURUSD, d.h. wir nehmen den Ask-Preis Das heißt, wir kaufen EUR während wir gleichzeitig USD verkaufen. 
  2. Bewerten wir nun EURUSD mittels der anderen Paare.
  3. GBPUSD: hat kein EUR. Stattdessen gibt es USD, die verkauft werden. Um aber USD mittels GBPUSD zu verkaufen, kaufen wir das Paar. Das heißt wir nehmen den Ask-Preis. Wenn wir das so machen, kaufen wir GBP während wir wieder USD verkaufen.
  4. EURGBP: Wir müssen EUR kaufen und GBP verkaufen, die wir eh nicht brauchen. Kaufen von EURGBP mit dem Ask-Preis. Wir kaufen also EUR und verkaufen GBP.

Insgesamt haben wir jetzt: (Ask) EURUSD = (Ask) GBPUSD * (Ask) EURGBP. Wir haben den gewünschten Ausgleich hergestellt. Um damit Gewinn zu erzielen, sollten wir eine Seite kaufen und die andere verkaufen. Hier gibt es zwei Möglichkeiten:

  1. Wir kaufen EURUSD billiger als wir es verkaufen können, stellen es aber anders dar: (Ask) EURUSD < (Bid) GBPUSD * (Bid) EURGBP 
  2. Wir verkaufen EURUSD teurer als wir es verkaufen können, stellen es aber anders dar: (Bid) EURUSD > (Ask) GBPUSD * (Ask) EURGBP 

Jetzt müssen wir nur noch einen solchen Fall warten und Profit daraus schlagen.

Beachten Sie, dass das Dreieck auf eine andere Weise gebildet werden kann, indem Sie alle drei Paare in eine Richtung bewegen und mit 1 vergleichen. Alle Varianten sind identisch, aber ich glaube, dass die oben beschriebene leichter zu erkennen und zu erklären ist.

Indem wir die Situation beobachten, können wir auf den Moment für gleichzeitiges Kaufen und Verkaufen warten. Dieser Fall ist sofort profitabel, aber solche Momente sind selten.
Häufiger sind die Fälle, in denen eine Seite billiger kaufen aber gleichzeitig die andere Seite nicht mit Gewinn verkaufen können. Dann warten wir, bis dieses Ungleichgewicht verschwunden ist. Eine Position eröffnet zu haben, ist sicher für uns, da unsere Position fast Null ist, was bedeutet, dass wir nicht mehr im Markt sind. Beachten Sie hier allerdings das Wort "fast". Für einen perfekten Ausgleich der Handelsvolumina benötigen wir eine Präzision, die uns nicht zur Verfügung steht. Das Handelsvolumen wird meistens auf zwei Dezimalstellen gerundet, was für unsere Strategie zu grob ist.

Nun, da wir die Theorie beschrieben haben, ist es an der Zeit, den EA zu schreiben. Der EA wird in einem prozeduralen Stil entwickelt, so dass es sowohl für Neulinge als auch für diejenigen, die aus irgendeinem Grund OOP nicht mögen, verständlich ist. 

Kurze Beschreibung des EAs

Zuerst erstellen wir alle möglichen Dreiecke, platzieren sie korrekt und erhalten alle notwendigen Daten für jedes Währungspaar.

Alle diese Informationen sind im Struktur-Array MxThree gespeichert. Jedes Dreieck hat das Feld status. Sein Anfangswert ist 0. Wenn das Dreieck geöffnet werden soll, wird der Status auf 1 gesetzt. Nach der Bestätigung, dass das Dreieck vollständig geöffnet ist, wechselt sein Status auf 2. Wenn sich das Dreieck nur teilweise eröffnet wurde oder es an der Zeit ist, es zu schließen, wechselt der Status auf 3. Nach erfolgreichem Schließen des Dreiecks kehrt der Status auf 0 zurück.

Das Öffnen und Schließen von Dreiecken wird in einer Protokolldatei gespeichert, die es uns ermöglicht, die Richtigkeit der Aktionen zu überprüfen und die Historie wiederherzustellen. Der Name der Protokolldatei lautet Three Point Arbitrage Control YYYYY.DD.MM.csv.

Um einen Test durchzuführen, laden Sie alle notwendigen Währungspaare in den Tester. Starten Sie dazu den EA im Modus "Create file with symbols", bevor Sie den Tester starten. Wenn keine solche Datei existiert, führt der EA den Test mit dem vorgegebenen Dreieck EUR+GBP+USD durch.  

Verwendete Variablen

In meinem Entwicklungsprozess beginnt der Code eines jeden Roboters mit dem Laden der Headerdatei. Es werden alle Includes, Bibliotheken etc. aufgelistet. Dieser Roboter ist keine Ausnahme: Nach dem Beschreibungsteil folgt #include "head.mqh" etc.:

#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>  
#include <Trade\TerminalInfo.mqh> 

#include "var.mqh"
#include "fnWarning.mqh"
#include "fnSetThree.mqh"
#include "fnSmbCheck.mqh"
#include "fnChangeThree.mqh"
#include "fnSmbLoad.mqh"
#include "fnCalcDelta.mqh"
#include "fnMagicGet.mqh"
#include "fnOpenCheck.mqh"
#include "fnCalcPL.mqh"
#include "fnCreateFileSymbols.mqh"
#include "fnControlFile.mqh"
#include "fnCloseThree.mqh"
#include "fnCloseCheck.mqh"
#include "fnCmnt.mqh"
#include "fnRestart.mqh"
#include "fnOpen.mqh"

Diese Liste mag Ihnen im Moment nicht ganz verständlich sein, aber der Artikel folgt dem Code, so dass hier nicht von der Struktur des Programms abgewichen wird. Weiter unten wird alles klar werden. Alle Funktionen, Klassen und Codeeinheiten sind des Komforts wegen in separaten Dateien abgelegt. In meinem Fall beginnt jede Include-Datei, mit Ausnahme der Standardbibliothek, mit #include "head.mqh". Dies ermöglicht die Verwendung von IntelliSense in den Include-Dateien, wodurch die Notwendigkeit entfällt, die Namen aller notwendigen Elemente im Speicher zu behalten.

Danach verbinden Sie die Datei für den Tester. Das können wir nirgendwo anders machen, also lassen Sie es uns hier erklären. Diese Zeichenkette wird benötigt, um Symbole in den Mehrwährungstester zu laden:

#property tester_file FILENAME

Als nächstes beschreiben wir die Variablen, die im Programm verwendet werden. Die Beschreibung befindet sich in der separaten Datei var.mqh:

// Makros
#define DEVIATION       3                                                                 // Maximal möglicher Schlupf
#define FILENAME        "Three Point Arbitrage.csv"                                       // die verwendeten Symbole werden hier gespeichert
#define FILELOG         "Three Point Arbitrage Control "                                  // Name der Logdatei
#define FILEOPENWRITE(nm)  FileOpen(nm,FILE_UNICODE|FILE_WRITE|FILE_SHARE_READ|FILE_CSV)  // Datei zum Schreiben öffnen
#define FILEOPENREAD(nm)   FileOpen(nm,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV)   // Datei zum Lesen öffnen
#define CF              1.2                                                               // Erhöhen der Verhältnisses für die Marge
#define MAGIC           200                                                               // Bereich für die Magicnummern
#define MAXTIMEWAIT     3                                                                 // Maximale Wartezeit für das Öffnen des Dreiecks, in Sekunden

// Struktur der Währungspaare
struct stSmb
   {
      string            name;            // Währungspaar
      int               digits;          // Dezimalstellen der Preise
      uchar             digits_lot;      // Dezimalstellen der Lotgröße, zum Runden
      int               Rpoint;          // 1/point, für die Multiplikation (statt zu dividieren) in den Gleichungen
      double            dev;             // erlaubter Schlupf, wird unmittelbar in points umgewandelt
      double            lot;             // Handelsvolumen eines Währungspaares
      double            lot_min;         // Mindestvolumen
      double            lot_max;         // Maximalvolumen
      double            lot_step;        // Schrittweite der Lots
      double            contract;        // Kontraktgröße
      double            price;           // Eröffnungspreis des Paares im Dreieck. Benötigt wird für "netting"-Konten
      ulong             tkt;             // Ticketnummer einer Position, um sie zu eröffnen. Benötigt der Einfachheit halber für "hedge"-Konten
      MqlTick           tick;            // Aktueller Preis des Paares
      double            tv;              // aktueller "TickValue"
      double            mrg;             // Aktuell notwendige Marge für das Eröffnen
      double            sppoint;         // Schlupf in points, als Ganzzahl
      double            spcost;          // Schlupf in Geldwert des eröffneten Volumens
      stSmb(){price=0;tkt=0;mrg=0;}   
   };

// Struktur des Dreiecks
struct stThree
   {
      stSmb             smb1;
      stSmb             smb2;
      stSmb             smb3;
      double            lot_min;          // Mindestvolumen des gesamten Dreiecks
      double            lot_max;          // Maximalvolumen des gesamten Dreiecks
      ulong             magic;            // Magicnummer des Dreiecks
      uchar             status;           // Status des Dreiecks. 0 - auslassen. 1 - im Prozess der Eröffnung. 2 - erfolgreich eröffnet. 3 - im Prozess des Schließens
      double            pl;               // Gewinn des Dreiecks
      datetime          timeopen;         // Zeitpunkt der Eröffnung des Dreiecks
      double            PLBuy;            // Möglicher Gewinn beim Kauf des Dreiecks
      double            PLSell;           // Möglicher Gewinn beim Verkauf des Dreiecks
      double            spread;           // Summe aller drei Schlüpfe (mit Kommission!)
      stThree(){status=0;magic=0;}
   };

  
// EA Arbeitsweisen  
enum enMode
   {
      STANDART_MODE  =  0, /*Symbols from Market Watch*/                  // Standard-Modus. Symbole des Market Watch
      USE_FILE       =  1, /*Symbols from file*/                          // Verwenden der Datei mit Symbolen
      CREATE_FILE    =  2, /*Create file with symbols*/                   // Erstellen der Datei für den Tester oder dem Handeln
      //END_ADN_CLOSE  =  3, /*Not open, wait profit, close & exit*/      // Schließen aller Positionen und EA beenden
      //CLOSE_ONLY     =  4  /*Not open, not wait profit, close & exit*/
   };


stThree  MxThree[];           // Hauptarray zur Sicherung der Dreiecke mit all ihren Daten

CTrade         ctrade;        // Klasse CTrade der Standardbibliothek
CSymbolInfo    csmb;          // Klasse CSymbolInfo der Standardbibliothek
CTerminalInfo  cterm;         // Klasse CTerminalInfo der Standardbibliothek

int         glAccountsType=0; // Kontotyp: hedging oder netting
int         glFileLog=0;      // Handle der Logdatei


// Eingaben

sinput      enMode      inMode=     0;          // Arbeitsmodus
input       double      inProfit=   0;          // Kommission
input       double      inLot=      1;          // Handelsvolumen
input       ushort	inMaxThree= 0;          // offene Dreiecke
sinput      ulong       inMagic=    300;        // EA Magicnummer
sinput      string      inCmnt=     "R ";       // Kommentar

Definitionen stehen an erster Stelle, sie sind einfach und kommentiert. Vermutlich sind sind leicht zu verstehen.

Es gibt zwei Strukturen — stSmb und stThree. Die Logik ist wie folgt: Jedes Dreieck besteht aus drei Währungspaaren. Nachdem wir eines von ihnen einmal beschrieben und dreimal benutzt haben, erhalten wir ein Dreieck. Die Struktur stSmb beschreibt ein Währungspaar und seine Spezifikation: Die mögliche Handelsvolumina, die Variablen _Digits und _Point, den aktuelle Preise zum Zeitpunkt der Eröffnung und einige andere. In der Struktur stThree wird stSmb dreimal verwendet. So entsteht unser Dreieck. Auch einige Eigenschaften, die sich auf das Dreieck beziehen (aktueller Gewinn, magische Zahl, offene Zeit usw.), werden hier hinzugefügt. Dann gibt es Betriebsarten, die später beschreiben werden, mit den Eingabevariablen. Die Eingabevariablen sind auch in den Kommentaren beschrieben. Wir werden uns zwei davon näher ansehen:

Der Parameter inMaxThree speichert die maximal mögliche Anzahl gleichzeitig geöffneter Dreiecke. 0 — Auslassen. Wenn der Parameter z.B. auf 2 gesetzt ist, können nicht mehr als zwei Dreiecke gleichzeitig geöffnet werden.

Der Parameter inProfit umfasst auch die Kommission, wenn es eine gibt.

Ersteinstellung

Nachdem wir nun die Include-Dateien und die verwendeten Variablen beschrieben haben, fahren wir mit OnInint() fort.

Überprüfen Sie vor dem Start des EA die Korrektheit der eingegebenen Parameter und warten Sie ggf. die ersten Daten ab. Wenn alles in Ordnung ist, fangen wir an. Ich verwende in der Regel für einen EA die geringstmögliche Anzahl von Eingaben, und dieser Roboter ist keine Ausnahme.

Nur eine von sechs Eingaben kann verhindern, dass der EA funktioniert, und das ist ein Handelsvolumen. Schauen wir uns den Code an. Wir können keine Position mit einem negativem Volumen eröffnen. Alle anderen Einstellungen haben keinen Einfluss auf das Funktionieren. Die Prüfungen werden gleich zu Beginn in OnInit() durchgeführt.

Schauen wir und den Code an.

void fnWarning(int &accounttype, double lot, int &fh)
   {   
      // Prüfen des Handelsvolumen, es darf nicht negativ sein
      if (lot<0)
      {
         Alert("Trade volume < 0");  
         ExpertRemove();         
      }      
      
      // Falls 0, warne und verwende das kleinst mögliche Volumen.
      if (lot==0) Alert("Always use the same minimum trading volume");  

Da der Roboter in einem prozeduralen Stil geschrieben ist, müssen wir mehrere globale Variablen anlegen. Eine davon ist das Handle zur Logdatei. Der Name besteht aus einem unveränderlichen Teil und dem Startdatum des Roboters - dies dient der einfachen Kontrolle, so dass man nicht suchen muss, wo das Protokoll für einen bestimmten Start in derselben Datei beginnt. Beachten Sie, dass sich der Name bei jedem Neustart ändert und eine existierende Datei gleichen Namens, falls vorhanden, gelöscht wird.

Der EA verwendet zwei Dateien in seiner Arbeit: Die Datei mit den erkannten Dreiecken (erstellt nach eigenem Ermessen) und die Logdatei, in die die Zeit des Öffnens und Schließens des Dreiecks, die Eröffnungspreise und einige zusätzliche Daten für eine einfache Kontrolle geschrieben wird. Die Protokollierung bleibt zu jeder Zeit aktiv.

      // Erstelle die Logdatei nur, wenn der Modus zum Erstellen der Dreiecke aus einer Datei nicht aktiv ist.                                  
      if(inMode!=CREATE_FILE)
      {
         string name=FILELOG+TimeToString(TimeCurrent(),TIME_DATE)+".csv";      
         FileDelete(name);      
         fh=FILEOPENWRITE(name);
         if (fh==INVALID_HANDLE) Alert("The log file is not created");      
      }   
      
      // Im Allgemeinen ist die Kontraktgröße der Broker für Währungspaare = 100000, aber manchmal gibt es auch Ausnahmen..
      // Sie sind jedoch so selten, dass es einfacher ist, diesen Wert beim Start zu überprüfen, und wenn er nicht 100 000 beträgt, dann meldet er das,
      // so dass die Nutzer selbst entscheiden kann, ob es für sie wichtig ist oder nicht. Der EA fährt fort, ohne auf die Momente hinzuweisen, 
      // wenn die Paare des Dreieck unterschiedliche Kontraktgröße haben.
      for(int i=SymbolsTotal(true)-1;i>=0;i--)
      {
         string name=SymbolName(i,true);
         
         // Das Prüfen der Verfügbarkeit des Symbols wird auch bei der Bildung eines Dreiecks verwendet.
         // Wir betrachten es später
         if(!fnSmbCheck(name)) continue;
         
         double cs=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE);
         if(cs!=100000) Alert("Attention: "+name+", contract size = "+DoubleToString(cs,0));      
      }
      
      // Abfrage des Kontotyps: hedging oder netting
      accounttype=(int)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   }

Bilden der Dreiecke

Um Dreiecke zu bilden, müssen wir folgende Aspekte berücksichtigen:

  1. Die Daten werden dem Market Watch oder einer vorbereiteten Datei entnommen.
  2. Sind wir im Tester? Wenn ja, laden Sie Symbole in den Market Watch. Es macht keinen Sinn, alles Mögliche zu laden, da ein normaler Heim-PC die Last einfach nicht verkraften kann. Suchen Sie nach einer Datei mit vorbereiteten Symbolen für den Tester. Andernfalls testen Sie die Strategie auf dem Standarddreieck: EUR+USD+GBP.
  3. Um den Code zu vereinfachen, führen Sie eine Einschränkung ein: Alle Dreieckssymbole sollten die gleiche Kontraktgröße haben.
  4. Vergessen Sie nicht, dass die Dreiecke nur aus Währungspaaren gebildet werden können.

Die erste, notwendige Funktion ist das Bilden von Dreiecken aus der Market Watch.

void fnGetThreeFromMarketWatch(stThree &MxSmb[])
   {
      // Die Gesamtzahl der Symbole
      int total=SymbolsTotal(true);
      
      // Variablen für den Vergleich der Kontraktgrößen    
      double cs1=0,cs2=0;              
      
      // Verwenden des ersten Symbols aus der Liste, in der ersten Schleife
      for(int i=0;i<total-2 && !IsStopped();i++)    
      {//1
         string sm1=SymbolName(i,true);
         
         // Prüfen des Symbols auf verschiedene Beschränkungen
         if(!fnSmbCheck(sm1)) continue;      
              
         // Abfrage und normalisieren der Kontraktgröße, da sie später verglichen werden wird 
         if (!SymbolInfoDouble(sm1,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue; 
         cs1=NormalizeDouble(cs1,0);
         
         // Abfrage der Basiswährung und dem Währungsgewinn für einen späteren Vergleich (statt des Namens des Paars)
         string sm1base=SymbolInfoString(sm1,SYMBOL_CURRENCY_BASE);     
         string sm1prft=SymbolInfoString(sm1,SYMBOL_CURRENCY_PROFIT);
         
         // Verwenden des zweiten Symbols in der zweiten Schleife
         for(int j=i+1;j<total-1 && !IsStopped();j++)
         {//2
            string sm2=SymbolName(j,true);
            if(!fnSmbCheck(sm2)) continue;
            if (!SymbolInfoDouble(sm2,SYMBOL_TRADE_CONTRACT_SIZE,cs2)) continue;
            cs2=NormalizeDouble(cs2,0);
            string sm2base=SymbolInfoString(sm2,SYMBOL_CURRENCY_BASE);
            string sm2prft=SymbolInfoString(sm2,SYMBOL_CURRENCY_PROFIT);
            // Das erste und das zweite Paar sollten eine Übereinstimmung in einer der Währungen haben
            // Falls nichtz kann kein Dreieck gebildet werden.    
            // Es gibt keinen Grund eine komplette Prüfung zu veranstalten. Zum Beispiel ist es unmöglich, 
            // ein Dreieck mit eurusd und eurusd.xxx zu bilden.
            if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else continue;
                  
            // Die Kontraktgröße müssen gleich sein            
            if (cs1!=cs2) continue;
            
            // Suchen des letzten Symbols des Dreiecks in der dritten Schleife
            for(int k=j+1;k<total && !IsStopped();k++)
            {//3
               string sm3=SymbolName(k,true);
               if(!fnSmbCheck(sm3)) continue;
               if (!SymbolInfoDouble(sm3,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue;
               cs1=NormalizeDouble(cs1,0);
               string sm3base=SymbolInfoString(sm3,SYMBOL_CURRENCY_BASE);
               string sm3prft=SymbolInfoString(sm3,SYMBOL_CURRENCY_PROFIT);
               
               // Wir wissen, dass das erste und zweite Symbol eine gemeinsame Währung haben. Um ein Dreieck zu bilden, müssen wir
               // die dritte Währung finden, die gleich eine der beiden Währungen des ersten Symbols ist, während dessen zweite Währung
               // zu einer der beiden des zweiten Symbols passt. Gelingt das nicht, kann mit diesem Paar kein Dreieck gebildet werden.
               if(sm3base==sm1base || sm3base==sm1prft || sm3base==sm2base || sm3base==sm2prft);else continue;
               if(sm3prft==sm1base || sm3prft==sm1prft || sm3prft==sm2base || sm3prft==sm2prft);else continue;
               if (cs1!=cs2) continue;
               
               // Das Erreichen dieses Zustands bedeutet, es wurden alle Prüfungen bestanden und es wurde drei Währungen zum Bilden eines Dreiecks gefunden
               // dem Array zuweisen
               int cnt=ArraySize(MxSmb);
               ArrayResize(MxSmb,cnt+1);
               MxSmb[cnt].smb1.name=sm1;
               MxSmb[cnt].smb2.name=sm2;
               MxSmb[cnt].smb3.name=sm3;
               break;
            }//3
         }//2
      }//1    
   }

Die zweite notwendige Funktion ist das Lesen der Dreiecke aus der Datei

void fnGetThreeFromFile(stThree &MxSmb[])
   {
      // Falls die Datei mit den Symbolen nicht gefunden wurde, wird eine entsprechende Meldung gemacht und der EA beendet sich
      int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV);
      if(fh==INVALID_HANDLE)
      {
         Print("File with symbols not read!");
         ExpertRemove();
      }
      
      // Setzen des Dateipointers auf den Anfang
      FileSeek(fh,0,SEEK_SET);
      
      // Überspringe den Anfang (erste Zeile des Datei)      
      while(!FileIsLineEnding(fh)) FileReadString(fh);
      
      
      while(!FileIsEnding(fh) && !IsStopped())
      {
         // Lesen der drei Symbole. Prüfen der Verfügbarkeit
         // Der Roboter kann die Datei mit den Dreiecken automatisch verarbeiten. Falls der Nutzer
         // in einer falschen Weise ändert, wird angenommen, er tat es bewusst
         string smb1=FileReadString(fh);
         string smb2=FileReadString(fh);
         string smb3=FileReadString(fh);
         
         // Sind die Daten des Symbole verfügbar, werden sie dem Array für die Dreiecke nach dem Erreichen des Zeilenendes zugewiesen
         if (!csmb.Name(smb1) || !csmb.Name(smb2) || !csmb.Name(smb3)) {while(!FileIsLineEnding(fh)) FileReadString(fh);continue;}
         
         int cnt=ArraySize(MxSmb);
         ArrayResize(MxSmb,cnt+1);
         MxSmb[cnt].smb1.name=smb1;
         MxSmb[cnt].smb2.name=smb2;
         MxSmb[cnt].smb3.name=smb3;
         while(!FileIsLineEnding(fh)) FileReadString(fh);
      }
   }

Die letzte Funktion, die in diesem Abschnitt benötigt wird, ist eine Kapselung der beiden vorhergehenden Funktionen. Sie ist verantwortlich für die Auswahl der Quelle der Dreiecke in Abhängigkeit von den EA-Eingaben. Sie prüft auch, wo der Roboter gestartet wurde. Im Tester werden die Dreiecke aus der Datei geladen, unabhängig von der Wahl des Benutzers. Wenn es keine Datei gibt, wird mit dem standardmäßige Dreieck EURUSD+GBPUSD+EURGBP gearbeitet.

void fnSetThree(stThree &MxSmb[],enMode mode)
   {
      // Rücksetzen des Arrays für die Dreiecke
      ArrayFree(MxSmb);
      
      // Prüfen, ob er im Tester läuft oder nicht
      if((bool)MQLInfoInteger(MQL_TESTER))
      {
         // Falls ja, prüfen, ob die Datei mit den Symbolen existiert, und starte mit dem Dreieck aus der Datei
         if(FileIsExist(FILENAME)) fnGetThreeFromFile(MxSmb);
         
         // Falls die Datei nicht gefunden wurde, gehe durch alle Symbole, um das standardmäßig Dreieck EURUSD+GBPUSD+EURGBP zu finden
         else{               
            char cnt=0;         
            for(int i=SymbolsTotal(false)-1;i>=0;i--)
            {
               string smb=SymbolName(i,false);
               if ((SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="GBP") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="GBP" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD"))
               {
                  if (SymbolSelect(smb,true)) cnt++;
               }               
               else SymbolSelect(smb,false);
               if (cnt>=3) break;
            }  
            
            // Nachdem die Symbole des vorgegebenen Dreiecks im Market Watch geladen wurden, beginnt die Verarbeitung         
            fnGetThreeFromMarketWatch(MxSmb);
         }
         return;
      }
      
      // Falls  EA nicht im Tester gestartet wurde, prüfen des vom Nutzer gewählten Startmodus: 
      // übernehmen der Symbole entweder aus dem Market Watch oder aus der Datei
      if(mode==STANDART_MODE || mode==CREATE_FILE) fnGetThreeFromMarketWatch(MxSmb);
      if(mode==USE_FILE) fnGetThreeFromFile(MxSmb);     
   }

Dazu verwenden wir die Hilfsfunktion — fnSmbCheck(). Sie prüft, ob die Arbeit mit den Symbolen Einschränkungen unterliegt. Falls ja, überspringen. Unten ist der Code.

bool fnSmbCheck(string smb)
   {
      // Ein Dreieck kann nur aus Währungspaaren gebildet werden
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) return(false);
      
      // Falls es Handelsbeschränkungen gibt, überspringen des Symbols
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) return(false);   
      
      // Falls es für das Symbol zeitliche Einschränkungen gibt, wird es auch ausgelassen, da diese Parameter nicht für den Handel verwendet werden
      if(SymbolInfoInteger(smb,SYMBOL_START_TIME)!=0)return(false);
      if(SymbolInfoInteger(smb,SYMBOL_EXPIRATION_TIME)!=0) return(false);
      
      // Verfügbarkeit der Auftragstypen. Obwohl, der Roboter nur Marktorders verwendet, sollte es immer noch keine Beschränkungen geben
      int som=(int)SymbolInfoInteger(smb,SYMBOL_ORDER_MODE);
      if((SYMBOL_ORDER_MARKET&som)==SYMBOL_ORDER_MARKET); else return(false);
      if((SYMBOL_ORDER_LIMIT&som)==SYMBOL_ORDER_LIMIT); else return(false);
      if((SYMBOL_ORDER_STOP&som)==SYMBOL_ORDER_STOP); else return(false);
      if((SYMBOL_ORDER_STOP_LIMIT&som)==SYMBOL_ORDER_STOP_LIMIT); else return(false);
      if((SYMBOL_ORDER_SL&som)==SYMBOL_ORDER_SL); else return(false);
      if((SYMBOL_ORDER_TP&som)==SYMBOL_ORDER_TP); else return(false);
       
      // Prüfen der Standardbibliothek auf den Datenzugriff         
      if(!csmb.Name(smb)) return(false);
      
      // Der folgende Check wird nur in der realen Arbeit benötigt, weil manchmal SymbolInfoTick() aus irgendeinem Grund konkrete Preise 
      // zurückgibt, während aber Bid und Ask tatsächlich immer noch Null sind.
      // Im Tester ausschalten, weil dort die Preise später erscheinen können.
      if(!(bool)MQLInfoInteger(MQL_TESTER))
      {
         MqlTick tk;      
         if(!SymbolInfoTick(smb,tk)) return(false);
         if(tk.ask<=0 ||  tk.bid<=0) return(false);      
      }

      return(true);
   }

So, die Dreiecke wurden gebildet. Die Funktionen für ihre Verarbeitung finden sich in der Datei fnSetThree.mqh. Die Funktion zur Prüfung des Symbols auf Einschränkungen finden sich in der separate Datei fnSmbCheck.mqh.

Wir bilden alle möglichen Dreiecke. Deren Paare können in beliebiger Reihenfolge angeordnet werden, aber das verursacht eine Menge Unannehmlichkeiten, denn wir müssen bestimmen, wie man ein Währungspaar durch das andere ausdrücken kann. Um Ordnung herzustellen, betrachten wir alle möglichen Reihenfolgen am Beispiel von EUR-USD-GBP:

# symbol 1 symbol 2
symbol 3
1 EURUSD = GBPUSD  х EURGBP
2 EURUSD = EURGBP  х GBPUSD
3 GBPUSD = EURUSD  / EURGBP
4 GBPUSD = EURGBP  0 EURUSD
5 EURGBP = EURUSD  / GBPUSD
6 EURGBP = GBPUSD  0 EURUSD

'x' = multiplizieren, '/' = dividieren. '0' = unmöglich

In der obigen Tabelle können wir sehen, dass das Dreieck auf sechs verschiedene Arten gebildet werden kann, obwohl zwei von ihnen - die Zeilen 4 und 6 - es nicht erlauben, das erste Symbol durch die beiden verbleibenden auszudrücken. Das bedeutet, dass diese Optionen verworfen werden sollten. Die restlichen 4 Optionen sind identisch. Es spielt keine Rolle, welches Symbol wir ausdrücken wollen und mit welchen Symbolen wir das tun. Wichtig ist hier einzig und allein die Geschwindigkeit. Die Division ist langsamer als die Multiplikation, so dass die Optionen 3 und 5 verworfen werden. Die einzigen verbleibenden Optionen sind die in den Zeilen 1 und 2.

Betrachten wir die Option 2 wegen ihrer Einfachheit. Somit müssen wir keine zusätzlichen Eingabefelder für das erste, zweite und dritte Symbol einfügen. Das ist unmöglich, weil wir alle möglichen Dreiecke tauschen und nicht nur ein einziges.

Die Bequemlichkeit lenkt unserer Wahl: Da wir Arbitrage handeln und diese Strategie eine neutrale Position impliziert, sollten wir den gleichen Vermögenswert kaufen und verkaufen. Beispiel: Kaufen 0,7 Lots von EURUSD und verkaufen 0.7 Lots von EURGBP - damit haben wir 70.000 € gekauft und verkauft. So haben wir eine Position, obwohl wir uns außerhalb des Marktes befinden, da das gleiche Volumen sowohl beim Kauf als auch beim Verkauf vorhanden ist (wenn auch unterschiedlich ausgedrückt). Wir müssen sie anpassen, indem wir einen Handel mit GBPUSD durchführen. Mit anderen Worten, wir wissen sofort, dass die Symbole 1 und 2 ein ähnliches Volumen, aber eine andere Richtung haben sollten. Es ist auch im Voraus bekannt, dass das dritte Paar ein Volumen hat, das dem Preis des zweiten Paares entspricht.

Die Funktion ordnet die Paare in einem Dreieck richtig an:

void fnChangeThree(stThree &MxSmb[])
   {
      int count=0;
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for         
         // Zuerst bestimmen wir, was gehört auf den dritten Platz. 
         // Das ist das Paar mit der Basiswährung, die zu keine der anderen Basiswährungen passt
         string sm1base="",sm2base="",sm3base="";
         
         // Wenn auch welchen Gründen immer keine Basiswährung bestimmt werden kann, wird das Dreieck nicht verwendet
         if(!SymbolInfoString(MxSmb[i].smb1.name,SYMBOL_CURRENCY_BASE,sm1base) ||
         !SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_BASE,sm2base) ||
         !SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE,sm3base)) {MxSmb[i].smb1.name="";continue;}
                  
         // Wenn die Basiswährung von Symbol 1 und 2 übereinstimmen, überspringe den Schritt. Sonst tausche den Platz der Paare
         if(sm1base!=sm2base)
         {         
            if(sm1base==sm3base)
            {
               string temp=MxSmb[i].smb2.name;
               MxSmb[i].smb2.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
            
            if(sm2base==sm3base)
            {
               string temp=MxSmb[i].smb1.name;
               MxSmb[i].smb1.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
         }
         
         // Bestimmen wir jetzt den erste und zweiten Platz. 
         // Den zweiten Platz erhält das Symbol, dessen Währung des Gewinns zu der Basiswährung des dritten Symbols passt. 
         // In diese Fall verwenden wir immer die Multiplikation.
         sm3base=SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE);
         string sm2prft=SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_PROFIT);
         
         // Platztausch der Symbole eins und zwei. 
         if(sm3base!=sm2prft)
         {
            string temp=MxSmb[i].smb1.name;
            MxSmb[i].smb1.name=MxSmb[i].smb2.name;
            MxSmb[i].smb2.name=temp;
         }
         
         // Anzeige der verwendeten Dreiecks. 
         Print("Use triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
         count++;
      }//
      // Informieren über die Gesamtzahl der verwendeten Dreiecke. 
      Print("All used triangles: "+(string)count);
   }

Die Funktion befindet sich vollständig in der separaten Datei fnChangeThree.mqh.

Der letzte Schritt, der notwendig ist, um die Vorbereitung der Dreiecke abzuschließen: Laden Sie alle Daten der verwendeten Paare sofort hoch, so dass Sie keine Zeit aufwenden müssen, um sie später nachzuladen. Wir brauchen Folgendes:

  1. Minimales und maximales Handelsvolumen für jedes Symbol;
  2. Anzahl der Dezimalstellen von Preis und Lotzahl zum Runden;
  3. Die Variablen Point und Ticksize. Ich habe noch nie Situationen erlebt, in denen sie sich unterschieden. Wie auch immer, holen wir uns alle Daten und nutzen sie, wenn nötig.
void fnSmbLoad(double lot,stThree &MxSmb[])
   {
      
      // Einfaches Makro zum Drucken   
      #define prnt(nm) {nm="";Print("NOT CORRECT LOAD: "+nm);continue;}
      
      // Schleife über alle Dreiecke. Hier verschwenden wir CPU-Zeit, da die Daten desselben Symbol mehrfach berechnet werden, 
      // da das aber nur einmal beim Start des Roboters geschieht, können wir das immer noch angehen und den Code verkleinern.
      // Verwenden der Standardbibliothek für die Daten. 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Durch das Zuweisen eines Symbol zur Klasse CSymbolInfo, initiieren wir das Sammeln aller notwendigen Daten
         // Prüfen deren Verfügbarkeit währenddessen. Falls etwas schief läuft, wird das Dreieck als funktionsunfähig markiert.                  
         if (!csmb.Name(MxSmb[i].smb1.name))    prnt(MxSmb[i].smb1.name); 
         
         // Abfragen von _capacity für jedes Symbol
         MxSmb[i].smb1.digits=csmb.Digits();
         
         // Konvertieren des ganzzahligen Schlupfes in points. Wir benötigen dieses Format für weitere Berechnungen
         MxSmb[i].smb1.dev=csmb.TickSize()*DEVIATION;         
         
         // Konvertieren der Preise in eine Anzahl von points, da wir oft den Preis durch den Wert _Point dividieren müssen.
         // Es ist sinnvoller, den Wert 1/point zu speichern, um so die Divisionen durch eine Multiplikationen zu ersetzen. 
         // csmb.Point() wird nicht auf 0 geprüft: Es kann nicht 0 werden, aber wenn 
         // der Parameter warum auch immer nicht angekommen ist, wird das Dreieck ignoriert, einfach durch die Zeile (!csmb.Name(MxSmb[i].smb1.name)).            
         MxSmb[i].smb1.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         
         // Anzahl der Dezimalstellen für das Runden der Lotzahl. 
         MxSmb[i].smb1.digits_lot=csup.NumberCount(csmb.LotsStep());
         
         // Volumensbeschränkungen (sofort normalisiert)
         MxSmb[i].smb1.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb1.digits_lot); 
         
         //Kontraktgröße 
         MxSmb[i].smb1.contract=csmb.ContractSize();
         
         // Das Gleiche wie oben nur für das Symbol 2
         if (!csmb.Name(MxSmb[i].smb2.name))    prnt(MxSmb[i].smb2.name);
         MxSmb[i].smb2.digits=csmb.Digits();
         MxSmb[i].smb2.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb2.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb2.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb2.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb2.digits_lot);         
         MxSmb[i].smb2.contract=csmb.ContractSize();
         
         // Das Gleiche wie oben nur für das Symbol 3
         if (!csmb.Name(MxSmb[i].smb3.name))    prnt(MxSmb[i].smb3.name);
         MxSmb[i].smb3.digits=csmb.Digits();
         MxSmb[i].smb3.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb3.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb3.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb3.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb3.digits_lot);           
         MxSmb[i].smb3.contract=csmb.ContractSize();   
         
         // Anpassen des Handelsvolumen. Es gibt eine Grenze für jedes Währungspaar und für das ganze Dreieck.  
         // Die Beschränkungen der Paare finden sich hier: MxSmb[i].smbN.lotN,
         // die Beschränkungen des ganzen Dreiecks hier: MxSmb[i].lotN
         
         // Wähle den höchsten unter den niedrigsten Werte. Runden auf den größten Wert.
         // Diesen Abschnitt gibt es nur für den Fall, dass das Volumen wir folgt ist: 0.01+0.01+0.1. 
         // In diesem Fall wird das kleinstmögliche Handelsvolumen auf 0.1 gesetzt und auf eine Dezimalstelle gerundet.
         double lt=MathMax(MxSmb[i].smb1.lot_min,MathMax(MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min));
         MxSmb[i].lot_min=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // Auch der niedrigste Volumen wird aus den höchsten ermittelt und sofort gerundet. 
         lt=MathMin(MxSmb[i].smb1.lot_max,MathMin(MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max));
         MxSmb[i].lot_max=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // Wenn es 0 in den Eingangsparametern des Handelsvolumens gibt, verwenden Sie das kleinstmögliche Volumen, aber nicht das geringste irgendeines Paares, 
         // sondern das kleinste aller Paare. 
         if (lot==0)
         {
            MxSmb[i].smb1.lot=MxSmb[i].lot_min;
            MxSmb[i].smb2.lot=MxSmb[i].lot_min;
            MxSmb[i].smb3.lot=MxSmb[i].lot_min;
         } else
         {
            // Wenn das Volumen angepasst werden muss, sind die Volumina für die Paare 1 und 2 bekannt, während das des dritten kurz vor der Eröffnung ermittelt wird. 
            MxSmb[i].smb1.lot=lot;  
            MxSmb[i].smb2.lot=lot;
            
            // Falls das eingegebene Volumen nicht in die aktuellen Beschränkungen fällt, wird das Dreieck nicht verwendet. 
            // Eine Alert-Meldung informiert darüber
            if (lot<MxSmb[i].smb1.lot_min || lot>MxSmb[i].smb1.lot_max || lot<MxSmb[i].smb2.lot_min || lot>MxSmb[i].smb2.lot_max) 
            {
               MxSmb[i].smb1.name="";
               Alert("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - not correct the trading volume");
               continue;
            }            
         }
      }
   }

Die Funktion findet sich in der eigenen Datei fnSmbLoad.mqh.

Das ist alles über die Bildung der Dreiecke. Machen wir weiter.

Die Funktionsarten des EAs

Beim Start des Roboters können wir eine der verfügbaren Funktionsarten wählen:
  1. Symbols from Market Watch.
  2. Symbols from file.
  3. Create file with symbols.

"Symbols from Market Watch" bedeutet, dass wir den Roboter mit dem aktuellen Symbol starten und die Dreiecke mit den Symbolen aus dem Market Watch bilden. Dies ist die normale Funktionsart und erfordert keine zusätzliche Bearbeitung.

"Symbols from Market Watch" unterscheidet sich von der ersten nur durch die Quelle der Dreiecke - aus einer zuvor erstellten Datei.

"Create file with symbols" erstellt eine Datei mit zukünftig verwendbaren Dreiecken, entweder nach einem weiteren EA-Start oder im Tester. Dieser Modus bildet nur die Dreiecke. Danach beendet sich der EA.

Beschreiben wir dessen Logik:

      if(inMode==CREATE_FILE)
      {
         // Löschen der Datei, wenn sie existiert.
         FileDelete(FILENAME);  
         int fh=FILEOPENWRITE(FILENAME);
         if (fh==INVALID_HANDLE) 
         {
            Alert("File with symbols not created");
            ExpertRemove();
         }
         // Schreiben der Dreiecke und weitere Daten in die Datei
         fnCreateFileSymbols(MxThree,fh);
         Print("File with symbols created");
         
         // Schließen der Datei und beenden des EAs
         FileClose(fh);
         ExpertRemove();
      }

Das Schreiben der Daten in die Datei ist einfach und erfordert weitere Kommentare:

void fnCreateFileSymbols(stThree &MxSmb[], int filehandle)
   {
      // Schreiben der Kopfzeile
      FileWrite(filehandle,"Symbol 1","Symbol 2","Symbol 3","Contract Size 1","Contract Size 2","Contract Size 3",
      "Lot min 1","Lot min 2","Lot min 3","Lot max 1","Lot max 2","Lot max 3","Lot step 1","Lot step 2","Lot step 3",
      "Common min lot","Common max lot","Digits 1","Digits 2","Digits 3");
      
      // Schreiben in die Datei entsprechend der Kopfzeile von oben
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         FileWrite(filehandle,MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name,
         MxSmb[i].smb1.contract,MxSmb[i].smb2.contract,MxSmb[i].smb3.contract,
         MxSmb[i].smb1.lot_min,MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min,
         MxSmb[i].smb1.lot_max,MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max,
         MxSmb[i].smb1.lot_step,MxSmb[i].smb2.lot_step,MxSmb[i].smb3.lot_step,
         MxSmb[i].lot_min,MxSmb[i].lot_max,
         MxSmb[i].smb1.digits,MxSmb[i].smb2.digits,MxSmb[i].smb3.digits);         
      }
      FileWrite(filehandle,"");      
      // Schreiben einer Leerzeile nach allen Symbolen
      
      // Schreiben aller Daten auf die Festplatte, zur Sicherheit 
      FileFlush(filehandle);
   }

Zusätzlich zu den Dreiecken schreiben wir auch weiter Daten: Erlaubte Handelsvolumina, Kontraktgröße, Anzahl der Dezimalstellen der Preise. Wir benötigen diese Daten in der Datei nur, um die Eigenschaften der Symbole visuell zu überprüfen zu können.

Die Funktion befindet sich in der separaten Datei fnCreateFileSymbols.mqh.

Neustart des Roboters

Wir haben die Anfangseinstellungen des EA fast abgeschlossen. Wir haben jedoch noch eine Frage zu beantworten: Wie funktioniert die Wiederherstellung nach einem Crash? Wir müssen uns keine Sorgen um einen kurzfristigen Ausfall der Internetverbindung machen. Der Roboter arbeitet normal weiter, nachdem das Terminal sich wieder mit dem Server verbunden hat. Aber wenn wir den Roboter neu starten müssen, dann müssen wir unsere Positionen finden und mit ihnen weiterarbeiten.

Unten ist die Funktion, die die Probleme beim Neustart des Roboters löst:

void fnRestart(stThree &MxSmb[],ulong magic,int accounttype)
   {
      string   smb1,smb2,smb3;
      long     tkt1,tkt2,tkt3;
      ulong    mg;
      uchar    count=0;    //Counter of restored triangles
      
      switch(accounttype)
      {
         // Es ist ganz einfach, Positionen im Falle eines Hedging-Kontos wiederherzustellen: alle offenen Positionen durchgehen, die notwendigen Positionen mit Hilfe einer magischen Zahl identifizieren und  
         // sie zum Dreieck formen.
         // Der Fall wird komplizierter, wenn es sich um ein Netting-Konto handelt - zuerst müssen wir in unsere Datenbank schauen, in der die Positionen gespeichert sind, die der Roboter geöffnet hat. 
         
         // Der Algorithmus, die notwendigen Positionen zu suchen und sie in ein Dreieck umzuwandeln, wurde ziemlich einfach und nicht-optimiert 
         // implementiert. Da dieser Vorgang nur selten benötigt wird, müssen wir nicht auf die Leistung schauen,
         // um den Code zu optimieren. 
         
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            // Durchgehen aller Positionen und identifizieren der Magicnummern. 
            // Speichern der Magicnummer bei der ersten erkannten Position, um die anderen beiden mit ihr zu erkennen. 

            
            for(int i=PositionsTotal()-1;i>=2;i--)
            {//for i
               smb1=PositionGetSymbol(i);
               mg=PositionGetInteger(POSITION_MAGIC);
               if (mg<magic || mg>(magic+MAGIC)) continue;
               
               // Sichern der Ticketnummer, um die Position leichter zu erreichen. 
               tkt1=PositionGetInteger(POSITION_TICKET);
               
               // Suche nach der zweiten Position mit der selben Magicnummer. 
               for(int j=i-1;j>=1;j--)
               {//for j
                  smb2=PositionGetSymbol(j);
                  if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;  
                  tkt2=PositionGetInteger(POSITION_TICKET);          
                    
                  // Suche nach der dritten Position.
                  for(int k=j-1;k>=0;k--)
                  {//for k
                     smb3=PositionGetSymbol(k);
                     if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;
                     tkt3=PositionGetInteger(POSITION_TICKET);
                     
                     // Wenn Sie dieses Stadium erreichen, wurde ein offene Dreieck gefunden. Dessen Daten sind damit auch bekannt. Der Roboter berechnet die übrigen Daten mit dem nächsten Tick.
                     
                     for(int m=ArraySize(MxSmb)-1;m>=0;m--)
                     {//for m
                        // Gehe durch das Array mit allen Dreiecken und ignoriere die bereits geöffneten.
                        if (MxSmb[m].status!=0) continue; 
                        
                        // Das ist ziemlich "grob". Zuerst scheint es, dass wir  
                        // auf dieselben Währungspaare mehrfach zugreifen könnten. Aber das ist nicht der Fall, da nach der Identifizierung eines anderen Währungspaares,
                        // wir die Suche weiter mit dem nächsten Paar fortsetzen, statt die Schleife wieder von Beginn an zu durchlaufen

                        if (  (MxSmb[m].smb1.name==smb1 || MxSmb[m].smb1.name==smb2 || MxSmb[m].smb1.name==smb3) &&                               (MxSmb[m].smb2.name==smb1 || MxSmb[m].smb2.name==smb2 || MxSmb[m].smb2.name==smb3) &&                               (MxSmb[m].smb3.name==smb1 || MxSmb[m].smb3.name==smb2 || MxSmb[m].smb3.name==smb3)); else continue;                                                  // Wir haben das Dreieck identifiziert. Jetzt weisen wir ihm des entsprechenden Zustand zu                         MxSmb[m].status=2;                         MxSmb[m].magic=magic;                         MxSmb[m].pl=0;                                                  // Anpassen der Ticketnummern in der korrekten Reihenfolge. Damit ist das Dreieck wieder im Einsatz..                         if (MxSmb[m].smb1.name==smb1) MxSmb[m].smb1.tkt=tkt1;                         if (MxSmb[m].smb1.name==smb2) MxSmb[m].smb1.tkt=tkt2;                         if (MxSmb[m].smb1.name==smb3) MxSmb[m].smb1.tkt=tkt3;                                if (MxSmb[m].smb2.name==smb1) MxSmb[m].smb2.tkt=tkt1;                         if (MxSmb[m].smb2.name==smb2) MxSmb[m].smb2.tkt=tkt2;                         if (MxSmb[m].smb2.name==smb3) MxSmb[m].smb2.tkt=tkt3;                                  if (MxSmb[m].smb3.name==smb1) MxSmb[m].smb3.tkt=tkt1;                         if (MxSmb[m].smb3.name==smb2) MxSmb[m].smb3.tkt=tkt2;                         if (MxSmb[m].smb3.name==smb3) MxSmb[m].smb3.tkt=tkt3;                                                    count++;                                                 break;                        }//for m                                 }//for k                              }//for j                     }//for i                  break;          default:          break;       }              if (count>0) Print("Restore "+(string)count+" triangles");                }

Wie vorher ist auch diese Funktion in einer eigenen Datei: fnRestart.mqh

Die letzten Schritte:

      ctrade.SetDeviationInPoints(DEVIATION);
      ctrade.SetTypeFilling(ORDER_FILLING_FOK);
      ctrade.SetAsyncMode(true);
      ctrade.LogLevel(LOG_LEVEL_NO);
      
      EventSetTimer(1);

Achten Sie auf die asynchrone Arbeitsweise beim Versenden von Aufträgen. Die Strategie geht von maximalen operativen Aktionen aus, so dass wir diese Art der Platzierung nutzen. Es gibt auch Komplikationen: Wir benötigen zusätzlichen Code, um zu kontrollieren, ob die Position erfolgreich eröffnet wurde. Wenden wir uns dem etwas weiter unten zu.

Die Funktion OnInit() ist somit besprochen. Kommen wir jetzt zum Eigentlichen des Roboters.

OnTick

Zuerst wollen wir sehen, ob wir eine Beschränkung der maximal zulässigen Anzahl offener Dreiecke in den Einstellungen haben. Wenn eine solche Beschränkung existiert und wir sie erreicht haben, dann kann ein bedeutender Teil des Codes bei einem neuen Tick übersprungen werden:

      ushort OpenThree=0;                          // Anzahl der offenen Dreiecke
      for(int j=ArraySize(MxThree)-1;j>=0;j--)
      if (MxThree[j].status!=0) OpenThree++;       // Nicht Geschlossenen werden auch berücksichtigt         

Die Prüfung ist einfach. Wir deklarierten eine lokale Variable, um offene Dreiecke zu zählen und durchlaufen das Array in einer Schleife. Wenn der Dreieckstatus ungleich 0 ist, dann ist es aktiv. 

Nachdem die offenen Dreiecke berechnet wurden (und wenn die Beschränkung vorliegt), werden die verbleibenden Dreiecke angeschaut und ihr Status kontrolliert. Die Funktion fnCalcDelta() ist dafür verantwortlich:

      if (inMaxThree==0 || (inMaxThree>0 && inMaxThree>OpenThree))
         fnCalcDelta(MxThree,inProfit,inCmnt,inMagic,inLot,inMaxThree,OpenThree);   // Berechnen wir die Diskrepanz und öffnen sofort

Analysieren wir den Code im Detail:

void fnCalcDelta(stThree &MxSmb[],double prft, string cmnt, ulong magic,double lot, ushort lcMaxThree, ushort &lcOpenThree)
   {     
      double   temp=0;
      string   cmnt_pos="";
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for i
         // Ist das Dreieck aktive, überspringen
         if(MxSmb[i].status!=0) continue; 
         
         // Erneutes Prüfen der Verfügbarkeit von allen drei Paaren: Falls auch nur eines nicht verfügbar ist,
         // gibt es keine Möglichkeit, das ganze Dreieck zu berechnen
         if (!fnSmbCheck(MxSmb[i].smb1.name)) continue;  
         if (!fnSmbCheck(MxSmb[i].smb2.name)) continue;  //Eine Position des Dreiecks wurde geschlossen
         if (!fnSmbCheck(MxSmb[i].smb3.name)) continue;     
         
         // Ermitteln der Anzahl der offenen Dreiecke zu Beginn eines neuen Ticks,
         // aber sie können auch innerhalb einen Ticks eröffnet werden. Daher muss ihre Anzahl ständig überprüft werden
         if (lcMaxThree>0) {if (lcMaxThree>lcOpenThree); else continue;}     

         
         // Danach abfragen aller benötigten Daten für die Berechnung. 
         
         // Abfragen der TickValues für jedes Paar.
         if(!SymbolInfoDouble(MxSmb[i].smb1.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb1.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb2.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb2.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb3.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb3.tv)) continue;
         
         // Abfragen des aktuellen Preises.
         if(!SymbolInfoTick(MxSmb[i].smb1.name,MxSmb[i].smb1.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb2.name,MxSmb[i].smb2.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb3.name,MxSmb[i].smb3.tick)) continue;
         
         // Prüfen ob Ask und Bid 0 sind.
         if(MxSmb[i].smb1.tick.ask<=0 || MxSmb[i].smb1.tick.bid<=0 || MxSmb[i].smb2.tick.ask<=0 || MxSmb[i].smb2.tick.bid<=0 || MxSmb[i].smb3.tick.ask<=0 || MxSmb[i].smb3.tick.bid<=0) continue;
         
         // Berechnen des Volumens des dritten Paares. Wir kennen die Volumina der ersten beiden Paare - sie ist gleich und unveränderlich.
         // Das Volumen des dritten Paares ändert sich immer. Sie wird jedoch nur dann berechnet, wenn die Lotgröße der Variablen anfänglich ungleich 0 ist.
         // Falls die Lotgröße Null ist, wird das Mindestvolumen verwendet.
         // Die Logik der Volumenberechnung ist einfach. Kehren wir zu unserem Dreieck zurück: EURUSD=EURGBP*GBPUSD. Anzahl der gekauften oder verkauften GBP
         // hängt direkt von der EURGBP-Notierung ab, während im dritten Paar diese dritte Währung an erster Stelle steht. Wir können so einige Berechnungen vermeiden,
         // indem man den Preis des zweiten Paares als Volumen verwendet. Ich habe den Durchschnitt zwischen Ask und Bid genommen.
         // Vergessen Sie nicht die Korrektur des Input-Handelsvolumens.
         
         if (lot>0)
         MxSmb[i].smb3.lot=NormalizeDouble((MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.tick.bid)/2*MxSmb[i].smb1.lot,MxSmb[i].smb3.digits_lot);
         
         // Falls das Volumen die erlaubten Grenzen überschreitet, wird der Nutzer informiert.
         // Das Dreieck wird als funktionsunfähig markiert.
         if (MxSmb[i].smb3.lot<MxSmb[i].smb3.lot_min || MxSmb[i].smb3.lot>MxSmb[i].smb3.lot_max)
         {
            Alert("The calculated lot for ",MxSmb[i].smb3.name," is out of range. Min/Max/Calc: ",
            DoubleToString(MxSmb[i].smb3.lot_min,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot_max,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot)); 
            Alert("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - DISABLED");
            MxSmb[i].smb1.name="";   
            continue;  
         }
         
         // Aufsummieren der Kosten, d.h. Spread + Kommission. pr = spread als ganze Zahl.
         // Der Spread hindert uns daran, mit dieser Strategie Geld zu verdienen, daher sollte er immer berücksichtigt werden. 
         // Statt einer Preisdifferenz, multipliziert mit dem Kehrwert von point, können wir den Spread in points verwenden.

         
         MxSmb[i].smb1.sppoint=NormalizeDouble(MxSmb[i].smb1.tick.ask-MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits)*MxSmb[i].smb1.Rpoint;
         MxSmb[i].smb2.sppoint=NormalizeDouble(MxSmb[i].smb2.tick.ask-MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits)*MxSmb[i].smb2.Rpoint;
         MxSmb[i].smb3.sppoint=NormalizeDouble(MxSmb[i].smb3.tick.ask-MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits)*MxSmb[i].smb3.Rpoint;
         if (MxSmb[i].smb1.sppoint<=0 || MxSmb[i].smb2.sppoint<=0 || MxSmb[i].smb3.sppoint<=0) continue;
         
         // Nun, lassen Sie uns den Spread in der Depotwährung berechnen. 
         // In der Währung ist der Preis von 1 Tick immer gleich SYMBOL_TRADE_TICK_VALUE.
         // Aber, das Volumen darf nicht vergessen werden
         MxSmb[i].smb1.spcost=MxSmb[i].smb1.sppoint*MxSmb[i].smb1.tv*MxSmb[i].smb1.lot;
         MxSmb[i].smb2.spcost=MxSmb[i].smb2.sppoint*MxSmb[i].smb2.tv*MxSmb[i].smb2.lot;
         MxSmb[i].smb3.spcost=MxSmb[i].smb3.sppoint*MxSmb[i].smb3.tv*MxSmb[i].smb3.lot;
         
         // Damit sind das unsere Kosten eines speziellen Handelsvolumen inklusive der vom Nutzer angegebenen Kommission
         MxSmb[i].spread=MxSmb[i].smb1.spcost+MxSmb[i].smb2.spcost+MxSmb[i].smb3.spcost+prft;
         
         // Wir können jetzt auf die Situation warten, in denen im Portfolio Ask < Bid auftritt, aber solche Fälle sind selten 
         // und kann einzeln beobachtet werden. Inzwischen ist die zeitlich verteilte Arbitrage auch in der Lage, eine solche Situation zu bewältigen.
         // Die Position ist aus diesem Grund risikofrei: Nehmen wir an, wir haben EURUSD gekauft,
         // und sofort wieder verkaufte über EURGBP und GBPUSD. 
         // Mit anderen Worten, wir sahen, dass Ask EURUSD < Bid EURGBP * Bid GBPUSD. Solche Fälle sind zahlreich, aber das reicht für eine erfolgreichen Eröffnung nicht aus.
         // Berechnen der Kosten durch den Spread. Anstatt mechanisch in den Markt einzutreten, wenn Ask < Bid ist, sollten wir warten, bis der Unterschied zwischen
         // ihnen die Kosten des Spreads übertrifft.          
         
         // Wir sind uns einig, Kaufen bedeutet, das erste Symbol zu kaufen und die beiden verbleibenden Symbole zu verkaufen,
         // verkaufen bedeutet, das erste Paar zu verkaufen und die beiden verbleibenden zu kaufen.
         
         temp=MxSmb[i].smb1.tv*MxSmb[i].smb1.Rpoint*MxSmb[i].smb1.lot;
         
         // Schauen wir uns die Gleichung für die Berechnung genauer an. 
         // 1. 1. In den Klammern wird jeder Preis um den Schlupf in die verlustbringende Richtung korrigiert: MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev
         // 2. Wie in der obigen Gleichung dargestellt, Bid EURGBP * Bid GBPUSD - multipliziert die Preise des zweiten und dritten Symbols:
         //    (MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)
         // 3. Dann wird die Differenz zwischen Ask und Bid berechnet
         // 4. Wir erhalten eine Differenz von points, die jetzt in Geldwerte umgewandelt werden sollten: Durch die Multiplikation 
         // des TickValues mit dem Handelsvolumen. Wir nehmen dazu die Werte des ersten Paares. 
         // Wenn wir ein Dreieck bilden würden, indem wir alle Paare auf ein Seite haben würden, um das dann mit 1 zu vergleichen, hätten wir mehr zu berechnen. 

         MxSmb[i].PLBuy=((MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)-(MxSmb[i].smb1.tick.ask+MxSmb[i].smb1.dev))*temp;
         MxSmb[i].PLSell=((MxSmb[i].smb1.tick.bid-MxSmb[i].smb1.dev)-(MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.ask+MxSmb[i].smb3.dev))*temp;
         
         // Wir haben die Berechnung für den Betrag erhalten, der verdient oder verloren werden kann, wenn wir das Dreieck kaufen oder verkaufen. 
         // Jetzt müssen wir nur noch die Kosten beurteilen, um zu entscheiden, ob das Dreieck eröffnet werden soll. Normalisieren wir alles auf 2 Dezimalstellen. 
         MxSmb[i].PLBuy=   NormalizeDouble(MxSmb[i].PLBuy,2);
         MxSmb[i].PLSell=  NormalizeDouble(MxSmb[i].PLSell,2);
         MxSmb[i].spread=  NormalizeDouble(MxSmb[i].spread,2);                  
         
         // Gibt es einen möglichen Gewinn, wird auf ausreichende Geldmittel für die Eröffnung geprüft.         
         if (MxSmb[i].PLBuy>MxSmb[i].spread || MxSmb[i].PLSell>MxSmb[i].spread)
         {
            // Wir haben einfach nur die gesamte Marge für den Kauf berechnet. Da sie immer noch höher ist als die für den Verkauf, müssen wir die Handelsrichtung nicht berücksichtigen.  
            // Achten Sie auch auf den Erhöhungsfaktor. Wir können kein Dreieck öffnen, wenn die Marge praktisch nicht ausreicht. Der standardmäßige Erhöhungsfaktor beträgt 20%.

            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb1.name,MxSmb[i].smb1.lot,MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb2.name,MxSmb[i].smb2.lot,MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb3.name,MxSmb[i].smb3.lot,MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.mrg))
            if(AccountInfoDouble(ACCOUNT_MARGIN_FREE)>((MxSmb[i].smb1.mrg+MxSmb[i].smb2.mrg+MxSmb[i].smb3.mrg)*CF))  //check the free margin
            {
               // Wir sind schon fast bereit für die Eröffnung. Nun brauchen wir nur noch eine freie Magicnummer aus unserem Bereich. 
               // Die initiale Magicnummer ist in der Variablen inMagic angegeben. Der Standardwert ist 300. 
               // Der Bereich der Magicnummern wird in der Definition MAGIC angegeben, der Standardwert ist 200.
               MxSmb[i].magic=fnMagicGet(MxSmb,magic);   
               if (MxSmb[i].magic<=0)
               { // Falls 0 sind alle Magicnummer belegt. Informieren darüber mit einer Nachricht und beenden.
                  Print("Free magic ended\nNew triangles will not open");
                  break;
               }  
               
               // Übernehmen der erhaltenen Magicnummer
               ctrade.SetExpertMagicNumber(MxSmb[i].magic); 
               
               // Schreiben des Kommentars über das Dreieck
               cmnt_pos=cmnt+(string)MxSmb[i].magic+" Open";               
               
               // Öffnen, während der Zeitpunkt der Eröffnung gesichert wird. 
               // Das ist notwendig, um jede Verzögerung zu vermeiden. 
               // Standardmäßig ist die Wartezeit für das vollständige Eröffnen in MAXTIMEWAIT definiert mit 3 Sekunden.
               // Wenn es nicht in dieser Zeitspanne komplett eröffnet wurde, wird alles wieder geschlossen.
               
               MxSmb[i].timeopen=TimeCurrent();
               
               if (MxSmb[i].PLBuy>MxSmb[i].spread)    fnOpen(MxSmb,i,cmnt_pos,true,lcOpenThree);
               if (MxSmb[i].PLSell>MxSmb[i].spread)   fnOpen(MxSmb,i,cmnt_pos,false,lcOpenThree);               
               
               // Ausdruck der Nachricht, dass das Dreieck eröffnet wurde. 
               if (MxSmb[i].status==1) Print("Open triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic);
            }
         }         
      }//for i
   }

Die Funktion wird von ausführlichen Kommentaren und Erklärungen begleitet, um alles zu erklären. Zwei Dinge wurden jedoch noch nicht beleuchtet: Der eingesetzte Mechanismus zur Auswahl der Magicnummern und die Öffnung des Dreiecks.

Unten sieht man, wie die verfügbaren Magicnummern ausgewählt werden:

ulong fnMagicGet(stThree &MxSmb[],ulong magic)
   {
      int mxsize=ArraySize(MxSmb);
      bool find;
      
      // Wir könnten durch alle offenen Dreiecke in unserem Array gehen. 
      // Aber es wurde eine andere Option gewählt - durchlaufen wir den Bereich der Magicnummern,
      // und dann durch das Array der offenen Dreiecke.  
      for(ulong i=magic;i<magic+MAGIC;i++)
      {
         find=false;
         
         // Magicnummer i. Prüfen, ob sie bereits von einem offenen Dreieck verwendet wird.
         for(int j=0;j<mxsize;j++)
         if (MxSmb[j].status>0 && MxSmb[j].magic==i)
         {
            find=true;
            break;   
         }   
         
         // Falls keine Magicnummer verwendet wird, wird die Schleife vorzeitig beendet.    
         if (!find) return(i);            
      }  
      return(0);  
   }

Und so wird jetzt ein Dreieck eröffnet:

bool fnOpen(stThree &MxSmb[],int i,string cmnt,bool side, ushort &opt)
   {
      // Des ersten Auftrags "openflag". 
      bool openflag=false;
      
      // Keine Position ohne Erlaubnis. 
      if (!cterm.IsTradeAllowed())  return(false);
      if (!cterm.IsConnected())     return(false);
      
      switch(side)
      {
         case  true:
         
         // Wenn nach dem Senden eines Handelsauftrages 'false' zurückgegeben wird, ist es sinnlos, die zwei verbleibende Paare Eröffnen zu wollen. 
         // Versuchen wir es erneut bei nächsten Tick Aber der Roboter eröffnet das Dreieck ja nicht stückweise. 
         // Wenn Teile nach Auftragserteilung nicht eröffnet, warte
         // noch die in MAXTIMEWAIT definierte Zeitspanne und dann schließe die offenen Teile des Dreiecks. 
         if(ctrade.Buy(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;
            opt++;
            // Die weitere Logik bleibt gleich: Ist eine Eröffnung unmöglich, wird das Dreieck geschlossen. 
            if(ctrade.Sell(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Sell(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);               
         }            
         break;
         case  false:
         
         if(ctrade.Sell(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;  
            opt++;        
            if(ctrade.Buy(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Buy(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);         
         }           
         break;
      }      
      return(openflag);
   }

Wie üblich befinden sich die oben genannten Funktionen in den separaten Dateien fnCalcDelta.mqh, fnMagicGet.mqh und fnOpen.mqh.

Wir haben also ein entsprechendes Dreieck gefunden und zur Eröffnung geschickt. Sowohl in MetaTrader 4 als auch in MetaTrader 5 mit Hedging-Konten bedeutet dies eigentlich das Ende der Arbeit des EA. Aber wir müssen immer noch das Ergebnis der Öffnung des Dreiecks kontrollieren. Die Ereignisse OnTrade und OnTradeTransaction werden nicht dafür verwendet, da sie keinen Erfolg garantieren. Stattdessen wird die Anzahl der aktuellen Positionen überprüft - eine 100% Indiz.

Werfen wir einen Blick auf die Management-Funktion für die Positionseröffnung:

void fnOpenCheck(stThree &MxSmb[], int accounttype, int fh)
   {
      uchar cnt=0;       // Positionszähler des Dreiecks
      ulong   tkt=0;     // Aktuelle Ticketnummer
      string smb="";     // Aktuelles Symbol
      
      // Prüfen des Arrays mit den Dreiecken
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Betrachten der Dreiecke mit den Status 1, d.h. Eröffnung abgeschickt
         if(MxSmb[i].status!=1) continue;
                          
         if ((TimeCurrent()-MxSmb[i].timeopen)>MAXTIMEWAIT)
         {     
            // Falls die Zeitspanne für das Eröffnen überschritten wurde, wird das Dreieck als zu schließen gekennzeichnet         
            MxSmb[i].status=3;
            Print("Not correct open: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
            continue;
         }
         
         cnt=0;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            
            // Prüfen aller offenen Positionen. Prüfen von jedem Dreieck. 

            for(int j=PositionsTotal()-1;j>=0;j--)
            if (PositionSelectByTicket(PositionGetTicket(j)))
            if (PositionGetInteger(POSITION_MAGIC)==MxSmb[i].magic)
            {
               // Abfrage von  Symbol und Ticketnummer der betrachten Position. 

               tkt=PositionGetInteger(POSITION_TICKET);                smb=PositionGetString(POSITION_SYMBOL);                               // Prüfen, ob die offene Position unter denen des betrachteten Dreiecks.                // Falls ja, erhöhe den Zähler und sichere die Ticketnummer und den Eröffnungspreis.                if (smb==MxSmb[i].smb1.name){ cnt++;   MxSmb[i].smb1.tkt=tkt;  MxSmb[i].smb1.price=PositionGetDouble(POSITION_PRICE_OPEN);} else                if (smb==MxSmb[i].smb2.name){ cnt++;   MxSmb[i].smb2.tkt=tkt;  MxSmb[i].smb2.price=PositionGetDouble(POSITION_PRICE_OPEN);} else                if (smb==MxSmb[i].smb3.name){ cnt++;   MxSmb[i].smb3.tkt=tkt;  MxSmb[i].smb3.price=PositionGetDouble(POSITION_PRICE_OPEN);}                               // Wenn es die drei entsprechenden Positionen gibt, wurde das Dreieck erfolgreich eröffnet. Ändern des Status auf 2 (Eröffnet).                // Schreibe die Daten der Eröffnung in die Logdatei                if (cnt==3)                {                   MxSmb[i].status=2;                   fnControlFile(MxSmb,i,fh);                   break;                  }             }             break;             default:             break;          }       }    }

Die Funktion etwas in eine Logdatei zu schreiben, ist einfach:

void fnControlFile(stThree &MxSmb[],int i, int fh)
   {
      FileWrite(fh,"============");
      FileWrite(fh,"Open:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
      FileWrite(fh,"Tiket:",MxSmb[i].smb1.tkt,MxSmb[i].smb2.tkt,MxSmb[i].smb3.tkt);
      FileWrite(fh,"Lot",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
      FileWrite(fh,"Margin",DoubleToString(MxSmb[i].smb1.mrg,2),DoubleToString(MxSmb[i].smb2.mrg,2),DoubleToString(MxSmb[i].smb3.mrg,2));
      FileWrite(fh,"Ask",DoubleToString(MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.digits));
      FileWrite(fh,"Bid",DoubleToString(MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits));               
      FileWrite(fh,"Price open",DoubleToString(MxSmb[i].smb1.price,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.price,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.price,MxSmb[i].smb3.digits));
      FileWrite(fh,"Tick value",DoubleToString(MxSmb[i].smb1.tv,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tv,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tv,MxSmb[i].smb3.digits));
      FileWrite(fh,"Spread point",DoubleToString(MxSmb[i].smb1.sppoint,0),DoubleToString(MxSmb[i].smb2.sppoint,0),DoubleToString(MxSmb[i].smb3.sppoint,0));
      FileWrite(fh,"Spread $",DoubleToString(MxSmb[i].smb1.spcost,3),DoubleToString(MxSmb[i].smb2.spcost,3),DoubleToString(MxSmb[i].smb3.spcost,3));
      FileWrite(fh,"Spread all",DoubleToString(MxSmb[i].spread,3));
      FileWrite(fh,"PL Buy",DoubleToString(MxSmb[i].PLBuy,3));
      FileWrite(fh,"PL Sell",DoubleToString(MxSmb[i].PLSell,3));      
      FileWrite(fh,"Magic",string(MxSmb[i].magic));
      FileWrite(fh,"Time open",TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_SECONDS));
      FileWrite(fh,"Time current",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
      
      FileFlush(fh);       
   }

Wir haben also ein Dreieck gefunden und die entsprechenden Positionen eröffnet. Jetzt müssen wir ermitteln, wie viel wir damit verdient haben.

void fnCalcPL(stThree &MxSmb[], int accounttype, double prft)
   {
      // Gehen wir noch einmal durch das Array aller Dreiecke. 
      // Die Geschwindigkeit des Öffnens und Schließens sind äußerst wichtige Bestandteile dieser Strategie. 
      // Sobald wir also ein Dreieck zum Schließen gefunden haben, schließen wir es sofort.
      
      bool flag=cterm.IsTradeAllowed() & cterm.IsConnected();      
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for
         // Wir interessieren uns nur für Dreiecke mit dem Status 2 oder 3.
         // Status 3 (Schließen des Dreiecks) kann durch ein nur teilweise eröffnetes Dreieck entstehen
         if(MxSmb[i].status==2 || MxSmb[i].status==3); else continue;                             
         
         // Rechnen wir, wie viel das Dreieck verdient hat 
         if (MxSmb[i].status==2)
         {
            MxSmb[i].pl=0;         // Reset the profit
            switch(accounttype)
            {//switch
               case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:  
                
               if (PositionSelectByTicket(MxSmb[i].smb1.tkt)) MxSmb[i].pl=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb2.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb3.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);                           
               break;
               default:
               break;
            }//switch
            
            // Runden auf zwei Dezimalstellen
            MxSmb[i].pl=NormalizeDouble(MxSmb[i].pl,2);
            
            // Betrachten das Schließen noch genauer. Folgende Logik wird verwendet:
            // Die Situationen mit Arbitrage sind nicht normal und sollten nicht auftreten. Tritt sie auf, können wir annehmen, 
            // dass sie bald wieder verschwindet. Können wir damit Geld verdienen? Anders gesagt, wir wissen nicht, 
            // ob wir damit einen Gewinn erzielen können. Daher könnte es angeraten sein, die Position sofort zu schließen, nachdem die Spreads und die Kommission gedeckt sind. 
            // Die Arbitrage eines Dreiecks wird in points ermittelt. Man kann hier keine große Bewegungen erwarten. 
            // Obwohl Sie auf einen gewünschten Gewinn in der Variablen der Kommission von der Eingabe warten können. 
            // Wenn wir mehr erhalten können, als wir ausgegeben haben, sollte die Position den Status "im Prozess des Schließens" erhalten.

            if (flag && MxSmb[i].pl>prft) MxSmb[i].status=3;                    
         }
         
         // Schließen des Dreiecks nur, wenn der Handel erlaubt ist.
         if (flag && MxSmb[i].status==3) fnCloseThree(MxSmb,accounttype,i); 
      }//for         
   }

Eine einfache Funktion schließt ein Dreieck:

void fnCloseThree(stThree &MxSmb[], int accounttype, int i)
   {
      // Vor dem Schließen muss die Verfügbarkeit aller drei Paare geprüft werden. 
      // Es wäre falsch und extrem gefährlich ein Dreieck zu zerreißen. Wenn es ein netting-Konto gibt,
      // könnte das später zu einen Durcheinander der Positionen führen. 
      
      if(fnSmbCheck(MxSmb[i].smb1.name))
      if(fnSmbCheck(MxSmb[i].smb2.name))
      if(fnSmbCheck(MxSmb[i].smb3.name))          
      
      // Liegt alles bereit, werden alle drei Positionen mit der Standardbibliothek geschlossen. 
      // Danach wird der Erfolg des Schließens geprüft. 
      switch(accounttype)
      {
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:     
         
         ctrade.PositionClose(MxSmb[i].smb1.tkt);
         ctrade.PositionClose(MxSmb[i].smb2.tkt);
         ctrade.PositionClose(MxSmb[i].smb3.tkt);              
         break;
         default:
         break;
      }       
   }  

Die Arbeit ist fast abgeschlossen. Jetzt müssen wir nur noch prüfen, ob der Abschluss erfolgreich war und eine Meldung auf dem Bildschirm ausgeben. Wenn der Roboter nichts schreibt, scheint es, dass er nicht funktioniert.

Unten ist die Prüfung des Schließens. Wir könnten eine einzige Funktion für die Öffnung und Schließen durch eine einfache Änderung der Handelsrichtung einführen, aber diese Option erscheint nicht opportun, da es zwischen diesen Beiden geringfügige, prozedurale Unterschiede gibt. 

Prüfen ob das Schließen erfolgreich war: 

void fnCloseCheck(stThree &MxSmb[], int accounttype,int fh)
   {
      // Durchgehen des Arrays der Dreiecke. 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Es interessieren nur die mit dem Status 3, d.h. bereits geschlossen oder zum Schließen bestimmt. 
         if(MxSmb[i].status!=3) continue;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: 
            
            // Wenn kein einziges Paar des Dreiecks gefunden werden konnte, war das Schließen erfolgreich. Rückgabe des Status 0
            if (!PositionSelectByTicket(MxSmb[i].smb1.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb2.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb3.tkt))
            {  // Bedeutet ds Schließen war erfolgreich
               MxSmb[i].status=0;   
               
               Print("Close triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic+"  P/L: "+DoubleToString(MxSmb[i].pl,2));
               
               // Schreiben der Daten vom Schließen in die Logdatei. 
               if (fh!=INVALID_HANDLE)
               {
                  FileWrite(fh,"============");
                  FileWrite(fh,"Close:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
                  FileWrite(fh,"Lot",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
                  FileWrite(fh,"Tiket",string(MxSmb[i].smb1.tkt),string(MxSmb[i].smb2.tkt),string(MxSmb[i].smb3.tkt));
                  FileWrite(fh,"Magic",string(MxSmb[i].magic));
                  FileWrite(fh,"Profit",DoubleToString(MxSmb[i].pl,3));
                  FileWrite(fh,"Time current",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
                  FileFlush(fh);               
               }                   
            }                  
            break;
         }            
      }      
   }

Zum Abschluss zeigen wir noch einen Kommentar auf dem Bildschirm zur visuellen Bestätigung. Es soll Folgendes ausgegeben werden:

  1. Die Zahl der beobachteten Dreiecke und
  2. Die Zahl der offenen Dreiecke
  3. Die fünf Dreiecke, die als nächstes eröffnet werden könnten
  4. Gegebenenfalls die offenen Dreiecke selbst

Unten ist der Code der Funktion:

void fnCmnt(stThree &MxSmb[], ushort lcOpenThree)
   {     
      int total=ArraySize(MxSmb);
      
      string line="=============================\n";
      string txt=line+MQLInfoString(MQL_PROGRAM_NAME)+": ON\n";
      txt=txt+"Total triangles: "+(string)total+"\n";
      txt=txt+"Open triangles: "+(string)lcOpenThree+"\n"+line;
      
      // Maximum der auf dem Schirm dargestellten Dreiecke
      short max=5;
      max=(short)MathMin(total,max);
      
      // Anzeige der Fünf, die als nächstes eröffnet werden könnten 
      short index[];                    // Index arrays
      ArrayResize(index,max);
      ArrayInitialize(index,-1);        // Not used
      short cnt=0,num=0;
      while(cnt<max && num<total)       // First max closed triangle indices are taken for the start
      {
         if(MxSmb[num].status!=0)  {num++;continue;}
         index[cnt]=num;
         num++;cnt++;         
      }
      
      // Es ist sinnvoll, nur dann zu sortieren und zu suchen, wenn die Anzahl der Elemente größer ist als die Anzahl der Elemente, die auf dem Bildschirm angezeigt werden können. 
      if (total>max) 
      for(short i=max;i<total;i++)
      {
         // Darstellen der offenen Dreiecke folgt unten.
         if(MxSmb[i].status!=0) continue;
         
         for(short j=0;j<max;j++)
         {
            if (MxSmb[i].PLBuy>MxSmb[index[j]].PLBuy)  {index[j]=i;break;}
            if (MxSmb[i].PLSell>MxSmb[index[j]].PLSell)  {index[j]=i;break;}
         }   
      }
      
      // Anzeige der Dreiecke, die als nächstes eröffnet werden könnten.
      bool flag=true;
      for(short i=0;i<max;i++)
      {
         cnt=index[i];
         if (cnt<0) continue;
         if (flag)
         {
            txt=txt+"Smb1           Smb2           Smb3         P/L Buy        P/L Sell        Spread\n";
            flag=false;
         }         
         txt=txt+MxSmb[cnt].smb1.name+" + "+MxSmb[cnt].smb2.name+" + "+MxSmb[cnt].smb3.name+":";
         txt=txt+"      "+DoubleToString(MxSmb[cnt].PLBuy,2)+"          "+DoubleToString(MxSmb[cnt].PLSell,2)+"            "+DoubleToString(MxSmb[cnt].spread,2)+"\n";      
      }            
      
      // Anzeige der offenen Dreiecke. 
      txt=txt+line+"\n";
      for(int i=total-1;i>=0;i--)
      if (MxSmb[i].status==2)
      {
         txt=txt+MxSmb[i].smb1.name+"+"+MxSmb[i].smb2.name+"+"+MxSmb[i].smb3.name+" P/L: "+DoubleToString(MxSmb[i].pl,2);
         txt=txt+"  Time open: "+TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
         txt=txt+"\n";
      }   
      Comment(txt);
   }

Testen


Es ist möglich, den Test im Modus der simulierten Ticks durchzuführen und mit dem Test mit echten Ticks zu vergleichen. Wir können sogar noch weiter gehen, indem wir die Testergebnisse an echten Ticks mit realem Handel vergleichen und feststellen, dass der Multi-Tester noch weit von der Realität entfernt ist. 

Die Ergebnisse zeigen, dass man durchschnittlich 3-4 Dreiecke pro Woche erwarten kann. Meistens wird eine Position nachts eröffnet, und das Dreieck besteht meist aus wenig liquiden Währungen wie TRY, NOK, SEK etc. Der Gewinn des Roboters hängt von einem gehandelten Volumen ab. Da die Dreiecke selten sind, kann der EA problemlos große Volumina handhaben, die parallel zu anderen Robotern arbeiten.

Das Risiko des Roboters ist einfach zu berechnen: 3 Spreads * Anzahl der offenen Dreiecke.

Um die Währungspaare vorzubereiten, mit denen wir arbeiten können, empfehle ich, zuerst alle Symbole anzuzeigen und diejenigen mit deaktiviertem Handel und diejenigen, die keine Währungspaare sind, auszublenden. Das geht schneller mit dem Skript, das für Fans von Mehrwährungsstrategien unentbehrlich ist: https://www.mql5.com/en/market/product/25256

Ich möchte Sie auch daran erinnern, dass die Historie im Tester nicht vom Server des Brokers geladen wird - sie sollte im Voraus in das Client-Terminal geladen werden. Daher sollte dies entweder eigenständig vor dem Testen oder durch erneute Verwendung des obigen Skripts geschehen.

Entwicklungsperspektiven

Können wir die Ergebnisse verbessern? Ja, natürlich. Dazu müssen wir unseren Liquiditätsaggregator einsetzen. Der Nachteil dieses Ansatzes ist die Notwendigkeit, Konten bei mehreren Brokern zu eröffnen.

Wir können auch die Testergebnisse beschleunigen. Dies kann auf zwei Arten geschehen, die miteinander kombiniert werden können. Der erste Schritt besteht darin, eine separate Berechnung einzuführen, die nur die Dreiecke kontinuierlich verfolgt, bei denen die Eintrittswahrscheinlichkeit sehr hoch ist. Die zweite Möglichkeit ist die Verwendung von OpenCL, was für diesen Roboter sehr sinnvoll ist.

Im Artikel verwendete Dateien

# Dateiname Beschreibung
1 var.mqh Beschreibung aller verwendeten Variablen, den Definitionen und Eingabevariablen.
2 fnWarning.mqh Überprüfung der Anfangsbedingungen für den korrekten Betrieb des EA: Dateneingaben, Rahmenbedingungen, Einstellungen.
3 fnSetThree.mqh Bildung der Dreiecke aus Währungspaaren. Die Quelle der Paare kann auch hier ausgewählt werden - Market Watch oder aus einer vorbereiteten Datei.
4 fnSmbCheck.mqh Die Funktion, ein Symbol auf Verfügbarkeit und andere Einschränkungen zu prüfen. NB: Handels- und Kurszeiten werden im Roboter nicht überprüft.
5 fnChangeThree.mqh Ändern der Position von Währungspaaren im Dreieck, um sie zu vereinheitlichen.
6 fnSmbLoad.mqh Abfrage verschiedener Daten zu Symbolen, Preisen, Punkten, Volumenbeschränkungen etc.
7 fnCalcDelta.mqh Berücksichtigung aller Trennungen im Dreieck, sowie aller Nebenkosten: Spread, Provisionen, Slippage.
8 fnMagicGet.mqh Suche nach der Magicnummer, die für das aktuelle Dreieck verwendet werden kann.
fnOpenCheck.mqh Überprüfen, ob das Dreieck erfolgreich geöffnet wurde.
10 fnCalcPL.mqh  Berechnen von Gewinn/Verlust des Dreiecks.
11  fnCreateFileSymbols.mqh Die Funktion, die die Datei mit Dreiecken für den Handel erstellt. Die Datei enthält auch zusätzliche Daten (für weitere Informationen).
12  fnControlFile.mqh Die Funktion, die eine Logdatei führt. Sie enthält alle Öffnungs- und Schließvorgänge mit den notwendigen Daten. 
13  fnCloseThree.mqh Schließen eines Dreiecks.
14  fnCloseCheck.mqh Prüfe, ob das Dreieck vollständig geschlossen wurde.
15  fnCmnt.mqh Anzeige der Kommentare auf dem Schirm.
16  fnRestart.mqh Prüfen, ob bereits offene Dreiecke bestehen, wenn der Roboter gestartet wird. Wenn ja, werden sie beobachtet. 
17  fnOpen.mqh Öffnen einen Dreiecks.
18 Support.mqh Weitere Hilfsklassen. Es gibt nur eine Funktion — zählen der Dezimalstellen für eine Bruchzahl.
19 head.mqh Die Kopfzeilen in allen oben erwähnten Dateien.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/3150

Beigefügte Dateien |
MQL5.zip (231.93 KB)
Testen der Muster, die beim Handel mit Körben von Währungspaaren auftreten. Teil II Testen der Muster, die beim Handel mit Körben von Währungspaaren auftreten. Teil II
Wir testen die Muster und prüfen die Methoden weiter, die in den Artikeln über den Handel mit Körben von Währugnspaaren beschrieben wurden. Betrachten wir in der Praxis, ob man die Muster verwenden kann, bei welchen die Grafik eines vereinigten WPR einen gleitenden Durchschnitt kreuzt, und wenn ja, dann wie genau.
Mini Market Emulator oder ein manueller Strategie-Tester Mini Market Emulator oder ein manueller Strategie-Tester
Der Mini Market Emulator ist ein Indikator, der für die partielle Emulation des Handels am Terminal entwickelt wurde. Vielleicht möchte jemand damit "manuell" seine Strategie einer Marktanalyse oder des Handels testen.
R-Quadrat als Gütemaß der Saldenkurve einer Strategie R-Quadrat als Gütemaß der Saldenkurve einer Strategie
Dieser Artikel beschreibt die Konstruktion des benutzerdefinierten Optimierungskriterium R². Anhand dieses Kriteriums kann die Qualität der Saldenkurve einer Strategie abgeschätzt und die bestgeeignete Strategie ausgewählt werden. Die Arbeit diskutiert die Grundsätze der Konstruktion und die statistischen Methoden, die in der Schätzung der Eigenschaften und Qualität dieser Metrik.
Vergleich verschiedener Typen gleitender Durchschnitte im Handel Vergleich verschiedener Typen gleitender Durchschnitte im Handel
Es wurden 7 Typen gleitender Durchschnitte (MA) betrachtet; es wurde eine Handelsstrategie für das Arbeiten mit ihnen entwickelt. Verschiedene gleitende Durchschnitte wurden anhand einer Handelsstrategie getestet, des Weiteren wurden diese hinsichtlich der Effektivität der Anwendung verglichen.