preview
Creating Custom Indicators in MQL5 (Part 6): Evolving RSI Calculations with Smoothing, Hue Shifts, and Multi-Timeframe Support

Creating Custom Indicators in MQL5 (Part 6): Evolving RSI Calculations with Smoothing, Hue Shifts, and Multi-Timeframe Support

MetaTrader 5Trading systems |
1 592 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

In our previous article (Part 5), we developed a WaveTrend Crossover indicator in MetaQuotes Language 5 (MQL5) that utilized a canvas for fog gradients, displayed signal bubbles to highlight key crossover events, and integrated risk management features, including stop-loss and take-profit calculations, to enhance trading decisions. In Part 6, we create an advanced Relative Strength Index (RSI) indicator featuring support for multiple RSI calculation methods—including classic, smoothed, and adaptive variants—various data smoothing techniques, dynamic color hue shifts for instant visual feedback, and multi-timeframe data processing with interpolation to align RSI readings across different periods. We will cover the following topics:

  1. Exploring RSI Variants, Smoothing Methods, and Dynamic Features
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end, you’ll have a fully functional and customizable MQL5 indicator for versatile RSI analysis, supporting multiple calculation, smoothing, visualization, and timeframe options—let’s dive in!


Exploring RSI Variants, Smoothing Methods, and Dynamic Features

The normal Relative Strength Index (RSI) measures the speed and change of price movements to identify overbought or oversold conditions, typically oscillating between 0 and 100 with boundaries at 70 and 30. We will advance the RSI by adding variations, such as Cuttler's, Ehlers', Harris', quick, basic, RSX, and gradual styles, to adjust its calculations and emphasize different aspects, including smoothing and responsiveness to market momentum. Data smoothing will apply averaging techniques—basic, growth-based, evened-out, or weighted linear—to preprocess price inputs, such as close, open, high, low, or derived averages, thereby reducing noise and providing clearer signals.

Hue shifts will change the indicator's color based on conditions like direction changes, center crossings, or boundary crossings, providing visual cues for trend reversals or strength. Dynamic boundaries will adapt overbought and oversold levels based on recent RSI extremes over a specified length, while static ones will use fixed percentages, and multi-timeframe support will allow analysis across different periods with optional interpolation for smoother visuals.

Our plan is to configure user inputs for selecting RSI variants, data sources, smoothing approaches, hue conditions, and boundary settings, then compute the RSI curve with these options, draw boundaries and fillings, handle multi-timeframe data, and trigger notifications on hue changes. In brief, this creates a customizable RSI tool that adapts to various market conditions and user preferences for technical analysis, producing an advanced RSI indicator compared to the traditional indicator, as illustrated below.

CONCEPTUAL FRAMEWORK

We can see that at the end, we’ll get an adaptive RSI engine that can calculate momentum from any price representation, not just the close. It will dynamically smooth both the input data and the RSI itself, letting us switch between fast, slow, or cycle-aware behavior without changing the core logic. On top of that, it will self-adjust its boundaries and visual states, so overbought/oversold signals adapt to market conditions instead of staying fixed. Let's get to implementing this!


Implementation in MQL5

To create the indicator in MQL5, just open the MetaEditor, go to the Navigator, locate the Indicators folder, click on the "New" tab, and follow the prompts to create the file. Once it is created, in the coding environment, we will define the indicator properties and settings, such as the number of buffers, plots, and individual line properties, such as the color, width, and label.

//+------------------------------------------------------------------+
//|                              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

We start the implementation by setting the indicator to display in a separate subwindow with "#property indicator_separate_window", keeping it distinct from the main chart for better clarity. Next, we allocate eight buffers for internal calculations using "#property indicator_buffers 8" and define five plots for visual elements with "#property indicator_plots 5". For the plots, we create a filled area for the RSI high/low regions colored in light green and orange tones, a dotted lime green line for the top boundary, a dotted silver center line, a dotted orange bottom boundary, and a colored RSI curve line that shifts between silver, lime green, and orange with a width of 2 for prominence. Next, we will define some input parameters to control the indicator.

//+------------------------------------------------------------------+
//| 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?

We continue by defining enumerations to categorize user options and enhance configurability. The "DataSourceType" enum lists various price data sources, from basic ones like closing or opening prices to derived averages such as midpoint or balanced price, and includes smoothed versions for noise reduction. Next, the "RsiVariant" enum offers different RSI calculation styles, including basic, RSX, Cuttler, Ehlers, Harris, quick, and gradual, allowing selection of the desired computation method.

