Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement
Einführung
In unserem vorherigen Artikel (Teil 4) haben wir den Indikator Smart WaveTrend Crossover in MetaQuotes Language 5 (MQL5) entwickelt, der zwei Oszillatoren verwendet – einen für Signale und einen für die Trendfilterung –, um Crossover-basierte Kauf- und Verkaufswarnungen mit optionaler Trendbestätigung zu erzeugen. In Teil 5 erweitern wir den WaveTrend Crossover-Indikator mit Zeichnungen auf der Basis von Canvas für Überlagerungen mit Nebelverläufen, Signalkästchen zur Erkennung von Ausbrüchen, anpassbaren Kauf- und Verkaufsblasen oder -dreiecken für visuelle Warnungen und integriertem Risikomanagement durch dynamische Take-Profit- und Stop-Loss-Niveaus. Diese Weiterentwicklung fügt fortschrittliche visuelle Elemente wie Nebelverläufe für den Marktkontext hinzu, neben Optionen für Trendfilterung, Box-Erweiterungen und Berechnungen über Kerzenmultiplikatoren oder Prozentsätze, die mit Linien und Tabellen angezeigt werden. Wir werden folgende Themen behandeln:
- Das erweiterte, canvasbasierte WaveTrend Crossover Framework mit visuellen und Risikofunktionen verstehen
- Implementation in MQL5
- Backtests
- Schlussfolgerung
Am Ende haben Sie einen funktionsfähigen MQL5-Indikator für erweiterte WaveTrend-Crossover mit visuellen und Risiko-Elementen, bereit für die Anpassung – lassen Sie uns einsteigen!
Das erweiterte, canvasbasierte WaveTrend Crossover Framework mit visuellen und Risikofunktionen verstehen
Das verbesserte WaveTrend Crossover-Framework auf der Basis von Canvas baut auf dem zentralen Momentum-Oszillator auf, indem es visuelle Overlays und Risikotools einbezieht, um uns eine realistischere und praktischere Handelsoberfläche zu bieten. Es werden zwei WaveTrend-Konfigurationen beibehalten – eine empfindliche Konfiguration zur Erkennung von Überkreuzungen, die potenzielle Einstiegspunkte signalisieren, und eine langsamere Konfiguration zum Filtern von Trends –, während gleichzeitig die Erkennung von Ausbrüchen durch Signalkästchen hinzugefügt wird, die sich um Überkreuzungspunkte herum bilden und bei Preisdurchbrüchen schließen, um bestätigte Momentumverschiebungen anzuzeigen. Über das Chart werden Nebelverläufe gelegt, die die Stärke eines Trends durch abnehmende Transparenz visuell darstellen und es uns ermöglichen, den Marktkontext auf einen Blick einzuschätzen. Hinzu kommen anpassbare Signale, die als Blasen mit Beschriftungen oder als einfache Dreiecke angezeigt werden und klare Kauf- und Verkaufshinweise liefern.
In einem Aufwärtstrend löst ein Aufwärts-Crossover des Signaloszillators, der optional durch einen Aufwärtstrend des langsameren Oszillators bestätigt wird, eine Box um den Bereich des Balkens aus; bei einem Ausbruch aus der Box nach oben wird ein Kaufsignal ausgelöst, wenn es mit der Richtung der Box übereinstimmt, wobei die Gelegenheit visuell hervorgehoben wird. Umgekehrt bildet ein abwärts gerichteter Crossover eine Box, und ein Ausbruch nach unten erzeugt unter entsprechenden Bedingungen ein Verkaufssignal, sodass wir auf Umkehrungen oder Fortsetzungen mit geringerem Rauschen reagieren können. Das Risikomanagement ist integriert, indem Take-Profit- und Stop-Loss-Niveaus auf der Grundlage durchschnittlicher Kerzengrößen oder prozentualer Bewegungen berechnet werden, die dynamisch angezeigt werden, um die Positionsgröße und die Ausstiegsplanung zu unterstützen. Auf diese Weise können wir die Trefferquote ermitteln.
Wir werden die MQL5-Bibliothek Canvas für die Darstellung von Nebelverläufen nutzen, die zwischen den Balken für eine glatte Trendvisualisierung interpolieren, Signalkästchen verfolgen, um Ausbrüche mit optionalen Erweiterungen unter Verwendung von durchschnittlichen Kerzenmultiplikatoren zu erkennen und zu schließen, flexible Signaltypen wie beschriftete Blasen für eine verbesserte Lesbarkeit anbieten und Risikoniveaus mit nutzerdefinierten Modi für Take-Profit und Stop-Loss berechnen, während wir gleichzeitig ein effizientes Neuzeichnen bei Chartänderungen sicherstellen. Kurz gesagt, hier ist eine visuelle Darstellung unserer Ziele.

