English
preview
Erstellen eines Keltner-Kanal-Indikators mit nutzerdefinierten Canvas-Grafiken in MQL5

Erstellen eines Keltner-Kanal-Indikators mit nutzerdefinierten Canvas-Grafiken in MQL5

MetaTrader 5Handelssysteme | 12 Juni 2025, 07:36
56 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In diesem Artikel erstellen wir einen nutzerdefinierten Keltner Channel-Indikator mit erweiterten Canvas-Grafiken in MetaQuotes Language 5 (MQL5). Der Keltner Channel berechnet dynamische Unterstützungs- und Widerstandsniveaus unter Verwendung der Indikatoren Moving Average (MA) und Average True Range (ATR) und hilft Händlern, Trendrichtungen und potenzielle Ausbrüche zu erkennen. In diesem Artikel werden wir unter anderem folgende Themen behandeln:

  1. Den Keltner-Kanal-Indikator verstehen
  2. Blaupause: Aufschlüsselung der Architektur des Indikators
  3. Implementation in MQL5
  4. Integrieren von nutzerdefinierten Grafiken
  5. Backtests des Keltner-Kanal-Indikators
  6. Schlussfolgerung


Den Keltner-Kanal-Indikator verstehen

Der Indikator Keltner Channel ist ein auf der Volatilität basierendes Instrument, das Händler verwenden. Er nutzt einen Moving Average (MA)-Indikator zur Glättung der Kursdaten und den Average True Range (ATR)-Indikator zur Festlegung dynamischer Unterstützungs- und Widerstandsniveaus. Er hat 3 Linien, die einen Kanal bilden. Die mittlere Linie des Kanals ist ein gleitender Durchschnitt, der in der Regel so gewählt wird, dass er den vorherrschenden Trend widerspiegelt. Gleichzeitig werden die oberen und unteren Bänder durch Addition und Subtraktion eines Vielfachen des ATR erzeugt. Diese Methode ermöglicht es dem Indikator, sich an die Marktvolatilität anzupassen, wodurch es einfacher wird, potenzielle Ausbruchspunkte oder Bereiche zu erkennen, in denen sich die Kursentwicklung umkehren könnte.

In der Praxis hilft uns der Indikator dabei, wichtige Marktniveaus zu identifizieren, bei denen sich die Dynamik verändern könnte. Wenn sich die Kurse außerhalb der oberen oder unteren Bänder bewegen, deutet dies auf einen überbewerteten Markt oder eine potenzielle Umkehrung hin, was sowohl für Trendfolge- als auch für Mean-Reversion-Strategien verwertbare Erkenntnisse liefert. Aufgrund seiner dynamischen Natur passt sich der Indikator an Veränderungen der Volatilität an und sorgt dafür, dass die Unterstützungs- und Widerstandsniveaus bei sich verändernden Marktbedingungen relevant bleiben. Hier ist ein visuelles Beispiel.

KANAL-MUSTER


Blaupause: Aufschlüsselung der Architektur des Indikators

Wir werden die Architektur des Indikators auf einer klaren Trennung der Verantwortlichkeiten aufbauen: Eingabeparameter, Indikatorpuffer und grafische Eigenschaften. Wir beginnen mit der Definition der wichtigsten Eingaben, wie z. B. der Periode des gleitenden Durchschnitts, der ATR-Periode und des ATR-Multiplikators, die das Verhalten des Indikators bestimmen. Wir werden dann drei Puffer zuweisen, um die Werte für den oberen Kanal, die mittlere Zeile und den unteren Kanal zu speichern. Diese Puffer werden mit grafischen Darstellungen verknüpft, deren Eigenschaften wie Farbe, Linienbreite und Verschiebung mit den integrierten Funktionen von MQL5 konfiguriert werden. Darüber hinaus werden wir die integrierten Funktionen für die Berechnungen nutzen, um sicherzustellen, dass sich der Indikator dynamisch an die Marktvolatilität anpasst.

Außerdem werden wir eine Fehlerbehandlung einbauen, um sicherzustellen, dass beide Indikator-Handles erfolgreich erstellt werden und eine zuverlässige Grundlage für die Berechnungen des Indikators bilden. Über die Kernlogik des Indikators hinaus werden wir nutzerdefinierte Grafiken des Hintergrunds (canvas) integrieren, um die visuelle Präsentation zu verbessern, einschließlich der Erstellung eines Bitmap-Labels, das das Chart überlagert. Dieser modulare Aufbau vereinfacht nicht nur die Fehlersuche und künftige Änderungen, sondern stellt auch sicher, dass jede Komponente - von der Datenberechnung bis zur visuellen Ausgabe - harmonisch funktioniert und ein robustes und visuell ansprechendes Handelsinstrument darstellt. Kurz gesagt, sind dies die drei Dinge, die wir erreichen werden.

ARCHITEKTUR DES INDIKATORS


Implementation in MQL5

Um den Indikator in MQL5 zu erstellen, öffnen Sie einfach den MetaEditor, gehen zum Navigator, suchen den Ordner Indikatoren, klicken Sie auf die Registerkarte „Neu“ und folgen Sie den Anweisungen, um die Datei zu erstellen. Sobald der Indikator erstellt ist, werden wir in der Codierungsumgebung die Eigenschaften und Einstellungen des Indikators festlegen, z. B. die Anzahl der Puffer, die Plots und die Eigenschaften der einzelnen Linien, wie Farbe, Breite und Kennzeichnung.

//+------------------------------------------------------------------+
//|                             Keltner Channel Canvas Indicator.mq5 |
//|                        Copyright 2025, Forex Algo-Trader, Allan. |
//|                                 "https://t.me/Forex_Algo_Trader" |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "Description: Keltner Channel Indicator"
#property indicator_chart_window

//+------------------------------------------------------------------+
//| Indicator properties and settings                                |
//+------------------------------------------------------------------+

// Define the number of buffers used for plotting data on the chart
#property indicator_buffers 3  // We will use 3 buffers: Upper Channel, Middle (MA) line, and Lower Channel

// Define the number of plots on the chart
#property indicator_plots   3  // We will plot 3 lines (Upper, Middle, and Lower)

//--- Plot settings for the Upper Keltner Channel line
#property indicator_type1   DRAW_LINE        // Draw the Upper Channel as a line
#property indicator_color1  clrBlue           // Set the color of the Upper Channel to Blue
#property indicator_label1  "Upper Keltner"   // Label of the Upper Channel line in the Data Window
#property indicator_width1  2                 // Set the line width of the Upper Channel to 2 pixels

