English
preview
Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 6): Weiterentwicklung der RSI-Berechnungen mit Glättung, Farbwechsel und Multi-Timeframe-Unterstützung

Erstellen von nutzerdefinierten Indikatoren in MQL5 (Teil 6): Weiterentwicklung der RSI-Berechnungen mit Glättung, Farbwechsel und Multi-Timeframe-Unterstützung

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

Einführung

In unserem vorherigen Artikel (Teil 5) haben wir einen WaveTrend Crossover-Indikator in MetaQuotes Language 5 (MQL5) entwickelt, der eine Zeichenfläche (Canvas) für Nebelgradienten verwendet, Signalblasen anzeigt, um wichtige Crossover-Ereignisse hervorzuheben, und Risikomanagementfunktionen, einschließlich Stop-Loss- und Take-Profit-Berechnungen, integriert, um Handelsentscheidungen zu verbessern. In Teil 6 erstellen wir mit dem Relative Strength Index (RSI) einen fortschrittlichen Indikator, der mehrere RSI-Berechnungsmethoden – einschließlich klassischer, geglätteter und adaptiver Varianten –, verschiedene Datenglättungsverfahren, dynamische Farbwechsel (hue shifts) für ein sofortiges visuelles Feedback und Datenverarbeitung mit mehreren Zeitrahmen und Interpolation unterstützt, um RSI-Messwerte über verschiedene Zeiträume hinweg anzugleichen. Wir werden die folgenden Themen behandeln:

  1. Erforschung von RSI-Varianten, Glättungsmethoden und dynamischen Merkmalen
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende haben Sie einen voll funktionsfähigen und anpassbaren MQL5-Indikator für die vielseitige RSI-Analyse, der mehrere Optionen der Berechnung, Glättung, Visualisierung und Zeitrahmen unterstützt – beginnen wir!


Erforschung von RSI-Varianten, Glättungsmethoden und dynamischen Merkmalen

Der normale Relative Strength Index (RSI) misst die Geschwindigkeit und Veränderung von Kursbewegungen, um überkaufte oder überverkaufte Bedingungen zu identifizieren. Er schwankt normalerweise zwischen 0 und 100 mit Grenzen bei 70 und 30. Wir werden den RSI weiterentwickeln, indem wir Varianten wie den Cuttler-, den Ehlers-, den Harris-, den Quick-, den Basic-, den RSX- und den Gradual-Stil hinzufügen, um seine Berechnungen anzupassen und verschiedene Aspekte zu betonen, einschließlich der Glättung und der Reaktion auf die Marktdynamik. Bei der Datenglättung werden Mittelwertbildungstechniken – einfache, wachstumsbasierte, ausgeglichene oder gewichtete lineare Mittelwerte – zur Vorverarbeitung von Kursdaten wie Schluss-, Eröffnungs-, Höchst- und Tiefstkursen oder abgeleiteten Durchschnittswerten eingesetzt, um das Rauschen zu reduzieren und klarere Signale zu liefern.

Farbwechsel ändern die Farbe des Indikators auf der Grundlage von Bedingungen wie Richtungsänderungen, Überschreiten des Mittelpunkts oder Überschreiten von Grenzen und liefern visuelle Hinweise auf Trendumkehr oder Stärke. Dynamische Grenzen aktualisieren die überkauften und überverkauften Niveaus auf der Grundlage der jüngsten RSI-Extreme über einen bestimmten Zeitraum, während statische Grenzen feste Prozentsätze verwenden, und die Unterstützung mehrerer Zeitrahmen ermöglicht die Analyse über verschiedene Zeiträume mit optionaler Interpolation für eine glattere Darstellung.

Unser Plan ist es, Nutzereingaben für die Auswahl von RSI-Varianten, Datenquellen, Glättungsansätzen, Farbtonbedingungen und Begrenzungseinstellungen zu konfigurieren, dann die RSI-Kurve mit diesen Optionen zu berechnen, Begrenzungen und Füllungen zu zeichnen, Daten mit mehreren Zeitrahmen zu verarbeiten und Benachrichtigungen bei Farbtonänderungen auszulösen. Kurz gesagt, entsteht so ein anpassbares RSI-Werkzeug, das sich an unterschiedliche Marktbedingungen und Nutzerpräferenzen für die technische Analyse anpassen lässt und einen fortschrittlichen RSI-Indikator im Vergleich zum traditionellen Indikator erzeugt, wie unten dargestellt.

KONZEPTIONELLER RAHMEN

Wir können sehen, dass wir am Ende eine adaptive RSI-Engine erhalten, die das Momentum aus jeder Kursdarstellung berechnen kann, nicht nur aus dem Schlusskurs. Sie glättet dynamisch sowohl die Eingabedaten als auch den RSI selbst, sodass wir zwischen schnellem, langsamem oder zyklusabhängigem Verhalten wechseln können, ohne die Kernlogik zu ändern. Darüber hinaus passt er seine Grenzen und visuellen Zustände selbst an, sodass sich überkaufte/überverkaufte Signale den Marktbedingungen anpassen, anstatt starr zu bleiben. Fangen wir an, das umzusetzen!


Implementierung in MQL5

Um den Indikator in MQL5 zu erstellen, öffnen Sie einfach den MetaEditor, gehen Sie zum Navigator, suchen Sie 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 Programmierumgebung die Eigenschaften und Einstellungen des Indikators festlegen, z. B. die Anzahl der Puffer, die Darstellungen und die Eigenschaften der einzelnen Linien, wie Farbe, Breite und Beschriftung.

//+------------------------------------------------------------------+
//|                              Multi-Method RSI with Smoothing.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_separate_window
#property indicator_buffers 8
#property indicator_plots   5

#property indicator_label1  "RSI High/Low Area"
#property indicator_type1   DRAW_FILLING
#property indicator_color1  C'209,243,209',C'255,230,183'
#property indicator_label2  "RSI Top Boundary"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrLimeGreen
#property indicator_style2  STYLE_DOT
#property indicator_label3  "RSI Center Line"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrSilver
#property indicator_style3  STYLE_DOT
#property indicator_label4  "RSI Bottom Boundary"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrOrange
#property indicator_style4  STYLE_DOT
#property indicator_label5  "RSI Curve"
#property indicator_type5   DRAW_COLOR_LINE
#property indicator_color5  clrSilver,clrLimeGreen,clrOrange
#property indicator_width5  2

Wir beginnen die Implementierung, indem wir den Indikator mit „#property indicator_separate_window“ so einstellen, dass er in einem separaten Unterfenster angezeigt wird, das sich zur besseren Übersichtlichkeit vom Hauptchart unterscheidet. Als Nächstes weisen wir acht Puffer für interne Berechnungen mit „#property indicator_buffers 8“ zu und definieren fünf Plots für visuelle Elemente mit „#property indicator_plots 5“. Für die Plots erstellen wir einen gefüllten Bereich für die RSI-Hoch/Tief-Regionen in hellgrünen und orangefarbenen Tönen, eine gepunktete limonengrüne Linie für die obere Begrenzung, eine gepunktete silberne Mittellinie, eine gepunktete orangefarbene untere Begrenzung und eine farbige RSI-Kurvenlinie, die zwischen Silber, Limonengrün und Orange mit einer Breite von 2 zur besseren Sichtbarkeit wechselt. Als Nächstes werden wir einige Eingabeparameter zur Steuerung des Indikators definieren.

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum DataSourceType {
   Data_ClosePrice,      // Use closing price
   Data_OpenPrice,       // Use opening price
   Data_HighPrice,       // Use highest price
   Data_LowPrice,        // Use lowest price
   Data_MidPoint,        // Use midpoint price
   Data_StandardPrice,   // Use standard price
   Data_BalancedPrice,   // Use balanced price
   Data_OverallAverage,  // Use overall average price
   Data_MidBodyAverage,  // Use mid-body average
   Data_DirectionAdjusted, // Use direction-adjusted price
   Data_ExtremeAdjusted, // Use extreme-adjusted price
   Data_SmoothedClose,   // Use smoothed close
   Data_SmoothedOpen,    // Use smoothed open
   Data_SmoothedHigh,    // Use smoothed high
   Data_SmoothedLow,     // Use smoothed low
   Data_SmoothedMid,     // Use smoothed midpoint
   Data_SmoothedStandard,// Use smoothed standard
   Data_SmoothedBalanced,// Use smoothed balanced
   Data_SmoothedOverall, // Use smoothed overall
   Data_SmoothedMidBody, // Use smoothed mid-body
   Data_SmoothedAdjusted,// Use smoothed adjusted
   Data_SmoothedExtreme  // Use smoothed extreme
};

enum RsiVariant {
   Variant_CuttlerStyle,  // Cuttler-style RSI
   Variant_EhlersStyle,   // Ehlers-style smoothed RSI
   Variant_HarrisStyle,   // Harris-style RSI
   Variant_QuickStyle,    // Quick RSI
   Variant_BasicStyle,    // Basic RSI
   Variant_RsxStyle,      // RSX-style
   Variant_GradualStyle   // Gradual RSI
};

enum HueShiftCondition {
   Hue_OnDirectionShift,   // Shift hue on direction change
   Hue_OnCenterCrossing,   // Shift hue on center crossing
   Hue_OnBoundaryCrossing  // Shift hue on boundary crossing
};