Implementation in MQL5
Um mit der Implementierung der Erweiterungen zu beginnen, müssen wir zunächst die internen Puffer des Indikators anpassen, um zusätzlichen Datenspeicher für die zusätzlichen Funktionen zu schaffen, die wir hinzufügen werden.
//+------------------------------------------------------------------+ //| 1. Smart WaveTrend Crossover PART2.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property indicator_chart_window #property indicator_buffers 28 #property indicator_plots 3 #property indicator_label1 "Colored Candles" #property indicator_type1 DRAW_COLOR_CANDLES #property indicator_color1 clrTeal, clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 #property indicator_label2 "Buy Signals" #property indicator_type2 DRAW_ARROW #property indicator_color2 clrForestGreen #property indicator_style2 STYLE_SOLID #property indicator_width2 1 #property indicator_label3 "Sell Signals" #property indicator_type3 DRAW_ARROW #property indicator_color3 clrOrangeRed #property indicator_style3 STYLE_SOLID #property indicator_width3 1
Hier erhöhen wir einfach die Puffer für die Indikatoren von 23 auf 28, um die zusätzlichen Berechnungen für die zusätzlichen Funktionen zu bewältigen. Wir haben die spezifischen Änderungen für die Zeilen mit den Änderungen zur Verdeutlichung hervorgehoben. Als Nächstes binden wir die Bibliothek Canvas für nutzerdefinierte Zeichnungen in das Chart ein. Sie wird benötigt, um fortgeschrittene grafische Elemente wie Nebelverläufe, nutzerdefinierte Boxen und Beschriftungen zu ermöglichen, die von den standardmäßigen MQL5-Darstellungsfunktionen nicht unterstützt werden, um die visuelle Darstellung von Signalen und Trends zu verbessern. Hier ist der Ansatz, den wir verwendet haben, um dies zu erreichen.
#include <Canvas/Canvas.mqh>
Mit „#include <Canvas/Canvas.mqh>“ binden wir die Bibliothek Canvas ein, die die Klassen und Funktionen für nutzerdefinierte grafische Darstellungen auf dem Chart bereitstellt und es uns ermöglicht, fortgeschrittene visuelle Darstellungen wie Nebelverläufe, Boxen und Beschriftungen programmatisch zu rendern, ohne auf Standard-Charttypen angewiesen zu sein. Als Nächstes müssen wir weitere Eingabeparameter hinzufügen, um die Steuerung über die Schnittstelle zu verbessern.
input group "Signal Settings" input bool use_trend_filter = true; // Use Trend Filter for Boxes? enum signal_options { Triangles, // Triangles Labels_Buy_Sell // Labels Buy Sell }; input signal_options signal_type = Labels_Buy_Sell; // Signal Type input color signal_buy_col = clrForestGreen; // Buy Signal Color input color signal_sell_col = clrOrangeRed; // Sell Signal Color input bool show_only_matching = true; // Show Only Matching Signals? input bool use_box_multiplier = false; // Extend Box by Average Candle Size? input double box_multiplier = 1.0; // Box Extension Multiplier input int base_offset = 10; // Base Signal Offset from Candle input group "Box Settings" input color box_bull_fill = clrBlue; // Box Bull Fill Color input color box_bear_fill = clrGold; // Box Bear Fill Color input int box_fill_transp = 80; // Box Fill Transparency (0-100) input group "Fog" input bool show_fog = true; // Fog input double offset_mult = 0.7; // Fog Height × Avg Candle input int base_transp = 80; // Base Transparency input int transp_inc = 4; // Transparency Increment input group "Risk Management" input bool showTPSL = true; // Show TP/SL Levels enum tp_sl_modes { Candle_Multiplier, // Candle Multiplier Percentage // Percentage }; input tp_sl_modes tpSlMode = Candle_Multiplier; // TP/SL Calculation Mode input int tp_sl_length = 50; // Average Candle Length Period input double tp1Multiplier = 2.0; // TP1× input double tp2Multiplier = 3.0; // TP2× input double tp3Multiplier = 4.0; // TP3× input double slMultiplier = 2.0; // SL× input double tp1Percent = 2.0; // TP1 % input double tp2Percent = 3.0; // TP2 % input double tp3Percent = 4.0; // TP3 % input double slPercent = 1.5; // SL %
Wir fahren damit fort, Nutzereingaben in gruppierten Abschnitten zu definieren, um die Anpassung von erweiterten Funktionen zu ermöglichen. In der Gruppe „Signal Settings“ gibt es eine boolesche Eingabe, die standardmäßig auf true gesetzt ist, um die Trendfilterung auf Box-basierte Signale anzuwenden, gefolgt von der Enumeration „signal_options“ mit den Auswahlmöglichkeiten „Triangles“ für Pfeildarstellungen oder „Labels_Buy_Sell“ für Textblasen, die standardmäßig auf letztere gesetzt ist, um die Art der Signalvisualisierung zu bestimmen. Wir haben auch Farbeingaben für Kauf- und Verkaufssignale, die standardmäßig auf waldgrün und orange-rot eingestellt sind, einen booleschen Wert, der standardmäßig aktiviert ist, um nur Signale anzuzeigen, die mit der Richtung der Box übereinstimmen, einen weiteren booleschen Wert, der standardmäßig deaktiviert ist, um die Boxen anhand der durchschnittlichen Kerzengröße zu erweitern, einen doppelten Multiplikator, der für diese Erweiterung auf 1,0 eingestellt ist, und einen ganzzahligen Offset von 10 für die Positionierung der Signale relativ zu den Kerzen.
Als Nächstes fügen wir in der Gruppe „Box Settings“ Farbeingaben für bullishe und bearishe Boxfüllungen hinzu, die standardmäßig auf Blau und Gold eingestellt sind, sowie eine ganze Zahl für die Fülltransparenz, die von 0 bis 100 reicht und auf 80 eingestellt ist, um die Deckkraft der gezeichneten Boxen zu steuern. Die Gruppe „Fog“ enthält einen booleschen Wert, der standardmäßig aktiviert ist, um Nebelverläufe anzuzeigen, einen reellen Multiplikator von 0,7, um die Nebelhöhe auf der Grundlage der durchschnittlichen Kerzengröße zu skalieren, eine ganzzahlige Basistransparenz von 80 und eine Schrittweite von 4 für allmähliche Transparenzänderungen im Farbverlaufseffekt. In der Gruppe „Risk Management“ schließlich bieten wir einen booleschen Wert, der standardmäßig aktiviert ist, um die Take-Profit- und Stop-Loss-Ebenen anzuzeigen, die Enumeration „tp_sl_modes“ mit den Optionen „Candle_Multiplier“ oder „Percentage“, wobei die erstere für die Berechnungsmethoden standardmäßig verwendet wird, eine ganzzahlige Periode von 50 für die Mittelung der Kerzenlängen und reelle Werte für Multiplikatoren oder Prozentsätze für drei Take-Profit-Ebenen und einen Stop-Loss, z. B. 2,0 für den ersten Take-Profit-Multiplikator und 1,5 für den Stop-Loss-Prozentsatz, sodass wir die Risikoparameter anpassen können.
Anschließend werden wir die globalen Puffer um die durchschnittlichen Kerzengrößen erweitern, die für neue Funktionen wie Box-Erweiterungen, Nebelhöhe und TP/SL-Berechnungen benötigt werden, die sich auf Volatilitätsmaße zur dynamischen Skalierung von Grafiken und Niveaus stützen.
double avg_candle_size[]; //--- Average candle size buffer
Danach müssen wir die globalen Variablen erweitern, um Canvas-Objekte, Charteigenschaften für dynamisches Rendering, Zeitstempel für das Neuzeichnen, eine Struktur zum Speichern von Box- und Signaldaten, Arrays für deren Speicherung, Zeilen-/Tabellennamen für TP/SL, Verlängerungszeitraum, Objektpräfix und Schriftgröße zu behandeln. Wir benötigen diese, um nutzerdefinierte Zeichnungen zu verwalten, sichtbare Chartbereiche für die Optimierung zu verfolgen, dauerhafte Daten für Boxen/Signale zu speichern und TP/SL-Visualisierungen zu handhaben, die ein effizientes Neuzeichnen und Skalieren ermöglichen.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CCanvas obj_Canvas; //--- Canvas object int currentChartWidth = 0; //--- Current chart width int currentChartHeight = 0; //--- Current chart height int currentChartScale = 0; //--- Current chart scale int firstVisibleBarIndex = 0; //--- First visible bar index int visibleBarsCount = 0; //--- Visible bars count double minPrice = 0.0; //--- Minimum price double maxPrice = 0.0; //--- Maximum price static datetime lastRedrawTime = 0; //--- Last redraw time //+------------------------------------------------------------------+ //| Box information structure | //+------------------------------------------------------------------+ struct BoxInfo { datetime left_time; // Store left time datetime right_time; // Store right time double top; // Store top price double bottom; // Store bottom price int dir; // Store direction }; BoxInfo all_boxes[]; //--- All boxes array //+------------------------------------------------------------------+ //| Signal information structure | //+------------------------------------------------------------------+ struct SignalInfo { datetime time; // Store signal time int dir; // Store signal direction }; SignalInfo all_signals[]; //--- All signals array string slLine = "SL_Line"; //--- SL line name string tp1Line = "TP1_Line"; //--- TP1 line name string tp2Line = "TP2_Line"; //--- TP2 line name string tp3Line = "TP3_Line"; //--- TP3 line name string tpSlTableObjects[11]; //--- TP/SL table objects long extendSeconds = PeriodSeconds() * 100; //--- Extend seconds string objPrefix = "SWTC_"; //--- Object prefix int current_font_size; //--- Current font size
Hier werden die globalen Variablen deklariert, um den Zustand des Indikators und die nutzerdefinierten Grafiken zu verwalten, beginnend mit einer Instanz der Klasse CCanvas mit dem Namen „obj_Canvas“, um Canvas-basierte Zeichenoperationen im gesamten Programm zu verwalten. Anschließend richten wir ganzzahlige Variablen ein, um die Breite, die Höhe, die Skalierung, den Index des ersten sichtbaren Balkens und die Anzahl der sichtbaren Balken des aktuellen Charts zu verfolgen, sowie reelle Variablen für die Tiefst- und Höchstpreise im sichtbaren Bereich des Charts, um dynamische Anpassungen während des Neuaufbaus zu ermöglichen. Eine statische Datetime-Variable „lastRedrawTime“ wird auf Null initialisiert, um den Zeitstempel der letzten Aktualisierung der Leinwand aufzuzeichnen und so die Häufigkeit des Neuzeichnens zu optimieren.
Wir definieren die Struktur „BoxInfo“, um Details für jedes Signalkästchen zu speichern, einschließlich linker und rechter Zeitstempel, oberem und unterem Preis und einer ganzen Zahl für die Richtung, und erstellen ein Array „all_boxes“, um mehrere solcher Strukturen für die Verwaltung von Breakout-Boxen zu speichern.
In ähnlicher Weise erstellen wir die Struktur „SignalInfo“ mit Feldern für Zeitstempel und Richtung sowie ein Array „all_signals“, um eine Liste der erzeugten Signale zu Anzeigezwecken zu erhalten. Wir initialisieren String-Variablen für die Benennung von Take-Profit- und Stop-Loss-Linien, z. B. „SL_Line“ für den Stop-Loss, und ein Array „tpSlTableObjects“ der Größe 11, um auf Objekte in der Risikomanagement-Tabelle zu verweisen. Der ganzzahligen Variable „extendSeconds“ wird PeriodSeconds multipliziert mit 100 zugewiesen, um einen Verlängerungszeitraum für bestimmte visuelle Elemente zu definieren. Schließlich legen wir ein String-Präfix „SWTC_“ für Objektnamen fest, um Namenskonflikte zu vermeiden, und eine Ganzzahl „current_font_size“, um die Textgröße dynamisch an die Skalierung des Charts anzupassen. Dann werden wir einige Hilfsfunktionen für die Visualisierung der Objekte definieren.
//+------------------------------------------------------------------+ //| Darken color | //+------------------------------------------------------------------+ color DarkenColor(color c, double factor = 0.5) { uchar r = uchar((c & 0xFF) * factor); //--- Compute red component uchar g = uchar(((c >> 8) & 0xFF) * factor); //--- Compute green component uchar b = uchar(((c >> 16) & 0xFF) * factor); //--- Compute blue component return (color)((b << 16) | (g << 8) | r); //--- Return darkened color } //+------------------------------------------------------------------+ //| Draw rectangle label | //+------------------------------------------------------------------+ bool drawRectangleLabel(string objectName, int xDistance, int yDistance, int xSize, int ySize, color rectColor, int borderType = BORDER_FLAT, bool back = true) { bool objectExists = (ObjectFind(0, objectName) >= 0); //--- Check if object exists if (!objectExists) { //--- Handle new object if (!ObjectCreate(0, objectName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label Print("Failed to create ", objectName); //--- Log failure return false; //--- Return failure } } ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, xDistance); //--- Set x distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, yDistance); //--- Set y distance ObjectSetInteger(0, objectName, OBJPROP_XSIZE, xSize); //--- Set x size ObjectSetInteger(0, objectName, OBJPROP_YSIZE, ySize); //--- Set y size ObjectSetInteger(0, objectName, OBJPROP_COLOR, rectColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_BORDER_TYPE, borderType); //--- Set border type ObjectSetInteger(0, objectName, OBJPROP_BACK, back); //--- Set background ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Disable selectable ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false); //--- Disable selected return true; //--- Return success } //+------------------------------------------------------------------+ //| Draw label | //+------------------------------------------------------------------+ bool drawLabel(string objectName, int xDistance, int yDistance, string text, color labelColor) { bool objectExists = (ObjectFind(0, objectName) >= 0); //--- Check if object exists if (!objectExists) { //--- Handle new object if (!ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0)) { //--- Create label Print("Failed to create ", objectName); //--- Log failure return false; //--- Return failure } } ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, xDistance); //--- Set x distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, yDistance); //--- Set y distance ObjectSetString(0, objectName, OBJPROP_TEXT, text); //--- Set text ObjectSetInteger(0, objectName, OBJPROP_COLOR, labelColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 10); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetInteger(0, objectName, OBJPROP_BACK, false); //--- Disable background ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Disable selectable ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false); //--- Disable selected return true; //--- Return success }
Wir definieren die Funktion „DarkenColor“, um einen dunkleren Farbton einer gegebenen Farbe zu erzeugen, indem wir eine Eingabefarbe und einen optionalen Faktor mit dem Standardwert 0,5, die roten, grünen und blauen Komponenten durch bitweise Operationen wie „& 0xFF“ für Rot, Rechtsverschiebung um 8 und „& 0xFF“ für Grün und Rechtsverschiebung um 16 und „& 0xFF“ für Blau extrahiert, dann mit dem Faktor multipliziert, in uchar umgewandelt und mit Linksverschiebungen und bitweisem ODER wieder zu einem neuen Farbwert zusammengesetzt.
Als Nächstes erstellen wir die Funktion „drawRectangleLabel“, um das Zeichnen oder Aktualisieren einer Rechteckbeschriftung im Chart zu handhaben. Dabei prüfen wir zunächst, ob das Objekt mit ObjectFind vorhanden ist, und erstellen es über ObjectCreate mit dem Typ OBJ_RECTANGLE_LABEL, falls dies nicht der Fall ist, protokollieren eine Fehlermeldung mit Print und geben im Fehlerfall false zurück; andernfalls legen wir Eigenschaften wie x- und y-Abstände, Größen, Farbe, Rahmentyp (standardmäßig BORDER_FLAT) und Hintergrundflagge (standardmäßig true) fest und deaktivieren die Wählbarkeit und Auswahl, bevor wir true zurückgeben.
Ebenso implementieren wir die Funktion „drawLabel“ für Textbeschriftungen, wobei wir die Existenz mit „ObjectFind“ prüfen und sie bei Bedarf über „ObjectCreate“ mit dem TypOBJ_LABEL erstellen, eine Fehlermeldung ausgeben und „false“ zurückgeben, falls die Erstellung fehlschlägt. Dann konfigurieren wir die x- und y-Abstände, den Textinhalt, die Farbe, setzen die Schriftgröße auf 10, und die Schriftart auf Arial, deaktivieren den Hintergrund, die Auswählbarkeit und die Auswahl und geben bei Erfolg „true“ zurück. Bei der Initialisierung müssen wir nun den neuen Puffer für die Berechnung der neuen durchschnittlichen Kerzengrößen binden. Außerdem müssen wir die Leinwand initialisieren und die Tabellenbeschriftungen festlegen.
//+------------------------------------------------------------------+ //| Initialize indicator | //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "Smart WaveTrend Crossover"); //--- Set short name PlotIndexSetInteger(1, PLOT_ARROW, 233); //--- Set buy arrow symbol PlotIndexSetInteger(1, PLOT_SHOW_DATA, signal_type == Triangles); //--- Set buy visibility PlotIndexSetInteger(1, PLOT_LINE_COLOR, 0, signal_buy_col); //--- Set buy color PlotIndexSetInteger(2, PLOT_ARROW, 234); //--- Set sell arrow symbol PlotIndexSetInteger(2, PLOT_SHOW_DATA, signal_type == Triangles); //--- Set sell visibility PlotIndexSetInteger(2, PLOT_LINE_COLOR, 0, signal_sell_col); //--- Set sell color SetIndexBuffer(23, avg_candle_size, INDICATOR_CALCULATIONS); //--- Bind avg candle size currentChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width currentChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height string canvas_name = "SWTC_Canvas"; //--- Set canvas name if (!obj_Canvas.CreateBitmapLabel(0, 0, canvas_name, 0, 0, currentChartWidth, currentChartHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create canvas Print("Failed to create canvas"); //--- Log failure return(INIT_FAILED); //--- Return failure } tpSlTableObjects[0] = objPrefix + "Table_Frame"; //--- Set table frame tpSlTableObjects[1] = objPrefix + "Table_Level"; //--- Set table level tpSlTableObjects[2] = objPrefix + "Table_Price"; //--- Set table price tpSlTableObjects[3] = objPrefix + "Table_TP1"; //--- Set TP1 label tpSlTableObjects[4] = objPrefix + "Table_TP1_Price"; //--- Set TP1 price tpSlTableObjects[5] = objPrefix + "Table_TP2"; //--- Set TP2 label tpSlTableObjects[6] = objPrefix + "Table_TP2_Price"; //--- Set TP2 price tpSlTableObjects[7] = objPrefix + "Table_TP3"; //--- Set TP3 label tpSlTableObjects[8] = objPrefix + "Table_TP3_Price"; //--- Set TP3 price tpSlTableObjects[9] = objPrefix + "Table_SL"; //--- Set SL label tpSlTableObjects[10] = objPrefix + "Table_SL_Price"; //--- Set SL price current_font_size = 10; //--- Initialize font size return(INIT_SUCCEEDED); //--- Return success }
In der Ereignisbehandlung von OnInit konfigurieren wir zusätzliche Eigenschaften für den Indikator, indem wir seinen Kurznamen mit IndicatorSetString festlegen, den wir aus Gründen der Allgemeinheit geändert haben. Für die Darstellung der Kaufsignale legen wir das Pfeilsymbol wie zuvor mit PlotIndexSetInteger als 233 fest, schalten aber seine Sichtbarkeit abhängig davon um, ob der Signaltyp „Dreiecke“ ist, da wir die Signalblasen anders als im Canvas zeichnen werden, und wenden die nutzerdefinierte Farbe für einen Kauf an. In ähnlicher Weise setzen wir für die Darstellung der Verkaufssignale den Pfeil auf 234, steuern die Sichtbarkeit auf die gleiche Weise und weisen die Farbe für Verkäufe zu. Wir binden den Puffer für die durchschnittliche Kerzengröße an den Index 23 als Berechnungspuffer, um Nebel- und Box-Erweiterungen zu unterstützen.
Wir rufen die aktuellen Chartabmessungen mit ChartGetInteger für Breite und Höhe ab, um die Leinwand richtig zu initialisieren. Wir definieren einen Canvas-Namen und erstellen ein Bitmap-Label darauf mit der Methode „CreateBitmapLabel“ des Canvas-Objekts, wobei wir das Unterfenster, die Position, die Größe und das Farbformat „COLOR_FORMAT_ARGB_NORMALIZE“ angeben; wenn die Erstellung fehlschlägt, protokollieren wir einen Fehler mit Print und geben INIT_FAILED zurück. Wir füllen das Array der Tabellenobjekte mit vorangestellten Namen für die Anzeigeelemente des Risikomanagements, wie z. B. den Rahmen, Beschriftungen für Niveaus und Preise sowie spezifische Einträge für Take-Profits und Stop-Loss. Wir initialisieren die Variable für die Schriftgröße auf 10 für die Textdarstellung. Schließlich geben wir INIT_SUCCEEDED zurück, um die erfolgreiche Einrichtung zu bestätigen. Bei der Initialisierung erhalten wir folgendes Ergebnis.