//--- Plot settings for the Middle Keltner Channel line (the moving average)
#property indicator_type2   DRAW_LINE        // Draw the Middle (MA) Channel as a line
#property indicator_color2  clrGray           // Set the color of the Middle (MA) Channel to Gray
#property indicator_label2  "Middle Keltner"  // Label of the Middle (MA) line in the Data Window
#property indicator_width2  2                 // Set the line width of the Middle (MA) to 2 pixels

//--- Plot settings for the Lower Keltner Channel line
#property indicator_type3   DRAW_LINE        // Draw the Lower Channel as a line
#property indicator_color3  clrRed            // Set the color of the Lower Channel to Red
#property indicator_label3  "Lower Keltner"   // Label of the Lower Channel line in the Data Window
#property indicator_width3  2                 // Set the line width of the Lower Channel to 2 pixels

Wir beginnen mit dem Setzen der Metadata des Indikators, wie Version etc., mit dem Schlüsselwort #property. Als Nächstes deklarieren wir drei Indikatorpuffer mit der Eigenschaft indicator_buffers, die die berechneten Werte für „Upper Channel“, „Middle Moving Average (MA)“ und „Lower Channel“ speichern und verwalten. Außerdem setzen wir den „indicator_plots“ auf 3, um festzulegen, dass drei separate grafische Darstellungen auf dem Chart gezeichnet werden sollen. Für jedes dieser Elemente konfigurieren wir spezifische Visualisierungseigenschaften:

  • Oberer Keltner Kanal: Wir weisen das Makro DRAW_LINE als „Indikatortyp“ zu, was bedeutet, dass es als durchgehende Linie gezeichnet wird. Die Farbe wird mit clrBlue auf „Blau“ gesetzt, und die Bezeichnung „Upper Keltner“ hilft, ihn im Datenfenster zu identifizieren. Zur besseren Sichtbarkeit haben wir die Linienbreite auf 2 Pixel eingestellt.
  • Mittlerer Keltner-Kanal (Gleitender Durchschnitt): In ähnlicher Weise setzen wir seinen Typ auf „DRAW_LINE“, verwenden die Farbe „Gray“ und weisen ihm die Bezeichnung „Middle Keltner“ zu. Diese Linie stellt den zentralen gleitenden Durchschnitt dar, der als zentrale Referenz für die oberen und unteren Bänder dient.
  • Unterer Keltner-Kanal: Diese Linie wird ebenfalls als DRAW_LINE definiert, mit der Farbe „Red“, um sie von den anderen zu unterscheiden. Die Kennzeichnung „Lower Keltner“ wird zugewiesen, und die Zeilenbreite wird auf 2 Pixel festgelegt.

Mit den Eigenschaften können wir dann zur Definition der Eingabeparameter übergehen.

//+------------------------------------------------------------------+
//| Input parameters for the indicator                               |
//+------------------------------------------------------------------+

//--- Moving Average parameters
input int    maPeriod=20;                 // Moving Average period (number of bars to calculate the moving average)
input ENUM_MA_METHOD maMethod=MODE_EMA;   // Method of the Moving Average (EMA, in this case)
input ENUM_APPLIED_PRICE maPrice=PRICE_CLOSE; // Price used for the Moving Average (closing price of each bar)

//--- ATR parameters
input int    atrPeriod=10;                // ATR period (number of bars used to calculate the Average True Range)
input double atrMultiplier=2.0;           // Multiplier applied to the ATR value to define the channel distance (upper and lower limits)
input bool   showPriceLabel=true;         // Option to show level price labels on the chart (true/false)

Hier definieren wir die Eingabeeigenschaften. Für den gleitenden Durchschnitt legen wir „maPeriod“ (Standardwert 20) fest, um die Anzahl der verwendeten Balken zu definieren. Die „maMethod“ vom Datentyp ENUM_MA_METHOD ist auf „MODE_EMA“ gesetzt, was einen exponentiellen gleitenden Durchschnitt angibt, und „maPrice“ vom Datentyp ENUM_APPLIED_PRICE ist auf „PRICE_CLOSE“ gesetzt, was bedeutet, dass die Berechnungen auf Schlusskursen basieren.

Bei „ATR“ bestimmt „atrPeriod“ (Standardwert 10), wie viele Balken zur Berechnung der Volatilität verwendet werden, während „atrMultiplier“ (Standardwert 2,0) den Abstand der oberen und unteren Bänder vom gleitenden Durchschnitt festlegt. Schließlich steuert „showPriceLabel“ (Standardwert true), ob im Chart PreisKennzeichnungen erscheinen. Diese Einstellungen gewährleisten Flexibilität bei der Anpassung des Indikators an unterschiedliche Marktbedingungen. Schließlich müssen wir die Indikator-Handles definieren, die wir verwenden werden.

//+------------------------------------------------------------------+
//| Indicator handle declarations                                    |
//+------------------------------------------------------------------+

//--- Indicator handles for the Moving Average and ATR
int    maHandle = INVALID_HANDLE;   // Handle for Moving Average (used to store the result of iMA)
int    atrHandle = INVALID_HANDLE;  // Handle for ATR (used to store the result of iATR)

//+------------------------------------------------------------------+
//| Indicator buffers (arrays for storing calculated values)         |
//+------------------------------------------------------------------+

//--- Buffers for storing the calculated indicator values
double upperChannelBuffer[];  // Buffer to store the Upper Channel values (Moving Average + ATR * Multiplier)
double movingAverageBuffer[]; // Buffer to store the Moving Average values (middle of the channel)
double lowerChannelBuffer[];  // Buffer to store the Lower Channel values (Moving Average - ATR * Multiplier)

//+------------------------------------------------------------------+
//| Global variables for the parameter values                        |
//+------------------------------------------------------------------+

//--- These variables store the actual input parameter values, if necessary for any further use or calculations
int    maPeriodValue;      // Store the Moving Average period value
int    atrPeriodValue;     // Store the ATR period value
double atrMultiplierValue; // Store the ATR multiplier value

//+------------------------------------------------------------------+

Hier deklarieren wir die Indikator-Handles, Puffer und einige globale Variablen, die wir für die Berechnungen des Keltner Kanals benötigen. In den Handles werden Verweise auf die Indikatoren gespeichert, sodass wir ihre Werte dynamisch abrufen können. Wir initialisieren „maHandle“ und „atrHandle“ auf INVALID_HANDLE, um eine ordnungsgemäße Handle-Verwaltung vor der Zuweisung sicherzustellen.

