Entwicklung eines Symbolauswahl- und Navigationsprogramms in MQL5 und MQL4
Einführung
Erfahrene Händler sind sich der Tatsache bewusst, dass die meisten zeitaufwendigen Dinge im Handel nicht das Öffnen und Verfolgen von Positionen sind, sondern das Auswählen von Symbolen und das Suchen von Einstiegspunkten.
Natürlich sind das keine großen Probleme, wenn man nur mit 1-2 Symbolen arbeitet. Aber wenn Ihr Handelsansatz Hunderte von Aktien und Dutzenden von Forex-Symbolen umfasst, kann es mehrere Stunden dauern, nur um geeignete Einstiegspunkte zu finden.
In diesem Artikel werden wir ein EA entwickeln, das die Suche nach Aktien vereinfacht. Der EA soll den Händler auf drei Arten unterstützen:
- Er soll die Aktien filtern, um eine Liste derjenigen zu liefern, die unseren Bedingungen entsprechen.
- Er soll die Navigation durch die erstellte Liste der Aktien erleichtern.
- Er soll zusätzliche Daten anzuzeigen, die für eine Entscheidung notwendig sind.
Erste EA-Vorlage
Zunächst werden wir den EA auf MQL5 entwickeln. Da viele Broker jedoch immer noch keine MetaTrader 5-Konten anbieten, werden wir den EA neu entwickeln, so dass er auch auf dem MetaTrader 4 am Ende des Artikels läuft.
Bereiten wir also die Vorlage vor, die sich fast nicht von derjenigen unterscheidet, die mit dem MQL5-Assistenten erstellt werden kann:
//+------------------------------------------------------------------+ //| _finder.mq5 | //| Copyright 2018, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Klymenko Roman (needtome@icloud.com)" #property link "https://logmy.net" #property version "1.00" #property strict //+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten | //+------------------------------------------------------------------+ int OnInit() { //--- Timer erstellen EventSetTimer(1); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Deinitialisierungsfunktion des Experten | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ //| Timer Funktion | //+------------------------------------------------------------------+ void OnTimer() { } //+------------------------------------------------------------------+ void OnChartEvent(const int id, // Ereignis-ID const long& lparam, // Ereignisparameter vom Typ long const double& dparam, // Ereignisparameter vom Typ double const string& sparam) // Ereignisparameter vom Typ string { }
In dieser Vorlage installieren wir den Timer beim Erstellen eines EAs. Unser Timer wird jede Sekunde aktiviert. Das bedeutet, dass die Standardfunktion OnTimer einmal pro Sekunde aufgerufen werden wird.
Die einzige Zeile, die sich von einer typischen vom MQL5-Assistenten generierten Vorlage unterscheidet, ist #property strict. Diese Zeile ist notwendig, damit der EA in MetaTrader 4 korrekt funktioniert. Da sie keinen signifikanten Einfluss auf MetaTrader 5 hat, fügen wir es am Anfang unserer Vorlage hinzu.
Wir werden die folgenden Standardfunktionen verwenden:
- OnInit: Anzeige der Schaltflächen von Handelsinstrumenten, die unseren Bedingungen entsprechen, auf dem Chart;
- OnDeinit: Entfernt den Timer und alle vom EA erstellten grafischen Objekte;
- OnTimer: Der Timer soll verwendet werden, um Klicks auf grafische Objekte in dem vom EA erstellten Chart zu bestimmen;
- OnChartEvent: Reaktion auf das Anklicken von grafischen Objekten, die im Chart erstellt wurden, auf dem der EA gestartet wurde.
Die Liste der Symbole, die unseren Bedingungen entsprechen, wird im Objekt vom Typ CArrayString gespeichert. Wir binden daher die MQH-Datei mit der Objektbeschreibung in unserem EA ein:
#include <Arrays\ArrayString.mqh>
Wir werden auch das Objekt vom Typ CChart benötigen, um mit dem Chart zu arbeiten. Auch das binden wir ein:
#include <Charts\Chart.mqh>
All das steht zu Beginn unserer Vorlage, außerhalb von jeder Funktion und nach dem Block der Zeilen mit #property.
Als Nächstes müssen wir uns für die Breite und Höhe aller vom EA erstellten Buttons entscheiden. Definieren wir diese Werte in Form von Makros, die wir nach dem Block mit den Zeilen #include eintragen:
#define BTN_HEIGHT (20) #define BTN_WIDTH (100)
Eingaben
Der EA wird über die Eingabeparameter geregelt. Werfen wir einen Blick darauf, damit wir sofort erkennen können, welche Funktionen, weiter unten im Artikel, implementiert werden:
sinput string delimeter_01=""; // --- Filtereinstellungen --- input bool noSYMBmarketWath=true; // Ausblenden, wenn nicht im Market Watch input bool noSYMBwithPOS=true; // Ausblenden, wenn es Positionen gibt input ValueOfSpread hide_SPREAD=spread_b1; // Ausblenden, wenn der Spread zu hoch ist input uint hide_PRICE_HIGH=0; // Ausblenden, wenn der Preis zu hoch ist input uint hide_PRICE_LOW=0; // Ausblenden, wenn der Preis zu niedrig ist input bool hideProhibites=true; // Ausblenden, wenn der Handel unmöglich ist input bool hideClosed=true; // Ausblenden, wenn der Markt geschlossen ist input StartHour hide_HOURS=hour_any; // Anzeigen, wenn Handelszeit ist input double hideATRcents=0.00; // Ausblenden, wenn der ATR kleiner als der Dollarwert ist sinput string delimeter_02=""; // --- Charteinstellungen --- input bool addInfoWatch=false; // Chart zum Market Watch hinzufügen input bool viewCandle=true; // Öffnen eines Kerzencharts input bool viewVolumes=true; // Anzeigen des Tick-Volumens input bool showInfoSymbol=true; // Anzeigen der Bewegungsrichtung input bool showNameSymbol=true; // Anzeigen des Symbolnamens
Wir können sofort feststellen, dass zwei Eingabeparameter einen benutzerdefinierten Typ haben. Deshalb fügen wir die Definition dieser Typen vor den Eingabeparameter hinzu. Beide benutzerdefinierten Typen sind Enumerationen.
Die Enumeration ValueOfSpread definiert mögliche Bedingungen für einen Wert des Spread der Symbole, die vom EA angezeigt werden sollen:
enum ValueOfSpread { spread_no,//No spread_b05,// > 0.05% spread_b1,// > 0.1% spread_b15,// > 0.15% spread_l15,// < 0.15% spread_l1,// < 0.1% };
Ein Spread, der 0,1% des Preises übersteigt, gilt als erhöht. Daher werden wir standardmäßig nur Symbole mit einem Spread von weniger als 0,1% anzeigen. Der Wert des Parameters Hide bei einem Spread ist also > 0,1%. Wenn die Liste der von Ihrem Broker bereitgestellten Symbole jedoch zu klein ist, können Sie einen anderen Wert für diesen Parameter wählen.
Die Enumeration StartHour enthält die Liste der Hauptperioden, in denen sich einige der Märkte öffnen:
enum StartHour { hour_any, //Any time hour_9am, // 9 am hour_10am,// 10 am hour_4pm, // 4 pm hour_0am, // Midnight };
9 am (oder ein anderer Wert) bedeutet nicht, dass nur Symbole angezeigt werden sollen, die sich genau zu dem angegebenen Zeitpunkt öffnen. Stattdessen bedeutet dies, dass die Symbole, die sich innerhalb dieser Stunde öffnen (z.B. um 9:05 Uhr), angezeigt werden sollen.
Dementsprechend bedeutet 4 pm, dass nur US-Aktien angezeigt werden sollen, die um 16:30 Uhr eröffnet wurden.
10 am bezieht sich hauptsächlich auf russische und europäische Börsenmärkte.
9 am ist die Öffnungszeit einiger Indizes.
Schließlich ist midnight eine offene Zeit am Devisenmarkt, da er rund um die Uhr arbeitet.
Globale Variablen
Bevor wir mit dem Inhalt von Standardfunktionen arbeiten, müssen wir noch eine Reihe von Variablen deklarieren, die überall im EA sichtbar sein müssen. Fügen wir sie nach den Eingabeparametern hinzu:
// Präfix, das den Namen aller vom EA erstellten grafischen Objekte hinzugefügt werden soll: string exprefix="finder"; // Array der Symbole, die unsere Bedingungen erfüllen: CArrayString arrPanel1; // Index des aktuellen Symbols im Array arrPanel1: int panel1val; // Array, das die vomn EA erstellten charts speichert (es gibt in diesem Moment nur einen Chart): CChart charts[]; // Array, der die Pointer zuu den vom EA erstellten Charts speichert (es gibt in diesem Moment nur einen Pointer): long curChartID[];
Die Kommentare sollten klären, warum wir diese Variablen brauchen.
Alle Vorbereitungen sind abgeschlossen. Jetzt können wir mit der Entwicklung des EAs beginnen. Werfen wir zuerst einen Blick auf das Ergebnis:
//+------------------------------------------------------------------+ //| _finder.mq5 | //| Copyright 2018, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Klymenko Roman (needtome@icloud.com)" #property link "https://logmy.net" #property version "1.00" #property strict #include <Arrays\ArrayString.mqh> #include <Charts\Chart.mqh> #define BTN_HEIGHT (20) #define BTN_WIDTH (100) enum ValueOfSpread { spread_no,//No spread_b05,// > 0.05 % spread_b1,// > 0.1 % spread_b15,// > 0.15 % spread_l15,// < 0.15 % spread_l1,// < 0.1 % }; enum StartHour { hour_any,//Any time hour_9am,// 9 am hour_10am,// 10 am hour_4pm,// 4 pm hour_0am,// Midnight }; input bool noSYMBmarketWath=true; // Ausblenden, wenn das Symbol nicht im Panel des Market Watch ist input bool noSYMBwithPOS=true; // Symbole mit offenen Positionen ausblenden input ValueOfSpread hide_SPREAD=spread_b1; // Symbole mit zu großen Spread ausblenden input uint hide_PRICE_HIGH=0; // Symbole mit einem zu hohem Preis ausblenden (0 - nichts ausblenden) input uint hide_PRICE_LOW=0; // Symbole mit einem zu niedrigem Preis ausblenden (0 - nichts ausblenden) input bool hideProhibites=true; // nicht handelbare Symbole ausblenden input StartHour hide_HOURS=hour_any; // nur eröffnete Symbole zeigen input bool viewCandle=true; // Öffnen eines Kerzencharts // Präfix, das den Namen aller vom EA erstellten grafischen Objekte hinzugefügt werden soll: string exprefix="finder"; // Array der Symbole, die unsere Bedingungen erfüllen: CArrayString arrPanel1; // Index des aktuellen Symbols im Array arrPanel1: int panel1val; // Array, das die vomn EA erstellten charts speichert (es gibt in diesem Moment nur einen Chart): CChart charts[]; // Array, der die Pointer zuu den vom EA erstellten Charts speichert (es gibt in diesem Moment nur einen Pointer): long curChartID[]; //+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten | //+------------------------------------------------------------------+ int OnInit() { //--- Timer erstellen EventSetTimer(1); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Deinitialisierungsfunktion des Experten | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(reason!=REASON_CHARTCHANGE){ ObjectsDeleteAll(0, exprefix); } EventKillTimer(); } //+------------------------------------------------------------------+ //| Timer Funktion | //+------------------------------------------------------------------+ void OnTimer() { } void OnChartEvent(const int id, // Ereignis-ID const long& lparam, // Ereignisparameter vom Typ long const double& dparam, // Ereignisparameter vom Typ double const string& sparam) // Ereignisparameter vom Typ string { }
Dir Funktion zum Filtern der Symbole
Wir beginnen mit der Funktion, die die Schaltflächen der Symbole erstellt, die unseren Bedingungen auf dem Chart entsprechen. Benennen wir diese Funktion start_symbols. Die Funktion wird innerhalb der Funktion OnInit aufgerufen. Dadurch nimmt die Funktion OnInit ihre endgültige Form an:
//+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten | //+------------------------------------------------------------------+ int OnInit() { start_symbols(); //--- Timer erstellen EventSetTimer(1); //--- return(INIT_SUCCEEDED); }
Warum brauchen wir eine separate Funktion, wenn alles innerhalb von OnInit implementiert werden könnte? Ganz einfach. Wir werden diese Funktion nicht nur beim Start des EA aufrufen, sondern auch beim Drücken der Taste R. Auf diese Weise können wir die Liste der Zeichen leicht aktualisieren, ohne den EA vom Chart entfernen und wieder starten zu müssen.
Da sich die Spreads ständig ändern, müssen wir die Symbolliste sehr oft aktualisieren. Außerdem ändert sich auch das Vorhandensein von offenen Positionen auf bestimmten Symbolen. Vergessen Sie daher nicht, die Symbolliste zu aktualisieren (durch Drücken von R), bevor Sie den zuvor gestarteten EA wieder verwenden, um die aktuellen Daten anzuzeigen.
Werfen wir einen Blick auf die Funktion start_symbols. Sie dient auch als Wrapper zum Starten anderer Funktionen:
void start_symbols(){ // setzen des Index des aktuellen Symbols in der Liste auf Null (es ist das erste Symbol der Liste): panel1val=0; // Vorbereitung der Symbolliste: prepare_symbols(); // Entfernen vorher erstellter Symboltasten vom Chart: ObjectsDeleteAll(0, exprefix); // Anzeigen der Symbolliste: show_symbols(); // Aktualisierung des Charts, um die Änderungen zu sehen: ChartRedraw(0); }
Wir haben zwei weitere benutzerdefinierte Funktionen kennengelernt: prepare_symbols und show_symbols. Die erste erstellt den Array mit den von Symbolen, die unseren Bedingungen erfüllen. Die zweite zeigt die Schaltflächen dieser Symbole auf dem Chart, auf dem der EA läuft.
Die Darstellung der Schaltflächen auf dem Chart ist einfach. Zuerst finden wir die X- und Y-Koordinaten, mit denen eine Schaltfläche angezeigt wird, so dass sie sich nicht mit anderen Tasten überschneidet. Dann sollten wir sie anzeigen:
void show_symbols(){ // Initialisieren der Variablen für die X- und Y-Koordinaten int btn_left=0; int btn_line=1; int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77; // Tasten auf dem Chart für jedes Symbol im Array anzeigen // Schreioben des Symbolnamens auf die Taste for( int i=0; i<arrPanel1.Total(); i++ ){ if( btn_left>btn_right-BTN_WIDTH ){ btn_line++; btn_left=0; } ObjectCreate(0, exprefix+"btn"+(string) i, OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XDISTANCE,btn_left); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_FONTSIZE,8); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_COLOR,clrBlack); ObjectSetString(0,exprefix+"btn"+(string) i,OBJPROP_TEXT,arrPanel1.At(i)); ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false); btn_left+=BTN_WIDTH; } }
Damit werden die Symbole, die unsere Bedingungen erfüllen, auf dem Chart dargestellt:
Konzentrieren wir uns jetzt auf die Bedingungen für die Symbolauswahl (die Funktion prepare_symbols). Zuerst tragen wir alle Symbole in die Liste ein:
void prepare_symbols(){ // Variable für das Zwischenspeichern der Symbolnamen string name; // Variable für das Speichern der letzten Kurse der Symbole MqlTick lastme; // Rücksetzen des Symbolarray, falls es Werte enthält arrPanel1.Resize(0); // Bilden des Zwischenarrays tmpSymbols // es soll alle verfügbaren Symbole aufnehmen CArrayString tmpSymbols; for( int i=0; i<SymbolsTotal(noSYMBmarketWath); i++ ){ tmpSymbols.Add(SymbolName(i, noSYMBmarketWath)); } // Bedingungen werden hier überprüft // und ein Symbol soll in die Liste eingetragen werden // wenn es die Bedingungen erfüllt for( int i=0; i<tmpSymbols.Total(); i++ ){ name=tmpSymbols[i]; // Entfernen des überzähligen Leerzeichen im Symbolnamen, // da wir nicht genau wissen, von wo es ist StringTrimLeft(name); StringTrimRight(name); if( !StringLen(name) ){ continue; } // Hauptfilter der Symbole wird im Weiteren durchgeführt // ... // wenn ein Symbol alle Bedingungen erfüllt, wird es in die Liste eingetragen arrPanel1.Add(name); } }
Zuerst tragen wir alle Symbole in einem temporären Array ein. Die anfängliche Filterung erfolgt bereits an dieser Stelle durch den Eingabeparameter Hide symbols absent in the Market Watch panel.
Das Platzieren von Symbolen in den temporären Arrays ist nicht erforderlich. Stattdessen können wir die benötigten Symbole in die Hauptliste aufnehmen. Aber in diesem Fall müssten wir den Code neu schreiben, zum Beispiel, wenn wir ein Eingabeparameter hinzufügen müssen, die nur die Symbole hinzufügt, die in der Liste angezeigt werden sollen. Mit anderen Worten, wir müssten benutzerdefinierte Symbole in der erforderlichen Reihenfolge verwenden, anstatt alle vom Broker angebotenen Symbole zu nehmen.
Aus den gleichen Gründen wird zunächst ein Symbolname aus Leerzeichen in der Schleife gelöscht, die alle Symbole aus dem temporären Array aufzählt. Wenn Sie die oben beschriebene Eingabeparameter implementieren möchten, können Sie nicht auf die Filterung der benutzerdefinierten Eingabe verzichten.
Nun, lassen Sie uns die erhaltenen Symbole auf Grund der Eingabeparameter sortieren. Wir fügen die folgenden Codeblöcke unterhalb der Kommentarzeile hinzu: Als Nächstes erfolgt die Hauptfilterung der Symbole der Funktion prepare_symbols (in der Schleife zum Hinzufügen von Symbolen zur Liste).
Symbole mit Positionen ausblenden:
// Symbole mit offenen Positionen ausblenden: bool isskip=false; if( noSYMBwithPOS ){ // Anzeigen der Liste aller offenen Positionen int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ // auslassen, wenn es eine Position gibt für das aktuelle Symbol if(PositionGetSymbol(ti) == name ){ isskip=true; break; } } if(!isskip){ int cntMyPosO=OrdersTotal(); if(cntMyPosO>0){ for(int ti=cntMyPosO-1; ti>=0; ti--){ ulong orderTicket=OrderGetTicket(ti); if( OrderGetString(ORDER_SYMBOL) == name ){ isskip=true; break; } } } } }
Überprüfen wir zunächst, ob es eine Position mit einem Symbol gibt. Wenn es keine Position gibt, überprüfen wir, ob es eine Limit-Order gibt. Wenn eine offene Position oder eine Limit-Order vorhanden ist, überspringen wir das Symbol.
Symbole mit einem Spread ausblenden:
// wenn die Eingabeparameter bezüglich der Symbolpreise aktiviert sind, // Versuch des Abrufs der aktuellen Werte if(hide_PRICE_HIGH>0 || hide_PRICE_LOW>0 || hide_SPREAD>0 ){ SymbolInfoTick(name, lastme); if( lastme.bid==0 ){ Alert("Failed to get BID value. Some filtration functions may not work."); } } if(hide_SPREAD>0 && lastme.bid>0){ switch(hide_SPREAD){ // wenn der aktuelle Spread den Wert von 0.05% des Preises überschreitet, auslassen des Symbols case spread_b05: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.05 ){ isskip=true; } break; // wenn der aktuelle Spread den Wert von 0.1% des Preises überschreitet, auslassen des Symbols case spread_b1: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.1 ){ isskip=true; } break; // wenn der aktuelle Spread den Wert von 0.15% des Preises überschreitet, auslassen des Symbols case spread_b15: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.15 ){ isskip=true; } break; // wenn der aktuelle Spread den Wert von 0.15% des Preises unterschreitet, auslassen des Symbols case spread_l15: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.15 ){ isskip=true; } break; // wenn der aktuelle Spread den Wert von 0.1% des Preises unterschreitet, auslassen des Symbols case spread_l1: if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.1 ){ isskip=true; } break; } } if(isskip){ continue; }
Je kleiner der Spread, desto besser. Aus dieser Sicht ist es am besten, mit Symbolen zu arbeiten, deren Spread kleiner als 0,05% des Preises ist. Nicht alle Broker bieten solche guten Bedingungen, insbesondere beim Handel an der Börse.
Symbole mit dem höheren Preis ausblenden (0 - nicht ausblenden):
// Symbole mit dem höheren Preis ausblenden (0 - nicht ausblenden) if(hide_PRICE_HIGH>0 && lastme.bid>0 && lastme.bid>hide_PRICE_HIGH){ continue; }
Symbole mit dem niedrigeren Preis ausblenden (0 - nicht ausblenden):
if(hide_PRICE_LOW>0 && lastme.bid>0 && lastme.bid<hide_PRICE_LOW){ continue; }
nicht handelbare Symbole ausblenden:
if(hideProhibites){ // Auslassen, wenn das Mindestvolumen einer Position eines Symbols 0 ist if( SymbolInfoDouble(name, SYMBOL_VOLUME_MIN)==0 ) continue; // Auslassen, wenn das Eröffnen von Position eines Symbols deaktiviert ist if(SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED || SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_CLOSEONLY ){ continue; } }
Natürlich ist es möglich, die Symbole auszublenden, für die der Handel ohne Bedingungen von den Eingaben deaktiviert ist. Aber man könnte trotzdem solche Symbole in der Liste haben wollen. Aus diesem Grund fügen wir diesen Eingabeparameter hinzu.
Zeige Symbole, die nur zu einer bestimmten Stunde geöffnet werden:
// liefert den aktuellen Tag in der Variable curDay. MqlDateTime curDay; TimeCurrent(curDay); MqlDateTime curDayFrom; datetime dfrom; datetime dto; // falls es eine Einschränkung bei der Eröffnungszeit des Marktes gibt // und wir die Eröffnungszeit der aktuellen Aktie für den aktuellen Tag regeln können, dann ... if( hide_HOURS!=hour_any && SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto)){ TimeToStruct(dfrom, curDayFrom); if(hide_HOURS==hour_9am && curDayFrom.hour != 9){ continue; } if(hide_HOURS==hour_10am && curDayFrom.hour != 10){ continue; } if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){ continue; } if(hide_HOURS==hour_0am && curDayFrom.hour != 0){ continue; } }
Ausblenden, wenn der Markt geschlossen ist. Wenn man den EA am Sonntag startet, würde man wohl kaum den Aktienmarkt analysieren wollen. Aber vielleicht möchten man sich mit den am Sonntag verfügbaren Symbole wie TA25-Index oder Kryptowährungen beschäftigen? Dieser Eingabeparameter erlaubt es uns, dies zu tun.
Natürlich wäre es möglich, nur die heute gehandelten Symbole anzuzeigen, anstatt eine separate Eingabe vorzunehmen. Aber was ist, wenn es Sonntag ist und wir uns trotzdem auf den nächsten Handelstag vorbereiten wollen, indem wir geeignete Aktien auswählen, etc. Wir werden diese Option als Eingabeparameter implementieren.
Um zu definieren, ob der Markt heute geöffnet sein wird, benötigen wir die Funktion SymbolInfoSessionTrade. Wenn sie false zurückgibt, dann ist das Symbol anscheinend heute nicht für den Handel verfügbar. Um zu vermeiden, dass die Funktion zweimal aufgerufen wird, müssen wir den Code neu schreiben, so dass nur Symbole anzeigt werden, die geöffnet sind:
MqlDateTime curDay; TimeCurrent(curDay); MqlDateTime curDayFrom; datetime dfrom; datetime dto; bool sessionData=SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto); // Ausblenden eines Symbol , wenn der Markt heute geschlossen bleibt if( hideClosed && !sessionData ){ continue; } // Anzeigen nur der geöffneten Symbole // Zuweisen des aktuellen Tages der Variablen curDay // falls es eine Einschränkung bei der Eröffnungszeit des Marktes gibt, // und wir die Eröffnungszeit der aktuellen Aktie für den aktuellen Tag regeln können, dann ... if( hide_HOURS!=hour_any && sessionData){ TimeToStruct(dfrom, curDayFrom); if(hide_HOURS==hour_9am && curDayFrom.hour != 9){ continue; } if(hide_HOURS==hour_10am && curDayFrom.hour != 10){ continue; } if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){ continue; } if(hide_HOURS==hour_0am && curDayFrom.hour != 0){ continue; } }
Ausblenden, wenn der ATR kleiner als der Dollarwert ist. Wenn man während eines Tages handelt und darauf wartet, dass sich der Preis für mindestens 50-90 Cent bewegt, benötigen man wahrscheinlich keine Symbole, die sich statistisch für nicht mehr als 30 Cent pro Tag bewegen. Dieser Parameter ermöglicht es uns, solche Symbole zu sortieren, indem wir die notwendige Mindestgröße der täglichen Kursbewegung festlegen:
// Ausblenden, wenn der ATR kleiner als der eingestellte Wert in Dollar ist. if(hideATRcents>0){ MqlRates rates[]; ArraySetAsSeries(rates, true); double atr; if(CopyRates(name, PERIOD_D1, 1, 5, rates)==5){ atr=0; for(int j=0; j<5; j++){ atr+=rates[j].high-rates[j].low; } atr/=5; if( atr>0 && atr<hideATRcents ){ continue; } } }
Ich denke, das ist genug für ein vollständiges Filtern nach den meisten Parametern. Wenn Sie jedoch andere Filterbedingungen benötigen, können Sie diese jederzeit innerhalb der Funktionsschleife prepare_symbols hinzufügen.
Chart öffnen
Wir haben bereits gelernt, wie die Symboltasten entstehen, die unseren Bedingungen entsprechen. Wir könnten an dieser Stelle anhalten und die Charts der erhaltenen Symbole manuell öffnen. Aber das ist umständlich. Glücklicherweise können wir den Prozess vereinfachen und ein benötigtes Chart öffnen, wenn wir auf die Schaltfläche klicken.
Damit die Aktion beim Klicken auf die Schaltfläche ausgeführt wird, sollte diese Aktion in der Standardfunktion OnChartEvent MQL-Sprache beschrieben werden. Die Funktion fängt jedes Ereignis auf dem Chart ab. Mit anderen Worten, sie wird bei jedem Ereignis, das auf dem Chart passiert, aufgerufen.
Die Funktion OnChartEvent verfügt über vier Argumente. Der allererste Parameter (id) enthält die ID eines Ereignisses, das gerade von der Funktion OnChartEvent abgefangen wurde. Um zu verstehen, dass die Funktion OnChartEvent genau nach dem Anklicken einer Diagrammschaltfläche aufgerufen wurde, vergleichen wir den Parameterwert mit dem benötigten.
Das Ereignis eines Klicks auf die Taste hat die ID CHARTEVENT_OBJECT_CLICK. Fügen wir daher den folgenden Code zur Funktion OnChartEvent hinzu, um Tastenklicks auf dem Chart zu verarbeiten:
void OnChartEvent(const int id, // Ereignis-ID const long& lparam, // Ereignisparameter vom Typ long const double& dparam, // Ereignisparameter vom Typ double const string& sparam) // Ereignisparameter vom Typ string { switch(id){ case CHARTEVENT_OBJECT_CLICK: // ausgeführter Code nach einem Tastenklick break; } }
Wie können wir genau erkennen, welche Taste auf dem Chart gedrückt wurde? Der zweite Parameter der Funktion OnChartEvent (sparam) kann uns dabei helfen. Für das Ereignis CHARTEVENT_OBJECT_CLICK enthält er den Namen der Taste, die von einem Benutzer angeklickt wurde. Wir müssen diesen Namen nur mit dem Namen vergleichen, der von unserem EA generiert wurde. Wenn dies die EA-Taste ist, öffnet sich die entsprechende Symboltabelle. Der Name eines geöffneten Symbols wird aus dem Text auf der Schaltfläche übernommen. Als Ergebnis haben wir den folgenden Code, der in die Bedingung case CHARTEVENT_OBJECT_CLICK eingefügt werden sollte:
// wenn der Name der Taste eine Zeile enthält, die in allen grafischen Objekten vorhanden ist. // erstellt von Ihrem EA, dann... if( StringFind(sparam, exprefix+"btn")>=0 ){ // zuweisen des Index der aktuellen Taste // (Position eines aktuellen Symbols in der Symbolliste) der Variablen panel1val string tmpme=sparam; StringReplace(tmpme, exprefix+"btn", ""); panel1val=(int) tmpme; // öffnen des Charts des aktuellen Symbols showcharts(ObjectGetString(0,sparam,OBJPROP_TEXT)); }
Der Chart eines ausgewählten Symbols wird mit der nutzerdefinierten Funktion showcharts geöffnet. Die Funktion öffnet nicht nur den notwendige Chart, sondern auch zusätzlich:
- Schließt Charts, die zuvor vom EA geöffnet wurden;
- Fügt dem Panel Market Watch ein Symbol hinzu, wenn es nicht vorhanden ist;
- Schaltet den Chart bei Bedarf in den Modus Kerzendarstellung um.
- Ändert den Maßstab des Charts (ich habe diese Funktion einfach deshalb hinzugefügt, weil ich einen nutzerdefinierten Maßstab anstelle des Standardmaßstabs verwende).
void showcharts(string name){ // wenn die Charts bereits geöffnet sind, schließe sie closecharts(); // Hinzufügen eines Symbols zum Panel "Market Watch", falls es noch nicht existiert // und falls der Eingabeparameter "Add chart to Market Watch" 'true' ist if( addInfoWatch ){ SymbolSelect(name, true); } // Öffnen des Charts und platzieren der Chart-ID im Array curChartID curChartID[ArrayResize(curChartID,ArraySize(curChartID)+1)-1]=charts[(uchar) ArrayResize(charts,ArraySize(charts)+1)-1].Open( name, PERIOD_D1 ); // wenn der Eingabeparameter "Open candlestick charts" 'true' ist, // wechsle den Chart in den Kerzenmodus if(viewCandle){ ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_MODE, CHART_CANDLES); } // wenn der Eingabeparameter "Show tick volumes" 'true' ist, // zeige das Tick-Volumen if(viewVolumes){ ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SHOW_VOLUMES, CHART_VOLUME_TICK); } // ändern der Maßstäbe des Charts ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SCALE, 2); // 0,3 Sekunden auf die Implementierung warten Sleep(333); // aktualisieren des geöffneten Charts mit allen Änderungen ChartRedraw(curChartID[ArraySize(curChartID)-1]); }Die Funktion closecharts schließt alle vorher vom EA geöffneten Charts. Der Code ist ganz einfach:
void closecharts(){ // wenn das Array der von dem EA geöffneten Charts nicht leer ist, dann... if(ArraySize(charts)){ // schließen aller Charts der Reihe nach for( int i=0; i<ArraySize(charts); i++ ){ charts[i].Close(); } // Löschen des Arrays der Charts ArrayFree(charts); } // wenn das Array der IDs der vom EA geöffneten Charts nicht leer ist, wird es gelöscht if(ArraySize(curChartID)){ ArrayFree(curChartID); } }
Anzeigen zusätzlicher Symbolinformationen
Es wäre gut, nicht nur ein Symbolchart zu öffnen, sondern auch zusätzliche Daten wie die Beschreibung (einige Broker machen Symbolnamen so unverständlich, dass sie eine Art Chiffre zu sein scheinen) und die Symbolbewegungsrichtung des letzten Tages und der letzten Stunde anzuzeigen.
Diese Informationen können über grafische Objekte angezeigt werden. Wir werden jedoch einen einfacheren Ansatz verfolgen. Wir werden die Informationen als Kommentar auf dem Charts anzeigen.
Wir fügen dazu den folgenden Code zur Funktion showcharts hinzu, bevor wir die Funktion Sleep aufrufen:
//Zusatzinformationen zum Chart anzeigen string msg=""; if(showNameSymbol){ StringAdd(msg, getmename_symbol(name)+"\r\n"); } if(showInfoSymbol){ StringAdd(msg, getmeinfo_symbol(name, false)+"\r\n"); } if( StringLen(msg)>0 ){ ChartSetString(curChartID[ArraySize(curChartID)-1], CHART_COMMENT, msg); }
Wenn der Eingabeparameter showNameSymbol true ist, wird die Funktion getmename_symbol aufgerufen, die die Zeile mit dem Symbolnamen zurückgibt. Wenn der Eingabeparameter showInfoSymbol true ist, wird die Funktion getmeinfo_symbol aufgerufen, die die Zeile mit der Symbolinformation zurückgibt:
string getmename_symbol(string symname){ return SymbolInfoString(symname, SYMBOL_DESCRIPTION); } string getmeinfo_symbol(string symname, bool show=true){ MqlRates rates2[]; ArraySetAsSeries(rates2, true); string msg=""; if(CopyRates(symname, PERIOD_D1, 0, 1, rates2)>0){ if(show){ StringAdd(msg, (string) symname+": "); } StringAdd(msg, "D1 "); if( rates2[0].close > rates2[0].open ){ StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2) +"%"); }else{ if( rates2[0].close < rates2[0].open ){ StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2) +"%"); }else{ StringAdd(msg, "0%"); } } } if(CopyRates(symname, PERIOD_H1, 0, 1, rates2)>0){ StringAdd(msg, ", H1 "); if( rates2[0].close > rates2[0].open ){ StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2)+"% (+"+DoubleToString(rates2[0].close-rates2[0].open, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")"); }else{ if( rates2[0].close < rates2[0].open ){ StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2)+"% (-"+DoubleToString(rates2[0].open-rates2[0].close, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")"); }else{ StringAdd(msg, "0%"); } } } return msg; }
Im Ergebnis sehen wir folgende Informationen auf den neueröffneten Chart:
Steuerung des EAs über die Tastatur
Während wir uns noch in der Funktion OnChartEvent befinden, lassen Sie uns eine Antwort auf das Drücken einiger Tasten dort hinzufügen:
- Wenn Sie R drücken, aktualisiert sich die Liste der Symbole, die unseren Bedingungen entsprechen:
- Wenn Sie X drücken, wird der EA vom Chart entfernt.
Das Ereignis mit der ID CHARTEVENT_KEYDOWN ermöglicht das Abfangen des Tastendrucks. Der Code einer gedrückten Taste wird im bereits erwähnten Parameter sparam übergeben. Daher müssen wir dem Operator switch einfach die folgende Bedingung hinzufügen:
case CHARTEVENT_KEYDOWN: switch((int) sparam){ case 45: //x ExpertRemove(); break; case 19: //r start_symbols(); break; } break;
Wie man sieht, rufen wir beim Drücken von R einfach die zuvor erstellte Funktion start_symbols auf.
Hinzufügen einer Navigation auf dem Chart
Wir haben bereits gelernt, wie man nicht nur Symboltasten anzeigt, sondern auch, wie man beim Anklicken dieser Tasten die Charts der benötigten Symbole öffnet. Aber wir sind hier noch nicht fertig. Unser Hilfsmittel ist immer noch umständlich zu verwenden. Nachdem wir eine Symbolchart geöffnet haben, müssen wir es manuell schließen und auf die Taste für den nächsten Chart klicken. Dies sollte jedes Mal geschehen, wenn wir zum nächsten Symbol wechseln müssen, was die Arbeit sehr mühsam macht. Fügen wir nun Navigationstasten für die Symbolleiste hinzu, um Charts zu öffnen.
Wir werden nur drei weitere Tasten hinzufügen: um zum nächsten Diagramm zu gelangen, um zum vorherigen Chart zu gelangen und um das Chart zu schließen.
Es bleibt nur noch zu entscheiden, wie das umgesetzt werden soll. Wir können die Tasten direkt in der Funktion showcharts hinzufügen, wenn wir ein neues Chart erstellen. Die Anzahl der Tasten kann jedoch in Zukunft zunehmen. Das Erstellen der Tasten und anderer grafischer Objekte kann das Öffnen des Diagramms verlangsamen, was unerwünscht ist.
Deshalb werden wir die Tasten in der standardmäßigen Funktion OnTimer erstellen. Wir werden regelmäßig überprüfen, ob ein Chart durch den EA geöffnet wurde, und, wenn der Chart geöffnet ist, ob es Tasten hat. Wenn es keine Tasten gibt, erstellen wir sie:
void OnTimer() { // wenn das Array der Chart-IDs Werte enthält, dann... uchar tmpCIDcnt=(uchar) ArraySize(curChartID); if(tmpCIDcnt>0 ){ // wenn die letzte ID im Array gültig ist, dann ... if(curChartID[tmpCIDcnt-1]>0){ // wenn der Chart mit der ID keine Tasten hat, erstelle dieselben if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){ createBTNS(curChartID[tmpCIDcnt-1]); } } } }
Die Tasten auf dem Chart werden in der Nutzerfunktion createBTNS erstellt. Der Code ist ganz einfach:
void createBTNS(long CID){ ObjectCreate(CID, exprefix+"_p_btn_prev", OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YDISTANCE,90); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetString(CID,exprefix+"_p_btn_prev",OBJPROP_TEXT,"Prev chart"); ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_SELECTABLE,false); ObjectCreate(CID, exprefix+"_p_btn_next", OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YDISTANCE,65); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetString(CID,exprefix+"_p_btn_next",OBJPROP_TEXT,"Next chart"); ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_SELECTABLE,false); ObjectCreate(CID, exprefix+"_p_btn_close", OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XDISTANCE,110); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YDISTANCE,40); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XSIZE,BTN_WIDTH); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YSIZE,BTN_HEIGHT); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetString(CID,exprefix+"_p_btn_close",OBJPROP_TEXT,"Close chart"); ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_SELECTABLE,false); // Aktualisierung des Charts, um die Änderungen zu sehen ChartRedraw(CID); }
Im Ergebnis schaut ein neuer Chart dann so aus:
Hinzufügen einer Reaktion auf einen Tastendruck
Bisher sind die dem Chart hinzugefügten Tasten nur eine Dekoration. Beim Drücken passiert nichts. Teilen wir ihnen mit, wie sie auf das Drücken reagieren sollen.
Leider hilft uns die Standardfunktion OnChartEvent hier nicht weiter, da sie nur auf die Ereignisse reagiert, die auf einem Chart passiert sind, auf dem den EA gestartet wird, während die Buttons einem neuen Chart hinzugefügt werden.
Vielleicht gibt es einige bequemere Möglichkeiten. Ich habe nur einen Weg gefunden, um auf Änderungen zu reagieren, die in einem anderen Chart aufgetreten sind. Es handelt sich um die Standardfunktion OnTimer. Wenn der Chart die Tasten enthält, werden wir prüfen, ob welche davon gedrückt sind. Wenn ja, wird eine notwendige Aktion durchgeführt. Infolgedessen wird die Bedingung:
if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){ createBTNS(curChartID[tmpCIDcnt-1]); }
... wird neugeschrieben wie folgt:
if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){ createBTNS(curChartID[tmpCIDcnt-1]); }else{ if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_prev",OBJPROP_STATE)==true ){ prevchart(); return; } if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_next",OBJPROP_STATE)==true ){ nextchart(); return; } if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_close",OBJPROP_STATE)==true ){ closecharts(); return; } }
Wenn die Taste Prev chart gedrückt wird, wird die Funktion prevchart aufgerufen. Wenn die Taste Next chart gedrückt wird, wird die Funktion nextchart aufgerufen. Wenn die Taste Close chart gedrückt wird, wird die Funktion closecharts aufgerufen. Die Funktionen prevchart und nextchart sind ähnlich:
void nextchart(){ // wenn die Symbolliste das nächste Symbol anbietet, öffne dessen Chart // andernfalls schließe den Chart if(arrPanel1.Total()>(panel1val+1)){ panel1val++; showcharts(arrPanel1[panel1val]); }else{ closecharts(); } } void prevchart(){ // wenn die Symbolliste das vorherige Symbol anbietet, öffne dessen Chart // andernfalls schließe den Chart if(arrPanel1.Total()>(panel1val-1) && (panel1val-1)>=0){ panel1val--; showcharts(arrPanel1[panel1val]); }else{ closecharts(); } }
Schlussfolgerung
Das war's. Wie Sie sehen können, ist der Umfang des gesamten Codes nicht überwältigend, während der Vorteil offensichtlich ist. Wir müssen die Charts nicht mehr selber öffnen und immer wieder schließen. Stattdessen können wir auf die entsprechende Taste klicken, und alles ist für uns erledigt.
Natürlich kann es mehr Möglichkeiten geben, unseren EA zu verbessern. Aber in seiner jetzigen Form ist es bereits ein vollwertiges Produkt, das die Auswahl der Aktien deutlich vereinfacht.
Konvertieren des Hilfsprogramms nach MQL4
Nun versuchen wir, unser Hilfsprogramm nach MQL4 zu konvertieren. Überraschenderweise müssen wir nur einen einzigen Codeblock neu schreiben. Dies dauert etwa fünf Minuten.
Wir erstellen zunächst einen neuen EA in MetaEditor 4. Danach kopieren wir den Quellcode des MQL5 EA in ihn.
Dann kompilieren wir den EA. Der Versuch endet natürlich mit einem Fehler. Aber als Ergebnis erhalten wir eine Liste von Fehlern, die wir beheben müssen. Es sind nur drei von ihnen:
- 'PositionsTotal' - Funktion nicht definiert
- 'PositionGetSymbol' - Funktion nicht definiert
- 'OrderGetTicket' - Funktion nicht definiert
Mit einem Doppelklick auf den ersten Fehler, gelangt man zur entsprechenden Zeile.
'PositionsTotal' - Funktion nicht definiert. Der Fehler wird im folgenden Block des Funktionscodes prepare_symbols erkannt:
int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ // auslassen, wenn es bereits eine Position mit dem aktuellen Symbol gibt if(PositionGetSymbol(ti) == name ){ isskip=true; break; } } if(!isskip){ int cntMyPosO=OrdersTotal(); if(cntMyPosO>0){ for(int ti=cntMyPosO-1; ti>=0; ti--){ ulong orderTicket=OrderGetTicket(ti); if( OrderGetString(ORDER_SYMBOL) == name ){ isskip=true; break; } } } }
Einer der wesentlichen Unterschiede zwischen den Sprachen MQL4 und MQL5 ist die Handhabung von Positionen und Aufträgen. Daher sollten wir den Codeblock wie folgt umschreiben, damit der EA im MetaTrader 4 korrekt funktioniert:
int cntMyPos=OrdersTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if(OrderSymbol() == name ){ isskip=true; break; } }
Da MQL4 keine Unterscheidung zwischen Positionen und Aufträgen vornimmt, ist der resultierende Code viel kleiner.
Die restlichen Fehler werden automatisch behoben, da sie in dem von uns bearbeiteten Codeblock aufgetreten sind.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/5348
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
Und wofür gibt es precompiler direktiven ???
Aber egal, man muss es ja so oder so entweder .mq4 oder .mq5 nennen.