Der Indikator Market Profile (Teil 2) Optimierung und Rendering auf Leinwand
Inhalt
Einführung
Im letzten Artikel haben wir uns mit dem Marktprofil-Indikator befasst. Wie sich herausstellt, verbraucht die Erstellung eines Marktprofils mit gewöhnlichen grafischen Objekten ziemlich viele Ressourcen. Jeder Preispunkt zwischen Tief und Hoch des Tagesbalkens wird mit rechteckigen grafischen Objekten in der Anzahl der Balken gefüllt, die dieses Preisniveau im Laufe des Tages erreicht haben. Dies gilt für jede Position - sie alle enthalten viele grafische Objekte, und alle diese Objekte werden für jeden Tag, an dem das Profildiagramm gezeichnet wird, erstellt und gezeichnet. Wenn ein Indikator Tausende von grafischen Objekten erstellt, kann dies zu erheblichen Verlangsamungen bei der Handhabung anderer grafischer Objekte und der Neuzeichnung des Charts führen.
Starten wir den Indikator auf dem M30-Chart und erstellen ein Marktprofil für nur drei Tage:

Das führt zur Erstellung von 4697 rechteckigen grafischen Objekten:

Dies ist wirklich keine optimale Nutzung der Ressourcen. Wenn wir die Anzahl der angezeigten Tage in den Einstellungen erhöhen, steigt die Anzahl der erstellten Objekte, die zum Zeichnen des Marktprofils auf dem Chart für jeden angezeigten Tag verwendet werden, drastisch an.
Aber hier zeichnen wir einfach Charte mit grafischen Objekten - Rechtecken. Ein kurzes Liniensegment des Profilhistogramms ist ein grafisches Objekt. Das bedeutet, dass wir nicht direkt auf dem Chart zeichnen können, sondern nur auf einem grafischen Objekt - einer Leinwand (canvas), die sich wiederum auf dem Chart entlang der gewünschten Koordinaten befindet. Dann haben wir nur ein (!) grafisches Objekt für einen Tag. Und drei Tage lang wird es drei Objekte statt 4697 geben! Das ist ein erheblicher Unterschied! Dies kann mit Hilfe der Klasse CCanvas geschehen, die das Rendering von nutzerdefinierten Bildern vereinfacht, die als Teil der Standardbibliothek des Client-Terminals bereitgestellt werden.
Die Version des Market Profile-Indikators, die das Profilhistogramm auf der Leinwand darstellt, ist im Terminal in der Datei \MQL5\Indikatoren\Freie Indikatoren\, MarketProfile Canvas.mq5 verfügbar. Bei der Untersuchung des Codes können wir feststellen, dass hier, anders als in der ersten Version (MarketProfile.mq5), die Grafikausgabe auf Objekten der Klasse CCanvas erfolgt. Die Logik des Indikators bleibt dieselbe, und wir haben sie bereits im Abschnitt „Struktur und Grundsätze“ des ersten Artikels erörtert. Das Rendering erfolgt mit der Klasse CMarketProfile, die auf CCanvas zeichnet.
Die Funktionsweise ist denkbar einfach:
- in einer Schleife mit der angegebene Anzahl von Tagen,
- ein Objekt der Klasse CMarketProfile für den aktuellen Tag in der Schleife erstellen oder abrufen,
- das Profil des Tages auf der Leinwand zeichnen oder neu zeichnen, das dem aktuellen Tag in der Schleife entspricht.
Die Hauptarbeit beim Zeichnen des Profildiagramms wird innerhalb der Klasse CMarketProfile ausgeführt. Werfen wir einen Blick auf die Struktur und Funktionsweise dieser Klasse.
Die Klasse CMarketProfile
Wir öffnen die Datei \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 und suchen darin den Code der Klasse CMarketProfile. Schauen wir uns an, was es dort gibt, und diskutieren wir, wozu das alles gut sein soll:
//+------------------------------------------------------------------+ //| Class to store and draw Market Profile for the daily bar | //+------------------------------------------------------------------+ class CMarketProfile { public: CMarketProfile() {}; CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]); ~CMarketProfile(void); //--- checks if the object was created for the specified date bool Check(string prefix, datetime time); //--- set high/low and array of intraday bars void SetHiLoBars(double high, double low, MqlRates &bars[]); //--- set canvas dimensions and drawing options void UpdateSizes(void); //--- is the profile in the visible part of the chart? bool isVisibleOnChart(void); //--- has the graph scale changed? bool isChartScaleChanged(void); //--- calculates profile by sessions bool CalculateSessions(void); //--- draws a profile void Draw(double multiplier=1.0); //--- protected: CCanvas m_canvas; // CCanvas class object for drawing profile uchar m_alpha; // alpha channel value that sets transparency string m_prefix; // unique prefix of the OBJ_BITMAP object string m_name; // name of the OBJ_BITMAP object used in m_canvas double m_high; // day's High double m_low; // day's Low datetime m_time1; // start time of the day datetime m_time2; // end time of the day int m_day_size_pt; // daily bar height in points int m_height; // daily bar height in pixels on the chart int m_width; // daily bar width in pixels on the chart MqlRates m_bars[]; // array of bars of the current timeframe between m_time1 and m_time2 vector m_asia; // array of bar counters for the Asian session vector m_europe; // array of bar counters for the European session vector m_america; // array of bar counters for the American session double m_vert_scale; // vertical scaling factor double m_hor_scale; // horizontal scaling factor };Die in der Klasse deklarierten öffentliche Methoden:
- Mit der Methode Check() wird geprüft, ob ein für einen bestimmten Tag erstelltes Marktprofilobjekt existiert;
- Die Methode SetHiLoBars() wird verwendet, um die Höchst- und Tiefstpreise des Tages in das Marktprofilobjekt zu setzen und ein Array von Intraday-Balken an das Objekt zu übergeben;
- Die Methode UpdateSizes() legt die Leinwandabmessungen und Skalierungsfaktoren für das Zeichnen von Rechtecken im Marktprofilobjekt fest;
- Die Methode isVisibleOnChart() gibt das Flag zurück, das anzeigt, dass das Marktprofil innerhalb der Sichtbarkeit des Charts liegt;
- Die Methode isChartScaleChanged() ist in der Klasse deklariert, aber nicht implementiert;
- Die Methode CalculateSessions() berechnet die Parameter und füllt die Arrays der Handelssitzungen;
- Die Methode Draw() zeichnet ein Marktprofilhistogramm auf der Leinwand, das auf Daten aus allen Handelssitzungen basiert.
Der Zweck von Variablen, die im geschützten Bereich einer Klasse deklariert sind, ist ziemlich klar. Ich möchte auf die Arrays der Zähler der Balken einer Sitzung eingehen.
Alle werden als Vektorvariablen deklariert, was es ermöglicht, sie wie Datenarrays zu behandeln, auch wenn dies etwas einfacher ist:
Die Verwendung von Vektoren und Matrizen oder besser gesagt von speziellen Methoden der entsprechenden Typen ermöglicht die Erstellung eines einfacheren, kürzeren und klareren Codes, der der mathematischen Notation nahe kommt. Mit diesen Methoden müssen wir keine verschachtelten Schleifen erstellen oder auf die korrekte Indizierung von Arrays in Berechnungen achten. Die Verwendung von Matrix- und Vektormethoden erhöht daher die Zuverlässigkeit und Geschwindigkeit bei der Entwicklung komplexer Programme.
Betrachten wir nun die Implementierung der deklarierten Klassenmethoden.
Konstruktor:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ void CMarketProfile::CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]): m_prefix(prefix), m_time1(time1), m_time2(time2), m_high(high), m_low(low), m_vert_scale(NULL), m_hor_scale(NULL) { //--- copy the array of intraday bars to the array of MqlRates structures, //--- create a name for the graphical object and define the size of the daily candle ArrayCopy(m_bars, bars); m_name=ExtPrefixUniq+"_MP_"+TimeToString(time1, TIME_DATE); m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT)); //--- set vector sizes for trading sessions m_asia=vector::Zeros(m_day_size_pt); m_europe=vector::Zeros(m_day_size_pt); m_america=vector::Zeros(m_day_size_pt); //--- set the width and height of the canvas UpdateSizes(); //--- if this is the first tick at the beginning of the day, then the canvas dimensions will be zero - set the dimensions to 1 pixel in height and width m_height=m_height?m_height:1; m_width=m_width?m_width:1; //--- create a graphical object if(m_canvas.CreateBitmap(m_name, m_time1, m_high, m_width, m_height, COLOR_FORMAT_ARGB_NORMALIZE)) ObjectSetInteger(0, m_name, OBJPROP_BACK, true); else { Print("Error creating canvas: ", GetLastError()); Print("time1=", m_time1, " high=", m_high, " width=", m_width, " height=", m_height); } }
Der parametrische Konstruktor erhält das Präfix des Namens des zu erstellenden Canvas-Objekts (auf dem das Tagesprofil gerendert werden soll), die Start- und Endzeit des Tages, die Höchst- und Tiefstpreise des Tages und ein Array von Intraday-Balken. Die Werte dieser Variablen werden auf die entsprechenden Klassenvariablen im Initialisierungsstring gesetzt. Weiter:
- wird das als Referenz übergebene Array in das Klassenarray kopiert, ein eindeutiger Name des grafischen Objekts aus dem in den Eingaben des Präfixes, der Abkürzung „_MP_“ und der Tagesöffnungszeit übergebenen erstellt und die Tageskerze in Punkten berechnet;
- jedes der Arrays der Handelssitzungen erhält eine Größe, die der Größe des Tagesbalkens in Punkten entspricht, und wird gleichzeitig mit Nullen gefüllt - initialisiert;
- werden die Abmessungen der Leinwand für das Zeichnen des Profils festgelegt, und wenn dies der erste Tick des Tages ist, wird die Größe auf Null gesetzt, und die Breite und Höhe werden auf die minimal zulässigen Abmessungen von einem Pixel in beiden Dimensionen festgelegt;
- wird eine Zeichenfläche mit den angegebenen Abmessungen erstellt.
Die Methode, mit der geprüft wird, ob ein für einen bestimmten Tag erstelltes Marktprofilobjekt vorhanden ist:
//+------------------------------------------------------------------+ //| Checks if CMarketProfile object is for the specified 'time' date | //+------------------------------------------------------------------+ bool CMarketProfile::Check(string prefix, datetime time) { string calculated= prefix+"_MP_"+TimeToString(time, TIME_DATE); return (m_name==(calculated)); };
Da der Name jedes Profil-Canvas-Objekts im Klassenkonstruktor festgelegt wird und der dargestellte Name eine Zeichenkette der Startzeit des Tages verwendet, wird zur Überprüfung, ob das Objekt für eine bestimmte Zeit erstellt wurde, die Startzeit des Tages an die Methode übergeben, eine Zeichenkette erstellt, der mit der Zeichenkette des Objektnamens identisch ist, und die erstellte Zeichenkette wird mit dem tatsächlichen Namen des Objekts verglichen. Das Ergebnis der Prüfung wird von der Methode zurückgegeben.
Die Methode zum Festlegen der Tageshöchst- und -tiefstpreise in einem Marktprofilobjekt und zur Übergabe eines Arrays von Intraday-Balken an das Objekt:
//+------------------------------------------------------------------+ //| Sets High/Low and a set of current-timeframe bars | //+------------------------------------------------------------------+ void CMarketProfile::SetHiLoBars(double high, double low, MqlRates &bars[]) { //--- if the maximum of the day has changed, move the OBJ_BITMAP object to the new Y coordinate if(high>m_high) { m_high=high; if(!ObjectSetDouble(0, m_name, OBJPROP_PRICE, m_high)) PrintFormat("Failed to update canvas for %s, error %d", TimeToString(m_time1, TIME_DATE), GetLastError()); } ArrayCopy(m_bars, bars); m_high=high; m_low=low; //--- daily range in points m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT)); //--- reset vector sizes for trading sessions m_asia=vector::Zeros(m_day_size_pt); m_europe=vector::Zeros(m_day_size_pt); m_america=vector::Zeros(m_day_size_pt); }
Die Methode empfängt die Hoch- und Tiefstwerte der Tageskerze sowie ein Array von Intraday-Balken im Format der Struktur von MqlRates als Referenz.
- In die Objektvariable wird der Höchstpreis geschrieben und die Leinwand wird an eine neue Koordinate verschoben.
- Die Intraday-Balken werden aus dem übergebenen Array von Balken in das interne Array kopiert.
- Das Tief des Tages wird auf die Klassenvariable gesetzt.
- Die neue Größe des Tagesbalken wird in Punkten berechnet.
- Die Arrays der Handelssitzungen werden um den berechneten Wert der Größe des Tagesbalkens in Punkten erhöht und mit Nullen gefüllt - initialisiert.
Es ist zu beachten, dass die Matrix- und Vektormethode Zeros() zur Initialisierung der Vektoren verwendet wird. Die Methode legt sowohl die Größe des Vektors fest als auch füllt das gesamte Array mit Nullen auf.
Für ein einfaches Array müssten wir zwei Operationen durchführen: ArrayResize() und ArrayInitialize().
Die Methode zum Festlegen der Leinwandabmessungen und Skalierungsfaktoren für das Zeichnen von Rechtecken im Marktprofilobjekt:
//+------------------------------------------------------------------+ //| Sets drawing parameters | //+------------------------------------------------------------------+ void CMarketProfile::UpdateSizes(void) { //--- convert time/price to x/y coordinates int x1, y1, x2, y2; ChartTimePriceToXY(0, 0, m_time1, m_high, x1, y1); ChartTimePriceToXY(0, 0, m_time2, m_low, x2, y2); //--- calculate canvas dimensions m_height=y2-y1; m_width =x2-x1; //--- calculate ratios for transforming vertical price levels //--- and horizontal bar counters to chart pixels m_vert_scale=double(m_height)/(m_day_size_pt); m_hor_scale =double(m_width*PeriodSeconds(PERIOD_CURRENT))/PeriodSeconds(PERIOD_D1); //--- change the canvas size m_canvas.Resize(m_width, m_height); }
Die Logik der Methode ist im Code kommentiert. Skalierungsverhältnisse werden verwendet, um die Größe der auf der Leinwand gezeichneten Rechtecke auf der Grundlage des Verhältnisses zwischen der Leinwandgröße und der Größe des Chartfensters festzulegen.
Die berechneten Verhältnisse werden bei der Berechnung der Höhe und Breite der gerenderten Rechtecke berücksichtigt.
Die Methode, die ein Flag zurückgibt, das besagt, dass das Marktprofil innerhalb der Chart-Sichtbarkeit liegt:
//+------------------------------------------------------------------+ //| Checks that the profile is in the visible part of the chart | //+------------------------------------------------------------------+ bool CMarketProfile::isVisibleOnChart(void) { long last_bar=ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); // last visible bar on the chart on the left long first_bar=last_bar+-ChartGetInteger(0, CHART_VISIBLE_BARS); // first visible bar on the chart on the right first_bar=first_bar>0?first_bar:0; datetime left =iTime(Symbol(), Period(), (int)last_bar); // time of the left visible bar on the chart datetime right=iTime(Symbol(), Period(), (int)first_bar); // time of the right visible bar on the chart //--- return a flag that the canvas is located inside the left and right visible bars of the chart return((m_time1>= left && m_time1 <=right) || (m_time2>= left && m_time2 <=right)); }
Hier finden wir die Nummern der linken und rechten sichtbaren Balken auf dem Chart, ermitteln ihre Zeit und geben das Flag zurück, dass die Zeit des linken und rechten Rands der Leinwand innerhalb des Bereichs der sichtbaren Balken auf dem Chart liegt.
Die Methode, die die Parameter berechnet und die Arrays der Handelssitzungen füllt:
//+------------------------------------------------------------------+ //| Prepares profile arrays by sessions | //+------------------------------------------------------------------+ bool CMarketProfile::CalculateSessions(void) { double point=SymbolInfoDouble(Symbol(), SYMBOL_POINT); // one point value //--- if the array of intraday bars is not filled, leave if(ArraySize(m_bars)==0) return(false); //---- iterate over all the bars of the current day and mark the cells of the arrays (vectors) that contain the bars being iterated over in the loop int size=ArraySize(m_bars); for(int i=0; i<size; i++) { //--- get the bar hour MqlDateTime bar_time; TimeToStruct(m_bars[i].time, bar_time); uint hour =bar_time.hour; //--- calculate price levels in points from the Low of the day reached by the price on each bar of the loop int start_box=(int)((m_bars[i].low-m_low)/point); // index of the beginning of price levels reached by the price on the bar int stop_box =(int)((m_bars[i].high-m_low)/point); // index of the end of price levels reached by the price on the bar //--- American session if(hour>=InpAmericaStartHour) { //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(int ind=start_box; ind<stop_box; ind++) m_america[ind]++; } else { //--- European session if(hour>=InpEuropeStartHour && hour<InpAmericaStartHour) //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(int ind=start_box; ind<stop_box; ind++) m_europe[ind]++; //--- Asian session else //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(int ind=start_box; ind<stop_box; ind++) m_asia[ind]++; } } //--- vectors of all sessions are ready return(true); }
Im vorangegangenen Artikel haben wir uns eingehend mit der Logik der Definition der Anzahl der Balken in einer Handelssitzung befasst, deren Kurs die Niveaus in Punkten vom Tiefst- bis zum Höchstkurs des Tages erreicht hat. Wurde in der vorherigen Version des Indikators all dies in der Hauptschleife des Indikators durchgeführt, so wird hier diese gesamte Berechnung in eine separate Methode des Tagesprofilobjekts ausgelagert. Hier geht es darum, die Anzahl der Balken zu zählen und in die Zellen des Arrays (Vektors) zu schreiben, die jedes in Punkten berechnete Preisniveau vom Tiefst- bis zum Höchststand des Tages überschreiten. Nachdem die Methode ihre Arbeit beendet hat, werden alle Vektoren entsprechend der Preisbewegung auf den Preisstufen gefüllt. Die Anzahl der Balken, die jede Ebene überschritten haben, wird in den entsprechenden Zellen des Arrays (Vektors) eingetragen.
Die Methode, die ein Histogramm des Marktprofils auf der Leinwand zeichnet, das auf den Daten aller Handelssitzungen basiert:
//+------------------------------------------------------------------+ //| Draw Market Profile on the canvas | //+------------------------------------------------------------------+ void CMarketProfile::Draw(double multiplier=1.0) { //--- sum up all sessions for rendering vector total_profile=m_asia+m_europe+m_america; // profile that combines all sessions vector europe_asia=m_asia+m_europe; // profile that combines only the European and Asian sessions //--- set a completely transparent background for the canvas m_canvas.Erase(ColorToARGB(clrBlack, 0)); //--- variables for drawing rectangles int x1=0; // X coordinate of the left corner of the rectangle always starts at zero int y1, x2, y2; // rectangle coordinates int size=(int)total_profile.Size(); // size of all sessions //--- render the American session with filled rectangles for(int i=0; i<size; i++) { //--- skip zero vector values if(total_profile[i]==0) continue; //--- calculate two points to draw a rectangle, x1 is always 0 (X of the lower left corner of the rectangle) y1=m_height-int(i*m_vert_scale); // Y coordinate of the lower left corner of the rectangle y2=(int)(y1+m_vert_scale); // Y coordinate of the upper right corner of the rectangle x2=(int)(total_profile[i]*m_hor_scale*multiplier); // X coordinate of the upper right corner of the rectangle //--- draw a rectangle at the calculated coordinates with the color and transparency set for the American session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAmericaSession, InpTransparency)); } //--- render the European session with filled rectangles for(int i=0; i<size; i++) { //--- skip zero vector values if(total_profile[i]==0) continue; //--- calculate two points to draw a rectangle y1=m_height-int(i*m_vert_scale); y2=(int)(y1+m_vert_scale); x2=(int)(europe_asia[i]*m_hor_scale*multiplier); //--- draw a rectangle over the rendered American session using the calculated coordinates //--- with color and transparency set for the European session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpEuropeSession, InpTransparency)); } //--- draw the Asian session with filled rectangles for(int i=0; i<size; i++) { //--- skip zero vector values if(total_profile[i]==0) continue; //--- calculate two points to draw a rectangle y1=m_height-int(i*m_vert_scale); y2=(int)(y1+m_vert_scale); x2=(int)(m_asia[i]*m_hor_scale*multiplier); //--- draw a rectangle over the rendered European session using the calculated coordinates //--- with color and transparency set for the Asian session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAsiaSession, InpTransparency)); } //--- update the OBJ_BITMAP object without redrawing the chart m_canvas.Update(false); }
Die Methodenlogik wurde in den Codekommentaren ausführlich beschrieben. Kurz gesagt, wir haben Arrays (Vektoren) von drei Sitzungen - asiatisch, europäisch und amerikanisch - berechnet und gefüllt. Es ist erforderlich, für jede Sitzung ein Profilhistogramm zu erstellen. Zuerst wird die amerikanische Sitzung gerendert, dann wird die europäische Sitzung darüber gerendert und schließlich wird die asiatische Sitzung über den beiden gezeichneten Sitzungen gerendert.
Warum werden die Sitzungen in umgekehrter Reihenfolge ihrer Laufzeit dargestellt?
- Die amerikanische Sitzung, oder besser gesagt ihr Histogramm, enthält sowohl die bereits gehandelte Zeit der beiden vorangegangenen Sitzungen als auch die Zeit der amerikanischen Sitzung, d. h. es handelt sich um das vollständigste Histogramm des Profils des gesamten Tages. Aus diesem Grund wird sie zuerst wiedergegeben.
- Dann wird die europäische Sitzung wiedergegeben, die die Zeit der bereits gehandelten asiatischen Sitzung enthält. Da es hier nur zwei Sitzungen gibt - die asiatische und die europäische - wird das Histogramm auf der X-Achse der amerikanischen Sitzung kürzer sein, was bedeutet, dass es über der amerikanischen Sitzung gerendert werden muss.
- Dann wird das kürzeste Histogramm der asiatischen Sitzung entlang der X-Achse wiedergegeben.
Ich möchte anmerken, wie praktisch es ist, Array-Daten bei der Verwendung von Vektoren zu kombinieren:
//--- sum up all sessions for rendering vector total_profile=m_asia+m_europe+m_america; // profile that combines all sessions vector europe_asia=m_asia+m_europe; // profile that combines only the European and Asian sessions
Im Wesentlichen handelt es sich um eine Element-für-Element-Verkettung mehrerer Arrays derselben Größe zu einem einzigen Array, das durch den folgenden Code dargestellt werden kann:
#define SIZE 3 double array_1[SIZE]={0,1,2}; double array_2[SIZE]={3,4,5}; double array_3[SIZE]={6,7,8}; Print("Contents of three arrays:"); ArrayPrint(array_1); ArrayPrint(array_2); ArrayPrint(array_3); for(int i=0; i<SIZE; i++) { array_1[i]+=array_2[i]+=array_3[i]; } Print("\nResult of the merge:"); ArrayPrint(array_1); /* Contents of three arrays: 0.00000 1.00000 2.00000 3.00000 4.00000 5.00000 6.00000 7.00000 8.00000 Result of the merge: 9.00000 12.00000 15.00000 */
Der nachstehende Code bewirkt das Gleiche wie die Codezeile in der oben beschriebenen Methode:
vector total_profile=m_asia+m_europe+m_america; // profile that combines all sessions
Ich denke, es ist unnötig zu sagen, wie viel bequemer und übersichtlicher der Code ist...
Das erstellte Canvas-Objekt wird im Destruktor der Klasse gelöscht und das Chart wird neu gezeichnet, um die Änderungen anzuzeigen:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CMarketProfile::~CMarketProfile(void) { //--- delete all graphical objects after use ObjectsDeleteAll(0, m_prefix, 0, OBJ_BITMAP); ChartRedraw(); }
Anstatt mit grafischen Objekten in der Indikatorschleife zu zeichnen, genügt es nun, für jeden Tagesbalken eine Instanz der betrachteten Klasse zu erstellen, die Daten aller Sitzungen zu berechnen und ein Marktprofilhistogramm für jeden Tag auf der Leinwand zu zeichnen. Die Anzahl der grafischen Objekte, die erstellt werden, hängt von der Anzahl der Tage ab, die in den Einstellungen für die Profildarstellung angegeben sind, im Gegensatz zur vorherigen Version des Indikators, bei der jede Linie des Histogramms mit einem eigenen grafischen Objekt gezeichnet wird.
Optimierung des Indikators
Schauen wir uns nun an, wie der Indikator mit Hilfe der Klasse Marktprofil erstellt wird. Öffnen wir die Indikator-Datei \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 von Anfang an und schauen sie uns an.
Zunächst einmal sind die Klassendateien für die vereinfachte Erstellung von nutzerdefinierten Renderings von CCanvas sowie die Klassendatei für die Erstellung von stark typisierten Listen CArrayList<T> enthalten:
//+------------------------------------------------------------------+ //| MarketProfile Canvas.mq5 | //| Copyright 2009-2024, MetaQuotes Ltd | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #property indicator_plots 0 #include <Canvas\Canvas.mqh> #include <Generic\ArrayList.mqh> //--- input parameters
Es folgen die Liste der Eingänge des Indikators, ein eindeutiges Präfix der grafischen Objekte, die angegebene Marktprofilklasse und die angegebene Liste der Klassenobjekte:
//--- input parameters input uint InpStartDate =0; /* day number to start calculation */ // 0 - current, 1 - previous, etc. input uint InpShowDays =7; /* number of days to display */ // starting with and including the day in InpStartDate input int InpMultiplier =1; /* histogram length multiplier */ input color InpAsiaSession =clrGold; /* Asian session */ input color InpEuropeSession =clrBlue; /* European session */ input color InpAmericaSession =clrViolet; /* American session */ input uchar InpTransparency =150; /* Transparency, 0 = invisible */ // market profile transparency, 0 = fully transparent input uint InpEuropeStartHour =8; /* European session opening hour */ input uint InpAmericaStartHour=14; /* American session opening hour */ //--- unique prefix to identify graphical objects belonging to the indicator string ExtPrefixUniq; //--- declare CMarketProfile class class CMarketProfile; //--- declare a list of pointers to objects of the CMarketProfile class CArrayList<CMarketProfile*> mp_list;
Da die Marktprofilklasse unterhalb des Indikatorcodes geschrieben wird, ist eine Vorwärts-Deklaration der Klasse erforderlich, um den Fehler eines unbekannten Variablentyps bei der Kompilierung zu vermeiden:
'CMarketProfile' - unexpected token
Die stark typisierte Liste enthält Zeiger auf Objekte der Klasse CMarketProfile, die unten im Code festgelegt sind.
In OnInit() erstellen wir das Präfix der grafischen Objekte als die letzten 4 Ziffern der Anzahl der Millisekunden, die seit dem Systemstart vergangen sind:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- create a prefix for object names string number=StringFormat("%I64d", GetTickCount64()); ExtPrefixUniq=StringSubstr(number, StringLen(number)-4); Print("Indicator \"Market Profile Canvas\" started, prefix=", ExtPrefixUniq); return(INIT_SUCCEEDED); }
Schauen wir uns den vollständigen Code von OnCalculate() an:
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- opening time of the current daily bar datetime static open_time=0; //--- number of the last day for calculations //--- (if InpStartDate = 0 and InpShowDays = 3, lastday = 3) //--- (if InpStartDate = 1 and InpShowDays = 3, lastday = 4) etc ... uint lastday=InpStartDate+InpShowDays; //--- if the first calculation has already been made if(prev_calculated!=0) { //--- get the opening time of the current daily bar datetime current_open=iTime(Symbol(), PERIOD_D1, 0); //--- if we do not calculate the current day if(InpStartDate!=0) { //--- if the opening time was not received, leave if(open_time==current_open) return(rates_total); } //--- update opening time open_time=current_open; //--- we will only calculate one day from now on, since all other days have already been calculated during the first run lastday=InpStartDate+1; } //--- in a loop for the specified number of days (either InpStartDate+InpShowDays on first run, or InpStartDate+1 on each tick) for(uint day=InpStartDate; day<lastday; day++) { //--- get the data of the day with index day into the structure MqlRates day_rate[]; //--- if the indicator is launched on weekends or holidays when there are no ticks, you should first open the daily chart of the symbol //--- if we have not received bar data for the day index of the daily period, we leave until the next call to OnCalculate() if(CopyRates(Symbol(), PERIOD_D1, day, 1, day_rate)==-1) return(prev_calculated); //--- get day start and end time datetime start_time=day_rate[0].time; datetime stop_time=start_time+PeriodSeconds(PERIOD_D1)-1; //--- get all intraday bars of the current day MqlRates bars_in_day[]; if(CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day)==-1) return(prev_calculated); CMarketProfile *market_profile; //--- if the Market Profile has already been created and its drawing has been performed earlier if(prev_calculated>0) { //--- find the Market Profile object (CMarketProfile class) in the list by the opening time of the day with the 'day' index market_profile=GetMarketProfileByDate(ExtPrefixUniq, start_time); //--- if the object is not found, return zero to completely recalculate the indicator if(market_profile==NULL) { PrintFormat("Market Profile not found for %s. Indicator will be recalculated for all specified days", TimeToString(start_time, TIME_DATE)); return(0); } //--- CMarketProfile object is found in the list; set it to High and Low values of the day and pass the array of intraday bars //--- in this case, the object is shifted to a new coordinate corresponding to the High of the daily candle, and all arrays (vectors) are reinitialized market_profile.SetHiLoBars(day_rate[0].high, day_rate[0].low, bars_in_day); } //--- if this is the first calculation else { //--- create a new object of the CMarketProfile class to store the Market Profile of the day with 'day' index market_profile = new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[0].high, day_rate[0].low, bars_in_day); //--- add a pointer to the created CMarketProfile object to the list mp_list.Add(market_profile); } //--- set canvas dimensions and line drawing parameters market_profile.UpdateSizes(); //--- calculate profiles for each trading session market_profile.CalculateSessions(); //--- draw the Market Profile market_profile.Draw(InpMultiplier); } //--- redraw the chart after the loop has been completed and all objects have been created and updated ChartRedraw(0); //--- return the number of bars for the next OnCalculate call return(rates_total); }
Die Logik der Timer ist in den Kommentaren zum Code ausführlich beschrieben. Kurz gesagt, ist sie wie folgt:
- In einer Schleife nach der Anzahl der angezeigten Marktprofiltage;
- den Tag in die Struktur aufnehmen, der dem Schleifenindex entspricht;
- die Anzahl der Balken des aktuellen Chartzeitraums abrufen, die in dem in der Schleife ausgewählten Tag enthalten sind;
- entweder ein zuvor erstelltes Marktprofilobjekt für den ausgewählten Tag abrufen oder ein neues erstellen, wenn es noch nicht in der Liste enthalten ist;
- die Größe des Tagesbalkens von Tief bis Hoch in Chart-Pixeln ermitteln und die Arrays (Vektoren) der Handelssitzungen reinitialisieren;
- entsprechend der neuen Größe des Balkens des ausgewählten Tages, ändern wir die Größe der Leinwand;
- das Marktprofil des Tages für jede Sitzung neu zu berechnen;
- die Profile der einzelnen Handelssitzungen auf der Leinwand neu zeichnen.
- Am Ende der Schleife zeichnen wir das Chart neu.
In OnDeinit() des Indikators löschen wir alle erstellten grafischen Objekte:
//+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete all Market Profile graphical objects after use Print("Indicator \"Market Profile Canvas\" stopped, delete all objects CMarketProfile with prefix=", ExtPrefixUniq); //--- in a loop by the number of CMarketProfile objects in the list int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- get the pointer to the CMarketProfile object from the list by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- if the pointer is valid and the object exists, delete it if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) delete market_profile; } //--- redraw the chart to display the result immediately ChartRedraw(0); }
Wir ändern in der Ereignisbehandlung duch OnChartEvent() die Leinwandgröße für jeden Tag des Marktprofils:
//+------------------------------------------------------------------+ //| Custom indicator chart's event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { //--- if this is a user event, leave if(id>=CHARTEVENT_CUSTOM) return; //--- if there is a chart change, update the sizes of all objects of the CMarketProfile class with redrawing the chart if(CHARTEVENT_CHART_CHANGE==id) { //--- in a loop by the number of CMarketProfile objects in the list int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- get the pointer to the CMarketProfile object by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- if the object is received and if it is in the visible area of the chart if(market_profile) if(market_profile.isVisibleOnChart()) { //--- update canvas dimensions and redraw market profile histograms market_profile.UpdateSizes(); market_profile.Draw(InpMultiplier); } } //--- update the chart after recalculating all Profiles ChartRedraw(); } }
Da die Skalierung der Chartanzeige vertikal und horizontal geändert werden kann, sollten grafische Objekte mit Handelssitzungs-Histogrammen auch in der Größe an die neuen Chartgrößen angepasst werden. Daher sollten in der Ereignisbehandlung Handler bei einer Änderung des Charts alle Objekte der Klasse CMarketProfile in der Größe aktualisiert und auf der Leinwand neu gezeichnet werden, die eine neue Größe entsprechend dem neuen Maßstab des Charts erhalten hat.
Die Funktion, die ein Marktprofilobjekt zurückgibt, das für eine bestimmte Tagesstartzeit erstellt wurde:
//+------------------------------------------------------------------+ //| Returns CMarketProfile or NULL by the date | //+------------------------------------------------------------------+ CMarketProfile* GetMarketProfileByDate(string prefix, datetime time) { //--- in a loop by the number of CMarketProfile objects in the list int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- get the pointer to the CMarketProfile object by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- if the pointer is valid and the object exists, if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) { //--- if the Market Profile object obtained by the pointer was created for the required time, return the pointer if(market_profile.Check(prefix, time)) return(market_profile); } } //--- nothing found - return NULL return(NULL); }
Die Funktion wird in der Indikatorschleife nach Handelstagen verwendet und liefert einen Zeiger auf das Objekt der Klasse CMarketProfile aus der Liste, die für einen Tagesbalken mit einer bestimmten Tageseröffnungszeit erstellt wurde. Die Funktion ermöglicht es uns, das gewünschte Objekt rechtzeitig zu erhalten, um es weiter zu aktualisieren.
Schlussfolgerung
Wir haben die Möglichkeit in Betracht gezogen, den Indikatorcode zu optimieren, um den Ressourcenverbrauch zu verringern. Wir haben Tausende von grafischen Objekten entfernt und sie durch ein einziges grafisches Objekt für einen einzigen Tag ersetzt, für den das Marktprofil gerendert wird.
Als Ergebnis der Optimierung wird jeder Handelstag in der in den Einstellungen angegebenen Anzahl (standardmäßig 7) auf einer eigenen Leinwand (OBJ_BITMAP-Objekt) angezeigt, auf der drei Handelssitzungen in Form von Histogrammen dargestellt werden - asiatisch, europäisch und amerikanisch, jede in ihrer eigenen, in den Einstellungen angegebenen Farbe. Drei Handelstage lang wird das Marktprofil letztlich so aussehen:

Hier haben wir nur drei grafische Objekte, auf denen die Histogramme der Handelssitzungen mit Hilfe der Klasse CCanvas gezeichnet werden. Es ist deutlich zu sehen, dass das erneute Rendern von sogar drei Bitmap-Grafikobjekten im laufenden Betrieb zu einem deutlichen Flackern und Zucken der Bilder führt. Dies deutet darauf hin, dass der Code noch weiter optimiert werden kann. Auf jeden Fall haben wir jetzt statt mehrerer tausend grafischer Objekte nur noch drei. Dies führt zu einer spürbaren Verbesserung des Ressourcenverbrauchs. Visuelle Artefakte können durch eine weitere Analyse des Codes korrigiert werden (denken Sie z. B. an die nicht implementierte Methode isChartScaleChanged() der Klasse CMarketProfile, die es uns ermöglicht, das Chart nur dann neu zu zeichnen, wenn sich der Maßstab tatsächlich ändert).
Zusammenfassend können wir mit Sicherheit sagen, dass jeder Code immer optimiert werden kann. Auch wenn dies den Rückgriff auf ein anderes Konzept für die Konstruktion der visuellen Komponente erfordern könnte, wie es in diesem Indikator geschieht.
Der Artikel enthält eine vollständig kommentierte Indikatordatei, die Sie herunterladen und selbst studieren und, wenn Sie es wünschen, weiter optimieren können.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/16579
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Neuronale Netze im Handel: Ein parameter-effizienter Transformer mit segmentierter Aufmerksamkeit (PSformer)
Neuronale Netze im Handel: Verbesserung des Wirkungsgrads der Transformer durch Verringerung der Schärfe (letzter Teil)
Erstellen von 3D-Balken auf der Grundlage von Zeit, Preis und Volumen
Zyklen im Handel
- 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.
Warum nicht ein perfektes Bandprofil schreiben?
Was ist mit "perfekt" gemeint?
Warum nicht das perfekte Volumenprofil schreiben
Das ist ein interessantes Thema. Ich bin auch auf Canvas umgestiegen, um mein Profil anzuzeigen. Aber diese Methode hat einige Nachteile. Wenn grafische Figuren "gezeichnet und vergessen" werden, skaliert mt5 sie selbst, wenn man den Chart bewegt, die TF ändert, usw. Aktionen, dann sollte Canvas immer neu gezeichnet werden, bei jedem CHARTEVENT_CHART_CHANGE, auch wenn VP irgendwo in der Geschichte ist, auf bereits unveränderten Daten. Und diese Daten müssen beschafft werden und nicht immer sind sie (Tickdaten) im mt5-Cache, hier entweder den Speicher speichern und prüfen, ob sich die VP-Grenzen geändert haben oder ständig von mt5 anfordern (jetzt habe ich es so implementiert), was nicht schnell funktioniert und so werden die Daten nicht immer vollständig gegeben, und nicht sofort sicher.
Das Zeichnen ist ein Teil des Problems. Der zweite Teil ist, dass ein Trader nicht immer nur ein Bild braucht, um es zu bewundern, er braucht Signale, die auf diesem Bild basieren, wie POC, VHL, VLL, "entry into the void", etc. Und wir können Ereignisse nur an grafische Objekte binden (bedingt, wir können sie in einem Array irgendwo im Indikator aufbewahren, aber es ist immer noch wünschenswert zu visualisieren, wo dieses Niveau ist, wo der Alarm ausgelöst wurde). Das heißt, wir brauchen immer noch grafische Objekte für die Kanvas-VP, obwohl sie natürlich um eine oder sogar mehrere Größenordnungen kleiner sind als bei der Darstellung der VP durch grafische Objekte.
Generell lässt sich zusammenfassen, dass wir (noch?) kein volumetrisches Terminal von mt5 bekommen können (und wir brauchen Cluster, Deltas usw.).
Das Bild ist ein Beispiel dafür, wie VP auf einer Impulswelle aussieht, POC wird durch eine Linie dargestellt, hier sind Tick-Volumina, wenn es echte Volumina gäbe, würde ich mit ihnen arbeiten (finam, amp global). Wie die Praxis des Vergleichs von Tick-Volumen und realen Volumina bei älteren TFs gezeigt hat, stimmen sie entweder überein oder liegen sehr nahe beieinander, außer in Fällen, in denen große Volumina durch die Hoch- oder Tiefkerzen gehen; hier sind die Tick-Volumina machtlos, uns zu helfen.
Das ist ein interessantes Thema. Ich bin auch auf Canvas umgestiegen, um mein Profil anzuzeigen. Aber diese Methode hat einige Nachteile. Wenn grafische Figuren "gezeichnet und vergessen" werden, skaliert mt5 sie selbst, wenn man den Chart bewegt, die TF ändert, usw. Aktionen, dann sollte Canvas immer neu gezeichnet werden, bei jedem CHARTEVENT_CHART_CHANGE, auch wenn VP irgendwo in der Geschichte ist, auf bereits unveränderten Daten. Und diese Daten müssen beschafft werden und nicht immer sind sie (Tickdaten) im mt5-Cache, hier entweder den Speicher speichern und prüfen, ob sich die VP-Grenzen geändert haben oder ständig von mt5 anfordern (jetzt habe ich es so implementiert), was nicht schnell funktioniert und die Daten nicht immer vollständig gegeben sind, und nicht sofort sicher.
Das Zeichnen ist ein Teil des Problems. Der zweite Teil ist, dass ein Trader nicht immer nur ein Bild braucht, um es zu bewundern, er braucht Signale, die auf diesem Bild basieren, wie POC, VHL, VLL, "entry into the void", etc. Und wir können Ereignisse nur an grafische Objekte binden (bedingt, wir können sie in einem Array irgendwo im Indikator aufbewahren, aber es ist immer noch wünschenswert zu visualisieren, wo dieses Niveau ist, wo der Alarm ausgelöst wurde). D.h. wir brauchen immer noch grafische Objekte für die Canvas-VP, obwohl sie natürlich um eine oder sogar mehrere Größenordnungen kleiner sein werden als bei der Darstellung der VP durch grafische Objekte.
Im Allgemeinen lautet die Zusammenfassung wie folgt: Wir können (noch?) kein volumetrisches Terminal von mt5 bekommen (und wir brauchen Cluster, Deltas, etc.).
Das Bild ist ein Beispiel dafür, wie VP auf einer Impulswelle aussieht, POC wird durch eine Linie dargestellt, hier sind Tick-Volumina, wenn es echte Volumina gäbe, würde ich mit ihnen arbeiten (finam, amp global). Wie die Praxis des Vergleichs von Tick-Volumen und realen Volumina bei älteren TFs gezeigt hat, stimmen sie entweder überein oder liegen sehr nahe beieinander, außer in Fällen, in denen große Volumina durch die Hoch- oder Tiefkerzen gehen; hier sind die Tick-Volumina machtlos, uns zu helfen.
Das ist nicht ganz so. Dies sind keine Nachteile, sondern Eigenschaften, die Sie kennen müssen, um das Tool mit maximalen Möglichkeiten und Handlungsfreiheit zu nutzen.

Alles ist möglich, auch wenn es sich um 3D-Cluster handelt.
Hier ist ein MQL5-Beispiel , das nur Canvas verwendet, ohne OpenCL zu nutzen. Wenn Sie OpenCL (d. h. eine Grafikkarte) verwenden, können Sie eine erhebliche Beschleunigung (um das Dutzendfache) erreichen.