Als Nächstes werden die Indikatorpuffer definiert, bei denen es sich um Arrays handelt, in denen die berechneten Werte für die Darstellung gespeichert werden: „upperChannelBuffer“ enthält die oberen Grenzwerte, „movingAverageBuffer“ speichert die mittlere MA-Linie, und „lowerChannelBuffer“ enthält die unteren Grenzwerte. Diese Puffer ermöglichen eine reibungslose Visualisierung des Keltner-Kanals auf dem Chart. Schließlich führen wir die globalen Variablen ein, um Eingabeparameter für die weitere Verwendung zu speichern. „maPeriodValue“ und „atrPeriodValue“ enthalten die nutzerdefinierten Perioden für „MA“ und „ATR“, während „atrMultiplierValue“ den Multiplikator speichert, der zur Bestimmung der Kanalbreite verwendet wird. Wir können nun zum Initialisierungs-Ereignishandler übergehen, wo wir alle notwendigen Indikatorplots und -zuordnungen sowie die Initialisierung der Indikator-Handles vornehmen.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- Indicator buffers mapping
   // Indicator buffers are used to store calculated indicator values. 
   // We link each buffer to a graphical plot for visual representation.
   SetIndexBuffer(0, upperChannelBuffer, INDICATOR_DATA);  // Buffer for the upper channel line
   SetIndexBuffer(1, movingAverageBuffer, INDICATOR_DATA); // Buffer for the middle (moving average) line
   SetIndexBuffer(2, lowerChannelBuffer, INDICATOR_DATA);  // Buffer for the lower channel line

   //--- Set the starting position for drawing each plot
   // The drawing for each line will only begin after a certain number of bars have passed
   // This is to avoid showing incomplete calculations at the start
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, maPeriod + 1); // Start drawing Upper Channel after 'maPeriod + 1' bars
   PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, maPeriod + 1); // Start drawing Middle (MA) after 'maPeriod + 1' bars
   PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, maPeriod + 1); // Start drawing Lower Channel after 'maPeriod + 1' bars

   //--- Set an offset for the plots
   // This shifts the plotted lines by 1 bar to the right, ensuring that the values are aligned properly
   PlotIndexSetInteger(0, PLOT_SHIFT, 1); // Shift the Upper Channel by 1 bar to the right
   PlotIndexSetInteger(1, PLOT_SHIFT, 1); // Shift the Middle (MA) by 1 bar to the right
   PlotIndexSetInteger(2, PLOT_SHIFT, 1); // Shift the Lower Channel by 1 bar to the right

   //--- Define an "empty value" for each plot
   // Any buffer value set to this value will not be drawn on the chart
   // This is useful for gaps where there are no valid indicator values
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0); // Empty value for Upper Channel
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0.0); // Empty value for Middle (MA)
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, 0.0); // Empty value for Lower Channel

   //--- Set the short name of the indicator (displayed in the chart and Data Window)
   // This sets the name of the indicator that appears on the chart
   IndicatorSetString(INDICATOR_SHORTNAME, "Keltner Channel");

   //--- Customize the label for each buffer in the Data Window
   // This allows for better identification of the individual plots in the Data Window
   string short_name = "KC:"; // Shortened name of the indicator
   PlotIndexSetString(0, PLOT_LABEL, short_name + " Upper");  // Label for the Upper Channel
   PlotIndexSetString(1, PLOT_LABEL, short_name + " Middle"); // Label for the Middle (MA)
   PlotIndexSetString(2, PLOT_LABEL, short_name + " Lower");  // Label for the Lower Channel

   //--- Set the number of decimal places for the indicator values
   // _Digits is the number of decimal places used in the current chart symbol
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits); // Ensures indicator values match the chart's price format

   //--- Create indicators (Moving Average and ATR)
   // These are handles (IDs) for the built-in indicators used to calculate the Keltner Channel
   // iMA = Moving Average (EMA in this case), iATR = Average True Range
   maHandle = iMA(NULL, 0, maPeriod, 0, maMethod, maPrice); // Create MA handle (NULL = current chart, 0 = current timeframe)
   atrHandle = iATR(NULL, 0, atrPeriod); // Create ATR handle (NULL = current chart, 0 = current timeframe)

   //--- Error handling for indicator creation
   // Check if the handle for the Moving Average (MA) is valid
   if(maHandle == INVALID_HANDLE)
     {
      // If the handle is invalid, print an error message and return failure code
      Print("UNABLE TO CREATE THE MA HANDLE REVERTING NOW!");
      return (INIT_FAILED); // Initialization failed
     }

   // Check if the handle for the ATR is valid
   if(atrHandle == INVALID_HANDLE)
     {
      // If the handle is invalid, print an error message and return failure code
      Print("UNABLE TO CREATE THE ATR HANDLE REVERTING NOW!");
      return (INIT_FAILED); // Initialization failed
     }

   //--- Return success code
   // If everything works correctly, we return INIT_SUCCEEDED to signal successful initialization
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+

Hier wird der Keltner-Kanal-Indikator in OnInit initialisiert, indem Puffer, Plots, Offsets und Handles konfiguriert werden, um eine korrekte Visualisierung und Berechnung zu gewährleisten. Zunächst wird jeder Indikatorpuffer mit der Funktion SetIndexBuffer mit der entsprechenden grafischen Darstellung verknüpft, um sicherzustellen, dass der „Obere Kanal“, die „Mittlere Linie des gleitenden Durchschnitts (MA)“ und der „Untere Kanal“ korrekt angezeigt werden.

Als Nächstes definieren wir das Zeichenverhalten mit der Funktion PlotIndexSetInteger. Wir stellen die Zeichnung so ein, dass sie erst nach „maPeriod + 1“ Balken beginnt, um zu verhindern, dass unvollständige Berechnungen angezeigt werden. Zusätzlich wird mit PLOT_SHIFT eine Verschiebung nach rechts vorgenommen, um die gezeichneten Werte korrekt auszurichten. Um mit fehlenden Daten umzugehen, weisen wir jedem Puffer mit der Funktion PlotIndexSetDouble einen leeren Wert von „0.0“ zu.