We also create the "HueShiftCondition" enum to determine when the RSI curve changes color, with options for direction shifts, center crossings, or boundary crossings. Additionally, the "AveragingApproach" enum specifies smoothing techniques like basic, growth-based, evened-out, or weighted linear for preprocessing data. For user inputs, we group them into sections starting with chart and calculation settings, where "AnalysisTimeframe" selects the timeframe and "RsiLength" sets the RSI period. We have added comments to make them easy to understand. This will give us the following window.

INDICATOR INPUTS WINDOW

Next thing we will do is initialize the indicator, but let's first define some global variables we will use throughout the program.

//+------------------------------------------------------------------+
//| 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
   }
}

Here, we declare global arrays for the indicator buffers, including "rsiCurveValues" for the main RSI line, "rsiHueValues" for color indices, "areaFillTop" and "areaFillBottom" for the filled areas, "topBoundaryValues", "centerLineValues", and "bottomBoundaryValues" for the boundary lines, plus "processedBarCounts" for tracking calculated bars. We also initialize "multiFrameDataHandle" to INVALID_HANDLE for managing multi-timeframe data and declare "chosenTimeframe" to store the selected timeframe. Using a preprocessor directive, we define "MULTI_FRAME_ACCESS" as a call to iCustom with the symbol, chosen timeframe, file name, and all input parameters to facilitate accessing data from different timeframes.

Next, we set up arrays "timeframeCodes" with predefined period constants like PERIOD_M1 to "PERIOD_MN1" and "timeframeLabels" with corresponding string descriptions for human-readable timeframe names. The "convertTimeframeToText" function translates a timeframe code to its label by first resolving "PERIOD_CURRENT" to the actual period, then looping through "timeframeCodes" to find a match and return the appropriate label from "timeframeLabels". Finally, the "describeRsiVariant" function uses a switch statement on the variant input to return a descriptive string for each RSI style, such as "RSI" for basic or "Ehlers-style smoothed RSI" for the Ehlers variant, defaulting to an empty string if unmatched. We can now use these helpers to initialize the indicator.

//+------------------------------------------------------------------+
//| 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
}

In the OnInit event handler, we associate the allocated buffers with specific plot indices and data types. We link buffer 0 to "areaFillTop" as "INDICATOR_DATA" for the upper fill level, buffer 1 to "areaFillBottom" similarly for the lower fill, buffer 2 to "topBoundaryValues" for the upper boundary data, buffer 3 to "centerLineValues" for the midline, buffer 4 to "bottomBoundaryValues" for the lower boundary, buffer 5 to "rsiCurveValues" for the main RSI values, buffer 6 to "rsiHueValues" as INDICATOR_COLOR_INDEX for color indexing, and buffer 7 to "processedBarCounts" as "INDICATOR_CALCULATIONS" for tracking processed bars. We then configure plot visibility using PlotIndexSetInteger, hiding the data for plots 0 and 1 to avoid displaying raw fill values, while showing plots 2, 3, and 4 for the boundaries and center line.

Next, we determine the "chosenTimeframe" by taking the maximum between the current chart period and the user-input "AnalysisTimeframe" to support multi-timeframe operations. We set the indicator's short name with IndicatorSetString by combining the timeframe text from "convertTimeframeToText", the RSI variant description from "describeRsiVariant", and a string of key parameters like "RsiLength", "DataSmoothingLength", and "DynamicBoundaryLength" for easy identification in the chart. Finally, we return INIT_SUCCEEDED to indicate successful initialization. We can proceed to do our indicator calculations. We will start by determining the timeframe for the calculations as follows.

//+------------------------------------------------------------------+
//| 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
   }
}

First, we create the "checkTimeframeValidity" function to verify if the selected timeframe has sufficient historical data available for the current timestamp. We use a static "alerted" flag to track if an invalid state has been noted. If the first timestamp is before the series start date obtained via SeriesInfoInteger with SERIES_FIRSTDATE, we fetch the terminal's first date using "SERIES_TERMINAL_FIRSTDATE" and copy the timeframe's time with CopyTime to compare dates. If the start date is invalid or later than the timestamp, we set the alerted flag and return false; otherwise, we reset the flag if previously set and return true.

In the OnCalculate event handler, which processes indicator values on each new bar or data update, we first check if the available bars are less than the total requested using Bars, returning -1 if insufficient to signal recalculation needed. If the chosen timeframe differs from the current period, indicating multi-timeframe mode, we declare temporary arrays for data and timestamps, then call "checkTimeframeValidity" to ensure data availability, returning 0 if invalid.