Auf dem Bild können wir sehen, dass wir die Leinwand initialisiert haben, bereit für unsere Zeichnung. Als Nächstes müssen wir die Canvas-Objekte zeichnen, aber um das Zeichnen einfacher und unkomplizierter zu machen, werden wir einige Hilfsfunktionen definieren, um speziell die Boxen und die richtigen Preise für die Handelsstufen zu zeichnen, mit denen wir die Tabelle füllen werden.
//+------------------------------------------------------------------+ //| Draw right price label | //+------------------------------------------------------------------+ bool drawRightPrice(string objectName, datetime lineTime, double linePrice, color lineColor, ENUM_LINE_STYLE lineStyle = STYLE_SOLID, int lineWidth = 1) { bool objectExists = (ObjectFind(0, objectName) >= 0); //--- Check if object exists if (!objectExists) { //--- Handle new object if (!ObjectCreate(0, objectName, OBJ_ARROW_RIGHT_PRICE, 0, lineTime, linePrice)) { //--- Create right price Print("Failed to create ", objectName); //--- Log failure return false; //--- Return failure } } else { //--- Handle existing object ObjectSetInteger(0, objectName, OBJPROP_TIME, 0, lineTime); //--- Set time ObjectSetDouble(0, objectName, OBJPROP_PRICE, 0, linePrice); //--- Set price } ObjectSetInteger(0, objectName, OBJPROP_COLOR, lineColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_WIDTH, lineWidth); //--- Set width ObjectSetInteger(0, objectName, OBJPROP_STYLE, lineStyle); //--- Set style ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 10); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetInteger(0, objectName, OBJPROP_BACK, false); //--- Disable background ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Disable selectable ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false); //--- Disable selected ChartRedraw(0); //--- Redraw chart return true; //--- Return success } //+------------------------------------------------------------------+ //| Update font sizes | //+------------------------------------------------------------------+ void UpdateFontSizes() { long scale = 0; //--- Initialize scale if (ChartGetInteger(0, CHART_SCALE, 0, scale)) { //--- Get chart scale current_font_size = (int)(8 + scale * 1.5); //--- Compute font size current_font_size = MathMax(8, MathMin(18, current_font_size)); //--- Clamp font size ChartRedraw(0); //--- Redraw chart } } //+------------------------------------------------------------------+ //| Convert bar width | //+------------------------------------------------------------------+ int BarWidth(int chartScale) { return (int)MathPow(2.0, chartScale); //--- Return bar width } //+------------------------------------------------------------------+ //| Convert shift to x | //+------------------------------------------------------------------+ int ShiftToX(int bar_index) { return (firstVisibleBarIndex - bar_index) * BarWidth(currentChartScale); //--- Return x position } //+------------------------------------------------------------------+ //| Convert price to y | //+------------------------------------------------------------------+ int PriceToY(double price) { if (maxPrice - minPrice == 0.0) return 0; //--- Handle zero range return (int)MathRound(currentChartHeight * (maxPrice - price) / (maxPrice - minPrice)); //--- Return y position } //+------------------------------------------------------------------+ //| Draw box on canvas | //+------------------------------------------------------------------+ void DrawBoxOnCanvas(int x_left, int y_top, int x_right, int y_bottom, color fillColor, int fillTransp) { int x1 = MathMin(x_left, x_right); //--- Set min x int x2 = MathMax(x_left, x_right); //--- Set max x int y1 = MathMin(y_top, y_bottom); //--- Set min y int y2 = MathMax(y_top, y_bottom); //--- Set max y uchar alpha_fill = (uchar)(255 * (100 - fillTransp) / 100); //--- Compute fill alpha uint argb_fill = ColorToARGB(fillColor, alpha_fill); //--- Get fill ARGB obj_Canvas.FillRectangle(x1, y1, x2, y2, argb_fill); //--- Fill rectangle color borderColor = DarkenColor(fillColor, 0.7); //--- Get border color uint argb_border = ColorToARGB(borderColor, 255); //--- Get border ARGB obj_Canvas.LineAA(x1, y1, x2, y1, argb_border); //--- Draw top border obj_Canvas.LineAA(x1, y1 + 1, x2, y1 + 1, argb_border); //--- Draw top inner obj_Canvas.LineAA(x1, y2, x2, y2, argb_border); //--- Draw bottom border obj_Canvas.LineAA(x1, y2 - 1, x2, y2 - 1, argb_border); //--- Draw bottom inner obj_Canvas.LineAA(x1, y1, x1, y2, argb_border); //--- Draw left border obj_Canvas.LineAA(x1 + 1, y1, x1 + 1, y2, argb_border); //--- Draw left inner obj_Canvas.LineAA(x2, y1, x2, y2, argb_border); //--- Draw right border obj_Canvas.LineAA(x2 - 1, y1, x2 - 1, y2, argb_border); //--- Draw right inner }
Zunächst erstellen wir die Funktion „drawRightPrice“, um ein rechtsbündiges Preislabel im Chart zu zeichnen oder zu aktualisieren. Dabei prüfen wir mit ObjectFind, ob es vorhanden ist, und erstellen es mit ObjectCreate mit dem Typ OBJ_ARROW_RIGHT_PRICE, falls es nicht vorhanden ist, protokollieren einen Fehler über „Print“ und geben bei einem Fehlschlag false zurück; bei vorhandenen Objekten passen wir die Zeit- und Preiseigenschaften an, setzen Farbe, Breite und Stil auf „STYLE_SOLID“, Schriftgröße auf 10, Schriftart auf „Arial“ und deaktivieren Hintergrund, Selektierbarkeit und Auswahl, bevor wir das Chart mit ChartRedraw neu zeichnen und true zurückgeben. Als Nächstes definieren wir die Funktion „UpdateFontSizes“, um die Textgrößen dynamisch anzupassen, indem wir die Chartskalierung über ChartGetInteger abrufen, eine neue Schriftgröße als 8 plus das 1,5-fache der Skalierung berechnen, sie mithilfe von MathMax und MathMin zwischen 8 und 18 beschränken und ein Neuzeichnen des Charts auslösen.
Wir implementieren die Funktion „BarWidth“, um die Pixelbreite der Balken auf der Grundlage der Chartskalierung zu berechnen, wobei 2 hoch der Skala mit MathPow in eine ganze Zahl umgewandelt wird. Die Funktion „ShiftToX“ wandelt einen Taktindex in eine x-Koordinate auf der Leinwand um, indem sie die Differenz zum ersten sichtbaren Takt mit der Taktbreite multipliziert. In ähnlicher Weise ordnet „PriceToY“ einen Preiswert einer y-Koordinate zu, wobei der Nullbereich durch Rückgabe von 0 behandelt wird; andernfalls wird die proportionale Position vom Höchst- bis zum Mindestpreis unter Verwendung von MathRound berechnet und durch die Charthöhe skaliert.
Schließlich entwickeln wir die Funktion „DrawBoxOnCanvas“, um ein gefülltes Rechteck mit Rändern auf der Leinwand zu rendern, bestimmen Min- und Max-Koordinaten mit „MathMin“ und „MathMax“, berechnen Alpha aus der Transparenz für das Füllen, konvertieren die Farben in das ARGB-Format mit ColorToARGB, füllen den Bereich mit „FillRectangle“, leiten eine dunklere Randfarbe über „DarkenColor“ und zeichnen die Anti-Aliasing-Linien für äußere und innere Ränder auf allen Seiten mit „LineAA“. „AA“ steht für Antialiasing, falls Sie sich fragen, warum wir das gewählt haben. Es hilft, gezackte, stufenförmige Kanten an Linien zu glätten. Zum besseren Verständnis finden Sie unten ein Bild.

