English
preview
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement

Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 5): WaveTrend Crossover Evolution mit einer Leinwand für Nebelverläufe, Signalblasen und Risikomanagement

MetaTrader 5Handelssysteme |
17 0
Allan Munene Mutiiria
Allan Munene Mutiiria

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:

  1. Das erweiterte, canvasbasierte WaveTrend Crossover Framework mit visuellen und Risikofunktionen verstehen
  2. Implementation in MQL5
  3. Backtests
  4. 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.

MQL5 CANVAS-BASIERTES EVOLUTIONS-FRAMEWORK


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.

INITIALISIERTE LEINWAND

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.

ANTIALIASING (AA) VERGLEICH

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.

NEUZEICHNEN DES NEBELS

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.

KANVASKÄSTCHEN

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:

SIGNALBLASEN

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.

SIGNAL ALERT

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.

KOMPLETTE LEINWANDZEICHNUNG MIT SL/TP-NIVEAUS

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.

BACKTEST GIF


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

Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite Datenbanken sind einfach (Teil 1): Ein leichtes ORM-Framework für MQL5 unter Verwendung von SQLite
Dieser Artikel stellt einen strukturierten Weg zur Verwaltung von SQLite-Daten in MQL5 durch eine ORM-Schicht für MetaTrader 5 vor. Es führt Kernklassen für die Entitätsmodellierung und den Datenbankzugriff ein, eine flüssige CRUD-API, Reflection Hooks für OnGet/OnSet und Makros zur schnellen Definition von Modellen. Der praxisnahe Code zeigt das Erstellen von Tabellen, das Binden von Feldern, Einfügen, Aktualisieren, Abfragen und Löschen von Datensätzen. Entwickler erhalten wiederverwendbare, typsichere Komponenten, die wiederholtes SQL auf ein Minimum reduzieren.
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 4): Smart WaveTrend Crossover mit zwei Oszillatoren Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 4): Smart WaveTrend Crossover mit zwei Oszillatoren
In diesem Artikel entwickeln wir einen nutzerdefinierten Indikator in MQL5 namens Smart WaveTrend Crossover, der zwei WaveTrend-Oszillatoren verwendet – einen für die Erzeugung der Signale über das Kreuzen und einen anderen für die Trendfilterung – mit anpassbaren Parametern für Kanal-, Durchschnitts- und gleitende Durchschnittslängen. Der Indikator stellt farbige Kerzen auf der Grundlage der Trendrichtung dar, zeigt Kauf- und Verkaufspfeilsignale bei Überkreuzungen an und enthält Optionen zur Aktivierung der Trendbestätigung und zur Anpassung visueller Elemente wie Farben und Offsets.
Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates Statistische Arbitrage durch kointegrierte Aktien (Teil 9): Backtests, Portfolio-Gewichtungen, Updates
Dieser Artikel beschreibt die Verwendung von CSV-Dateien für das Backtesting von Aktualisierungen der Portfoliogewichte in einer auf der Rückkehr zum Mittelwert basierenden Strategie, die statistische Arbitrage durch kointegrierte Aktien nutzt. Sie reicht von der Einspeisung der Ergebnisse der Rolling Windows Eigenvektor Comparison (RWEC) in die Datenbank bis zum Vergleich der Backtest-Berichte. In der Zwischenzeit werden in dem Artikel die Rolle der einzelnen RWEC-Parameter und ihre Auswirkung auf das Gesamtergebnis des Backtests detailliert beschrieben und gezeigt, wie der Vergleich des relativen Drawdowns uns helfen kann, diese Parameter weiter zu verbessern.
Python-MetaTrader 5 Strategie-Tester (Teil 02): Umgang mit Balken, Ticks und Überladung eingebauter Funktionen in einem Simulator Python-MetaTrader 5 Strategie-Tester (Teil 02): Umgang mit Balken, Ticks und Überladung eingebauter Funktionen in einem Simulator
In diesem Artikel stellen wir Funktionen vor, die denen des Moduls Python-MetaTrader 5 ähneln und einen Simulator mit einer vertrauten Schnittstelle und einer eigenen Art der internen Handhabung von Balken und Ticks bieten.