We obtain the multi-frame handle if invalid using the predefined "MULTI_FRAME_ACCESS" macro, and return 0 if it fails. We copy the processed bar count from buffer 7 into "interimData" with CopyBuffer, also returning 0 on failure. We define "FRAME_RATIO" as the ratio of seconds between the chosen and current periods to scale bar positions. We calculate the starting "currentPos" using the minimum and maximum functions based on previously processed bars and the adjusted total.

We loop from "currentPos" to "barTotal", checking the stop flag, and define a "TRANSFER_MULTI_FRAME" macro to copy each buffer's value from the multi-frame handle into the respective arrays like "areaFillTop", "areaFillBottom", boundaries, center line, "rsiCurveValues", and "rsiHueValues" at the current position via time-based "CopyBuffer", breaking on failure.

If multi-frame interpolation is enabled, we copy the active and following timestamps. We skip if it's the last bar or timestamps match, then count "stepsBack" to find the prior bar with an earlier timestamp. We loop forward from 1 to less than "stepsBack", defining a "SMOOTH_MULTI_FRAME" macro to linearly interpolate values in the arrays between the current and back positions, applying it to smooth the fill areas, boundaries, center line, and RSI curve for seamless multi-timeframe display.

Finally, we return the updated "currentPos" to indicate processed bars. With the timeframe set, we can proceed to do the indicator computations. We will define helper functions to make the code organized and modular. Let's start with the function to compute the adjusted data averages for smoothing methods.

#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
}

We define constants with preprocessor directives: "AVG_VARIANTS" set to 1 for the number of averaging variants, "AVG_ARRAY_X1" as 1 times variants for single-column arrays, and "AVG_ARRAY_X2" as 2 times variants for dual-column arrays to support different smoothing needs. In the "computeCustomAverage" function, we use a switch statement on the "avgApproach" parameter to route to the appropriate averaging method—basic, growth-based, evened-out, or weighted linear—returning the computed average from the respective function, or simply the "inputVal" in the default case if no match.

We declare "basicAvgArray" as a multidimensional array sized with "AVG_ARRAY_X2". The "computeBasicAvg" function first checks and resizes the array to match "barCnt" if necessary, multiplies "varIndex" by 2 for column offsetting, stores the "inputVal" in the first column, initializes the average in the second column, then loops over prior positions up to "avgLen" to sum previous values, divides by the actual offset count, and returns the result.

Similarly, for growth-based averaging, we declare "growthAvgArray" with "AVG_ARRAY_X1", resize if needed in "computeGrowthAvg", set the value directly, and if beyond the first bar and "avgLen" exceeds 1, update it using a formula that adds a weighted difference from the prior value, mimicking an exponential smooth, before returning. For evened-out averaging, "evenedAvgArray" uses "AVG_ARRAY_X1", and "computeEvenedAvg" resizes, sets the value, then, if past the second bar and "avgLen" over 1, adjusts by adding the difference from prior divided by length for a simple incremental average, and returns it.

Finally, "linearAvgArray" also uses "AVG_ARRAY_X1", and "computeLinearAvg" resizes, sets the value, returns early if "avgLen" is 1 or less, otherwise initializes totals for weights and weighted values with the current, loops over priors to accumulate decreasing weights and corresponding values, and returns the total values divided by total weights for a linear weighted average. Next, we will need to feed the averaging function with a dataset. Here is how we define the dataset production.

#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
}

For the dataset production logic, we first define constants "DATA_VARIANTS" as 1 to indicate the number of data processing variants and "DATA_VARIANT_SIZE" as 4 to set the column width per variant in the array. We declare "smoothedDataArray" as a multidimensional array sized to store smoothed price data across bars. In the "fetchChosenData" function, we select and return the appropriate price value based on the "dataType" for the given bar position.

If it's a smoothed type starting from "Data_SmoothedClose", we resize the array to match "barCnt" if necessary, scale "varIndex" by the variant size, and compute "smoothedOpen" as the average of the prior bar's stored open and close if beyond the first bar, or the current open-close midpoint otherwise. We calculate "smoothedClose" as the four-point average of open, high, low, and close, then derive "smoothedHigh" as the maximum among high, smoothedOpen, and smoothedClose, and "smoothedLow" as the minimum among low, smoothedOpen, and smoothedClose. We store these smoothedOpen and smoothedClose in the array at offsets +2 and +3.