Anschließend konfigurieren wir die Anzeigeeinstellungen. Der Name des Indikators wird mit der Funktion IndicatorSetString festgelegt, während PlotIndexSetString die Kennzeichnungen für jede Linie im „Datenfenster“ zuweist. Die Dezimalpräzision der Indikatorwerte wird mit Hilfe der Funktion „IndicatorSetInteger“ mit dem Preisformat des Charts synchronisiert. Schließlich erstellen wir die Indikator-Handles mit den Funktionen iMA und iATR. Wenn die Erstellung eines Handles fehlschlägt, werden Fehler behandelt, indem eine Fehlermeldung mit Print ausgegeben und INIT_FAILED zurückgegeben wird. Wenn alles erfolgreich ist, wird INIT_SUCCEEDED zurückgegeben, womit der Initialisierungsprozess abgeschlossen ist. Anschließend können wir zur Ereignisbehandlung der Hauptereignisse übergehen, der die Berechnungen des Indikators durchführt.

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,             // Total number of bars in the price series
                const int prev_calculated,        // Number of previously calculated bars
                const datetime &time[],           // Array of time values for each bar
                const double &open[],             // Array of open prices for each bar
                const double &high[],             // Array of high prices for each bar
                const double &low[],              // Array of low prices for each bar
                const double &close[],            // Array of close prices for each bar
                const long &tick_volume[],        // Array of tick volumes for each bar
                const long &volume[],             // Array of trade volumes for each bar
                const int &spread[])              // Array of spreads for each bar
{
   //--- Check if this is the first time the indicator is being calculated
   if(prev_calculated == 0) // If no previous bars were calculated, it means this is the first calculation
     {
      //--- Initialize indicator buffers (upper, middle, and lower) with zeros
      ArrayFill(upperChannelBuffer, 0, rates_total, 0);   // Fill the entire upper channel buffer with 0s
      ArrayFill(movingAverageBuffer, 0, rates_total, 0);  // Fill the moving average buffer with 0s
      ArrayFill(lowerChannelBuffer, 0, rates_total, 0);   // Fill the lower channel buffer with 0s

      //--- Copy Exponential Moving Average (EMA) values into the moving average buffer
      // This function requests 'rates_total' values from the MA indicator (maHandle) and copies them into movingAverageBuffer
      if(CopyBuffer(maHandle, 0, 0, rates_total, movingAverageBuffer) < 0)
         return(0); // If unable to copy data, stop execution and return 0

      //--- Copy Average True Range (ATR) values into a temporary array called atrValues
      double atrValues[];
      if(CopyBuffer(atrHandle, 0, 0, rates_total, atrValues) < 0)
         return(0); // If unable to copy ATR data, stop execution and return 0

      //--- Define the starting bar for calculations
      // We need to make sure we have enough data to calculate both the MA and ATR, so we start after the longest required period.
      int startBar = MathMax(maPeriod, atrPeriod) + 1; // Ensure sufficient bars for both EMA and ATR calculations

      //--- Loop from startBar to the total number of bars (rates_total)
      for(int i = startBar; i < rates_total; i++)
        {
         // Calculate the upper and lower channel boundaries for each bar
         upperChannelBuffer[i] = movingAverageBuffer[i] + atrMultiplier * atrValues[i]; // Upper channel = EMA + ATR * Multiplier
         lowerChannelBuffer[i] = movingAverageBuffer[i] - atrMultiplier * atrValues[i]; // Lower channel = EMA - ATR * Multiplier
        }

      //--- Calculation is complete, so we return the total number of rates (bars) calculated
      return(rates_total);
     }
}

Hier implementieren wir die zentrale Berechnungslogik des Keltner-Kanal-Indikators innerhalb von OnCalculate, einer Funktion, die die Preisdaten iteriert, um die Indikatorpuffer zu berechnen und zu aktualisieren. Zunächst wird geprüft, ob dies die erste Berechnung ist, indem „prev_calculated“ ausgewertet wird. Wenn er „0“ ist, werden die Puffer „Upper Channel“, „Middle Moving Average (MA)“ und „Lower Channel“ mit der Funktion ArrayFill initialisiert, wobei sichergestellt wird, dass alle Werte bei Null beginnen. Als Nächstes füllen wir den „movingAverageBuffer“ mit den MA-Werten, indem wir die Funktion CopyBuffer verwenden. Wenn das Kopieren fehlschlägt, wird die Ausführung durch Rückgabe von „0“ abgebrochen. In ähnlicher Weise rufen wir die ATR-Werte in dem temporären Array „atrValues“ ab.

Um sicherzustellen, dass wir genügend Daten für MA und ATR haben, bestimmen wir den Startbalken mit der Funktion MathMax, die den Maximalwert zwischen den Indikatorperioden liefert, und fügen 1 Balken hinzu, um zu verhindern, dass der aktuelle unvollständige Balken berücksichtigt wird. Anschließend wird eine for-Schleife verwendet, um jeden Balken von „startBar“ bis „rates_total“ zu durchlaufen und die „oberen“ und „unteren“ Kanalgrenzen anhand der Formel zu berechnen:

  • „UpperChannel = Gleitender Durchschnitt + (ATR * Multiplikator)“
  • „LowerChannel = Gleitender Durchschnitt - (ATR * Multiplikator)“

Schließlich wird „rates_total“ zurückgegeben, was die Anzahl der berechneten Balken angibt. Wenn es nicht der erste Indikatorlauf ist, aktualisieren wir einfach die Werte der letzten Balken durch Neuberechnung.

//--- If this is NOT the first calculation, update only the most recent bars
// This prevents re-calculating all bars, which improves performance
int startBar = prev_calculated - 2; // Start 2 bars back to ensure smooth updating

//--- Loop through the last few bars that need to be updated
for(int i = startBar; i < rates_total; i++)
  {
   //--- Calculate reverse index to access recent bars from the end
   int reverseIndex = rates_total - i; // Reverse indexing ensures we are looking at the most recent bars first

   //--- Copy the latest Exponential Moving Average (EMA) value for this specific bar
   double emaValue[];
   if(CopyBuffer(maHandle, 0, reverseIndex, 1, emaValue) < 0)
      return(prev_calculated); // If unable to copy, return the previous calculated value to avoid recalculation

   //--- Copy the latest Average True Range (ATR) value for this specific bar
   double atrValue[];
   if(CopyBuffer(atrHandle, 0, reverseIndex, 1, atrValue) < 0)
      return(prev_calculated); // If unable to copy, return the previous calculated value to avoid recalculation

   //--- Update the indicator buffers with new values for the current bar
   movingAverageBuffer[i] = emaValue[0]; // Update the moving average buffer for this bar
   upperChannelBuffer[i] = emaValue[0] + atrMultiplier * atrValue[0]; // Calculate the upper channel boundary
   lowerChannelBuffer[i] = emaValue[0] - atrMultiplier * atrValue[0]; // Calculate the lower channel boundary
  }
   
//--- Return the total number of calculated rates (bars)
return(rates_total); // This informs MQL5 that all rates up to 'rates_total' have been successfully calculated

Hier optimieren wir die Leistung, indem wir nur die jüngsten Balken aktualisieren, anstatt den gesamten Indikator bei jedem Tick neu zu berechnen. Wenn dies nicht die erste Berechnung ist, definieren wir „startBar“ als „prev_calculated - 2“, um sicherzustellen, dass wir die letzten Balken aktualisieren, ohne die Kontinuität zu verlieren. Dadurch werden unnötige Berechnungen vermieden, da die Daten für die vorherigen Balken im Chart bereits vorliegen.