enum AveragingApproach {
   Avg_Basic,          // Basic averaging
   Avg_GrowthBased,    // Growth-based averaging
   Avg_EvenedOut,      // Evened-out averaging
   Avg_WeightedLinear  // Weighted linear averaging
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "Chart and Calculation Settings";
input ENUM_TIMEFRAMES AnalysisTimeframe    = PERIOD_CURRENT;     // Choose timeframe for data analysis
input int             RsiLength            = 14;                 // Length for RSI computation

input group "Data Source and Variant Options";
input DataSourceType  SourceData           = Data_ClosePrice;    // Select data source for calculations
input RsiVariant      ChosenRsiVariant     = Variant_BasicStyle; // Select RSI computation variant
input int             DataSmoothingLength  = 0;                  // Smoothing length for data (0 or 1 disables)
input AveragingApproach DataSmoothingApproach = Avg_GrowthBased; // Approach for data smoothing

input group "Hue Adjustment Settings";
input HueShiftCondition HueAdjustmentOn    = Hue_OnBoundaryCrossing; // Condition for hue adjustment

input group "Boundary Configuration";
input int             DynamicBoundaryLength = 50;                // Length for dynamic boundaries (1 or less for static)
input double          TopBoundaryPercent   = 80.0;               // Top boundary percentage
input double          BottomBoundaryPercent = 20.0;              // Bottom boundary percentage

input group "Notification Preferences";
input bool            ActivateNotifications = false;             // Activate notifications?
input bool            NotifyOnActiveBar     = true;              // Notify on active bar?
input bool            InterpolateMultiFrame = true;              // Smooth multi-frame data?

Wir fahren fort mit der Definition von Enumerationen, um die Nutzeroptionen zu kategorisieren und die Konfigurierbarkeit zu verbessern. Die Enumeration „DataSourceType“ listet verschiedene Preisdatenquellen auf, von grundlegenden wie Schluss- oder Eröffnungskursen bis hin zu abgeleiteten Durchschnittswerten wie „midpoint“ oder „balanced price“, und enthält geglättete Versionen zur Rauschunterdrückung. Als Nächstes bietet die Enumeration „RsiVariant“ verschiedene RSI-Berechnungsstile, darunter Basic, RSX, Cuttler, Ehlers, Harris, Quick und Gradual, die die Auswahl der gewünschten Berechnungsmethode ermöglichen.

Wir erstellen auch die Enumeration „HueShiftCondition“, um zu bestimmen, wann die RSI-Kurve ihre Farbe ändert, mit Optionen für Richtungsverschiebungen, Überschreitungen der Mitte oder Überschreitungen der Grenzen. Darüber hinaus legt die Enumeration „AveragingApproach“ Glättungsverfahren wie „basic“, „growth-based“, „evened-out“ oder „weighted linear“ für die Vorverarbeitung der Daten fest. Die Nutzereingaben sind in Abschnitte unterteilt, beginnend mit den Chart- und Berechnungseinstellungen, wobei „AnalysisTimeframe“ den Zeitrahmen auswählt und „RsiLength“ die RSI-Periode festlegt. Wir haben Kommentare hinzugefügt, um sie leichter verständlich zu machen. Daraufhin wird das folgende Fenster angezeigt.

INDIKATOR-EINGANGSFENSTER

Als Nächstes werden wir den Indikator initialisieren, aber lassen Sie uns zunächst einige globale Variablen definieren, die wir im gesamten Programm verwenden werden.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
double rsiCurveValues[], rsiHueValues[], areaFillTop[], areaFillBottom[], topBoundaryValues[], centerLineValues[], bottomBoundaryValues[], processedBarCounts[]; //--- Declare buffers
int    multiFrameDataHandle = INVALID_HANDLE;                    //--- Initialize multi-frame handle
ENUM_TIMEFRAMES chosenTimeframe;                                 //--- Declare chosen timeframe

#define MULTI_FRAME_ACCESS iCustom(_Symbol, chosenTimeframe, __FILE__, PERIOD_CURRENT, RsiLength, SourceData, ChosenRsiVariant, DataSmoothingLength, DataSmoothingApproach, HueAdjustmentOn, DynamicBoundaryLength, TopBoundaryPercent, BottomBoundaryPercent, ActivateNotifications, NotifyOnActiveBar, InterpolateMultiFrame) //--- Define multi-frame access


int timeframeCodes[] = {PERIOD_M1, PERIOD_M2, PERIOD_M3, PERIOD_M4, PERIOD_M5, PERIOD_M6, PERIOD_M10, PERIOD_M12, PERIOD_M15, PERIOD_M20, PERIOD_M30, PERIOD_H1, PERIOD_H2, PERIOD_H3, PERIOD_H4, PERIOD_H6, PERIOD_H8, PERIOD_H12, PERIOD_D1, PERIOD_W1, PERIOD_MN1}; //--- Define timeframe codes
string timeframeLabels[] = {"1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "6 minutes", "10 minutes", "12 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours", "daily", "weekly", "monthly"}; //--- Define timeframe labels

//+------------------------------------------------------------------+
//| Convert timeframe to text                                        |
//+------------------------------------------------------------------+
string convertTimeframeToText(int code) {
   if (code == PERIOD_CURRENT)                                   //--- Check current
      code = _Period;                                            //--- Set period
   int pos;                                                      //--- Declare pos
   for (pos = 0; pos < ArraySize(timeframeCodes); pos++)         //--- Loop codes
      if (code == timeframeCodes[pos]) break;                    //--- Break on match
   return(timeframeLabels[pos]);                                 //--- Return label
}

//+------------------------------------------------------------------+
//| Describe RSI variant                                             |
//+------------------------------------------------------------------+
string describeRsiVariant(int variant) {
   switch (variant) {                                            //--- Switch variant
   case Variant_BasicStyle:                                      //--- Handle basic
      return("RSI");                                             //--- Return RSI
   case Variant_RsxStyle:                                        //--- Handle RSX
      return("RSX");                                             //--- Return RSX
   case Variant_CuttlerStyle:                                    //--- Handle Cuttler
      return("Cuttler-style RSI");                               //--- Return Cuttler
   case Variant_HarrisStyle:                                     //--- Handle Harris
      return("Harris-style RSI");                                //--- Return Harris
   case Variant_QuickStyle:                                      //--- Handle Quick
      return("Quick RSI");                                       //--- Return Quick
   case Variant_GradualStyle:                                    //--- Handle Gradual
      return("Gradual RSI");                                     //--- Return Gradual
   case Variant_EhlersStyle:                                     //--- Handle Ehlers
      return("Ehlers-style smoothed RSI");                       //--- Return Ehlers
   default:                                                      //--- Handle default
      return("");                                                //--- Return empty
   }
}

Hier werden globale Arrays für die Indikatorpuffer deklariert, darunter „rsiCurveValues“ für die Haupt-RSI-Linie, „rsiHueValues“ für Farbindizes, „areaFillTop“ und „areaFillBottom“ für die gefüllten Bereiche, „topBoundaryValues“, „centerLineValues“ und „bottomBoundaryValues“ für die Begrenzungslinien sowie „processedBarCounts“ für die Verfolgung der berechneten Bars. Außerdem initialisieren wir „multiFrameDataHandle“ auf INVALID_HANDLE für die Verwaltung von Multi-Zeitrahmen-Daten und deklarieren „chosenTimeframe“, um den ausgewählten Zeitrahmen zu speichern. Mithilfe einer Präprozessoranweisung definieren wir „MULTI_FRAME_ACCESS“ als einen Aufruf an iCustom mit dem Symbol, dem gewählten Zeitrahmen, dem Dateinamen und allen Eingabeparametern, um den Zugriff auf Daten aus verschiedenen Zeitrahmen zu erleichtern.

Als Nächstes richten wir Arrays „timeframeCodes“ mit vordefinierten Periodenkonstanten wie PERIOD_M1 bis „PERIOD_MN1“ und „timeframeLabels“ mit entsprechenden Stringbeschreibungen für menschenlesbare Zeitrahmennamen ein. Die Funktion „convertTimeframeToText“ übersetzt einen Zeitrahmencode in seine Bezeichnung, indem sie zunächst „PERIOD_CURRENT“ in die tatsächliche Periode auflöst, dann in einer Schleife „timeframeCodes“ durchläuft, um eine Übereinstimmung zu finden und die entsprechende Bezeichnung aus „timeframeLabels“ zurückzugeben. Schließlich verwendet die Funktion „describeRsiVariant“ eine Switch-Anweisung für die Varianteneingabe, um eine beschreibende Zeichenkette für jeden RSI-Stil zurückzugeben, z. B. „RSI“ für die Basisvariante oder „Ehlers-style smoothed RSI“ für die Ehlers-Variante, wobei bei Nichtübereinstimmung eine leere Zeichenkette vorgegeben wird. Wir können nun diese Hilfsmittel verwenden, um den Indikator zu initialisieren.

//+------------------------------------------------------------------+
//| Initialize indicator                                             |
//+------------------------------------------------------------------+
int OnInit() {
   SetIndexBuffer(0, areaFillTop, INDICATOR_DATA);               //--- Set top fill buffer
   SetIndexBuffer(1, areaFillBottom, INDICATOR_DATA);            //--- Set bottom fill buffer
   SetIndexBuffer(2, topBoundaryValues, INDICATOR_DATA);         //--- Set top boundary buffer
   SetIndexBuffer(3, centerLineValues, INDICATOR_DATA);          //--- Set center line buffer
   SetIndexBuffer(4, bottomBoundaryValues, INDICATOR_DATA);      //--- Set bottom boundary buffer
   SetIndexBuffer(5, rsiCurveValues, INDICATOR_DATA);            //--- Set RSI curve buffer
   SetIndexBuffer(6, rsiHueValues, INDICATOR_COLOR_INDEX);       //--- Set hue buffer
   SetIndexBuffer(7, processedBarCounts, INDICATOR_CALCULATIONS); //--- Set processed counts buffer

   PlotIndexSetInteger(0, PLOT_SHOW_DATA, false);                //--- Hide filling data
   PlotIndexSetInteger(1, PLOT_SHOW_DATA, false);                //--- Hide bottom data
   PlotIndexSetInteger(2, PLOT_SHOW_DATA, true);                 //--- Show top boundary
   PlotIndexSetInteger(3, PLOT_SHOW_DATA, true);                 //--- Show center line
   PlotIndexSetInteger(4, PLOT_SHOW_DATA, true);                 //--- Show bottom boundary

   chosenTimeframe = MathMax(_Period, AnalysisTimeframe);        //--- Set chosen timeframe
   IndicatorSetString(INDICATOR_SHORTNAME, convertTimeframeToText(chosenTimeframe) + " " + describeRsiVariant(ChosenRsiVariant) + " with Adjustment (" + (string)RsiLength + "," + (string)DataSmoothingLength + "," + (string)DynamicBoundaryLength + ")"); //--- Set short name

   return(INIT_SUCCEEDED);                                       //--- Return success
}

Im OnInit-Ereignis-Handler werden die zugewiesenen Puffer mit bestimmten Plot-Indizes und Datentypen verknüpft. Wir verknüpfen Puffer 0 mit „areaFillTop“ als „INDICATOR_DATA“ für den oberen Füllstand, Puffer 1 mit „areaFillBottom“ ebenfalls für den unteren Füllstand, Puffer 2 mit „topBoundaryValues“ für die oberen Begrenzungsdaten, Puffer 3 mit „centerLineValues“ für die Mittellinie, Puffer 4 mit „bottomBoundaryValues“ für die untere Begrenzung, Puffer 5 an „rsiCurveValues“ für die RSI-Hauptwerte, Puffer 6 an „rsiHueValues“ als INDICATOR_COLOR_INDEX für die Farbindexierung und Puffer 7 an „processedBarCounts“ als „INDICATOR_CALCULATIONS“ für die Verfolgung der verarbeiteten Bars. Anschließend konfigurieren wir die Sichtbarkeit der Plots mithilfe von PlotIndexSetInteger, wobei die Daten für die Plots 0 und 1 ausgeblendet werden, um die Anzeige der rohen Füllwerte zu vermeiden, während die Plots 2, 3 und 4 für die Grenzen und die Mittellinie angezeigt werden.

Als Nächstes bestimmen wir den „chosenTimeframe“, indem wir das Maximum zwischen der aktuellen Chart-Periode und dem vom Nutzer eingegebenen „AnalysisTimeframe“ nehmen, um Multi-Timeframe-Operationen zu unterstützen. Wir legen den Kurznamen des Indikators mit IndicatorSetString fest, indem wir den Zeitrahmentext aus „convertTimeframeToText“, die Beschreibung der RSI-Variante aus „describeRsiVariant“ und einen String mit Schlüsselparametern wie „RsiLength“, „DataSmoothingLength“ und „DynamicBoundaryLength“ zur einfachen Identifizierung im Chart kombinieren. Schließlich geben wir INIT_SUCCEEDED zurück, um die erfolgreiche Initialisierung anzuzeigen. Wir können mit der Berechnung unserer Indikatoren fortfahren. Wir beginnen mit der Festlegung des Zeitrahmens für die Berechnungen wie folgt.

//+------------------------------------------------------------------+
//| Check timeframe validity                                         |
//+------------------------------------------------------------------+
bool checkTimeframeValidity(ENUM_TIMEFRAMES frame, const datetime& timeStamps[]) {
   static bool alerted = false;                                    //--- Set alerted flag
   if (timeStamps[0] < SeriesInfoInteger(_Symbol, frame, SERIES_FIRSTDATE)) { //--- Check first date
      datetime startDate, checkDate[];                             //--- Declare dates
      if (SeriesInfoInteger(_Symbol, PERIOD_M1, SERIES_TERMINAL_FIRSTDATE, startDate)) //--- Get terminal date
         if (startDate > 0) {                                      //--- Check date
            CopyTime(_Symbol, frame, timeStamps[0], 1, checkDate); //--- Copy time
            SeriesInfoInteger(_Symbol, frame, SERIES_FIRSTDATE, startDate); //--- Get series date
         }
      if (startDate <= 0 || startDate > timeStamps[0]) {           //--- Check invalid
         alerted = true;                                           //--- Set alerted
         return(false);                                            //--- Return false
      }
   }
   if (alerted) {                                                  //--- Check alerted
      alerted = false;                                             //--- Reset alerted
   }
   return(true);                                                   //--- Return true
}

//+------------------------------------------------------------------+
//| Calculate indicator                                              |
//+------------------------------------------------------------------+
int OnCalculate(const int barTotal,
                const int prevProcessed,
                const datetime& timeStamps[],
                const double& opens[],
                const double& highs[],
                const double& lows[],
                const double& closes[],
                const long& volumeTicks[],
                const long& actualVolumes[],
                const int& spreadValues[]) {
   if (Bars(_Symbol, _Period) < barTotal) return(-1);             //--- Check insufficient bars

   if (chosenTimeframe != _Period) {                              //--- Check multi-frame
      double interimData[];                                       //--- Declare interim data
      datetime activeTimeStamp[], followingTimeStamp[];           //--- Declare timestamps
      if (!checkTimeframeValidity(chosenTimeframe, timeStamps)) return(0); //--- Check validity
      if (multiFrameDataHandle == INVALID_HANDLE) multiFrameDataHandle = MULTI_FRAME_ACCESS; //--- Get handle
      if (multiFrameDataHandle == INVALID_HANDLE) return(0);      //--- Check handle
      if (CopyBuffer(multiFrameDataHandle, 7, 0, 1, interimData) == -1) return(0); //--- Copy processed

#define FRAME_RATIO PeriodSeconds(chosenTimeframe) / PeriodSeconds(_Period) //--- Define frame ratio
      int currentPos = MathMin(MathMax(prevProcessed - 1, 0), MathMax(barTotal - (int)interimData[0] * FRAME_RATIO - 1, 0)); //--- Compute pos
      for (; currentPos < barTotal && !_StopFlag; currentPos++) { //--- Loop positions
#define TRANSFER_MULTI_FRAME(_array, _pos) if (CopyBuffer(multiFrameDataHandle, _pos, timeStamps[currentPos], 1, interimData) == -1) break; _array[currentPos] = interimData[0] //--- Define transfer
         TRANSFER_MULTI_FRAME(areaFillTop, 0);                    //--- Transfer top fill
         TRANSFER_MULTI_FRAME(areaFillBottom, 1);                 //--- Transfer bottom fill
         TRANSFER_MULTI_FRAME(topBoundaryValues, 2);              //--- Transfer top boundary
         TRANSFER_MULTI_FRAME(centerLineValues, 3);               //--- Transfer center line
         TRANSFER_MULTI_FRAME(bottomBoundaryValues, 4);           //--- Transfer bottom boundary
         TRANSFER_MULTI_FRAME(rsiCurveValues, 5);                 //--- Transfer RSI curve
         TRANSFER_MULTI_FRAME(rsiHueValues, 6);                   //--- Transfer hue

         if (!InterpolateMultiFrame) continue;                    //--- Skip if no interpolate
         CopyTime(_Symbol, chosenTimeframe, timeStamps[currentPos], 1, activeTimeStamp); //--- Copy active time
         if (currentPos < (barTotal - 1)) {                       //--- Check not last
            CopyTime(_Symbol, chosenTimeframe, timeStamps[currentPos + 1], 1, followingTimeStamp); //--- Copy following time
            if (activeTimeStamp[0] == followingTimeStamp[0]) continue; //--- Skip same time
         }

         int stepsBack = 1;                                       //--- Initialize steps back
         while ((currentPos - stepsBack) > 0 && timeStamps[currentPos - stepsBack] >= activeTimeStamp[0]) stepsBack++; //--- Count back

         for (int stepsForward = 1; (currentPos - stepsForward) >= 0 && stepsForward < stepsBack; stepsForward++) { //--- Loop forward
#define SMOOTH_MULTI_FRAME(_array) _array[currentPos - stepsForward] = _array[currentPos] + (_array[currentPos - stepsBack] - _array[currentPos]) * stepsForward / stepsBack //--- Define smooth
            SMOOTH_MULTI_FRAME(areaFillTop);                      //--- Smooth top fill
            SMOOTH_MULTI_FRAME(areaFillBottom);                   //--- Smooth bottom fill
            SMOOTH_MULTI_FRAME(topBoundaryValues);                //--- Smooth top boundary
            SMOOTH_MULTI_FRAME(bottomBoundaryValues);             //--- Smooth bottom boundary
            SMOOTH_MULTI_FRAME(centerLineValues);                 //--- Smooth center line
            SMOOTH_MULTI_FRAME(rsiCurveValues);                   //--- Smooth RSI curve
         }
      }
      return(currentPos);                                        //--- Return pos
   }
}

Zunächst erstellen wir die Funktion „checkTimeframeValidity“, um zu überprüfen, ob für den ausgewählten Zeitrahmen genügend historische Daten für den aktuellen Zeitstempel verfügbar sind. Wir verwenden das statische Flag „alerted“, um festzustellen, ob ein ungültiger Zustand festgestellt wurde. Liegt der erste Zeitstempel vor dem Startdatum der Serie, das über SeriesInfoInteger mit SERIES_FIRSTDATE ermittelt wurde, holen wir das erste Datum des Terminals mit „SERIES_TERMINAL_FIRSTDATE“ und kopieren die Uhrzeit des Zeitrahmens mit CopyTime, um die Daten zu vergleichen. Wenn das Startdatum ungültig oder später als der Zeitstempel ist, wird das Flag „alerted“ gesetzt und „false“ zurückgegeben; andernfalls wird das Flag zurückgesetzt, falls es zuvor gesetzt wurde, und „true“ zurückgegeben.

Im OnCalculate-Ereignishandler, der die Indikatorwerte bei jeder neuen Bar oder jeder Datenaktualisierung verarbeitet, wird zunächst geprüft, ob die Anzahl der verfügbaren Bars unter der mit Bars angeforderten Gesamtzahl liegt, und bei unzureichender Anzahl wird -1 zurückgegeben, um die erforderliche Neuberechnung zu signalisieren. Weicht der gewählte Zeitrahmen von der aktuellen Periode ab, was auf den Multi-Zeitrahmen-Modus hindeutet, werden temporäre Arrays für Daten und Zeitstempel deklariert und dann „checkTimeframeValidity“ aufgerufen, um die Datenverfügbarkeit sicherzustellen, wobei bei Ungültigkeit 0 zurückgegeben wird.

Wir erhalten das Handle für mehrere Zeitrahmen, wenn es ungültig ist, mit dem vordefinierten Makro „MULTI_FRAME_ACCESS“, und geben 0 zurück, wenn es fehlschlägt. Wir kopieren die verarbeitete Zahl der Bars aus Puffer 7 in „interimData“ mit CopyBuffer, wobei bei einem Fehler ebenfalls 0 zurückgegeben wird. Wir definieren „FRAME_RATIO“ als das Verhältnis der Sekunden zwischen dem gewählten und dem aktuellen Zeitraum, um die Positionen der Bar zu skalieren. Wir berechnen den Ausgangswert „currentPos“ mithilfe der Minimum- und Maximum-Funktionen auf der Grundlage der zuvor verarbeiteten Bars und der angepassten Summe.

Wir führen eine Schleife von „currentPos“ bis „barTotal“ durch, überprüfen das Stop-Flag und definieren das Makro „TRANSFER_MULTI_FRAME“, um den Wert jedes Puffers vom Handle für mehrere Zeitrahmen in die entsprechenden Arrays wie „areaFillTop“, „areaFillBottom“, Grenzen, Mittellinie, „rsiCurveValues“ und „rsiHueValues“ an der aktuellen Position über den zeitbasierten „CopyBuffer“ zu kopieren und bei Fehlschlag abzubrechen.

Wenn die Multi-Timeframe-Interpolation aktiviert ist, kopieren wir den aktiven und den nachfolgenden Zeitstempel. Diesen Schritt überspringen wir, wenn es die letzte Bar ist oder die Zeitstempel übereinstimmen, und zählen dann „stepsBack“, um die vorherige Bar mit einem früheren Zeitstempel zu finden. In einer Vorwärtsschleife von 1 bis kleiner als „stepsBack“ wird das Makro „SMOOTH_MULTI_FRAME“ definiert, um die Werte in den Arrays zwischen der aktuellen und der letzten Position linear zu interpolieren und die Füllbereiche, Begrenzungen, die Mittellinie und die RSI-Kurve für eine nahtlose Multitimeframe-Anzeige zu glätten.

Schließlich geben wir die aktualisierte „currentPos“ zurück, um die verarbeiteten Bars anzuzeigen. Nachdem der Zeitrahmen festgelegt wurde, können wir mit den Berechnungen des Indikators fortfahren. Wir werden Hilfsfunktionen definieren, um den Code übersichtlich und modular zu gestalten. Beginnen wir mit der Funktion zur Berechnung der angepassten Datendurchschnitte für Glättungsmethoden.

#define AVG_VARIANTS 1                                           //--- Define avg variants
#define AVG_ARRAY_X1 1 * AVG_VARIANTS                            //--- Define array x1
#define AVG_ARRAY_X2 2 * AVG_VARIANTS                            //--- Define array x2

//+------------------------------------------------------------------+
//| Compute custom average                                           |
//+------------------------------------------------------------------+
double computeCustomAverage(int avgApproach, double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   switch (avgApproach) {                                        //--- Switch approach
   case Avg_Basic:                                               //--- Handle basic
      return(computeBasicAvg(inputVal, (int)avgLen, pos, barCnt, varIndex)); //--- Return basic avg
   case Avg_GrowthBased:                                         //--- Handle growth
      return(computeGrowthAvg(inputVal, avgLen, pos, barCnt, varIndex)); //--- Return growth avg
   case Avg_EvenedOut:                                           //--- Handle evened
      return(computeEvenedAvg(inputVal, avgLen, pos, barCnt, varIndex)); //--- Return evened avg
   case Avg_WeightedLinear:                                      //--- Handle weighted
      return(computeLinearAvg(inputVal, avgLen, pos, barCnt, varIndex)); //--- Return linear avg
   default:                                                      //--- Handle default
      return(inputVal);                                          //--- Return input
   }
}

double basicAvgArray[][AVG_ARRAY_X2];                            //--- Declare basic avg array

//+------------------------------------------------------------------+
//| Compute basic average                                            |
//+------------------------------------------------------------------+
double computeBasicAvg(double inputVal, int avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(basicAvgArray, 0) != barCnt) ArrayResize(basicAvgArray, barCnt); //--- Resize array
   varIndex *= 2;                                                //--- Adjust index
   int offset;                                                   //--- Declare offset

   basicAvgArray[pos][varIndex + 0] = inputVal;                  //--- Set value
   basicAvgArray[pos][varIndex + 1] = inputVal;                  //--- Set avg
   for (offset = 1; offset < avgLen && (pos - offset) >= 0; offset++) //--- Loop offsets
      basicAvgArray[pos][varIndex + 1] += basicAvgArray[pos - offset][varIndex + 0]; //--- Accumulate avg
   basicAvgArray[pos][varIndex + 1] /= 1.0 * offset;            //--- Average
   return(basicAvgArray[pos][varIndex + 1]);                     //--- Return avg
}

double growthAvgArray[][AVG_ARRAY_X1];                           //--- Declare growth avg array

//+------------------------------------------------------------------+
//| Compute growth average                                           |
//+------------------------------------------------------------------+
double computeGrowthAvg(double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(growthAvgArray, 0) != barCnt) ArrayResize(growthAvgArray, barCnt); //--- Resize array