A switch statement then returns the specific smoothed value based on "dataType", such as the midpoint for "Data_SmoothedMid", the three-point average for "Data_SmoothedStandard", or direction-based adjustments like high-close average if close exceeds open for "Data_SmoothedAdjusted", or the extreme high/low/close for "Data_SmoothedExtreme". For non-smoothed types, another switch returns direct values like close for "Data_ClosePrice", or computed averages such as the high-low midpoint for "Data_MidPoint", the typical price for "Data_StandardPrice", or direction-adjusted prices similar to the smoothed cases but using raw data. If no type matches, we return 0 as a fallback. In the OnCalculate event handler, we can now call the functions to do the initial data computations.

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

Here, we continue the OnCalculate event handler for single-timeframe mode by setting "beginPos" to the maximum of "prevProcessed" minus 1 or 0, ensuring we start from the last processed bar or the beginning if none were processed. We loop over bars from "beginPos" to less than "barTotal", checking the _StopFlag to allow interruption, and for each bar, compute "adjustedData" by first fetching the selected price via "fetchChosenData" using the "SourceData" type and the current bar's OHLC arrays, then applying the chosen smoothing with "computeCustomAverage" based on "DataSmoothingApproach" and "DataSmoothingLength". After the loop, we return "beginPos" to report the number of processed bars. Upon compilation, we get the following outcome.

INITIAL INDICATOR DATA COMPUTATIONS

From the image, we can see that the heavy lifting of the indicator has been done. What we need to do next is compute the RSI values using the respective variants to give the final indicator heavy lifting for the curves.

#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
}

Here, we define "RSI_VARIANTS" as 1 to specify the number of RSI computation variants and declare "rsiComputeArray" as a multidimensional array with 13 columns per variant to store intermediate calculations across bars. We set position constants like "DATA_SHIFT_POS" at 0 for current data, "SHIFT_POS" at 1 for net change, "ABS_SHIFT_POS" at 2 for absolute change, "DATA_SHIFTS_POS" at 3 for smoothed shifts in some variants, and "RSI_COMPUTE_POS" (aliased to 1) for stored RSI values.

In the "computeRsiValue" function, we resize "rsiComputeArray" to match "barCnt" if needed, calculate an "arrayOffset" by multiplying "varIndex" by 13, and store "currData" at the data shift position for the current bar. We use a switch on "rsiVariant" to compute the RSI differently. For basic style, we calculate an alpha factor as 1 over "rsiLen", and for initial bars less than the length, sum absolute shifts and set average net and abs shifts; otherwise, update them exponentially with the current data shift, returning 50 times (net over abs plus 1).

For the gradual style, we sum positive and negative differences over the period, set the initial to 50, then incrementally update the prior RSI with a weighted adjustment based on the relative strength, and return it. For quick style, we similarly sum positive and negative differences, returning 100 times positives over total. For Ehlers' style, we smooth the data with a four-point average if beyond bar 2, sum diffs on the smoothed values, and return 50 plus 50 times net over total.

For Cuttler style, we sum positives and negative differences, compute 100 minus 100 over (1 plus positives/negatives), store, and return it. For the Harris style, we accumulate and count positives and negatives separately, average them if counts exist, then compute a similar formula to Cuttler but with averages, store, and return. For RSX style, we compute gain factors kg and hg, zero array offsets for initial bars under length and return 50; otherwise, calculate motion and absolute motion, apply double EMA-like updates in a loop for three passes on both, and return clamped 50 times (motion over abs plus 1) between 0 and 100. If no variant matches, we return 0. We can now call this function in our loop to calculate the RSI data.

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

We continue within the loop over bars in the single-timeframe mode of the "OnCalculate" event handler by computing the RSI value for each bar. After obtaining the "adjustedData" from smoothing, we pass it to "computeRsiValue" along with the selected "ChosenRsiVariant", "RsiLength", and bar details, storing the result in "rsiCurveValues" at the current position. Next, we determine the boundary levels: if "DynamicBoundaryLength" is 1 or less, indicating static boundaries, we set "topBoundaryValues" to "TopBoundaryPercent", "bottomBoundaryValues" to "BottomBoundaryPercent", and "centerLineValues" to their midpoint average.