Anschließend wird in einer for-Schleife von „startBar“ bis „rates_total“ durchgegangen. Um die jüngsten Balken zu priorisieren, berechnen wir „reverseIndex = rates_total - i“, sodass wir die neuesten Daten zuerst abrufen können. Für jeden Balken kopieren wir mit der Funktion CopyBuffer den letzten MA-Wert in „emaValue“. Wenn der Datenabruf fehlschlägt, geben wir „prev_calculated“ zurück, um redundante Berechnungen zu vermeiden. Die gleiche Logik gilt für ATR, dessen Wert in „atrValue“ gespeichert wird. Nach dem Abruf werden die Puffer aktualisiert:

  • „movingAverageBuffer[i] = emaValue[0];“ weist den EMA der mittleren Linie zu.
  • „upperChannelBuffer[i] = emaValue[0] + atrMultiplier * atrValue[0];“ berechnet die obere Grenze.
  • Mit „lowerChannelBuffer[i] = emaValue[0] - atrMultiplier * atrValue[0];“ wird die untere Grenze berechnet.

Schließlich wird „rates_total“ zurückgegeben, um zu signalisieren, dass alle erforderlichen Balken verarbeitet worden sind. Nach der Ausführung des Programms erhalten wir die folgende Ausgabe.

INDIKATORSBÄNDER

Anhand des Bildes können wir sehen, dass die Indikatorlinien korrekt im Chart abgebildet sind. Nun müssen die Kanäle gezeichnet werden, wofür wir die Canvas-Funktion benötigen. Dies wird im nächsten Abschnitt behandelt.


Integrieren von nutzerdefinierten Grafiken

Um die Canvas-Funktion für Grafiken zu integrieren, müssen wir die notwendigen Canvas-Klassendateien einfügen, damit wir die bereits vorhandene Struktur nutzen können. Wir erreichen dies durch die folgende Logik.

#include <Canvas/Canvas.mqh>
CCanvas obj_Canvas;

Wir binden die Bibliothek „Canvas.mqh“ mit dem Schlüsselwort #include ein, die Funktionen für das grafische Rendering auf dem Chart bereitstellt. Diese Bibliothek ermöglicht es uns, nutzerdefinierte Elemente, wie z. B. Indikatoren und Anmerkungen, direkt im Chartfenster zu zeichnen. Dann deklarieren wir „obj_Canvas“ als eine Instanz der Klasse CCanvas. Dieses Objekt wird zur Interaktion mit dem Canvas verwendet und ermöglicht es uns, grafische Elemente dynamisch zu erstellen, zu ändern und zu verwalten. Die Klasse CCanvas bietet Methoden zum Zeichnen von Formen, Linien und Text, um die visuelle Darstellung des Indikators zu verbessern. Als Nächstes müssen wir die Charteigenschaften wie die Skalierung abrufen, da wir das Chart mit dynamischen Formen ausrichten werden. Wir tun dies auf globaler Ebene.

int chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
int chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);
int chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
int chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
double chart_prcmin     = ChartGetDouble(0, CHART_PRICE_MIN, 0);
double chart_prcmax     = ChartGetDouble(0, CHART_PRICE_MAX, 0);

Wir verwenden verschiedene Funktionen wie ChartGetInteger und ChartGetDouble, um verschiedene Eigenschaften des aktuellen Chartfensters für weitere Berechnungen oder die grafische Platzierung von Elementen abzurufen. Zunächst wird die Breite des Charts mit „ChartGetInteger“ abgefragt, mit dem Parameter CHART_WIDTH_IN_PIXELS, der die Breite des Charts in Pixeln angibt. Wir speichern diesen Wert in der Variablen „chart_width“. In ähnlicher Weise wird die Höhe des Charts mit „ChartGetInteger“ und CHART_HEIGHT_IN_PIXELS abgerufen und in der Variablen „chart_height“ gespeichert.

Als Nächstes verwenden wir den Parameter „CHART_SCALE“, um den Maßstab des Charts abzurufen und speichern diesen Wert in „chart_scale“. Dies stellt die Zoomstufe des Charts dar. Mit „CHART_FIRST_VISIBLE_BAR“ wird auch der Index des ersten sichtbaren Balkens abgerufen und in „chart_first_vis_bar“ gespeichert, was für Berechnungen auf der Grundlage des sichtbaren Chartbereichs nützlich ist. Um zu berechnen, wie viele Balken im Chartfenster sichtbar sind, verwenden wir den Parameter CHART_VISIBLE_BARS und speichern das Ergebnis in „chart_vis_bars“. Wir „typisieren“ alle Werte als Ganzzahlen.

Schließlich verwenden wir die Funktion „ChartGetDouble“, um die minimalen und maximalen Preiswerte zu erhalten, die auf dem Chart mit „CHART_PRICE_MIN“ bzw. CHART_PRICE_MAX sichtbar sind. Diese Werte werden in den Variablen „chart_prcmin“ und „chart_prcmax“ gespeichert, die den aktuell im Chart angezeigten Preisbereich angeben. Mit diesen Variablen müssen wir bei der Initialisierung eine Bitmap-Kennzeichnung für das Chart erstellen, damit wir unseren Chartbereich bereit haben.

// Create a obj_Canvas bitmap label for custom graphics on the chart
obj_Canvas.CreateBitmapLabel(0, 0, short_name, 0, 0, chart_width, chart_height, COLOR_FORMAT_ARGB_NORMALIZE);

Hier verwenden wir die Funktion „obj_Canvas.CreateBitmapLabel“, um ein nutzerdefiniertes Bitmap-Label im Chart zu erstellen. Es werden Parameter für die Positionierung („0“, „0“), den Inhalt („short_name“), die Größe („0“, „0“ für automatische Größenanpassung) und die Chartabmessungen („chart_width“, „chart_height“) verwendet. Das Farbformat ist auf COLOR_FORMAT_ARGB_NORMALIZE eingestellt, was eine individuelle Transparenz und Farbe ermöglicht. Mit der Kennzeichnung können wir nun die Formen zeichnen. Allerdings benötigen wir einige Hilfsfunktionen, die die Chart- und Kerzenkoordinaten in Preis- und Balkenindizes umwandeln.