   growthAvgArray[pos][varIndex] = inputVal;                     //--- Set value
   if (pos > 0 && avgLen > 1)                                    //--- Check pos and len
      growthAvgArray[pos][varIndex] = growthAvgArray[pos - 1][varIndex] + (2.0 / (1.0 + avgLen)) * (inputVal - growthAvgArray[pos - 1][varIndex]); //--- Compute growth
   return(growthAvgArray[pos][varIndex]);                        //--- Return avg
}

double evenedAvgArray[][AVG_ARRAY_X1];                           //--- Declare evened avg array

//+------------------------------------------------------------------+
//| Compute evened average                                           |
//+------------------------------------------------------------------+
double computeEvenedAvg(double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(evenedAvgArray, 0) != barCnt) ArrayResize(evenedAvgArray, barCnt); //--- Resize array

   evenedAvgArray[pos][varIndex] = inputVal;                     //--- Set value
   if (pos > 1 && avgLen > 1)                                    //--- Check pos and len
      evenedAvgArray[pos][varIndex] = evenedAvgArray[pos - 1][varIndex] + (inputVal - evenedAvgArray[pos - 1][varIndex]) / avgLen; //--- Compute evened
   return(evenedAvgArray[pos][varIndex]);                        //--- Return avg
}

double linearAvgArray[][AVG_ARRAY_X1];                           //--- Declare linear avg array