For dynamic boundaries when the length exceeds 1, we initialize the lowest and highest values with the current RSI, then loop over prior bars up to the length to update the min and max using the MathMin and MathMax functions. We calculate the range as highest minus lowest, then set the top boundary as lowest plus the top percentage of the range, the bottom as lowest plus the bottom percentage, and the center as lowest plus half the range. We then set the color index in "rsiHueValues" based on "HueAdjustmentOn": for boundary crossing, assign 1 if RSI exceeds top, 2 if below bottom, else 0; for center crossing, similar but relative to the center line; by default, compare to the prior RSI for direction—1 for rising, 2 for falling, 0 otherwise, with 0 for the first bar. Finally, we configure the fill areas by setting "areaFillTop" to the current RSI value, and "areaFillBottom" to the top boundary if RSI is overbought, bottom boundary if oversold, or the RSI itself otherwise.

After the loop, we update "processedBarCounts" at the last bar index to the maximum of bars processed this call, plus 1 or just 1, tracking the computation extent. Upon compilation, we get the following outcome.

COMPLETE INDICATOR

From the image, we can see that the indicator is initialized, calculated, and plotted successfully. What remains is processing the alerts. We will simply house the logic in a function and call it in the event handler for processing.

//+------------------------------------------------------------------+
//| 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
   }
}

Here, we add the "processAlertTriggers" function to handle notifications based on hue changes in the RSI curve. If "ActivateNotifications" is false, we exit early. We set the "notifyIndex" to the last bar or the one before if "NotifyOnActiveBar" is false to avoid alerting on incomplete bars, then retrieve the timestamp at that index. We check if the hue value at "notifyIndex" differs from the prior one, and if so, call "triggerNotification" with the timestamp and "rising" for hue 1 or "falling" for hue 2.

In the "triggerNotification" function, we use static variables to track the previous trend and timestamp. If the current trend or stamp differs from the stored ones, we update them, format a notification text using "convertTimeframeToText" for the period, the symbol, local time via TimeToString, the RSI variant description, and the trend shift message, then send it with the Alert function. When we call it in the event handler, we get the following outcome.

ACTIVATED ALERT SYSTEM

From the image, we can see that we calculate the indicator, map the plots, and activate the alert system when enabled, hence achieving our objectives. The thing that remains is backtesting the program, and that is handled in the next section.


Backtesting

We did the testing, and below is the compiled visualization in a single Graphics Interchange Format (GIF) bitmap image format.

ADVANCED RSI BACKTEST


Conclusion

In conclusion, we’ve developed an improved version of the Relative Strength Index (RSI) indicator in MQL5 that supports multiple calculation variants, customizable data sources with smoothing approaches, and dynamic boundaries for overbought and oversold levels. We incorporated hue shifts for color-coded visuals, optional notifications for trend changes, and multi-timeframe data handling with interpolation for broader analysis. With this dynamic RSI indicator, you’re equipped to enhance your technical analysis, ready for further customization in your trading journey. Happy trading!

Larry Williams Market Secrets (Part 8): Combining Volatility, Structure and Time Filters Larry Williams Market Secrets (Part 8): Combining Volatility, Structure and Time Filters
An in-depth walkthrough of building a Larry Williams inspired volatility breakout Expert Advisor in MQL5, combining swing structure, volatility-based entries, trade day of the week filtering, time filters, and flexible risk management, with a complete implementation and reproducible test setup.
Introduction to MQL5 (Part 37): Mastering API and WebRequest Function in MQL5 (XI) Introduction to MQL5 (Part 37): Mastering API and WebRequest Function in MQL5 (XI)
In this article, we show how to send authenticated requests to the Binance API using MQL5 to retrieve your account balance for all assets. Learn how to use your API key, server time, and signature to securely access account data, and how to save the response to a file for future use.
MQL5 Trading Tools (Part 14): Pixel-Perfect Scrollable Text Canvas with Antialiasing and Rounded Scrollbar MQL5 Trading Tools (Part 14): Pixel-Perfect Scrollable Text Canvas with Antialiasing and Rounded Scrollbar
In this article, we enhance the canvas-based price dashboard in MQL5 by adding a pixel-perfect scrollable text panel for usage guides, overcoming native scrolling limitations through custom antialiasing and a rounded scrollbar design with hover-expand functionality. The text panel supports themed backgrounds with opacity, dynamic line wrapping for content like instructions and contacts, and interactive navigation via up/down buttons, slider dragging, and mouse wheel scrolling within the body area.
The MQL5 Standard Library Explorer (Part 6): Optimizing a generated Expert Advisor The MQL5 Standard Library Explorer (Part 6): Optimizing a generated Expert Advisor
In this discussion, we follow up on the previously developed multi-signal Expert Advisor with the objective of exploring and applying available optimization methods. The aim is to determine whether the trading performance of the EA can be meaningfully improved through systematic optimization based on historical data.