//+------------------------------------------------------------------+
//| Converts the chart scale property to bar width/spacing           |
//+------------------------------------------------------------------+
int GetBarWidth(int chartScale) 
{
   // The width of each bar in pixels is determined using 2^chartScale.
   // This calculation is based on the MQL5 chart scale property, where larger chartScale values mean wider bars.
   return (int)pow(2, chartScale); // Example: chartScale = 3 -> bar width = 2^3 = 8 pixels
}
//+------------------------------------------------------------------+
//| Converts the bar index (as series) to x-coordinate in pixels     |
//+------------------------------------------------------------------+
int GetXCoordinateFromBarIndex(int barIndex) 
{
   // The chart starts from the first visible bar, and each bar has a fixed width.
   // To calculate the x-coordinate, we calculate the distance from the first visible bar to the given barIndex.
   // Each bar is shifted by 'bar width' pixels, and we subtract 1 to account for pixel alignment.
   return (chart_first_vis_bar - barIndex) * GetBarWidth(chart_scale) - 1;
}
//+------------------------------------------------------------------+
//| Converts the price to y-coordinate in pixels                     |
//+------------------------------------------------------------------+
int GetYCoordinateFromPrice(double price)
{
   // To avoid division by zero, we check if chart_prcmax equals chart_prcmin.
   // If so, it means that all prices on the chart are the same, so we avoid dividing by zero.
   if(chart_prcmax - chart_prcmin == 0.0)
      return 0; // Return 0 to avoid undefined behavior

   // Calculate the relative position of the price in relation to the minimum and maximum price on the chart.
   // We then convert this to pixel coordinates based on the total height of the chart.
   return (int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - 1);
}
//+------------------------------------------------------------------+
//| Converts x-coordinate in pixels to bar index (as series)         |
//+------------------------------------------------------------------+
int GetBarIndexFromXCoordinate(int xCoordinate)
{
   // Get the width of one bar in pixels
   int barWidth = GetBarWidth(chart_scale);
   
   // Check to avoid division by zero in case barWidth somehow equals 0
   if(barWidth == 0)
      return 0; // Return 0 to prevent errors
   
   // Calculate the bar index using the x-coordinate position
   // This determines how many bar widths fit into the x-coordinate and converts it to a bar index
   return chart_first_vis_bar - (xCoordinate + barWidth / 2) / barWidth;
}
//+------------------------------------------------------------------+
//| Converts y-coordinate in pixels to price                         |
//+------------------------------------------------------------------+
double GetPriceFromYCoordinate(int yCoordinate)
{
   // If the chart height is 0, division by zero would occur, so we avoid it.
   if(chart_height == 0)
      return 0; // Return 0 to prevent errors

   // Calculate the price corresponding to the y-coordinate
   // The y-coordinate is converted relative to the total height of the chart
   return chart_prcmax - yCoordinate * (chart_prcmax - chart_prcmin) / chart_height;
}

Wir erstellen Funktionen, um zwischen Kartendaten und pixelbasierten Koordinaten abzubilden. Zunächst wird mit der Funktion „GetBarWidth“ die Breite jedes Balkens in Pixeln berechnet, indem die Skala des Charts verwendet und die Formel 2 hoch der Skala des Charts angewendet wird. So können wir die Balkenbreite an den Maßstab des Charts anpassen. Zu diesem Zweck verwenden wir die Funktion pow, um Potenzen von 2 zu berechnen.

Als Nächstes wird mit der Funktion „GetXCoordinateFromBarIndex“ ein Balkenindex in eine X-Koordinate in Pixeln umgewandelt. Dazu wird der Abstand zwischen dem ersten sichtbaren Balken und dem angegebenen Balkenindex berechnet. Wir multiplizieren dies mit der Balkenbreite und ziehen 1 ab, um die Pixelausrichtung zu berücksichtigen. Für die y-Koordinate berechnen wir mit der Funktion „GetYCoordinateFromPrice“ die relative Position eines Preises auf dem Chart. Wir bestimmen, wo der Preis zwischen dem minimalen und dem maximalen Chartpreis („chart_prcmin“ und „chart_prcmax“) liegt, und skalieren dann diesen relativen Wert, damit er in die Höhe des Charts passt. Wir achten darauf, eine Division durch Null zu verhindern, wenn die Preisspanne Null ist.

Ähnlich funktioniert die Funktion „GetBarIndexFromXCoordinate“ in umgekehrter Weise. Wir nehmen eine x-Koordinate und wandeln sie zurück in einen Balkenindex, indem wir berechnen, wie viele Balkenbreiten in die x-Koordinate passen. So können wir den Balken identifizieren, der einer bestimmten Position im Chart entspricht. Mit der Funktion „GetPriceFromYCoordinate“ schließlich wird eine y-Koordinate in einen Preis umgewandelt, indem die relative Position der y-Koordinate innerhalb des Preisbereichs des Charts verwendet wird. Wir sorgen dafür, dass eine Division durch Null vermieden wird, wenn die Charthöhe Null ist.

Zusammen bieten diese Funktionen die Möglichkeit, zwischen den Pixelkoordinaten des Charts und den Datenwerten zu übersetzen, sodass wir nutzerdefinierte Grafiken im Chart platzieren können, die genau auf den Preis und die Balken ausgerichtet sind. So können wir nun die Funktionen verwenden, um eine gemeinsame Funktion zu erstellen, mit der wir die notwendigen Formen zwischen zwei gegebenen Linien eines Kanals zeichnen können.