Aus dem Bild können Sie ersehen, warum wir uns für den Ansatz des Antialiasing entschieden haben: glatte Linien. Wir können nun dazu übergehen, diese Funktionen in den Indikatorberechnungen und Visualisierungen zu verwenden. Erstens wollen wir, dass die angezeigten Handelsstufen auf dem Chart bleiben, also müssen wir sie zwischen den Aufrufen statisch machen, es sei denn, sie werden bei neuen Signalen ausdrücklich geändert.
//+------------------------------------------------------------------+ //| Calculate indicator values | //+------------------------------------------------------------------+ 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[]) { static int last_dir = 0; //--- Last direction static double last_sl = 0.0; //--- Last SL static double last_tp1 = 0.0; //--- Last TP1 static double last_tp2 = 0.0; //--- Last TP2 static double last_tp3 = 0.0; //--- Last TP3 static datetime last_signal_time = 0; //--- Last signal time if (prev_calculated == 0) { //--- Handle initial calc //--- Existing initializations ArrayInitialize(avg_candle_size, EMPTY_VALUE); //--- Init avg candle ArrayInitialize(buyArrowBuf, EMPTY_VALUE); //--- Init buy arrows ArrayInitialize(sellArrowBuf, EMPTY_VALUE); //--- Init sell arrows ArrayResize(all_boxes, 0); //--- Clear boxes ArrayResize(all_signals, 0); //--- Clear signals last_dir = 0; //--- Reset direction last_sl = 0.0; //--- Reset SL last_tp1 = 0.0; //--- Reset TP1 last_tp2 = 0.0; //--- Reset TP2 last_tp3 = 0.0; //--- Reset TP3 last_signal_time = 0; //--- Reset signal time } }
Wir deklarieren statische Variablen innerhalb der Ereignisbehandlung von OnCalculate, um den Status über Neuberechnungen hinweg beizubehalten. Dazu gehören eine Ganzzahl für die letzte Signalrichtung, reelle Zahlen für die letzten Stop-Loss- und drei Take-Profit-Niveaus und eine „Datetime“ für den Zeitstempel des letzten Signals, um die Kontinuität der Risikomanagementanzeigen zu gewährleisten. Wenn „prev_calculated“ gleich Null ist, was die erste Berechnung oder einen vollständigen Reset signalisiert, erweitern wir die Initialisierung, indem wir den Puffer für die durchschnittliche Kerzengröße und die Pfeilpuffer über ArrayInitialize auf EMPTY_VALUE setzen, die Größe der Boxen und Signal-Arrays mit ArrayResize auf Null setzen, um alle vorherigen Daten zu löschen, und alle statischen Variablen auf ihren Anfangszustand wie Null oder 0,0 zurücksetzen, um einen sauberen Start zu ermöglichen. Um die Leinwand nur bei Bedarf zu aktualisieren, fügen wir wie folgt ein Flag für das Neuzeichnen hinzu.
bool new_signal_redraw = false; //--- New redraw flag
In die Berechnungsschleife müssen wir die Logik zur Berechnung der durchschnittlichen Kerzengröße aufnehmen, um die Volatilität wie folgt zu behandeln.
double sum_co = 0; //--- Init sum int cnt_co = 0; //--- Init count for (int k = 0; k < 50; k++) { //--- Loop candles if (i - k < 0) break; //--- Skip invalid sum_co += MathAbs(close[i - k] - open[i - k]); //--- Accumulate cnt_co++; //--- Increment } if (cnt_co > 0) avg_candle_size[i] = sum_co / cnt_co; //--- Set average else avg_candle_size[i] = MathAbs(close[i] - open[i]); //--- Set default
Innerhalb der Schleife initialisieren wir eine reelle Summenvariable auf Null und einen ganzzahligen Zähler auf Null. Dann durchlaufen wir eine Schleife über die letzten 50 Balken, beginnend mit dem aktuellen Index rückwärts; für jeden gültigen Balken addieren wir die absolute Körpergröße, die als Differenz zwischen Close und Open mit MathAbs berechnet wird, zur Summe und erhöhen die Zählung, wobei wir vorzeitig abbrechen, wenn der Index negativ wird. Wenn der Zählerstand positiv ist, wird die durchschnittliche Kerzengröße für den aktuellen Balken festgelegt, indem die Summe durch den Zählerstand geteilt wird; andernfalls wird die absolute Körpergröße des aktuellen Balkens als Standard festgelegt.
Außerdem müssen wir die Pfeilplatzierung durch die Box- und Ereignislogik ersetzen, um die unmittelbare Pfeilplatzierung durch die Erstellung von Boxen bei Kreuzungen, die Ausbrucherkennung für Ereignisse, die Array-Begrenzung und die Behandlung bedingter Signale (Pfeile oder Labels) zu ersetzen. Wir benötigen dies, um Range-Boxen einzuführen, die bis zu einem Ausbruch bestehen bleiben, Signale auf der Grundlage der Richtung filtern und alternative Anzeigetypen unterstützen, die kontextbezogenere Handelssignale liefern.
//--- BEFORE // buyArrowBuf[i] = EMPTY_VALUE; //--- Reset buy arrow // sellArrowBuf[i] = EMPTY_VALUE; //--- Reset sell arrow // if (signal_bull_cross[i] == 1 && (!use_trend_filter || trend_is_bull[i] == 1)) { //--- Check buy condition // buyArrowBuf[i] = low[i] - _Point * base_offset; //--- Place buy arrow // } // if (signal_bear_cross[i] == 1 && (!use_trend_filter || trend_is_bear[i] == 1)) { //--- Check sell condition // sellArrowBuf[i] = high[i] + _Point * base_offset; //--- Place sell arrow // } //--- AFTER double box_top = use_box_multiplier ? high[i] + avg_candle_size[i] * box_multiplier : high[i]; //--- Set top double box_bottom = use_box_multiplier ? low[i] - avg_candle_size[i] * box_multiplier : low[i]; //--- Set bottom if (signal_bull_cross[i] == 1 && (!use_trend_filter || trend_is_bull[i] == 1)) { //--- Check bull signal BoxInfo b; //--- Create box b.left_time = time[i]; //--- Set left b.right_time = 0; //--- Set right b.top = box_top; //--- Set top b.bottom = box_bottom; //--- Set bottom b.dir = 1; //--- Set dir ArrayResize(all_boxes, ArraySize(all_boxes) + 1); //--- Resize boxes all_boxes[ArraySize(all_boxes) - 1] = b; //--- Add box } if (signal_bear_cross[i] == 1 && (!use_trend_filter || trend_is_bear[i] == 1)) { //--- Check bear signal BoxInfo b; //--- Create box b.left_time = time[i]; //--- Set left b.right_time = 0; //--- Set right b.top = box_top; //--- Set top b.bottom = box_bottom; //--- Set bottom b.dir = -1; //--- Set dir ArrayResize(all_boxes, ArraySize(all_boxes) + 1); //--- Resize boxes all_boxes[ArraySize(all_boxes) - 1] = b; //--- Add box } bool buy_event = false; //--- Buy event flag bool sell_event = false; //--- Sell event flag for (int j = ArraySize(all_boxes) - 1; j >= 0; j--) { //--- Loop boxes if (all_boxes[j].right_time == 0) { //--- Check active if (close[i] > all_boxes[j].top) { //--- Check break up if (!show_only_matching || all_boxes[j].dir == 1) buy_event = true; //--- Set buy all_boxes[j].right_time = time[i]; //--- Close box } if (close[i] < all_boxes[j].bottom) { //--- Check break down if (!show_only_matching || all_boxes[j].dir == -1) sell_event = true; //--- Set sell all_boxes[j].right_time = time[i]; //--- Close box } } } while (ArraySize(all_boxes) > 500) { //--- Limit boxes bool removed = false; //--- Removed flag for (int j = 0; j < ArraySize(all_boxes); j++) { //--- Loop to remove if (all_boxes[j].right_time != 0) { //--- Check closed ArrayRemove(all_boxes, j, 1); //--- Remove box removed = true; //--- Set removed break; //--- Exit loop } } if (!removed) break; //--- No more to remove } if (signal_type == Triangles) { //--- Check triangles if (buy_event) { //--- Handle buy buyArrowBuf[i] = low[i] - _Point * base_offset; //--- Set arrow } if (sell_event) { //--- Handle sell sellArrowBuf[i] = high[i] + _Point * base_offset; //--- Set arrow } } else { //--- Handle labels if (buy_event) { //--- Handle buy SignalInfo s; //--- Create signal s.time = time[i]; //--- Set time s.dir = 1; //--- Set dir ArrayResize(all_signals, ArraySize(all_signals) + 1); //--- Resize signals all_signals[ArraySize(all_signals) - 1] = s; //--- Add signal new_signal_redraw = (i == rates_total - 1); //--- Set redraw } if (sell_event) { //--- Handle sell SignalInfo s; //--- Create signal s.time = time[i]; //--- Set time s.dir = -1; //--- Set dir ArrayResize(all_signals, ArraySize(all_signals) + 1); //--- Resize signals all_signals[ArraySize(all_signals) - 1] = s; //--- Add signal new_signal_redraw = (i == rates_total - 1); //--- Set redraw } } while (ArraySize(all_signals) > 500) { //--- Limit signals ArrayRemove(all_signals, 0, 1); //--- Remove oldest }
Wir legen die oberen und unteren Grenzen für potenzielle Signalkästchen fest, indem wir sie auf den Höchst- und Tiefstwert des Balkens setzen oder sie erweitern, falls aktiviert, indem wir die durchschnittliche Kerzengröße multipliziert mit dem Box-Multiplikator für einen zusätzlichen Puffer um die Preisspanne addieren oder subtrahieren. Wenn ein bullisher Crossover erkannt wird und die Bedingung des Trendfilters erfüllt, instanziieren wir eine „BoxInfo“-Struktur, füllen ihre Felder mit der aktuellen Zeit als links, Null für rechts, um sie als aktiv zu markieren, den berechneten oberen und unteren Kursen und der Richtung als 1 für aufwärts, erweitern dann das „all_boxes“-Array mit ArrayResize mit ArraySize plus eins und hängen die neue Struktur an das Ende an. Für einen bearishen Crossover unter ähnlichen Bedingungen erstellen wir eine weitere „BoxInfo“-Instanz, setzen die Felder entsprechend mit der Richtung -1 für Bär, ändern die Größe des Arrays und fügen es hinzu.
Wir initialisieren boolesche Flags für Kauf- und Verkaufsereignisse auf false, dann iterieren wir rückwärts durch das Array „all_boxes“, beginnend mit dem letzten Index; für jede aktive Box, bei der die richtige Zeit null ist, prüfen wir, ob der aktuelle Schlusskurs die Obergrenze für einen Ausbruch nach oben übersteigt, setzen das Kaufereignis auf true, wenn es mit der Richtung übereinstimmt oder der Abgleich deaktiviert ist, und schließen die Box, indem wir die aktuelle Zeit der richtigen Zeit zuordnen. Ähnlich verhält es sich, wenn der Schlusskurs bei einem Ausbruch nach unten unter die Untergrenze fällt: Wir setzen gegebenenfalls das Verkaufsereignis und schließen die Box. Um die Größe des Arrays zu verwalten, wird, wenn „all_boxes“ 500 Elemente übersteigt, von Anfang an gescannt, um das erste geschlossene Feld mit ArrayRemove zu finden und zu entfernen, wobei bei Erfolg ein Flag gesetzt und abgebrochen wird, wenn es entfernt wurde, oder die Schleife verlassen wird, wenn keine weiteren Elemente entfernt werden können.
Wenn der Signaltyp „Dreiecke“ ist, platzieren wir bei einem Kaufereignis den Kaufpfeil um den Versatz mal _Point unter dem Tiefststand und bei einem Verkaufsereignis über dem Höchststand in gleicher Weise. Andernfalls erstellen wir für Labeltypen bei einem Kaufereignis eine Struktur „SignalInfo“, setzen ihre Zeit auf den aktuellen Wert und die Richtung auf 1, ändern die Größe von „all_signals“ und fügen sie hinzu, dann kennzeichnen wir einen Redraw, wenn dies der letzte Balken ist; wir tun das Gleiche für Verkaufsereignisse mit Richtung -1. Wenn „all_signals“ den Wert von 500 überschreitet, wird der älteste Eintrag mit der Funktion „ArrayRemove“ vom Anfang entfernt. Damit sind wir in der Lage, die Leinwand zu zeichnen. Lassen Sie uns mit dem Nebel beginnen. Aus Gründen der Modularität werden wir die Logik in einer Funktion unterbringen.
//+------------------------------------------------------------------+ //| Redraw canvas | //+------------------------------------------------------------------+ void Redraw(int rates_total) { if (currentChartWidth <= 0 || currentChartHeight <= 0) return; //--- Handle invalid size double h[], l[], c[], acs[], th[]; //--- Declare arrays datetime t[]; //--- Declare time if (CopyHigh(_Symbol, _Period, 0, rates_total, h) != rates_total) return; //--- Copy high if (CopyLow(_Symbol, _Period, 0, rates_total, l) != rates_total) return; //--- Copy low if (CopyClose(_Symbol, _Period, 0, rates_total, c) != rates_total) return; //--- Copy close if (CopyTime(_Symbol, _Period, 0, rates_total, t) != rates_total) return; //--- Copy time ArrayCopy(acs, avg_candle_size, 0, 0, rates_total); //--- Copy avg size ArrayCopy(th, trend_hist, 0, 0, rates_total); //--- Copy hist uint default_color = 0; //--- Default color obj_Canvas.Erase(default_color); //--- Erase canvas current_font_size = (int)(10 + currentChartScale * 1.5); //--- Compute font current_font_size = MathMax(10, MathMin(24, current_font_size)); //--- Clamp font if (show_fog) { //--- Check fog int total = visibleBarsCount; //--- Set total int previousX = -1; //--- Prev x double previous_hl2 = 0.0; //--- Prev hl2 double previous_offset = 0.0; //--- Prev offset int previous_dir = 0; //--- Prev dir color previous_fog_color = clrNONE; //--- Prev color for (int i = 0; i < total; i++) { //--- Loop visible int bar_index = firstVisibleBarIndex - i; //--- Compute index if (bar_index < 0 || bar_index >= rates_total) continue; //--- Skip invalid int x = ShiftToX(bar_index); //--- Get x if (x >= currentChartWidth) continue; //--- Skip offscreen int buffer_index = rates_total - 1 - bar_index; //--- Compute buffer double hl2 = (h[buffer_index] + l[buffer_index]) / 2.0; //--- Compute hl2 double offset_val = acs[buffer_index] * offset_mult; //--- Compute offset int dir = th[buffer_index] >= 0 ? -1 : 1; //--- Set dir color fog_color = th[buffer_index] >= 0 ? col_up : col_dn; //--- Set color if (previousX != -1 && x > previousX) { //--- Check previous double deltaX = x - previousX; //--- Compute delta int endColumn = MathMin(x, currentChartWidth - 1); //--- Set end for (int column = previousX + 1; column <= endColumn; column++) { //--- Loop columns double t_val = (column - previousX) / deltaX; //--- Compute t double interp_hl2 = previous_hl2 + t_val * (hl2 - previous_hl2); //--- Interp hl2 double interp_offset = previous_offset + t_val * (offset_val - previous_offset); //--- Interp offset int interp_dir = previous_dir; //--- Interp dir color interp_fog_color = previous_fog_color; //--- Interp color double full_offset = 6.0 * interp_offset; //--- Full offset double edge_price = interp_hl2 + interp_dir * full_offset; //--- Edge price int slow_y = PriceToY(interp_hl2); //--- Slow y int fast_y = PriceToY(edge_price); //--- Fast y int upperY = MathMin(slow_y, fast_y); //--- Upper y int lowerY = MathMax(slow_y, fast_y); //--- Lower y upperY = MathMax(0, upperY); //--- Clamp upper lowerY = MathMin(currentChartHeight - 1, lowerY); //--- Clamp lower double height_pixels = MathAbs(slow_y - fast_y); //--- Height if (height_pixels == 0.0) continue; //--- Skip zero double total_inc = transp_inc * 6.0; //--- Total inc for (int row = upperY; row <= lowerY; row++) { //--- Loop rows double distanceFromSlow_pixels = MathAbs(row - slow_y); //--- Distance double gradientFraction = distanceFromSlow_pixels / height_pixels; //--- Fraction double transp = base_transp + total_inc * gradientFraction; //--- Transp if (transp > 100.0) transp = 100.0; //--- Clamp transp uchar alpha = (uchar)(255 * (100 - transp) / 100.0); //--- Alpha uint argb = ColorToARGB(interp_fog_color, alpha); //--- ARGB obj_Canvas.PixelSet(column, row, argb); //--- Set pixel } } } previousX = x; //--- Update prev x previous_hl2 = hl2; //--- Update prev hl2 previous_offset = offset_val; //--- Update prev offset previous_dir = dir; //--- Update prev dir previous_fog_color = fog_color; //--- Update prev color } } obj_Canvas.Update(); //--- Update canvas }
In der Funktion „Redraw“ wird zunächst geprüft, ob die aktuelle Chartbreite oder -höhe positiv ist. Ist beides null oder negativ, kehrt die Funktion frühzeitig zurück, um ungültige Operationen zu vermeiden. Wir deklarieren lokale Arrays für die Hochs, Tiefs und die Schlusskurse, die durchschnittlichen Kerzengrößen, die Trendhistogramme und die Zeiten. Wir füllen sie dann durch das Kopieren der Daten des Symbols mit CopyHigh, CopyLow, CopyClose und CopyTime vom Start bis zu den Gesamtkursen, wobei wir abbrechen, wenn eine Kopie nicht der erwarteten Anzahl entspricht. Wir übertragen Daten aus den Puffern für die durchschnittliche Kerzengröße und das Trendhistogramm über ArrayCopy in ihre lokalen Arrays, um sie beim Rendern zu verwenden. Mit der Methode „Erase“ wird die Leinwand auf eine Standardfarbe von 0 zurückgesetzt, um eine neue Zeichnung vorzubereiten. Wir berechnen die aktuelle Schriftgröße als 10 plus das 1,5-fache der Chartskalierung und beschränken sie dann mit MathMax und MathMin zwischen 10 und 24, um eine konsistente Textdarstellung zu gewährleisten.
Wenn die Nebeldarstellung aktiviert ist, setzen wir die Summe der Schleife auf die Anzahl der sichtbaren Balken und initialisieren die vorherigen Tracking-Variablen für x-Position, hl2-Preis, Offset, Richtung und Farbe. Wir durchlaufen alle sichtbaren Balken von links nach rechts: Wir berechnen den Balkenindex als „erster sichtbarer Balken minus Schleifenzähler“ und überspringen den Balken, wenn er außerhalb des Bereichs liegt oder der entsprechende Kursindex ungültig ist; wir ermitteln die x-Koordinate mit „ShiftToX“ und überspringen den Wert, wenn er die Breite des Charts überschreitet, leiten den Pufferindex als Gesamtanzahl der Kurse minus eins minus Balkenindex ab, berechnen hl2 als Mittelwert von Hoch und Tief, den Offset-Wert als durchschnittliche Kerzengröße mal dem Multiplikator, die Richtung als -1, wenn das Trendhistogramm nicht negativ ist, oder andernfalls als 1, und die Nebelfarbe basierend auf dem Vorzeichen des Histogramms unter Verwendung benutzerdefinierter Farben für Aufwärts- und Abwärtstrends.
Wenn ein vorheriges x existiert und das aktuelle x größer ist, ermitteln wir das Pixeldelta und setzen die Endspalte auf das Minimum aus aktuellem x oder der Diagrammbreite minus eins; anschließend durchlaufen wir die Spalten dazwischen: Wir interpolieren einen t-Faktor als relative Position im Delta, interpolieren hl2 und den Versatz zwischen vorherigem und aktuellem Wert linear, behalten die vorherige Richtung und Farbe bei; berechnen einen vollständigen Offset als das 6-fache des interpolierten Offsets und leiten einen Randpreis ab, indem wir diesen, skaliert nach Richtung, zu interpoliertem hl2 addieren, konvertieren interpoliertes hl2 und den Rand mit „PriceToY“ in y-Koordinaten, oberes und unteres y als Min und Max dieser Koordinaten festlegen, oberes mit MathMax und „MathMin“ auf mindestens 0 und unteres auf höchstens Chart-Höhe minus eins begrenzen, Pixelhöhe als absolute Differenz in y berechnen, die Zeilenschleife überspringen, wenn Null, Gesamtinkrement als Transparenzinkrement mal 6 ableiten.
Für jede Zeile von oben nach unten auf der y-Achse: Wir berechnen den Pixelabstand von der oberen y-Position (hl2-Position), leiten einen Gradientenanteil als Verhältnis von Abstand zu Höhe ab, berechnen die Transparenz als Basiswert plus Gesamtinkrement multipliziert mit dem Anteil, begrenzt auf maximal 100, wandeln den Alpha-Wert in einen UCHAR-Wert um, der dem 255-fachen des Quotienten aus (100 minus Transparenz) und 100 entspricht, rufen die ARGB-Farbe mit ColorToARGB ab unter Verwendung der interpolierten Nebelfarbe und des Alphas, und setzen das Pixel an der Spalte und Zeile mit der Funktion PixelSet. Wir aktualisieren die vorherigen Variablen mit den aktuellen Werten nach der Verarbeitung jedes Balkens. Schließlich aktualisieren wir die Leinwandanzeige mit „Update“, um alle gezeichneten Elemente zu übernehmen. Um den Fortschritt zu verdeutlichen, rufen wir diese Funktion am Ende auf, nach größeren Aktualisierungen im Berechnungs-Ereignishandler, um mit den neuen Informationen neu zu zeichnen.
bool hasChartChanged = false; //--- Change flag int newChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get new width int newChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get new height int newChartScale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get new scale int newFirstVisibleBar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); //--- Get new first bar int newVisibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); //--- Get new visible double newMinPrice = ChartGetDouble(0, CHART_PRICE_MIN, 0); //--- Get new min double newMaxPrice = ChartGetDouble(0, CHART_PRICE_MAX, 0); //--- Get new max if (newChartWidth != currentChartWidth || newChartHeight != currentChartHeight) { //--- Check size change obj_Canvas.Resize(newChartWidth, newChartHeight); //--- Resize canvas currentChartWidth = newChartWidth; //--- Update width currentChartHeight = newChartHeight; //--- Update height hasChartChanged = true; //--- Set changed } if (newChartScale != currentChartScale || newFirstVisibleBar != firstVisibleBarIndex || newVisibleBars != visibleBarsCount || newMinPrice != minPrice || newMaxPrice != maxPrice) { //--- Check other changes currentChartScale = newChartScale; //--- Update scale firstVisibleBarIndex = newFirstVisibleBar; //--- Update first bar visibleBarsCount = newVisibleBars; //--- Update visible minPrice = newMinPrice; //--- Update min maxPrice = newMaxPrice; //--- Update max hasChartChanged = true; //--- Set changed } datetime currentTime = TimeCurrent(); //--- Get current time if (hasChartChanged || rates_total > prev_calculated || new_signal_redraw) { //--- Check redraw Redraw(rates_total); //--- Call redraw lastRedrawTime = currentTime; //--- Update time } ChartRedraw(0); //--- Redraw chart
In der Ereignisbehandlung von OnCalculate initialisieren wir ein boolesches Flag, um festzustellen, ob sich das Chart geändert hat, und rufen dann die aktualisierten Charteigenschaften ab, wie die neue Breite und Höhe mit ChartGetInteger mit CHART_WIDTH_IN_PIXELS und „CHART_HEIGHT_IN_PIXELS“, die neue Skala mit CHART_SCALE, den ersten sichtbaren Balken mit „CHART_FIRST_VISIBLE_BAR“, die Anzahl der sichtbaren Balken mit „CHART_VISIBLE_BARS“ und die Mindest- und Höchstpreise über ChartGetDouble mit „CHART_PRICE_MIN“ und CHART_PRICE_MAX. Wenn die neue Breite oder Höhe von den aktuellen Werten abweicht, wird die Größe des Canvas-Objekts mit der Methode „Resize“ geändert, wobei die neuen Abmessungen übergeben werden, die globalen Breiten- und Höhenvariablen aktualisiert und das Änderungsflag auf „true“ gesetzt werden.
Wenn sich die Skala, der erste sichtbare Balken, die Anzahl der sichtbaren Balken, der Mindestpreis oder der Höchstpreis geändert haben, werden die entsprechenden globalen Variablen aktualisiert und das Flag auf true gesetzt. Wir erhalten die aktuelle Serverzeit mit TimeCurrent und prüfen dann, ob das Flag wahr ist oder ob neue Balken hinzugefügt wurden, indem wir rates_total mit prev_calculated vergleichen, oder ob ein neues Signal ein Redraw erfordert; wenn eine der Bedingungen zutrifft, rufen wir die Funktion „Redraw“ mit den Gesamtraten auf und aktualisieren den Zeitstempel des letzten Redraw. Schließlich erzwingen wir mit ChartRedraw eine Aktualisierung des Charts, um sicherzustellen, dass alle Aktualisierungen sichtbar sind. Wenn wir es in das Chart laden, sehen wir das folgende Ergebnis.