//+------------------------------------------------------------------+
//| Compute linear average                                           |
//+------------------------------------------------------------------+
double computeLinearAvg(double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(linearAvgArray, 0) != barCnt) ArrayResize(linearAvgArray, barCnt); //--- Resize array

   linearAvgArray[pos][varIndex] = inputVal;                     //--- Set value
   if (avgLen <= 1) return(inputVal);                            //--- Return if no avg

   double totalWeights = avgLen;                                 //--- Set total weights
   double totalValues = avgLen * inputVal;                       //--- Set total values

   for (int offset = 1; offset < avgLen && (pos - offset) >= 0; offset++) { //--- Loop offsets
      double currWeight = avgLen - offset;                       //--- Compute weight
      totalWeights += currWeight;                                //--- Accumulate weights
      totalValues += currWeight * linearAvgArray[pos - offset][varIndex]; //--- Accumulate values
   }
   return(totalValues / totalWeights);                           //--- Return avg
}

Wir definieren Konstanten mit Präprozessoranweisungen: „AVG_VARIANTS“ wird auf 1 gesetzt, um die Anzahl der Mittelungsvarianten festzulegen, „AVG_ARRAY_X1“ auf das 1-fache der Varianten für einspaltige Arrays und „AVG_ARRAY_X2“ auf das 2-fache der Varianten für zweispaltige Arrays, um unterschiedliche Anforderungen an die Glättung zu unterstützen. In der Funktion „computeCustomAverage“ verwenden wir eine Switch-Anweisung für den Parameter „avgApproach“, um die entsprechende Mittelwertbildungsmethode zu wählen – einfach, wachstumsbasiert, ausgeglichen oder gewichtet linear – und den berechneten Mittelwert aus der jeweiligen Funktion oder einfach den „inputVal“ im Standardfall zurückzugeben, wenn keine Übereinstimmung vorliegt.