//+------------------------------------------------------------------+
//| Fill the area between two indicator lines                        |
//+------------------------------------------------------------------+
void DrawFilledArea(double &upperSeries[], double &lowerSeries[], color upperColor, color lowerColor, uchar transparency = 255, int shift = 0)
{
   int startBar  = chart_first_vis_bar;      // The first bar that is visible on the chart
   int totalBars = chart_vis_bars + shift;   // The total number of visible bars plus the shift
   uint upperARGB = ColorToARGB(upperColor, transparency); // Convert the color to ARGB with transparency
   uint lowerARGB = ColorToARGB(lowerColor, transparency); // Convert the color to ARGB with transparency
   int seriesLimit = fmin(ArraySize(upperSeries), ArraySize(lowerSeries)); // Ensure series limits do not exceed array size
   int prevX = 0, prevYUpper = 0, prevYLower = 0; // Variables to store the previous bar's x, upper y, and lower y coordinates
   
   for(int i = 0; i < totalBars; i++)
     {
      int barPosition = startBar - i;             // Current bar position relative to start bar
      int shiftedBarPosition = startBar - i + shift; // Apply the shift to the bar position
      int barIndex = seriesLimit - 1 - shiftedBarPosition; // Calculate the series index for the bar

      // Ensure the bar index is within the valid range of the array
      if(barIndex < 0 || barIndex >= seriesLimit || barIndex - 1 < 0)
         continue; // Skip this bar if the index is out of bounds

      // Check if the series contains valid data (not EMPTY_VALUE)
      if(upperSeries[barIndex] == EMPTY_VALUE || lowerSeries[barIndex] == EMPTY_VALUE || shiftedBarPosition >= seriesLimit)
         continue; // Skip this bar if the values are invalid or if the position exceeds the series limit

      int xCoordinate  = GetXCoordinateFromBarIndex(barPosition); // Calculate x-coordinate of this bar
      int yUpper = GetYCoordinateFromPrice(upperSeries[barIndex]); // Calculate y-coordinate for upper line
      int yLower = GetYCoordinateFromPrice(lowerSeries[barIndex]); // Calculate y-coordinate for lower line
      uint currentARGB = upperSeries[barIndex] < lowerSeries[barIndex] ? lowerARGB : upperARGB; // Determine fill color based on which line is higher
            
      // If previous values are valid, draw triangles between the previous bar and the current bar
      if(i > 0 && upperSeries[barIndex - 1] != EMPTY_VALUE && lowerSeries[barIndex - 1] != EMPTY_VALUE)
        {
         if(prevYUpper != prevYLower) // Draw first triangle between the upper and lower parts of the two consecutive bars
            obj_Canvas.FillTriangle(prevX, prevYUpper, prevX, prevYLower, xCoordinate, yUpper, currentARGB);
         if(yUpper != yLower) // Draw the second triangle to complete the fill area
            obj_Canvas.FillTriangle(prevX, prevYLower, xCoordinate, yUpper, xCoordinate, yLower, currentARGB);
        }

      prevX  = xCoordinate; // Store the x-coordinate for the next iteration
      prevYUpper = yUpper;  // Store the y-coordinate of the upper series
      prevYLower = yLower;  // Store the y-coordinate of the lower series
     }
}

Wir deklarieren eine ungültige Funktion „DrawFilledArea“, die es ermöglicht, den Bereich zwischen zwei Indikatorlinien im Chart zu füllen. Zunächst definieren wir die sichtbaren Balken im Chart mit einer optionalen Verschiebung („shift“), um den Startpunkt anzupassen. Außerdem konvertieren wir die Farben („upperColor“ und „lowerColor“) mit der Funktion ColorToARGB in das ARGB-Format, einschließlich Transparenz. Anschließend wird die Grenze der Reihe mit Hilfe der Funktion fmin bestimmt, um zu vermeiden, dass die Array-Größen der oberen und unteren Indikator-Datenreihen („upperSeries“ und „lowerSeries“) überschritten werden. Wir initialisieren Variablen, um die Koordinaten des vorherigen Balkens für die obere und untere Linie zu speichern, die zum Zeichnen des Bereichs verwendet werden.

Als Nächstes durchlaufen wir eine Schleife durch die sichtbaren Balken und berechnen die Position jedes Balkens auf der X-Achse mithilfe der Funktion „GetXCoordinateFromBarIndex“. Die y-Koordinaten der oberen und unteren Linie werden mit der Funktion „GetYCoordinateFromPrice“ auf der Grundlage der Werte in „upperSeries“ und „lowerSeries“ berechnet. Wir prüfen, welche Linie höher ist, und weisen die entsprechende Farbe für die Füllung zu.

Wenn der vorherige Balken gültige Daten enthält, verwenden wir „obj_Canvas.FillTriangle“, um den Bereich zwischen den beiden Linien zu füllen. Wir zeichnen zwei Dreiecke für jedes Balkenpaar: ein Dreieck zwischen der oberen und der unteren Linie und ein weiteres, um die gefüllte Fläche zu vervollständigen. Die Dreiecke werden mit der zuvor festgelegten Farbe gezeichnet. Wir verwenden Dreiecke, weil sie unregelmäßige Punkte zwischen Linien präzise verbinden, insbesondere wenn die Linien nicht perfekt am Gitter ausgerichtet sind. Diese Methode gewährleistet glattere Füllungen und eine bessere Rendering-Effizienz im Vergleich zu Rechtecken. Hier ist eine Illustration.

RECHTECKE VS. DREIECKE FÜR UNREGELMÄSSIGE FORMEN

Schließlich aktualisieren wir die vorherigen x- und y-Koordinaten für die nächste Iteration, um sicherzustellen, dass der Bereich zwischen den Linien für jeden sichtbaren Balken kontinuierlich gefüllt wird. Mit der Funktion ausgestattet, verwenden wir die Funktion, um die Anzahl der erforderlichen Kanäle auf dem Chart zu zeichnen, mit den entsprechenden Farben wie gewünscht.

//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void RedrawChart(void)
{
   uint defaultColor = 0; // Default color used to clear the canvas
   color colorUp = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0); // Color of the upper indicator line
   color colorMid = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0); // Color of the mid indicator line
   color colorDown = (color)PlotIndexGetInteger(2, PLOT_LINE_COLOR, 0); // Color of the lower indicator line
   
   //--- Clear the canvas by filling it with the default color
   obj_Canvas.Erase(defaultColor);
   
   //--- Draw the area between the upper channel and the moving average
   // This fills the area between the upper channel (upperChannelBuffer) and the moving average (movingAverageBuffer)
   DrawFilledArea(upperChannelBuffer, movingAverageBuffer, colorUp, colorMid, 128, 1);
   
   //--- Draw the area between the moving average and the lower channel
   // This fills the area between the moving average (movingAverageBuffer) and the lower channel (lowerChannelBuffer)
   DrawFilledArea(movingAverageBuffer, lowerChannelBuffer, colorDown, colorMid, 128, 1);
   
   //--- Update the canvas to reflect the new drawing
   obj_Canvas.Update();
}

Wir deklarieren die Funktion „RedrawChart“ und definieren zunächst Standardfarben und rufen dann die Linienfarben für den oberen, mittleren und unteren Kanal aus den Eigenschaften des Indikators ab. Wir löschen den Hintergrund (canvas) mit der Standardfarbe und verwenden die Funktion „DrawFilledArea“, um die Bereiche zwischen dem oberen Kanal und dem gleitenden Durchschnitt sowie zwischen dem gleitenden Durchschnitt und dem unteren Kanal mit den jeweiligen Farben zu füllen. Schließlich wird der Hintergrund aktualisiert, um die Änderungen widerzuspiegeln und sicherzustellen, dass das Chart mit den neuen Füllungen neu gezeichnet wird. Wir können nun die Funktion in OnCalculate aufrufen, um den Hintergrund zu zeichnen.

RedrawChart(); // This function clears and re-draws the filled areas between the indicator lines

