Tracing, Debugging und strukturelle Analyse von Quellcodes
Einleitung
Dieser Beitrag beschreibt eine der Methoden zur Erstellung eines Aufrufs-Stacks während der Ausführung. Die folgenden Funktionen werden in diesem Beitrag beschrieben:
- Erstellung der Struktur verwendeter Klassen, Funktionen und Dateien.
- Erstellung des Aufrufs-Stacks unter Beibehaltung aller vorherigen Stacks. Die Reihenfolge ihrer Aufrufe.
- Ansicht des Zustands der Watch-Parameter während der Ausführung.
- Schrittweise Ausführung des Codes.
- Gruppierung und Sortierung erhaltener Stacks, Abruf von "extremen" Informationen.
Grundprinzipien der Entwicklung
Als Methode für die Darstellung der Struktur wird eine herkömmliche Herangehensweise genutzt: die Darstellung in Form einer Baumstruktur. Zu diesem Zweck benötigen wir zwei Informationsklassen. CNode – ein "Knoten" zum Schreiben aller Informationen über ein Stack. CTreeCtrl – ein "Baum", der alle Knoten verarbeitet. Und CTraceCtrl, der Tracer selbst, für die Verarbeitung der Bäume.
Die Klassen werden gemäß der folgenden Hierarchie implementiert:Die Klassen CNodeBase und CTreeBase beschreiben grundlegende Eigenschaften und Methoden der Arbeit mit Knoten und Bäumen.
Die vererbte Klasse CNode erweitert die Grundfunktionalität von CNodeBase und die Klasse CTreeBase arbeitet mit der abgeleiteten Klasse CNode. Dies geschieht, da die Klasse CNodeBase den anderen Standardknoten übergeordnet ist und als unabhängige Klasse für die praktische Umsetzung der Hierarchie und Vererbung isoliert ist.
Im Gegensatz zu CTreeNode aus der Standardbibliothek beinhaltet die Klasse CNodeBase ein Array aus Pointern zu Knoten, sodass die Menge der "Zweige" aus diesem Knoten unbegrenzt ist.
Die Klassen CNodeBase und CNode
class CNode; // forward declaration //------------------------------------------------------------------ class CNodeBase class CNodeBase { public: CNode *m_next[]; // list of nodes it points to CNode *m_prev; // parent node int m_id; // unique number string m_text; // text public: CNodeBase() { m_id=0; m_text=""; } // constructor ~CNodeBase(); // destructor }; //------------------------------------------------------------------ class CNode class CNode : public CNodeBase { public: bool m_expand; // expanded bool m_check; // marked with a dot bool m_select; // highlighted //--- run-time information int m_uses; // number of calls of the node long m_tick; // time spent in the node long m_tick0; // time of entering the node datetime m_last; // time of entering the node tagWatch m_watch[]; // list of name/value parameters bool m_break; // debug-pause //--- parameters of the call string m_file; // file name int m_line; // number of row in the file string m_class; // class name string m_func; // function name string m_prop; // add. information public: CNode(); // constructor ~CNode(); // destructor void AddWatch(string watch,string val); };
Sie finden die Umsetzung aller Klassen in den angehängten Dateien. In diesem Beitrag werden nur ihre Kopfzeilen und wichtige Funktionen gezeigt.
Gemäß der akzeptierten Klassifizierung stellt CTreeBase ein orientiertes azyklisches Diagramm dar. Die abgeleitete Klasse CTreeCtrlnutzt CNode und bedient ihre gesamte Funktionalität: Hinzufügen, Ändern und Löschen der Knoten von CNode.
CTreeCtrlund CNode können problemlos die entsprechenden Klassen der Standardbibliothek ersetzen, da sie eine etwas weiter gefasste Funktionalität bieten.
Die Klassen CTreeBase und CTreeCtrl
//------------------------------------------------------------------ class CTreeBase class CTreeBase { public: CNode *m_root; // first node of the tree int m_maxid; // counter of ID //--- base functions public: CTreeBase(); // constructor ~CTreeBase(); // destructor void Clear(CNode *root=NULL); // deletion of all nodes after a specified one CNode *FindNode(int id,CNode *root=NULL); // search of a node by its ID starting from a specified node CNode *FindNode(string txt,CNode *root=NULL); // search of a node by txt starting from a specified node int GetID(string txt,CNode *root=NULL); // getting ID for a specified Text, the search starts from a specified node int GetMaxID(CNode *root=NULL); // getting maximal ID in the tree int AddNode(int id,string text,CNode *root=NULL); // adding a node to the list, search is performed by ID starting from a specified node int AddNode(string txt,string text,CNode *root=NULL); // adding a node to the list, search is performed by text starting from a specified node int AddNode(CNode *root,string text); // adding a node under root }; //------------------------------------------------------------------ class CTreeCtrl class CTreeCtrl : public CTreeBase { //--- base functions public: CTreeCtrl() { m_root.m_file="__base__"; m_root.m_line=0; m_root.m_func="__base__"; m_root.m_class="__base__"; } // constructor ~CTreeCtrl() { delete m_root; m_maxid=0; } // destructor void Reset(CNode *root=NULL); // reset the state of all nodes void SetDataBy(int mode,int id,string text,CNode *root=NULL); // changing text for a specified ID, search is started from a specified node string GetDataBy(int mode,int id,CNode *root=NULL); // getting text for a specified ID, search is started from a specified node //--- processing state public: bool IsExpand(int id,CNode *root=NULL); // getting the m_expand property for a specified ID, search is started from a specified node bool ExpandIt(int id,bool state,CNode *root=NULL); // change the m_expand state, search is started from a specified node void ExpandBy(int mode,CNode *node,bool state,CNode *root=NULL); // expand node of a specified node bool IsCheck(int id,CNode *root=NULL); // getting the m_check property for a specified ID, search is started from a specified node bool CheckIt(int id,bool state,CNode *root=NULL); // change the m_check state to a required one starting from a specified node void CheckBy(int mode,CNode *node,bool state,CNode *root=NULL); // mark the whole tree bool IsSelect(int id,CNode *root=NULL); // getting the m_select property for a specified ID, search is started from a specified node bool SelectIt(int id,bool state,CNode *root=NULL); // change the m_select state to a required one starting from a specified node void SelectBy(int mode,CNode *node,bool state,CNode *root=NULL); // highlight the whole tree bool IsBreak(int id,CNode *root=NULL); // getting the m_break property for a specified ID, search is started from a specified node bool BreakIt(int id,bool state,CNode *root=NULL); // change the m_break state, search is started from a specified node void BreakBy(int mode,CNode *node,bool state,CNode *root=NULL); // set only for a selected one //--- operations with nodes public: void SortBy(int mode,bool ascend,CNode *root=NULL); // sorting by a property void GroupBy(int mode,CTreeCtrl *atree,CNode *node=NULL); // grouping by a property };
Die Architektur endet mit zwei Klassen: CTraceCtrl, deren einzige Instanz für direktes Tracing verwendet wird, enthält drei Instanzen der Klasse CTreeCtrlfür die Erstellung der erforderlichen Funktionsstruktur und einen temporären Container, die Klasse CIn. Dabei handelt es sich um eine reine Hilfsklasse, die zum Hinzufügen neuer Knoten zu CTraceCtrl genutzt wird.
Die Klassen CTraceCtrl und CIn
class CTraceView; // provisional declaration //------------------------------------------------------------------ class CTraceCtrl class CTraceCtrl { public: CTreeCtrl *m_stack; // object of graph CTreeCtrl *m_info; // object of graph CTreeCtrl *m_file; // grouping by files CTreeCtrl *m_class; // grouping by classes CTraceView *m_traceview; // pointer to displaying of class CNode *m_cur; // pointer to the current node CTraceCtrl() { Create(); Reset(); } // tracer created ~CTraceCtrl() { delete m_stack; delete m_info; delete m_file; delete m_class; } // tracer deleted void Create(); // tracer created void In(string afile,int aline,string aname,int aid); // entering a specified node void Out(int aid); // exit from a specified node bool StepBack(); // exit from a node one step higher (going to the parent) void Reset() { m_cur=m_stack.m_root; m_stack.Reset(); m_file.Reset(); m_class.Reset(); } // resetting all nodes void Clear() { m_cur=m_stack.m_root; m_stack.Clear(); m_file.Clear(); m_class.Clear(); } // resetting all nodes public: void AddWatch(string name,string val); // checking the debug mode for a node void Break(); // pause for a node }; //------------------------------------------------------------------ CIn class CIn { public: void In(string afile,int aline,string afunc) { if(NIL(m_trace)) return; // exit if there is no graph if(NIL(m_trace.m_tree)) return; if(NIL(m_trace.m_tree.m_root)) return; if(NIL(m_trace.m_cur)) m_trace.m_cur=m_trace.m_tree.m_root; m_trace.In(afile,aline,afunc,-1); // entering the next one } void ~CIn() { if(!NIL(m_trace)) m_trace.Out(-1); } // exiting higher };
Modell der Arbeit der Klasse CIn
Diese Klasse ist für die Erstellung des Stack-Baums verantwortlich.
Die Konstruktion des Diagramms geschieht schrittweise in zwei Etappen mithilfe zweier CTraceCtrl-Funktionen:
void In(string afile, int aline, string aname, int aid); // entering a specified node void Out(int aid); // exit before a specified node
In anderen Worten: Um einen Baum zu bilden, erfolgen kontinuierliche Aufrufe von In-Out-In-Out-In-In-Out-Out usw.
Das Paar In-Out funktioniert folgendermaßen:
1. Eintritt in einen Block (Funktion, Zyklus, Bedingung usw.), d. h. gleich nach der Klammer "{".
Beim Eintritt in den Block wird eine neue Instanz von CIn erstellt und erhält die aktuelle CTraceCtrl, die bereits mit vorherigen Knoten gestartet wurde. Die Funktion CTraceCtrl::In wird in CIn aufgerufen und erstellt einen neuen Knoten im Stack. Der Knoten wird unter dem aktuellen Knoten CTraceCtrl::m_cur erstellt. Alle aktuellen Informationen über den Eintritt werden hineingeschrieben: Dateiname, Zeilennummer, Klassenname, Funktionen, aktuelle Zeit usw.
2. Austritt aus dem Block beim Treffen auf die Klammer "}".
Beim Austritt aus dem Block ruft MQL automatisch den Destruktor CIn::~CIn auf. Im Destruktor wird CTraceCtrl::Out aufgerufen. Der Pointer des aktuellen Knotens CTraceCtrl::m_cur steigt eine Ebene höher im Baum. Dabei wird der Destruktor nicht für den neuen Knoten aufgerufen, der Knoten bleibt im Baum.
Schema des Aufbaus eines Stacks
Der Aufbau des Aufrufs-Stacks in Form eines Baums mit Eintragung aller Informationen über einen Aufruf erfolgt mithilfe des Containers CIn.
Makros zum Vereinfachen von Aufrufen
Um das Neuschreiben der langen Codezeilen für die Erstellung des Objekts CIn und den Eintritt eines Knotens in Ihrem Code zu vermeiden, besteht die bequeme Möglichkeit, dies durch den Aufruf eines Makros zu ersetzen:#define _IN CIn _in; _in.In(__FILE__, __LINE__, __FUNCTION__)
Wie Sie sehen können, wird das Objekt CIn erstellt und anschließend treten wir in den Knoten ein.
Da MQL eine Warnung ausgibt, wenn die Namen von lokalen Variablen die gleichen sind wie die der globalen Variablen, ist es besser (genauer und klarer), 3-4 analoge Definitionen mit anderen Namen von Variablen in der folgenden Form festzulegen:
#define _IN1 CIn _in1; _in1.In(__FILE__, __LINE__, __FUNCTION__) #define _IN2 CIn _in2; _in2.In(__FILE__, __LINE__, __FUNCTION__) #define _IN3 CIn _in3; _in3.In(__FILE__, __LINE__, __FUNCTION__)Wenn Sie tiefer gelegene Sub-Blöcke betreten, verwenden Sie die nächsten Makros _INx.
bool CSampleExpert::InitCheckParameters(int digits_adjust) { _IN; //--- initial data checks if(InpTakeProfit*digits_adjust<m_symbol.StopsLevel()) { _IN1; printf("Take Profit must be greater than %d",m_symbol.StopsLevel());
Mit dem Auftreten von Makros in Build 411 können Sie die Übergabe von Parametern mithilfe von #define uneingeschränkt nutzen.
Deshalb finden Sie in der Klasse CTraceCtrl die folgende Makro-Definition:
#define NIL(p) (CheckPointer(p)==POINTER_INVALID)
Sie ermöglicht es, die Überprüfung der Gültigkeit des Pointers zu verkürzen.
Beispielsweise wird die Zeile:
if (CheckPointer(m_tree))==POINTER_INVALID || CheckPointer(m_cur))==POINTER_INVALID) return;
durch eine kürzere Variante ersetzt:
if (NIL(m_tree) || NIL(m_cur)) return;
Vorbereitung Ihrer Dateien für Tracing
Um das Stack zu kontrollieren und abzurufen, müssen Sie drei Schritte durchführen.
1. Die erforderlichen Dateien hinzufügen#include <Trace.mqh>
Die gesamte Standardbibliothek basiert derzeit auf der Klasse CObject. Wenn sie also auch in Ihren Dateien als Basisklasse verwendet wird, genügt es, Trace.mqh nur zu Object.mqh hinzuzufügen.
2. Die _IN-Makros in den erforderlichen Blöcken platzieren (Sie können suchen und ersetzen)
Beispiel der Nutzung des _IN-Makros:bool CSampleExpert::InitCheckParameters(int digits_adjust) { _IN; //--- initial data checks if(InpTakeProfit*digits_adjust<m_symbol.StopsLevel()) { _IN1; printf("Take Profit must be greater than %d",m_symbol.StopsLevel());
3. Fügen Sie in den Funktionen OnInit, OnTime und OnDeinit, die das Hauptmodul des Programms darstellen, jeweils die Erstellung, Modifizierung bzw. Löschung des globalen Objekts CTraceCtrlein. Unten aufgeführt sehen Sie den nutzungsfertigen Code für die Einfügung:
Einbetten des Tracers im Hauptcode
//------------------------------------------------------------------ OnInit int OnInit() { //**************** m_traceview= new CTraceView; // created displaying of the graph m_trace= new CTraceCtrl; // created the graph m_traceview.m_trace=m_trace; // attached the graph m_trace.m_traceview=m_traceview; // attached displaying of the graph m_traceview.Create(ChartID()); // created chart //**************** // remaining part of your code… return(0); } //------------------------------------------------------------------ OnDeinit void OnDeinit(const int reason) { //**************** delete m_traceview; delete m_trace; //**************** // remaining part of your code… } //------------------------------------------------------------------ OnTimer void OnTimer() { //**************** if (m_traceview.IsOpenView(m_traceview.m_chart)) m_traceview.OnTimer(); else { m_traceview.Deinit(); m_traceview.Create(ChartID()); } // if the window is accidentally closed //**************** // remaining part of your code… } //------------------------------------------------------------------ OnChartEvent void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { //**************** m_traceview.OnChartEvent(id, lparam, dparam, sparam); //**************** // remaining part of your code… }
Klassen der Darstellung des Tracings
Das Stack ist also eingerichtet. Betrachten wir nun die Darstellung der erhaltenen Informationen.
Zu diesem Zweck müssen wir zwei Klassen erstellen. CTreeView für die Anzeige des Baums und CTraceView für die Steuerung der Darstellung von Bäumen und zusätzlichen Informationen über das Stack. Beide Klassen werden von der Basisklasse CView abgeleitet.
Die Klassen CTreeView und CTraceView
//------------------------------------------------------------------ class CTreeView class CTreeView: public CView { //--- basic functions public: CTreeView(); // constructor ~CTreeView(); // destructor void Attach(CTreeCtrl *atree); // attached the tree object for displaying it void Create(long chart,string name,int wnd,color clr,color bgclr,color selclr, int x,int y,int dx,int dy,int corn=0,int fontsize=8,string font="Arial"); //--- functions of processing of state public: CTreeCtrl *m_tree; // pointer to the tree object to be displayed int m_sid; // last selected object (for highlighting) int OnClick(string name); // processing the event of clicking on an object //--- functions of displaying public: int m_ndx, m_ndy; // size of margins from button for drawing int m_bdx, m_bdy; // size of button of nodes CScrollView m_scroll; bool m_bProperty; // show properties near the node void Draw(); // refresh the view void DrawTree(CNode *first,int xpos,int &ypos,int &up,int &dn); // redraw void DeleteView(CNode *root=NULL,bool delparent=true); // delete all displayed elements starting from a specified node }; //------------------------------------------------------------------ class CTreeView class CTraceView: public CView { //--- base functions public: CTraceView() { }; // constructor ~CTraceView() { Deinit(); } // destructor void Deinit(); // full deinitialization of representation void Create(long chart); // create and activate the representation //--- function of processing of state public: int m_hagent; // handler of the indicator-agent for sending messages CTraceCtrl *m_trace; // pointer to created tracer CTreeView *m_viewstack; // tree for displaying the stack CTreeView *m_viewinfo; // tree for displaying of node properties CTreeView *m_viewfile; // tree for displaying of the stack with grouping by files CTreeView *m_viewclass; // tree for displaying of stack with grouping by classes void OnTimer(); // handler of timer void OnChartEvent(const int,const long&,const double&,const string&); // handler of event //--- functions of displaying public: void Draw(); // refresh objects void DeleteView(); // delete the view void UpdateInfoTree(CNode *node,bool bclear); // displaying the window of detailed information about a node string TimeSeparate(long time); // special function for transformation of time into string };
Wir haben uns für die Anzeige des Stacks in einem separaten Unterfenster als optimale Variante entschieden.
In anderen Worten: Wenn die Klasse CTraceView in der Funktion CTraceView::Create erstellt wird, wird das Diagrammfenster erstellt und alle Objekte werden darin eingezeichnet, obwohl die Klasse CTraceView in einem Expert Advisor in einem anderen Fenster erstellt wird und arbeitet. Dies soll verhindern, dass die Arbeit des Quellcodes des getraceten Programms und die Anzeige ihrer eigenen Informationen im Diagramm durch die riesige Menge von Informationen behindert werden.
Doch um die Interaktion zwischen zwei Fenstern zu ermöglichen, müssen wir einen Indikator zum Fenster hinzufügen, das alle Ereignisse des Benutzers an das Basisfenster mit dem getraceten Programm senden wird.
Der Indikator wird ebenfalls in der Funktion CTraceView::Create erstellt. Er hat nur einen externen Parameter: die ID des Diagramms, an das er die Ereignisse senden soll.
Der Indikator TraceAgent
#property indicator_chart_window input long cid=0; // чарт получателя //------------------------------------------------------------------ OnCalculate int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[]) { return(rates_total); } //------------------------------------------------------------------ OnChartEvent void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { EventChartCustom(cid, (ushort)id, lparam, dparam, sparam); }
Als Ergebnis erhalten wir eine hinreichend strukturierte Darstellung des Stacks.
Im Baum TRACE auf der linken Seite wird das ursprüngliche Stack angezeigt.
Darunter befindet sich das Fenster INFO mit detaillierten Informationen über den ausgewählten Knoten (in diesem Beispiel CTraceView::OnChartEvent). Zwei benachbarte Fenster mit Bäumen zeigen das gleiche Stack, doch es ist nach Klassen (Baum CLASS in der Mitte) und nach Dateien (Baum FILE auf der rechten Seite) gruppiert.
Die Klassen- und Dateibäume haben einen eingebetteten Mechanismus zur Synchronisierung mit dem Hauptbaum des Stracks sowie bequeme Kontrollmöglichkeiten. Wenn Sie beispielsweise auf einen Klassennamen im Klassenbaum klicken, werden alle Funktionen dieser Klasse im Stack-Baum und im Dateibaum ausgewählt. Auf die gleiche Weise werden alle Funktionen und Klassen in einer Datei ausgewählt, wenn Sie auf einen Dateinamen klicken.
Dieser Mechanismus ermöglicht die schnelle Auswahl und Ansicht der erforderlichen Gruppen von Funktionen.Möglichkeiten der Arbeit mit dem Stack
- Hinzufügen von Watch-Parametern
Wie Sie bereits bemerkt haben, enthalten die Parameter des Knotens CNode das Struktur-Array tagWatch. Es wird nur für die bequeme Darstellung von Informationen erstellt. Es enthält einen benannten Wert einer Variable oder eines Ausdrucks.
Struktur eines Watch-Werts
//------------------------------------------------------------------ struct tagWatch struct tagWatch { string m_name; // name string m_val; // value };
Um einen neuen Watch-Wert zum aktuellen Knoten hinzuzufügen, müssen Sie die Funktion CTrace::AddWatch aufrufen und das Makro _WATCH benutzen.
#define _WATCH(w, v) if (!NIL(m_trace) && !NIL(m_trace.m_cur)) m_trace.m_cur.AddWatch(w, string(v));
Die spezielle Einschränkung hinzugefügter Werte (die gleiche wie bei Knoten) kontrolliert die Einzigartigkeit von Namen. Das bedeutet, dass der Name eines Watch-Werts auf Einzigartigkeit geprüft wird, bevor er zum Array CNode::m_watch[] hinzugefügt wird. Falls das Array einen Wert mit dem gleichen Namen enthält, wird der neue nicht hinzugefügt, aber der Wert des bestehenden wird aktualisiert.
Alle nachverfolgten Watch-Werte werden im Informationsfenster angezeigt.
- Schrittweise Ausführung des Codes
Eine weitere bequeme Funktion, die von MQL5 bereitgestellt wird, ist die Einrichtung einer erzwungenen Pause im Code während seiner Ausführung.
Die Pause wird mithilfe der einfachen Endlosschleife while (true) umgesetzt. Das Praktische an MQL5 ist in diesem Fall die Verarbeitung des Ereignisses des Verlassens dieser Schleife durch Klicken auf den roten Kontroll-Button. Um einen Unterbrechungspunkt während der Ausführung einzurichten, nutzen Sie die Funktion CTrace::Break.
Funktion für die Einrichtung von Unterbrechungspunkten
//------------------------------------------------------------------ Break void CTraceCtrl::Break() // checking the debug mode of a node { if(NIL(m_traceview)) return; // check of validity m_stack.BreakBy(TG_ALL,NULL,false); // removed the m_break flags from all nodes m_cur.m_break=true; // activated only at the current one m_traceview.m_viewstack.m_sid=m_cur.m_id; // moved selection to it m_stack.ExpandBy(TG_UP,m_cur,true,m_cur); // expand parent node if they are closed m_traceview.Draw(); // drew everything string name=m_traceview.m_viewstack.m_name+string(m_cur.m_id)+".dbg"; // got name of the BREAK button bool state=ObjectGetInteger(m_traceview.m_chart,name,OBJPROP_STATE); while(!state) // button is not pressed, execute the loop { Sleep(1000); // made a pause state=ObjectGetInteger(m_traceview.m_chart,name,OBJPROP_STATE); // check its state if(!m_traceview.IsOpenView()) break; // if the window is closed, exit m_traceview.Draw(); // drew possible changes } m_cur.m_break=false; // removed the flag m_traceview.Draw(); // drew the update }
Wird ein solcher Unterbrechungspunkt erreicht, werden die Stack-Bäume synchronisiert, um die Funktion anzuzeigen, die dieses Makro aufgerufen hat. Wenn ein Knoten geschlossen wird, wird der übergeordnete Knoten erweitert, um sie anzuzeigen. Und falls erforderlich, scrollt der Baum nach oben oder unten, um den Knoten in den Anzeigebereich zu bringen.
Klicken Sie zum Verlassen von CTraceCtrl::Break auf den roten Button neben dem Namen des Knotens.
Fazit
Nun verfügen wir über ein spannendes "Spielzeug". Beim Schreiben dieses Beitrags habe ich viele Varianten der Arbeit mit CTraceCtrl ausprobiert und mich davon überzeugt, dass MQL5 einzigartige Möglichkeiten der Steuerung von Expert Advisors und der Einrichtung ihrer Arbeit bietet. Alle Funktionen, die für die Entwicklung des Tracers verwendet wurden, sind in MQL4 nicht verfügbar, was die Vorteile von MQL5 und der breit gefächerten Möglichkeiten dieser Sprache abermals betont.
Im angehängten Code finden Sie alle in diesem Beitrag beschriebenen Klassen zusammen mit Service-Bibliotheken (in ihrem erforderlichen Mindestumfang, da sie hier nicht das Ziel sind). Zusätzlich habe ich das vorgefertigte Beispiel angehängt – aktualisierte Dateien der Standardbibliothek mit implementierten _IN-Makros. Alle Experimente wurden mit dem Expert Advisor durchgeführt, der im Standardpaket von MetaTrader 5 enthalten ist: MACD Sample.mq5.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/272
- 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.