Wir deklarieren „basicAvgArray“ als ein mehrdimensionales Array mit der Größe „AVG_ARRAY_X2“. Die Funktion „computeBasicAvg“ prüft zunächst die Größe des Arrays und passt es gegebenenfalls an „barCnt“ an, multipliziert „varIndex“ mit 2 für den Spaltenversatz, speichert den „inputVal“ in der ersten Spalte, initialisiert den Durchschnitt in der zweiten Spalte, durchläuft dann Schleifen über frühere Positionen bis zu „avgLen“, um frühere Werte zu summieren, dividiert durch die tatsächliche Versatzzahl und gibt das Ergebnis zurück.

In ähnlicher Weise wird für die wachstumsbasierte Mittelwertbildung „growthAvgArray“ mit „AVG_ARRAY_X1“ deklariert, die Größe bei Bedarf in „computeGrowthAvg“ angepasst, der Wert direkt festgelegt und, wenn er über die ersten Bars hinausgeht und „avgLen“ 1 übersteigt, mit einer Formel aktualisiert, die eine gewichtete Differenz zum vorherigen Wert hinzufügt und eine exponentielle Glättung imitiert, bevor sie zurückkehrt. Für eine ausgeglichene Mittelwertbildung verwendet „evenedAvgArray“ „AVG_ARRAY_X1“, und „computeEvenedAvg“ ändert die Größe, legt den Wert fest und passt ihn dann an, wenn er über die zweite Bar hinausgeht und „avgLen“ über 1 liegt, indem es die Differenz zur vorherigen Bar dividiert durch die Länge für einen einfachen inkrementellen Mittelwert addiert und zurückgibt.

Schließlich verwendet „linearAvgArray“ auch „AVG_ARRAY_X1“, und „computeLinearAvg“ ändert die Größe, setzt den Wert, gibt ihn frühzeitig zurück, wenn „avgLen“ 1 oder weniger ist, initialisiert andernfalls die Gesamtwerte für Gewichte und gewichtete Werte mit dem aktuellen Wert, führt eine Schleife über die Prioritäten durch, um abnehmende Gewichte und entsprechende Werte zu akkumulieren, und gibt die Gesamtwerte geteilt durch die Gesamtgewichte für einen linear gewichteten Durchschnitt zurück. Als Nächstes müssen wir die Mittelwertbildungsfunktion mit einem Datensatz füttern. So definieren wir die Erzeugung des Datensatzes.

#define DATA_VARIANTS 1                                          //--- Define data variants
#define DATA_VARIANT_SIZE 4                                      //--- Define variant size

double smoothedDataArray[][DATA_VARIANTS * DATA_VARIANT_SIZE];   //--- Declare smoothed data array

//+------------------------------------------------------------------+
//| Fetch chosen data                                                |
//+------------------------------------------------------------------+
double fetchChosenData(int dataType, const double& opens[], const double& closes[], const double& highs[], const double& lows[], int pos, int barCnt, int varIndex = 0) {
   if (dataType >= Data_SmoothedClose) {                         //--- Check smoothed
      if (ArrayRange(smoothedDataArray, 0) != barCnt) ArrayResize(smoothedDataArray, barCnt); //--- Resize array
      varIndex *= DATA_VARIANT_SIZE;                             //--- Adjust index

      double smoothedOpen;                                       //--- Declare smoothed open
      if (pos > 0)                                               //--- Check pos
         smoothedOpen = (smoothedDataArray[pos - 1][varIndex + 2] + smoothedDataArray[pos - 1][varIndex + 3]) / 2.0; //--- Compute smoothed open
      else                                                       //--- Handle initial
         smoothedOpen = (opens[pos] + closes[pos]) / 2;          //--- Set initial open

      double smoothedClose = (opens[pos] + highs[pos] + lows[pos] + closes[pos]) / 4.0; //--- Compute smoothed close
      double smoothedHigh = MathMax(highs[pos], MathMax(smoothedOpen, smoothedClose)); //--- Compute smoothed high
      double smoothedLow = MathMin(lows[pos], MathMin(smoothedOpen, smoothedClose)); //--- Compute smoothed low

      smoothedDataArray[pos][varIndex + 2] = smoothedOpen;       //--- Set smoothed open
      smoothedDataArray[pos][varIndex + 3] = smoothedClose;      //--- Set smoothed close

      switch (dataType) {                                        //--- Switch data type
      case Data_SmoothedClose:                                   //--- Handle smoothed close
         return(smoothedClose);                                  //--- Return close
      case Data_SmoothedOpen:                                    //--- Handle smoothed open
         return(smoothedOpen);                                   //--- Return open
      case Data_SmoothedHigh:                                    //--- Handle smoothed high
         return(smoothedHigh);                                   //--- Return high
      case Data_SmoothedLow:                                     //--- Handle smoothed low
         return(smoothedLow);                                    //--- Return low
      case Data_SmoothedMid:                                     //--- Handle smoothed mid
         return((smoothedHigh + smoothedLow) / 2.0);             //--- Return mid
      case Data_SmoothedMidBody:                                 //--- Handle smoothed mid body
         return((smoothedOpen + smoothedClose) / 2.0);           //--- Return mid body
      case Data_SmoothedStandard:                                //--- Handle smoothed standard
         return((smoothedHigh + smoothedLow + smoothedClose) / 3.0); //--- Return standard
      case Data_SmoothedBalanced:                                //--- Handle smoothed balanced
         return((smoothedHigh + smoothedLow + smoothedClose + smoothedClose) / 4.0); //--- Return balanced
      case Data_SmoothedOverall:                                 //--- Handle smoothed overall
         return((smoothedHigh + smoothedLow + smoothedClose + smoothedOpen) / 4.0); //--- Return overall
      case Data_SmoothedAdjusted:                                //--- Handle smoothed adjusted
         if (smoothedClose > smoothedOpen) return((smoothedHigh + smoothedClose) / 2.0); //--- Return high close
         else return((smoothedLow + smoothedClose) / 2.0);       //--- Return low close
      case Data_SmoothedExtreme:                                 //--- Handle smoothed extreme
         if (smoothedClose > smoothedOpen) return(smoothedHigh); //--- Return high
         if (smoothedClose < smoothedOpen) return(smoothedLow);  //--- Return low
         return(smoothedClose);                                  //--- Return close
      }
   }

   switch (dataType) {                                           //--- Switch data type
   case Data_ClosePrice:                                         //--- Handle close
      return(closes[pos]);                                       //--- Return close
   case Data_OpenPrice:                                          //--- Handle open
      return(opens[pos]);                                        //--- Return open
   case Data_HighPrice:                                          //--- Handle high
      return(highs[pos]);                                        //--- Return high
   case Data_LowPrice:                                           //--- Handle low
      return(lows[pos]);                                         //--- Return low
   case Data_MidPoint:                                           //--- Handle mid point
      return((highs[pos] + lows[pos]) / 2.0);                    //--- Return mid
   case Data_MidBodyAverage:                                     //--- Handle mid body
      return((opens[pos] + closes[pos]) / 2.0);                  //--- Return mid body
   case Data_StandardPrice:                                      //--- Handle standard
      return((highs[pos] + lows[pos] + closes[pos]) / 3.0);      //--- Return standard
   case Data_BalancedPrice:                                      //--- Handle balanced
      return((highs[pos] + lows[pos] + closes[pos] + closes[pos]) / 4.0); //--- Return balanced
   case Data_OverallAverage:                                     //--- Handle overall
      return((highs[pos] + lows[pos] + closes[pos] + opens[pos]) / 4.0); //--- Return overall
   case Data_DirectionAdjusted:                                  //--- Handle direction adjusted
      if (closes[pos] > opens[pos]) return((highs[pos] + closes[pos]) / 2.0); //--- Return high close
      else return((lows[pos] + closes[pos]) / 2.0);              //--- Return low close
   case Data_ExtremeAdjusted:                                    //--- Handle extreme adjusted
      if (closes[pos] > opens[pos]) return(highs[pos]);          //--- Return high
      if (closes[pos] < opens[pos]) return(lows[pos]);           //--- Return low
      return(closes[pos]);                                       //--- Return close
   }
   return(0);                                                    //--- Return zero
}

Für die Logik der Datensatzerstellung definieren wir zunächst die Konstanten „DATA_VARIANTS“ als 1, um die Anzahl der Datenverarbeitungsvarianten anzugeben, und „DATA_VARIANT_SIZE“ als 4, um die Spaltenbreite pro Variante im Array festzulegen. Wir deklarieren „smoothedDataArray“ als ein mehrdimensionales Array, das so groß ist, dass es geglättete Preisdaten über Bars hinweg speichern kann. In der Funktion „fetchChosenData“ wird der entsprechende Kurswert auf der Grundlage des „dataType“ für die angegebene Barposition ausgewählt und zurückgegeben.

Wenn es sich um einen geglätteten Typ handelt, der mit „Data_SmoothedClose“ beginnt, passen wir die Größe des Arrays an „barCnt“ an, skalieren „varIndex“ um die Größe der Variante und berechnen „smoothedOpen“ als Durchschnitt der gespeicherten Open- und Close-Werte der vorherigen Bar, wenn diese über die erste Bar hinausgeht, oder andernfalls den aktuellen Open-Close-Mittelpunkt. Wir berechnen „smoothedClose“ als den Vier-Punkte-Durchschnitt von Open, High, Low und Close und leiten dann „smoothedHigh“ als das Maximum aus High, smoothedOpen und smoothedClose und „smoothedLow“ als das Minimum aus Low, smoothedOpen und smoothedClose ab. Wir speichern diese smoothedOpen und smoothedClose in dem Array an den Offsets +2 und +3.