Da wir ein Indikator-Kanalobjekt haben, müssen wir es löschen, sobald wir den Indikator loswerden.

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   obj_Canvas.Destroy();
   ChartRedraw();
}

In OnDeinit verwenden wir die Methode „obj_Canvas.Destroy“, um alle nutzerdefinierten Zeichenobjekte im Chart zu bereinigen und zu entfernen, wenn der Indikator entfernt wird. Schließlich rufen wir die Funktion ChartRedraw auf, um das Chart zu aktualisieren und neu zu zeichnen und sicherzustellen, dass die nutzerdefinierten Grafiken aus der Anzeige gelöscht werden. Wenn wir das Programm ausführen, erhalten wir das folgende Ergebnis.

ENDGÜLTIGES ERGEBNIS

Anhand der Visualisierung können wir sehen, dass wir unser Ziel erreicht haben, den erweiterten Keltner-Kanalindikator mit den Hintergrund-Grafiken zu erstellen. Wir müssen den Indikator nun einem Backtest unterziehen, um sicherzustellen, dass er korrekt funktioniert. Dies geschieht im nächsten Abschnitt.


Backtests des Keltner-Kanal-Indikators

Während der Backtests haben wir beobachtet, dass die Kanalanzeige bei einer Änderung der Chartabmessungen hängen blieb und nicht auf die aktuellen Chartordinaten aktualisiert wurde. Wir meinen Folgendes.

DER INDIKATOR REAGIERT NICHT AUF ÄNDERUNGEN DES CHARTS

Um dies zu beheben, haben wir eine Logik zur Aktualisierung im OnChartEvent-Ereignishandler implementiert.

//+------------------------------------------------------------------+
//| Custom indicator chart event handler function                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam){
   if(id != CHARTEVENT_CHART_CHANGE)
      return;
   chart_width          = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   chart_height         = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   chart_scale          = (int)ChartGetInteger(0, CHART_SCALE);
   chart_first_vis_bar  = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   chart_vis_bars       = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   chart_prcmin         = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   chart_prcmax         = ChartGetDouble(0, CHART_PRICE_MAX, 0);
   if(chart_width != obj_Canvas.Width() || chart_height != obj_Canvas.Height())
      obj_Canvas.Resize(chart_width, chart_height);
//---
   RedrawChart();
}

Hier behandeln wir die Funktion OnChartEvent, die auf das Ereignis CHARTEVENT_CHART_CHANGE wartet. Wenn sich die Chartgröße ändern, werden zunächst die aktualisierten Charteigenschaften abgerufen, z. B. die Breite („CHART_WIDTH_IN_PIXELS“). Anschließend wird geprüft, ob die neue Breite und Höhe des Charts von der aktuellen Leinwandgröße abweicht, indem „obj_Canvas.Width“ und „obj_Canvas.Height“ verwendet werden. Wenn sie sich unterscheiden, ändern wir die Größe der Leinwand mit „obj_Canvas.Resize“. Schließlich rufen wir die Funktion „RedrawChart“ auf, um das Chart zu aktualisieren und sicherzustellen, dass alle visuellen Elemente mit den neuen Dimensionen korrekt dargestellt werden. Das Ergebnis ist wie folgt.

INDIKATOR, DER AUF ÄNDERUNGEN IM DIAGRAMM REAGIERT

Anhand der Visualisierung können wir sehen, dass die Änderungen dynamisch erfolgen, wenn wir die Größe des Charts ändern, und somit unser Ziel erreichen.


Schlussfolgerung

Abschließend wurde in diesem Artikel ein nutzerdefinierter MQL5-Indikator unter Verwendung der Instrumente gleitender Durchschnitt und Average True Range erstellt, der dynamische Kanäle zeichnet. Wir haben uns auf die Berechnung und Darstellung dieser Kanäle mit einem Füllmechanismus konzentriert und gleichzeitig Leistungsverbesserungen bei der Größenänderung von Charts und bei einem Backtest vorgenommen, um Effizienz und Genauigkeit für Händler zu gewährleisten.

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17155

Trendvorhersage mit LSTM für Trendfolgestrategien Trendvorhersage mit LSTM für Trendfolgestrategien
Long Short-Term Memory (LSTM) ist eine Art rekurrentes neuronales Netz (RNN), das für die Modellierung sequenzieller Daten entwickelt wurde, indem es langfristige Abhängigkeiten effektiv erfasst und das Problem des verschwindenden Gradienten löst. In diesem Artikel werden wir untersuchen, wie LSTM zur Vorhersage zukünftiger Trends eingesetzt werden kann, um die Leistung von Trendfolgestrategien zu verbessern. Der Artikel behandelt die Einführung von Schlüsselkonzepten und die Motivation hinter der Entwicklung, das Abrufen von Daten aus dem MetaTrader 5, die Verwendung dieser Daten zum Trainieren des Modells in Python, die Integration des maschinellen Lernmodells in MQL5 und die Reflexion der Ergebnisse und zukünftigen Bestrebungen auf der Grundlage von statistischem Backtesting.
Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 12): External Flow (III) TrendMap Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 12): External Flow (III) TrendMap
Das Marktgeschehen wird von den Kräften zwischen Bullen und Bären bestimmt. Es gibt bestimmte Niveaus, die der Markt aufgrund der auf ihn wirkenden Kräfte einhält. Fibonacci- und VWAP-Levels sind besonders wirkungsvoll, um das Marktverhalten zu beeinflussen. Begleiten Sie mich in diesem Artikel bei der Erforschung einer Strategie, die auf VWAP und Fibonacci-Levels zur Signalgenerierung basiert.
MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 54): Verstärkungslernen mit hybriden SAC und Tensoren MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 54): Verstärkungslernen mit hybriden SAC und Tensoren
Soft Actor Critic ist ein Reinforcement Learning-Algorithmus, den wir bereits in einem früheren Artikel vorgestellt haben, in dem wir auch Python und ONNX als effiziente Ansätze für das Training von Netzwerken vorgestellt haben. Wir überarbeiten den Algorithmus mit dem Ziel, Tensoren, Berechnungsgraphen, die häufig in Python verwendet werden, zu nutzen.
MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 53): Market Facilitation Index MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 53): Market Facilitation Index
Der Market Facilitation Index ist ein weiterer Bill-Williams-Indikator, der die Effizienz der Preisbewegung in Verbindung mit dem Volumen messen soll. Wie immer betrachten wir die verschiedenen Muster dieses Indikators im Rahmen einer Assistentensignalklasse und präsentieren eine Vielzahl von Testberichten und Analysen zu den verschiedenen Mustern.