Aus dem Bild können wir ersehen, dass wir den Nebel auf der Grundlage der Höhe der Kerzen eingestellt haben. Nun müssen wir die Boxen rendern. Wir werden die Logik bedingungslos in der gleichen Umzeichnungsfunktion unterbringen.
for (int j = 0; j < ArraySize(all_boxes); j++) { //--- Loop boxes int left_bar = iBarShift(_Symbol, _Period, all_boxes[j].left_time); //--- Get left bar int x_left = ShiftToX(left_bar); //--- Get left x int x_right; //--- Declare right x if (all_boxes[j].right_time == 0) { //--- Check active x_right = currentChartWidth - 1; //--- Set to end } else { //--- Handle closed int right_bar = iBarShift(_Symbol, _Period, all_boxes[j].right_time); //--- Get right bar x_right = ShiftToX(right_bar); //--- Get right x } int y_top = PriceToY(all_boxes[j].top); //--- Get top y int y_bottom = PriceToY(all_boxes[j].bottom); //--- Get bottom y color fill_col = all_boxes[j].dir == 1 ? box_bull_fill : box_bear_fill; //--- Set fill DrawBoxOnCanvas(x_left, y_top, x_right, y_bottom, fill_col, box_fill_transp); //--- Draw box }
Um die Boxen zu zeichnen, durchlaufen wir eine Schleife durch jeden Eintrag im Array „all_boxes“ und verwenden ArraySize, um die Gesamtzahl zu ermitteln, wobei wir von Index 0 bis zum Ende arbeiten. Für jedes Kästchen wird der linke Balkenindex mit iBarShift abgerufen, wobei das Symbol, die Periode und die linke Zeit des Kästchens übergeben werden, um den Zeitstempel in eine Balkenposition umzuwandeln, dann wird die linke x-Koordinate auf der Leinwand mit „ShiftToX“ berechnet.
Wir deklarieren eine Variable für die rechte x-Koordinate; wenn die rechte Zeit der Box null ist, was bedeutet, dass sie noch aktiv ist, setzen wir die rechte x-Koordinate auf die Chartbreite minus eins, um sie bis zum Rand zu verlängern; andernfalls, für geschlossene Boxen, erhalten wir den rechten Balkenindex ähnlich mit „iBarShift“ und berechnen seine x-Position mit „ShiftToX“. Wir konvertieren die oberen und unteren Preise der Box in y-Koordinaten mit „PriceToY“, wählen die Füllfarbe basierend auf der Richtung – mit der bullishen Füllung, wenn die Richtung 1 ist, oder der bearishen, wenn -1-, und rufen „DrawBoxOnCanvas“ mit den berechneten x- und y-Positionen, der gewählten Farbe und der Transparenz auf, um die Box visuell auf der Leinwand darzustellen. Wir kommen zu folgendem Ergebnis.

Wir sehen, dass die Kästchen perfekt gezeichnet sind. Jetzt bleibt nur noch der wichtigste Teil, nämlich die Signalblasen zu zeichnen, wenn sie ausgewählt sind. Um dies zu erreichen, verwenden wir die folgende Logik.
if (signal_type == Labels_Buy_Sell) { //--- Check labels for (int j = 0; j < ArraySize(all_signals); j++) { //--- Loop signals int bar = iBarShift(_Symbol, _Period, all_signals[j].time); //--- Get bar if (bar > firstVisibleBarIndex || bar < firstVisibleBarIndex - visibleBarsCount) continue; //--- Skip off view int x = ShiftToX(bar); //--- Get x int buffer_index = rates_total - 1 - bar; //--- Get buffer double price = (all_signals[j].dir == 1) ? l[buffer_index] : h[buffer_index]; //--- Set price int y = PriceToY(price); //--- Get y string text = (all_signals[j].dir == 1) ? "BUY" : "SELL"; //--- Set text color bg_col = (all_signals[j].dir == 1) ? signal_buy_col : signal_sell_col; //--- Set bg color border_col = DarkenColor(bg_col, 0.5); //--- Set border color text_col = clrWhite; //--- Set text color obj_Canvas.FontSet("Arial Bold", (uint)current_font_size, FW_BOLD); //--- Set font int text_width = obj_Canvas.TextWidth(text); //--- Get width int text_height = obj_Canvas.TextHeight(text); //--- Get height int padding_width = 6 + current_font_size / 3; //--- Compute width pad int padding_height = 4 + current_font_size / 4; //--- Compute height pad int rect_width = text_width + padding_width; //--- Set rect width int rect_height = text_height + padding_height; //--- Set rect height int label_offset = base_offset + currentChartScale; //--- Set offset int tri_base = 6 + currentChartScale * 2; //--- Set tri base tri_base = (tri_base / 2) * 2; //--- Ensure even int tri_height = (int)(tri_base * 0.5); //--- Set tri height int x_rect = x - rect_width / 2; //--- Set rect x int y_rect = (all_signals[j].dir == 1) ? y + label_offset : y - rect_height - label_offset; //--- Set rect y uchar alpha = (uchar)(255 * (100 - 20) / 100); //--- Set alpha uint argb_fill = ColorToARGB(bg_col, alpha); //--- Get fill obj_Canvas.FillRectangle(x_rect, y_rect, x_rect + rect_width, y_rect + rect_height, argb_fill); //--- Fill rect uint argb_border = ColorToARGB(border_col, 255); //--- Get border obj_Canvas.LineAA(x_rect, y_rect, x_rect + rect_width, y_rect, argb_border); //--- Draw top obj_Canvas.LineAA(x_rect + rect_width, y_rect, x_rect + rect_width, y_rect + rect_height, argb_border); //--- Draw right obj_Canvas.LineAA(x_rect + rect_width, y_rect + rect_height, x_rect, y_rect + rect_height, argb_border); //--- Draw bottom obj_Canvas.LineAA(x_rect, y_rect + rect_height, x_rect, y_rect, argb_border); //--- Draw left int x_center = x_rect + rect_width / 2; //--- Set center if (all_signals[j].dir == 1) { //--- Handle buy int tri_left = x_center - tri_base / 2; //--- Set left int tri_right = x_center + tri_base / 2; //--- Set right int tri_tip_y = y_rect - tri_height; //--- Set tip obj_Canvas.FillTriangle(tri_left, y_rect, tri_right, y_rect, x_center, tri_tip_y, argb_fill); //--- Fill tri obj_Canvas.LineAA(tri_left, y_rect, x_center, tri_tip_y, argb_border); //--- Draw left slant obj_Canvas.LineAA(tri_right, y_rect, x_center, tri_tip_y, argb_border); //--- Draw right slant } else { //--- Handle sell int tri_bottom_y = y_rect + rect_height; //--- Set bottom int tri_left = x_center - tri_base / 2; //--- Set left int tri_right = x_center + tri_base / 2; //--- Set right int tri_tip_y = tri_bottom_y + tri_height; //--- Set tip obj_Canvas.FillTriangle(tri_left, tri_bottom_y, tri_right, tri_bottom_y, x_center, tri_tip_y, argb_fill); //--- Fill tri obj_Canvas.LineAA(tri_left, tri_bottom_y, x_center, tri_tip_y, argb_border); //--- Draw left slant obj_Canvas.LineAA(tri_right, tri_bottom_y, x_center, tri_tip_y, argb_border); //--- Draw right slant } int text_x = x_rect + rect_width / 2; //--- Set text x int text_y = y_rect + rect_height / 2; //--- Set text y uint argb_text = ColorToARGB(text_col, 255); //--- Get text ARGB obj_Canvas.TextOut(text_x, text_y, text, argb_text, TA_CENTER | TA_VCENTER); //--- Draw text } }
Zunächst wird geprüft, ob der Signaltyp „Labels_Buy_Sell“ ist, um Textblasen darzustellen. Dann wird eine Schleife durch jeden Eintrag im Array „all_signals“ gezogen, wobei ArraySize für die Anzahl verwendet wird. Für jedes Signal wird der Zeitstempel in einen Balkenindex mit iBarShift umgewandelt, wobei das Symbol und die Periode übergeben werden und die Iteration übersprungen wird, wenn der Balken außerhalb des sichtbaren Bereichs liegt, indem mit dem ersten sichtbaren Index und der Anzahl der sichtbaren Balken verglichen wird. Wir berechnen die x-Koordinate mit „ShiftToX“, leiten den Pufferindex als Gesamtkurse minus eins minus den Balken ab, setzen den Referenzpreis auf das Tief für Kaufsignale (Richtung 1) oder das Hoch für Verkäufe und wandeln diesen Preis mit „PriceToY“ in y um. Wir bestimmen den Text des Labels je nach Richtung als „BUY“ oder „SELL“, wählen die Hintergrundfarbe entsprechend aus den Nutzereingaben aus, leiten eine Rahmenfarbe ab, indem wir den Hintergrund mit „DarkenColor“ um den Faktor 0,5 abdunkeln, und setzen die Textfarbe auf Weiß.
Wir konfigurieren die Canvas-Schriftart über „FontSet“ auf „Arial Bold“ mit der aktuellen Schriftgröße und dem Fett-Flag, messen die Textabmessungen mit „TextWidth“ und „TextHeight“, berechnen die Auffüllung für Breite und Höhe auf der Grundlage der Schriftgröße und leiten die Abmessungen des Rechtecks durch Hinzufügen der Auffüllung zu den Textgrößen ab. Wir legen einen Labelversatz fest, der Basisversatz und Chartskalierung kombiniert, berechnen eine Dreiecksbasis als 6 plus das Doppelte der Skala und stellen sicher, dass sie durch ganzzahlige Division und Multiplikation gleichmäßig ist, und setzen dann die Höhe des Dreiecks auf die Hälfte der Basis, die in int umgerechnet wird. Wir zentrieren das Rechteck x, indem wir die Hälfte seiner Breite vom Signal x abziehen, positionieren y je nach Richtung nach oben oder unten, berechnen einen Alpha-Wert als uchar mit dem Wert 255 mal (100 minus 20) geteilt durch 100, um eine Deckkraft von 80 % zu erhalten, und ermitteln die ARGB-Füllfarbe mit ColorToARGB und füllen den Rechteckbereich mit FillRectangle. Wir wählen ARGB für den Rand und zeichnen dann mit LineAA die Antialiasing-Linien für den oberen, rechten, unteren und linken Rand des Rechtecks.
Für Kaufsignale berechnen wir Dreieckspunkte, die links und rechts um die halbe Grundfläche unterhalb des Rechtecks und y oberhalb um die Höhe versetzt sind, füllen das Dreieck mit FillTriangle mit der ARGB-Füllung und zeichnen die schrägen Seiten mit „LineAA“. Das ist jetzt nicht neu für Sie. Sie werden verstehen, warum wir uns für diese und nicht für die Standardvariante entschieden haben. Bei Verkaufssignalen positionieren wir die Dreieckspunkte mit der Unterseite an der Unterseite des Rechtecks, links und rechts ähnlich, kippen in der Höhe nach unten, füllen es und zeichnen die Schrägstriche. Schließlich zentrieren wir den Text x und y innerhalb des Rechtecks, konvertieren die Textfarbe in ARGB und geben den Text mit TextOut unter Verwendung der Ausrichtungsflags center und vertical center aus. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

Wie wir sehen können, sind die Signale nun in Blasen angeordnet. Nun müssen wir die Stop-Loss- und Take-Profit-Niveaus berechnen.
if (showTPSL && (buy_event || sell_event) && i == rates_total - 1) { //--- Check TP/SL int lastSignal = buy_event ? 1 : -1; //--- Set signal double lastClose = close[i]; //--- Set close double sum_range = 0; //--- Init sum int cnt_range = 0; //--- Init count for (int k = 0; k < tp_sl_length; k++) { //--- Loop range if (i - k < 0) break; //--- Skip invalid sum_range += high[i - k] - low[i - k]; //--- Accumulate cnt_range++; //--- Increment } double avgRange = (cnt_range > 0) ? sum_range / cnt_range : 0; //--- Compute avg double sl_val = (lastSignal == 1) ? (tpSlMode == Candle_Multiplier ? lastClose - avgRange * slMultiplier : lastClose * (1 - slPercent / 100)) : (tpSlMode == Candle_Multiplier ? lastClose + avgRange * slMultiplier : lastClose * (1 + slPercent / 100)); //--- Compute SL double tp1_val = (lastSignal == 1) ? (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp1Multiplier : lastClose * (1 + tp1Percent / 100)) : (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp1Multiplier : lastClose * (1 - tp1Percent / 100)); //--- Compute TP1 double tp2_val = (lastSignal == 1) ? (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp2Multiplier : lastClose * (1 + tp2Percent / 100)) : (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp2Multiplier : lastClose * (1 - tp2Percent / 100)); //--- Compute TP2 double tp3_val = (lastSignal == 1) ? (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp3Multiplier : lastClose * (1 + tp3Percent / 100)) : (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp3Multiplier : lastClose * (1 - tp3Percent / 100)); //--- Compute TP3 last_dir = lastSignal; //--- Update dir last_sl = sl_val; //--- Update SL last_tp1 = tp1_val; //--- Update TP1 last_tp2 = tp2_val; //--- Update TP2 last_tp3 = tp3_val; //--- Update TP3 last_signal_time = time[i]; //--- Update time new_signal_redraw = true; //--- Set redraw } if (i == rates_total - 1) { //--- Check last bar static bool last_buy = false; //--- Last buy static bool last_sell = false; //--- Last sell if (buy_event && !last_buy) { //--- Handle new buy Alert("WaveTrend BUY " + _Symbol + " @" + DoubleToString(close[i], _Digits)); //--- Alert buy last_buy = true; //--- Set last buy new_signal_redraw = true; //--- Set redraw } else last_buy = buy_event; //--- Update last buy if (sell_event && !last_sell) { //--- Handle new sell Alert("WaveTrend SELL " + _Symbol + " @" + DoubleToString(close[i], _Digits)); //--- Alert sell last_sell = true; //--- Set last sell new_signal_redraw = true; //--- Set redraw } else last_sell = sell_event; //--- Update last sell }
Hier prüfen wir, ob die Take-Profit- und Stop-Loss-Anzeige aktiviert ist, ein Kauf- oder Verkaufsereignis eingetreten ist und wir den letzten Balken in den Kursen verarbeiten; wenn ja, bestimmen wir die Signalrichtung als 1 für Kauf oder -1 für Verkauf, erfassen den aktuellen Schlusskurs, initialisieren dann eine Summe und einen Zähler auf Null und ziehen eine Schleife über die vergangenen Balken bis zur tp_sl_length, wobei ungültige Indizes übersprungen werden, der Hoch-Tief-Bereich in der Summe akkumuliert und der Zähler erhöht wird.
Die durchschnittliche Reichweite wird berechnet, indem die Summe durch die Anzahl geteilt wird, wenn sie positiv ist, und ansonsten auf Null gesetzt wird. Abhängig von der Signalrichtung und dem gewählten „tpSlMode“ aus der Enumeration „Candle_Multiplier“ oder „Percentage“ berechnen wir den Stop-Loss-Wert: für Käufe im Multiplier-Modus als Close minus Average Range mal slMultiplier oder in Prozent als Close mal (1 minus slPercent über 100) und umgekehrt für Verkäufe addierend oder multiplizierend (1 plus slPercent über 100). Wir führen ähnliche bedingte Berechnungen für die drei Take-Profit-Werte durch, indem wir die jeweiligen Multiplikatoren oder Prozentsätze verwenden und die Addition oder Subtraktion je nach Richtung anpassen.
Wir aktualisieren die statische letzte Richtung, den Stop-Loss, die Take-Profits und die Signalzeit mit diesen Werten und setzen das Flag zum Neuzeichnen auf true, um die Grafik zu aktualisieren. Wenn es sich um den letzten Balken handelt, verwenden wir außerdem statische boolesche Werte, um frühere Kauf- und Verkaufszustände zu verfolgen; bei einem neuen Kaufereignis, das zuvor nicht gekennzeichnet wurde, lösen wir einen Alarm mit einer Nachricht aus, die „WaveTrend BUY“, das Symbol, „@“ und den Schlusskurs enthält, der mit DoubleToString auf die Ziffern des Symbols formatiert wird, setzen den letzten Kauf auf true und aktivieren die Neuzeichnung; andernfalls aktualisieren wir das Flag für den letzten Kauf. Wir behandeln Verkaufsereignisse analog mit einer Warnung für „WaveTrend SELL“, aktualisieren die letzte Verkaufsflagge und setzen Neuzeichnen, falls notwendig. Dadurch erhalten wir die Warnungen, wenn es ein Signal gibt, wie unten gezeigt.

Um die Niveaus im Chart zu visualisieren, gehen wir nach der folgenden Logik vor.
if (showTPSL) { //--- Check TP/SL if (last_dir == 0) { //--- Handle no dir ObjectDelete(0, slLine); //--- Delete SL ObjectDelete(0, tp1Line); //--- Delete TP1 ObjectDelete(0, tp2Line); //--- Delete TP2 ObjectDelete(0, tp3Line); //--- Delete TP3 for (int i = 0; i < ArraySize(tpSlTableObjects); i++) { //--- Loop table ObjectDelete(0, tpSlTableObjects[i]); //--- Delete object } } else { //--- Handle dir datetime extension_time = last_signal_time; //--- Set time color tp_color = (last_dir == 1) ? col_up : col_dn; //--- Set TP color color sl_color = (last_dir == 1) ? col_dn : col_up; //--- Set SL color drawRightPrice(slLine, extension_time, last_sl, sl_color, STYLE_SOLID, 2); //--- Draw SL drawRightPrice(tp1Line, extension_time, last_tp1, tp_color, STYLE_SOLID, 1); //--- Draw TP1 drawRightPrice(tp2Line, extension_time, last_tp2, tp_color, STYLE_SOLID, 1); //--- Draw TP2 drawRightPrice(tp3Line, extension_time, last_tp3, tp_color, STYLE_SOLID, 1); //--- Draw TP3 currentChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Update width drawRectangleLabel(tpSlTableObjects[0], currentChartWidth - 150, 20, 120, 120, clrGray, BORDER_FLAT, true); //--- Draw frame drawLabel(tpSlTableObjects[1], currentChartWidth - 140, 30, "Level", clrBlack); //--- Draw level drawLabel(tpSlTableObjects[2], currentChartWidth - 80, 30, "Price", clrBlack); //--- Draw price drawLabel(tpSlTableObjects[3], currentChartWidth - 140, 50, "TP1", tp_color); //--- Draw TP1 drawLabel(tpSlTableObjects[4], currentChartWidth - 80, 50, DoubleToString(last_tp1, _Digits), tp_color); //--- Draw TP1 price drawLabel(tpSlTableObjects[5], currentChartWidth - 140, 70, "TP2", tp_color); //--- Draw TP2 drawLabel(tpSlTableObjects[6], currentChartWidth - 80, 70, DoubleToString(last_tp2, _Digits), tp_color); //--- Draw TP2 price drawLabel(tpSlTableObjects[7], currentChartWidth - 140, 90, "TP3", tp_color); //--- Draw TP3 drawLabel(tpSlTableObjects[8], currentChartWidth - 80, 90, DoubleToString(last_tp3, _Digits), tp_color); //--- Draw TP3 price drawLabel(tpSlTableObjects[9], currentChartWidth - 140, 110, "SL", sl_color); //--- Draw SL drawLabel(tpSlTableObjects[10], currentChartWidth - 80, 110, DoubleToString(last_sl, _Digits), sl_color); //--- Draw SL price } }
Wir prüfen, ob die Take-Profit- und Stop-Loss-Anzeige aktiviert ist; wenn dies der Fall ist und die letzte Richtung auf Null steht, was bedeutet, dass kein aktives Signal vorliegt, entfernen wir alle zugehörigen Objekte, indem wir ObjectDelete für die Stop-Loss- und Take-Profit-Zeilen aufrufen und dann eine Schleife durch das Array der Tabellenobjekte mit ArraySize durchführen, um jedes einzelne zu löschen. Andernfalls setzen wir für eine aktive Richtung eine Verlängerungszeit auf den letzten Signalzeitstempel, wählen die Farbe der Gewinnmitnahme als Aufwärtsfarbe für Käufe oder Abwärtsfarbe für Verkäufe und die Stop-Loss-Farbe als das Gegenteil; dann rufen wir „drawRightPrice“ auf, um die Stop-Loss-Linie mit dem Stil „solid“ und der Breite 2 und jede Gewinnmitnahme-Linie mit dem Stil „solid“ und der Breite 1 darzustellen, alle zur Verlängerungszeit mit ihren jeweiligen Preisen und Farben.
Wir aktualisieren die aktuelle Chartbreite über ChartGetInteger mit CHART_WIDTH_IN_PIXELS, zeichnen den Tabellenrahmen mit „drawRectangleLabel“ 150 Pixel vom rechten Rand bei y 20 mit der Größe 120 × 120, grauer Farbe, flachem Rand und aktiviertem Hintergrund; dann platzieren wir Labels mit „drawLabel“ Beschriftungen für die Überschriften „Niveau“ und „Price“ in Schwarz und für jeden Take-Profit und Stop-Loss mit ihren Bezeichnungen wie „TP1“ und formatierten Preisen mit DoubleToString mit den Ziffern des Symbols, entsprechend gefärbt, in bestimmten Abständen vom rechten Rand. Sie können dies nach Ihren Wünschen einstellen. Nach der Kompilierung erhalten wir nun die gerenderten Ebenen.

Wir sehen, dass die Ebenen auf der Leinwand vollständig gerendert werden, wenn dies erlaubt ist. Jetzt müssen nur noch die Änderungen im Chart berücksichtigt werden, damit es nahtlos aktualisiert wird, wenn der Nutzer das Chart ändert. Wir werden die Erkennen von OnChartEvent verwenden, um die Änderungen im Chart zu erkennen.
//+------------------------------------------------------------------+ //| Handle chart event | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_CHART_CHANGE) { //--- Check change Redraw(Bars(_Symbol, _Period)); //--- Redraw UpdateFontSizes(); //--- Update fonts } }
In OnChartEvent werden eingehende Parameter verarbeitet, darunter die Ereignis-ID, ein ganzzahliger Wert, ein reeller Wert und eine Zeichenkette, um Nutzerinteraktionen mit dem Chart zu erkennen und darauf zu reagieren. Wenn die ID CHARTEVENT_CHART_CHANGE entspricht, was Anpassungen wie Zoomen, Schwenken oder Größenänderung bedeutet, lösen wir eine Aktualisierung aus, indem wir die Funktion „Redraw“ mit der Gesamtzahl der Balken aufrufen, die über „Bars“ unter Verwendung des aktuellen Symbols und Zeitrahmens abgerufen wurde, und führen außerdem „UpdateFontSizes“ aus, um die Textelemente für eine optimale Sichtbarkeit neu zu kalibrieren. Außerdem müssen wir die Chartobjekte löschen, wenn wir den Indikator aus dem Chart entfernen.
//+------------------------------------------------------------------+ //| Deinitialize indicator | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { obj_Canvas.Destroy(); //--- Destroy canvas ArrayResize(all_boxes, 0); //--- Clear boxes ArrayResize(all_signals, 0); //--- Clear signals ObjectDelete(0, slLine); //--- Delete SL line ObjectDelete(0, tp1Line); //--- Delete TP1 line ObjectDelete(0, tp2Line); //--- Delete TP2 line ObjectDelete(0, tp3Line); //--- Delete TP3 line for (int i = 0; i < ArraySize(tpSlTableObjects); i++) { //--- Loop through table objects ObjectDelete(0, tpSlTableObjects[i]); //--- Delete object } ChartRedraw(0); //--- Redraw chart }
In der Ereignisbehandlung von OnDeinit werden die Ressourcen beim Entfernen des Indikators aufgeräumt, indem zunächst das Canvas-Objekt mit seiner Destroy-Methode entfernt wird, um die grafischen Elemente freizugeben. Die Arrays „all_boxes“ und „all_signals“ werden mit ArrayResize auf Null gesetzt, um die gespeicherten Daten zu löschen und Speicher freizugeben. Wir entfernen die Stop-Loss- und Take-Profit-Linien, indem wir ObjectDelete für jedes benannte Linienobjekt aufrufen. Anschließend durchlaufen wir das Array der Tabellenobjekte auf der Grundlage seiner Größe aus ArraySize und löschen jedes einzelne mit „ObjectDelete“, um die Anzeige des Risikomanagements zu eliminieren. Schließlich rufen wir ChartRedraw auf, um das Chart zu aktualisieren und alle Löschungen zu berücksichtigen. Damit haben wir unsere Ziele erreicht. Bleibt nur noch der Backtest des Programms, und das wird im nächsten Abschnitt behandelt.
Backtests
Wir haben die Tests durchgeführt, und unten sehen Sie die kompilierte Visualisierung in einem einzigen Graphics Interchange Format (GIF) Bitmap-Bildformat.

Schlussfolgerung
Abschließend haben wir den Indikator WaveTrend Crossover in MQL5 mit einer leinwandbasierten Darstellung für Überlagerungen mit Nebelverläufen, Signalkästchen zur Erkennung von Ausbrüchen, anpassbaren Kauf- und Verkaufsblasen oder -dreiecken für visuelle Warnungen und einem integrierten Risikomanagement durch dynamische Take-Profit- und Stop-Loss-Niveaus verbessert. Dieses Upgrade beinhaltet fortschrittliche Visualisierungen wie Nebelverläufe für den Marktkontext sowie Optionen für Trendfilterung, Box-Erweiterungen und Berechnungen über Kerzenmultiplikatoren oder Prozentsätze, die mit Linien und Tabellen angezeigt werden. Diese Konfiguration bietet uns umfassende Tools für Momentumverschiebungen, Trendanalysen und Risikobewertungen. Mit diesem verbesserten Indikator WaveTrend Crossover sind Sie in der Lage, die visuellen und risikorelevanten Funktionen zu nutzen, um bessere Einblicke in den Handel zu erhalten und Ihre Trading-Reise weiter zu optimieren. Viel Spaß beim Handeln!
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20815
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.
Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 4): Smart WaveTrend Crossover mit zwei Oszillatoren
Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates
Python-MetaTrader 5 Strategie-Tester (Teil 02): Umgang mit Balken, Ticks und Überladung eingebauter Funktionen in einem Simulator
- 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.