Eine Switch-Anweisung gibt dann den spezifischen geglätteten Wert auf der Grundlage von „dataType“ zurück, z. B. den Mittelwert für „Data_SmoothedMid“, den Drei-Punkte-Durchschnitt für „Data_SmoothedStandard“ oder richtungsbasierte Anpassungen wie den High-Close-Durchschnitt, wenn der Close-Wert den Open-Wert übersteigt, für „Data_SmoothedAdjusted“ oder den extremen High/Low/Close-Wert für „Data_SmoothedExtreme“. Für nicht geglättete Typen gibt ein weiterer Schalter direkte Werte wie Close für „Data_ClosePrice“ oder berechnete Durchschnittswerte wie den High-Low Mittelwertpreis für „Data_MidPoint“, den typischen Preis für „Data_StandardPrice“ oder richtungsbereinigte Preise, ähnlich den geglätteten Fällen, aber unter Verwendung von Rohdaten, zurück. Wenn kein Typ übereinstimmt, wird 0 als Rückfall zurückgegeben. Im OnCalculate-Ereignishandler können wir nun die Funktionen aufrufen, um die ersten Datenberechnungen durchzuführen.

int beginPos = (int)MathMax(prevProcessed - 1, 0);            //--- Set begin pos
for (; beginPos < barTotal && !_StopFlag; beginPos++) {       //--- Loop bars
   double adjustedData = computeCustomAverage(DataSmoothingApproach, fetchChosenData(SourceData, opens, closes, highs, lows, beginPos, barTotal), DataSmoothingLength, beginPos, barTotal); //--- Compute adjusted data
}

return(beginPos);                                             //--- Return begin pos

Hier setzen wir den OnCalculate-Ereignishandler für den Einzelzeitrahmenmodus fort, indem wir „beginPos“ auf das Maximum von „prevProcessed“ minus 1 oder 0 setzen, um sicherzustellen, dass wir mit der letzten verarbeiteten Bar oder dem Anfang beginnen, wenn keiner verarbeitet wurde. Wir durchlaufen eine Schleife über die Bars von „beginPos“ bis kleiner als „barTotal“, überprüfen das _StopFlag, um eine Unterbrechung zu ermöglichen, und berechnen für jede Bar „adjustedData“, indem wir zunächst den ausgewählten Preis über „fetchChosenData“ abrufen unter Verwendung des „SourceData“-Typs und der OHLC-Arrays der aktuellen Bar, dann Anwendung der gewählten Glättung mit „computeCustomAverage“ auf der Grundlage von „DataSmoothingApproach“ und „DataSmoothingLength“. Nach der Schleife geben wir „beginPos“ zurück, um die Anzahl der verarbeiteten Bars zu melden. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

BERECHNUNG DER ERSTEN INDIKATORWERTE

An der Abbildung ist zu erkennen, dass die wesentlichen Berechnungsschritte des Indikators bereits umgesetzt sind. Als Nächstes müssen wir die RSI-Werte unter Verwendung der jeweiligen Varianten berechnen, um den endgültigen Indikator für die Kurven zu erhalten.

#define RSI_VARIANTS 1                                           //--- Define RSI variants

double rsiComputeArray[][RSI_VARIANTS * 13];                     //--- Declare RSI compute array

#define DATA_SHIFT_POS 0                                         //--- Define data shift pos
#define DATA_SHIFTS_POS 3                                        //--- Define data shifts pos
#define SHIFT_POS 1                                              //--- Define shift pos
#define ABS_SHIFT_POS 2                                          //--- Define abs shift pos
#define RSI_COMPUTE_POS 1                                        //--- Define RSI compute pos
#define RS_COMPUTE_POS 1                                         //--- Define RS compute pos

//+------------------------------------------------------------------+
//| Compute RSI value                                                |
//+------------------------------------------------------------------+
double computeRsiValue(int rsiVariant, double currData, double rsiLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(rsiComputeArray, 0) != barCnt) ArrayResize(rsiComputeArray, barCnt); //--- Resize array
   int arrayOffset = varIndex * 13;                              //--- Compute offset

   rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] = currData; //--- Set data shift

   switch (rsiVariant) {                                         //--- Switch variant
   case Variant_BasicStyle: {                                    //--- Handle basic
      double factorAlpha = 1.0 / MathMax(rsiLen, 1);             //--- Compute alpha
      if (pos < rsiLen) {                                        //--- Check initial
         int cnt;                                                //--- Initialize count
         double totalAbsShifts = 0;                              //--- Initialize total abs
         for (cnt = 0; cnt < rsiLen && (pos - cnt - 1) >= 0; cnt++) //--- Loop shifts
            totalAbsShifts += MathAbs(rsiComputeArray[pos - cnt][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - cnt - 1][arrayOffset + DATA_SHIFT_POS]); //--- Accumulate abs shifts
         rsiComputeArray[pos][arrayOffset + SHIFT_POS] = (rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[0][arrayOffset + DATA_SHIFT_POS]) / MathMax(cnt, 1); //--- Set shift
         rsiComputeArray[pos][arrayOffset + ABS_SHIFT_POS] = totalAbsShifts / MathMax(cnt, 1); //--- Set abs shift
      } else {                                                   //--- Handle non-initial
         double dataShift = rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute shift
         rsiComputeArray[pos][arrayOffset + SHIFT_POS] = rsiComputeArray[pos - 1][arrayOffset + SHIFT_POS] + factorAlpha * (dataShift - rsiComputeArray[pos - 1][arrayOffset + SHIFT_POS]); //--- Update shift
         rsiComputeArray[pos][arrayOffset + ABS_SHIFT_POS] = rsiComputeArray[pos - 1][arrayOffset + ABS_SHIFT_POS] + factorAlpha * (MathAbs(dataShift) - rsiComputeArray[pos - 1][arrayOffset + ABS_SHIFT_POS]); //--- Update abs shift
      }
      return(50.0 * (rsiComputeArray[pos][arrayOffset + SHIFT_POS] / MathMax(rsiComputeArray[pos][arrayOffset + ABS_SHIFT_POS], DBL_MIN) + 1)); //--- Return RSI
   }

   case Variant_GradualStyle: {                                  //--- Handle gradual
      double posSum = 0, negSum = 0;                             //--- Initialize sums
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      if (pos < 1) rsiComputeArray[pos][arrayOffset + RSI_COMPUTE_POS] = 50; //--- Set initial
      else rsiComputeArray[pos][arrayOffset + RSI_COMPUTE_POS] = rsiComputeArray[pos - 1][arrayOffset + RSI_COMPUTE_POS] + (1 / MathMax(rsiLen, 1)) * (100 * posSum / MathMax(posSum + negSum, DBL_MIN) - rsiComputeArray[pos - 1][arrayOffset + RSI_COMPUTE_POS]); //--- Compute gradual
      return(rsiComputeArray[pos][arrayOffset + RSI_COMPUTE_POS]); //--- Return value
   }

   case Variant_QuickStyle: {                                    //--- Handle quick
      double posSum = 0, negSum = 0;                             //--- Initialize sums
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      return(100 * posSum / MathMax(posSum + negSum, DBL_MIN));  //--- Return quick RSI
   }

   case Variant_EhlersStyle: {                                   //--- Handle Ehlers
      double posSum = 0, negSum = 0;                             //--- Initialize sums
      rsiComputeArray[pos][arrayOffset + DATA_SHIFTS_POS] = (pos > 2) ? (rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] + 2.0 * rsiComputeArray[pos - 1][arrayOffset + DATA_SHIFT_POS] + rsiComputeArray[pos - 2][arrayOffset + DATA_SHIFT_POS]) / 4.0 : currData; //--- Compute shifts
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFTS_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFTS_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      return(50 * (posSum - negSum) / MathMax(posSum + negSum, DBL_MIN) + 50); //--- Return Ehlers RSI
   }

   case Variant_CuttlerStyle: {                                  //--- Handle Cuttler
      double posSum = 0;                                         //--- Initialize positive sum
      double negSum = 0;                                         //--- Initialize negative sum
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS] = 100.0 - 100.0 / (1.0 + posSum / MathMax(negSum, DBL_MIN)); //--- Compute Cuttler
      return(rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS]);  //--- Return value
   }

   case Variant_HarrisStyle: {                                   //--- Handle Harris
      double avgPos = 0, avgNeg = 0, posCnt = 0, negCnt = 0;     //--- Initialize averages and counts
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][varIndex + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][varIndex + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) {                                     //--- Handle positive
            avgPos += dataDiff;                                  //--- Accumulate positive
            posCnt++;                                            //--- Increment positive count
         } else {                                                //--- Handle negative
            avgNeg -= dataDiff;                                  //--- Accumulate negative
            negCnt++;                                            //--- Increment negative count
         }
      }
      if (posCnt != 0) avgPos /= posCnt;                         //--- Average positive
      if (negCnt != 0) avgNeg /= negCnt;                         //--- Average negative
      rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS] = 100 - 100 / (1.0 + (avgPos / MathMax(avgNeg, DBL_MIN))); //--- Compute Harris
      return(rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS]);  //--- Return value
   }

   case Variant_RsxStyle: {                                      //--- Handle RSX
      double kgVal = 3.0 / (2.0 + rsiLen), hgVal = 1.0 - kgVal;  //--- Compute kg and hg
      if (pos < rsiLen) {                                        //--- Check initial
         for (int offset = 1; offset < 13; offset++) rsiComputeArray[pos][offset + arrayOffset] = 0; //--- Zero offsets
         return(50);                                             //--- Return initial
      }

      double motion = rsiComputeArray[pos][DATA_SHIFT_POS + arrayOffset] - rsiComputeArray[pos - 1][DATA_SHIFT_POS + arrayOffset]; //--- Compute motion
      double absMotion = MathAbs(motion);                        //--- Compute abs motion
      for (int offset = 0; offset < 3; offset++) {               //--- Loop offsets
         int subOffset = offset * 2;                             //--- Compute sub offset
         rsiComputeArray[pos][arrayOffset + subOffset + 1] = kgVal * motion + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 1]; //--- Update 1
         rsiComputeArray[pos][arrayOffset + subOffset + 2] = kgVal * rsiComputeArray[pos][arrayOffset + subOffset + 1] + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 2]; //--- Update 2
         motion = 1.5 * rsiComputeArray[pos][arrayOffset + subOffset + 1] - 0.5 * rsiComputeArray[pos][arrayOffset + subOffset + 2]; //--- Update motion

         rsiComputeArray[pos][arrayOffset + subOffset + 7] = kgVal * absMotion + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 7]; //--- Update 7
         rsiComputeArray[pos][arrayOffset + subOffset + 8] = kgVal * rsiComputeArray[pos][arrayOffset + subOffset + 7] + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 8]; //--- Update 8
         absMotion = 1.5 * rsiComputeArray[pos][arrayOffset + subOffset + 7] - 0.5 * rsiComputeArray[pos][arrayOffset + subOffset + 8]; //--- Update abs motion
      }
      return(MathMax(MathMin((motion / MathMax(absMotion, DBL_MIN) + 1.0) * 50.0, 100.00), 0.00)); //--- Return RSX
   }
   }
   return(0);                                                    //--- Return zero
}

Hier definieren wir „RSI_VARIANTS“ als 1, um die Anzahl der RSI-Berechnungsvarianten anzugeben, und deklarieren „rsiComputeArray“ als mehrdimensionales Array mit 13 Spalten pro Variante, um Zwischenberechnungen über Bars hinweg zu speichern. Wir setzen Positionskonstanten wie „DATA_SHIFT_POS“ auf 0 für aktuelle Daten, „SHIFT_POS“ auf 1 für Nettoänderungen, „ABS_SHIFT_POS“ auf 2 für absolute Änderungen, „DATA_SHIFTS_POS“ auf 3 für geglättete Verschiebungen in einigen Varianten und „RSI_COMPUTE_POS“ (alias 1) für gespeicherte RSI-Werte.

In der Funktion „computeRsiValue“ wird die Größe von „rsiComputeArray“ bei Bedarf an „barCnt“ angepasst, ein „arrayOffset“ berechnet, indem „varIndex“ mit 13 multipliziert wird, und „currData“ an der Datenverschiebungsposition für die aktuelle Bar gespeichert. Wir verwenden einen Schalter auf „rsiVariant“, um den RSI anders zu berechnen. Für den Basisstil wird ein Alpha-Faktor von 1 über „rsiLen“ berechnet, und für anfängliche Bars, die kürzer als die Länge sind, werden die absoluten Verschiebungen summiert und die durchschnittlichen Netto- und Abs-Verschiebungen festgelegt; andernfalls werden sie exponentiell mit der aktuellen Datenverschiebung aktualisiert, wobei das 50-fache (Netto über Abs plus 1) zurückgegeben wird.

Für den graduellen Stil summieren wir die positiven und negativen Differenzen über den Zeitraum, setzen den Ausgangswert auf 50, aktualisieren dann schrittweise den vorherigen RSI mit einer gewichteten Anpassung auf der Grundlage der relativen Stärke und geben ihn zurück. Für den schnellen Stil summieren wir positive und negative Differenzen auf ähnliche Weise, wobei wir 100-mal positive Differenzen über die Gesamtdifferenz erhalten. Für Ehlers' Stil glätten wir die Daten mit einem Vier-Punkte-Durchschnitt, wenn sie über Bar 2 hinausgehen, summieren die Differenzen der geglätteten Werte und geben 50 plus 50 mal Netto über Gesamt.

Beim Cuttler-Stil werden positive und negative Differenzen addiert, 100 minus 100 über (1 plus positive/negative) berechnet, gespeichert und zurückgegeben. Bei der Harris-Methode werden die positiven und negativen Ergebnisse getrennt gesammelt und gezählt, der Durchschnitt gebildet, wenn es Zählungen gibt, und dann eine ähnliche Formel wie bei Cuttler berechnet, jedoch mit Durchschnittswerten, Speicherung und Rückgabe. Für den RSX-Stil berechnen wir die Verstärkungsfaktoren kg und hg, setzen die Array-Offsets für die ersten Bars unter der Länge auf Null und geben 50 zurück; andernfalls berechnen wir die Bewegung und die absolute Bewegung, wenden doppelte EMA-ähnliche Aktualisierungen in einer Schleife für drei Durchgänge auf beide an und geben 50-mal (Bewegung über abs plus 1) zwischen 0 und 100 geklammert zurück. Wenn keine Variante passt, wird 0 zurückgegeben. Wir können diese Funktion nun in unserer Schleife aufrufen, um die RSI-Daten zu berechnen.

for (; beginPos < barTotal && !_StopFlag; beginPos++) {       //--- Loop bars
   double adjustedData = computeCustomAverage(DataSmoothingApproach, fetchChosenData(SourceData, opens, closes, highs, lows, beginPos, barTotal), DataSmoothingLength, beginPos, barTotal); //--- Compute adjusted data
   rsiCurveValues[beginPos] = computeRsiValue(ChosenRsiVariant, adjustedData, RsiLength, beginPos, barTotal); //--- Compute RSI value

   if (DynamicBoundaryLength <= 1) {                          //--- Check static boundary
      topBoundaryValues[beginPos] = TopBoundaryPercent;       //--- Set top boundary
      bottomBoundaryValues[beginPos] = BottomBoundaryPercent; //--- Set bottom boundary
      centerLineValues[beginPos] = (topBoundaryValues[beginPos] + bottomBoundaryValues[beginPos]) / 2; //--- Set center line
   } else {                                                   //--- Handle dynamic
      double lowestVal = rsiCurveValues[beginPos];            //--- Set initial low
      double highestVal = rsiCurveValues[beginPos];           //--- Set initial high
      for (int offset = 1; offset < DynamicBoundaryLength && beginPos - offset >= 0; offset++) { //--- Loop offsets
         lowestVal = MathMin(rsiCurveValues[beginPos - offset], lowestVal); //--- Update low
         highestVal = MathMax(rsiCurveValues[beginPos - offset], highestVal); //--- Update high
      }
      double valRange = highestVal - lowestVal;               //--- Compute range
      topBoundaryValues[beginPos] = lowestVal + TopBoundaryPercent * valRange / 100.0; //--- Set top boundary
      bottomBoundaryValues[beginPos] = lowestVal + BottomBoundaryPercent * valRange / 100.0; //--- Set bottom boundary
      centerLineValues[beginPos] = lowestVal + 0.5 * valRange; //--- Set center line
   }

   switch (HueAdjustmentOn) {                                 //--- Switch hue condition
   case Hue_OnBoundaryCrossing:                               //--- Handle boundary crossing
      rsiHueValues[beginPos] = (rsiCurveValues[beginPos] > topBoundaryValues[beginPos]) ? 1 : (rsiCurveValues[beginPos] < bottomBoundaryValues[beginPos]) ? 2 : 0; //--- Set hue
      break;
   case Hue_OnCenterCrossing:                                 //--- Handle center crossing
      rsiHueValues[beginPos] = (rsiCurveValues[beginPos] > centerLineValues[beginPos]) ? 1 : (rsiCurveValues[beginPos] < centerLineValues[beginPos]) ? 2 : 0; //--- Set hue
      break;
   default:                                                   //--- Handle default
      rsiHueValues[beginPos] = (beginPos > 0) ? (rsiCurveValues[beginPos] > rsiCurveValues[beginPos - 1]) ? 1 : (rsiCurveValues[beginPos] < rsiCurveValues[beginPos - 1]) ? 2 : 0 : 0; //--- Set hue
   }

   areaFillTop[beginPos] = rsiCurveValues[beginPos];          //--- Set top fill
   areaFillBottom[beginPos] = (rsiCurveValues[beginPos] > topBoundaryValues[beginPos]) ? topBoundaryValues[beginPos] : (rsiCurveValues[beginPos] < bottomBoundaryValues[beginPos]) ? bottomBoundaryValues[beginPos] : rsiCurveValues[beginPos]; //--- Set bottom fill
}

processedBarCounts[barTotal - 1] = MathMax(barTotal - prevProcessed + 1, 1); //--- Set processed counts

Wir fahren in der Schleife über die Bars im Einzelzeitrahmenmodus des „OnCalculate“-Ereignishandlers fort, indem wir den RSI-Wert für jede Bar berechnen. Nachdem wir die „adjustedData“ aus der Glättung erhalten haben, übergeben wir sie zusammen mit der ausgewählten „ChosenRsiVariant“, der „RsiLength“ und den Bardetails an „computeRsiValue“ und speichern das Ergebnis in „rsiCurveValues“ an der aktuellen Position. Als Nächstes bestimmen wir die Begrenzungsebenen: Wenn „DynamicBoundaryLength“ 1 oder weniger ist, was auf statische Begrenzungen hinweist, setzen wir „topBoundaryValues“ auf „TopBoundaryPercent“, „bottomBoundaryValues“ auf „BottomBoundaryPercent“ und „centerLineValues“ auf ihren Mittelwert.

Bei dynamischen Grenzen, wenn die Länge 1 übersteigt, werden der niedrigste und der höchste Wert mit dem aktuellen RSI initialisiert, dann wird eine Schleife über die vorherigen Bars bis zur Länge gezogen, um den Minimal- und Maximalwert mithilfe der Funktionen MathMin und MathMax zu aktualisieren. Wir berechnen den Bereich als höchsten minus niedrigsten Wert, setzen dann die obere Grenze als niedrigsten Wert plus den oberen Prozentsatz des Bereichs, den unteren Wert als niedrigsten Wert plus den unteren Prozentsatz und die Mitte als niedrigsten Wert plus die Hälfte des Bereichs. Anschließend wird der Farbindex in „rsiHueValues“ auf der Grundlage von „HueAdjustmentOn“ festgelegt: für Grenzübergänge wird 1 zugewiesen, wenn der RSI die Obergrenze überschreitet, 2, wenn er die Untergrenze unterschreitet, sonst 0; für das Kreuzen des Mittelwerts ähnlich, aber relativ zur Mittellinie; standardmäßig wird mit dem vorherigen RSI für die Richtung verglichen – 1 für steigende, 2 für fallende Kurse, sonst 0, mit 0 für die erste Bar. Schließlich konfigurieren wir die Füllbereiche, indem wir „areaFillTop“ auf den aktuellen RSI-Wert und „areaFillBottom“ auf die obere Begrenzung setzen, wenn der RSI überkauft ist, die untere Begrenzung, wenn er überverkauft ist, oder auf den RSI selbst setzen.

Nach der Schleife aktualisieren wir „processedBarCounts“ beim letzten Bartindex auf das Maximum der in diesem Aufruf verarbeiteten Bars, plus 1 oder nur 1, wobei wir den Berechnungsumfang verfolgen. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

VOLLSTÄNDIGER INDIKATOR

Aus dem Bild geht hervor, dass der Indikator erfolgreich initialisiert, berechnet und gezeichnet wurde. Als nächster Schritt folgt die Verarbeitung der Warnmeldungen. Wir werden die Logik einfach in einer Funktion unterbringen und sie im Event-Handler zur Verarbeitung aufrufen.

//+------------------------------------------------------------------+
//| Process alert triggers                                           |
//+------------------------------------------------------------------+
void processAlertTriggers(const datetime& timeStamps[], double& hueTrends[], int barTotal) {
   if (!ActivateNotifications) return;                           //--- Check notifications
   int notifyIndex = barTotal - 1;                               //--- Set notify index
   if (!NotifyOnActiveBar) notifyIndex = barTotal - 2;           //--- Adjust if not active
   datetime notifyStamp = timeStamps[notifyIndex];               //--- Get stamp

   if (hueTrends[notifyIndex] != hueTrends[notifyIndex - 1]) {   //--- Check hue change
      if (hueTrends[notifyIndex] == 1) triggerNotification(notifyStamp, "rising"); //--- Trigger rising
      if (hueTrends[notifyIndex] == 2) triggerNotification(notifyStamp, "falling"); //--- Trigger falling
   }
}

//+------------------------------------------------------------------+
//| Trigger notification                                             |
//+------------------------------------------------------------------+
void triggerNotification(datetime stamp, string trend) {
   static string prevTrend = "none";                             //--- Initialize previous trend
   static datetime prevStamp;                                    //--- Initialize previous stamp

   if (prevTrend != trend || prevStamp != stamp) {               //--- Check change
      prevTrend = trend;                                         //--- Update trend
      prevStamp = stamp;                                         //--- Update stamp

      string notifyText = convertTimeframeToText(_Period) + " " + _Symbol + " at " + TimeToString(TimeLocal(), TIME_SECONDS) + describeRsiVariant(ChosenRsiVariant) + " trend shifted to " + trend; //--- Format text
      Alert(notifyText);                                         //--- Send alert
   }
}

Hier fügen wir die Funktion „processAlertTriggers“ hinzu, um Benachrichtigungen aufgrund von Farbtonänderungen in der RSI-Kurve zu verarbeiten. Wenn „ActivateNotifications“ falsch ist, wird die Funktion sofort beendet. Wir setzen den „notifyIndex“ auf die letzte Bar oder den davor, wenn „NotifyOnActiveBar“ falsch ist, um eine Alarmierung bei unvollständigen Bars zu vermeiden, und rufen dann den Zeitstempel bei diesem Index ab. Wir prüfen, ob sich der Farbtonwert bei „notifyIndex“ vom vorherigen unterscheidet, und wenn ja, rufen wir „triggerNotification“ mit dem Zeitstempel und „rising“ für Farbton 1 oder „falling“ für Farbton 2 auf.

In der Funktion „triggerNotification“ verwenden wir statische Variablen, um den vorherigen Trend und den Zeitstempel zu verfolgen. Weicht der aktuelle Trend oder der Zeitstempel von den gespeicherten ab, aktualisieren wir diese, formatieren einen Benachrichtigungstext mit „convertTimeframeToText“ für die Periode, das Symbol, die lokale Zeit via TimeToString, die RSI-Variantenbeschreibung und die Trendverschiebungsmeldung und senden ihn dann mit der Alert-Funktion. Wenn wir es im Event-Handler aufrufen, erhalten wir folgendes Ergebnis.

AKTIVIERTES ALARMSYSTEM

Aus dem Bild ist ersichtlich, dass wir den Indikator berechnen, die Plots darstellen und das Warnsystem aktivieren, wenn es aktiviert ist, und somit unsere Ziele erreichen. Bleibt nur noch der Backtest der 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 Bild im Bitmap-Format Graphics Interchange Format (GIF).

FORTGESCHRITTENER RSI BACKTEST


Schlussfolgerung

Abschließend haben wir eine verbesserte Version des Relative Strength Index (RSI) Indikators in MQL5 entwickelt, die mehrere Berechnungsvarianten, anpassbare Datenquellen mit Glättungsansätzen und dynamische Grenzen für überkaufte und überverkaufte Levels unterstützt. Wir haben die Farbwechsel für farbkodierte Visualisierungen, optionale Benachrichtigungen für Trendänderungen und die Verarbeitung von Daten mit mehreren Zeitrahmen mit einer Interpolation für breitere Analysen integriert. Mit diesem dynamischen RSI-Indikator sind Sie bestens gerüstet, um Ihre technische Analyse zu verbessern und Ihr Trading weiter zu individualisieren. Viel Spaß beim Handeln!

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

Vom Einsteiger zum Experten: Statistische Validierung von Angebots- und Nachfragezonen Vom Einsteiger zum Experten: Statistische Validierung von Angebots- und Nachfragezonen
Heute decken wir die oft übersehene statistische Grundlage hinter den Handelsstrategien für Angebot und Nachfrage auf. Durch die Kombination von MQL5 mit Python über einen Jupyter-Notebook-Workflow führen wir eine strukturierte, datengesteuerte Untersuchung durch, die darauf abzielt, visuelle Marktannahmen in messbare Erkenntnisse zu verwandeln. Dieser Artikel behandelt den gesamten Forschungsprozess, einschließlich der Datenerfassung, der Python-basierten statistischen Analyse, des Algorithmusentwurfs, der Tests und der endgültigen Schlussfolgerungen. Um die Methodik und die Ergebnisse im Detail nachzuvollziehen, lesen Sie den vollständigen Artikel.
Integration externer Anwendungen mit MQL5 Community OAuth Integration externer Anwendungen mit MQL5 Community OAuth
Erfahren Sie, wie Sie Ihrer Android-App mit dem OAuth-2.0-Autorisierungscodefluss die Funktion „Sign in with MQL5“ hinzufügen. Die Anleitung behandelt die App-Registrierung, Endpunkte, Redirect URI, Custom Tabs, Deep-Link-Handling und ein PHP-Backend, das den Code für ein Access-Token über HTTPS austauscht. Sie werden echte MQL5-Nutzer authentifizieren und auf Profildaten wie Rang und Ruf zugreifen.
Entwicklung eines dynamischen Multi-Pair-EA (Teil 6): Adaptive Spread-Sensitivität für hochfrequente Symbolwechsel Entwicklung eines dynamischen Multi-Pair-EA (Teil 6): Adaptive Spread-Sensitivität für hochfrequente Symbolwechsel
In diesem Teil werden wir uns auf die Entwicklung einer intelligenten Ausführungsschicht konzentrieren, die die Spread-Bedingungen in Echtzeit über mehrere Symbole hinweg kontinuierlich überwacht und auswertet. Der EA passt seine Symbolauswahl dynamisch an, indem er den Handel auf der Grundlage der Spread-Effizienz und nicht nach festen Regeln aktiviert oder deaktiviert. Dieser Ansatz ermöglicht es Hochfrequenz-Multi-Pair-Systemen, kostengünstige Symbole zu priorisieren.
Larry Williams‘ Marktgeheimnisse (Teil 9): Mit Mustern zum Gewinn Larry Williams‘ Marktgeheimnisse (Teil 9): Mit Mustern zum Gewinn
Eine empirische Studie von Larry Williams' kurzfristigen Handelsmustern, die zeigt, wie klassische Setups in MQL5 automatisiert, an realen Marktdaten getestet und auf Konsistenz, Rentabilität und praktischen Handelswert bewertet werden können.