English Deutsch 日本語
preview
Создание пользовательских индикаторов в MQL5 (Часть 6): Усовершенствование расчётов RSI  — сглаживание, динамические цвета и и поддержка нескольких таймфреймов

Создание пользовательских индикаторов в MQL5 (Часть 6): Усовершенствование расчётов RSI — сглаживание, динамические цвета и и поддержка нескольких таймфреймов

MetaTrader 5Торговые системы |
63 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 5), мы разработали индикатор WaveTrend Crossover на MetaQuotes Language 5 (MQL5), в котором использовался холст (canvas) для создания дымчатых градиентов, отображались сигнальные пузырьки для выделения ключевых событий пересечения, а также были интегрированы функции управления рисками, включая расчёты Stop Loss и Take Profit, для повышения качества торговых решений. В шестой части мы создадим расширенный индикатор Relative Strength Index (RSI) с поддержкой нескольких методов расчёта RSI, включая классические, сглаженные и адаптивные варианты, различных техник сглаживания данных, динамической изменения оттенка для мгновенной визуальной индикации и мультитаймфреймовой обработки данных с интерполяцией для приведения значений RSI к разным таймфреймам. Мы рассмотрим следующие темы:

  1. Изучение вариантов RSI, методов сглаживания и динамических возможностей
  2. Реализация в MQL5
  3. Бэктестирование
  4. Заключение

К концу статьи у вас будет полностью рабочий и настраиваемый индикатор MQL5 для универсального анализа RSI с поддержкой различных вариантов расчёта, сглаживания, визуализации и выбора таймфреймов — приступим!


Изучение вариантов RSI, методов сглаживания и динамических возможностей

Обычный Relative Strength Index (RSI) измеряет скорость и изменение ценовых движений, помогая определять состояния перекупленности и перепроданности; обычно он колеблется в диапазоне от 0 до 100 с границами 70 и 30. Мы усовершенствуем RSI, добавив варианты Cuttler, Ehlers, Harris, Quick, Basic, RSX и Gradual RSI, чтобы изменять характер расчётов и акцентировать разные аспекты, включая сглаживание и чувствительность к рыночному импульсу. Сглаживание данных будет применять методы усреднения — базовый метод, метод сглаживания на основе прироста, сглаженное усреднение или линейно-взвешенный метод — для предварительной обработки ценовых входных данных, таких как Close, Open, High, Low или производные средние значения, тем самым снижая шум и формируя более ясные сигналы.

Изменение оттенка будет менять цвет индикатора в зависимости от таких условий, как смена направления, пересечение центральной линии или пересечение границ, предоставляя визуальные подсказки о разворотах или силе тренда. Динамические границы будут адаптировать уровни перекупленности и перепроданности на основе недавних экстремумов RSI за заданный период, тогда как статические границы будут использовать фиксированные проценты. Мультитаймфреймовая поддержка позволит проводить анализ на разных периодах с опциональной интерполяцией для более плавной визуализации.

Наш план состоит в том, чтобы настроить пользовательские входные параметры для выбора вариантов RSI, источников данных, подходов к сглаживанию, условий изменения оттенка и настроек границ, затем рассчитать кривую RSI с учётом этих параметров, отрисовать границы и заливки, обработать мультитаймфреймовые данные и запускать уведомления при изменении оттенка. В итоге мы получим настраиваемый инструмент RSI, который адаптируется к различным рыночным условиям и предпочтениям пользователя в техническом анализе, создавая более продвинутый индикатор RSI по сравнению с традиционным вариантом, как показано ниже.

КОНЦЕПТУАЛЬНАЯ СТРУКТУРА

В результате мы получим адаптивный механизм расчёта RSI, который сможет рассчитывать импульс по любому представлению цены, а не только по цене закрытия. Он будет динамически сглаживать как входные данные, так и сам RSI, позволяя переключаться между быстрым, медленным или учитывающим циклы поведением без изменения основной логики. Кроме того, он будет самостоятельно корректировать границы и визуальные состояния, поэтому сигналы перекупленности/перепроданности будут адаптироваться к рыночным условиям, а не оставаться фиксированными. Перейдём к реализации!


Реализация в MQL5

Чтобы создать индикатор в MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку Indicators, нажмите вкладку "New" и следуйте подсказкам для создания файла. После его создания в среде разработки мы определим свойства и настройки индикатора, такие как количество буферов, графических построений и отдельных свойств линий, включая цвет, ширину и метку.

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

Начинаем реализацию с настройки отображения индикатора в отдельном подокне с помощью "#property indicator_separate_window", чтобы отделить его от основного графика и улучшить читаемость. Затем мы выделяем восемь буферов для внутренних расчётов с помощью "#property indicator_buffers 8" и задаём пять графических построений для визуальных элементов через "#property indicator_plots 5". Для построений мы создаём залитую область для областей верхних/нижних значений RSI в светло-зелёных и оранжевых тонах, пунктирную лаймово-зелёную линию верхней границы, пунктирную серебристую центральную линию, пунктирную оранжевую нижнюю границу и цветную линию кривой RSI, которая переключается между серебристым, лаймово-зелёным и оранжевым цветами; для выразительности её ширина равна 2. Далее определим входные параметры для управления индикатором.

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

Продолжим, определив перечисления для классификации пользовательских параметров и расширения возможностей настройки. Перечисление "DataSourceType" содержит различные источники ценовых данных: от базовых, таких как цены закрытия или открытия, до производных средних, например средней точки или сбалансированной цены; также в него входят сглаженные версии для снижения шума. Далее перечисление "RsiVariant" предлагает разные стили расчёта RSI, включая базовый RSI, RSX, RSI Куттлера, RSI Элерса, RSI Харриса, быстрый RSI и вариант Gradual RSI, что позволяет выбрать нужный метод вычисления.

Мы также создаём перечисление "HueShiftCondition", которое определяет, когда кривая RSI меняет цвет: при смене направления, пересечении центральной линии или пересечении границ. Кроме того, перечисление "AveragingApproach" задаёт методы сглаживания для предварительной обработки данных: базовый метод, метод сглаживания на основе прироста, сглаженное усреднение или линейно-взвешенный метод. Пользовательские входные параметры мы группируем по разделам, начиная с настроек графика и расчётов, где "AnalysisTimeframe" выбирает таймфрейм, а "RsiLength" задаёт период RSI. Мы добавили комментарии, чтобы сделать параметры понятнее. В результате получим следующее окно.

ОКНО ВХОДНЫХ ПАРАМЕТРОВ ИНДИКАТОРА

Следующий шаг — инициализация индикатора, но сначала определим несколько глобальных переменных которые будут использоваться во всей программе.

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

Здесь мы объявляем глобальные массивы для буферов индикатора, включая "rsiCurveValues" для основной линии RSI, "rsiHueValues" для цветовых индексов, "areaFillTop" и "areaFillBottom" для залитых областей, "topBoundaryValues", "centerLineValues" и "bottomBoundaryValues" для линий границ, а также "processedBarCounts" для отслеживания рассчитанных баров. Мы также инициализируем "multiFrameDataHandle" значением INVALID_HANDLE для управления мультитаймфреймовыми данными и объявляем "chosenTimeframe" для хранения выбранного таймфрейма. С помощью директивы препроцессора мы определяем "MULTI_FRAME_ACCESS" как вызов iCustom с символом, выбранным таймфреймом, именем файла и всеми входными параметрами, чтобы упростить доступ к данным с разных таймфреймов.

Далее мы настраиваем массивы "timeframeCodes" с предопределёнными константами периодов, такими как PERIOD_M1 до "PERIOD_MN1", и "timeframeLabels" с соответствующими строковыми описаниями таймфреймов в человекочитаемом виде. Функция "convertTimeframeToText" преобразует код таймфрейма в его метку: сначала она приводит "PERIOD_CURRENT" к фактическому периоду, затем проходит по "timeframeCodes", чтобы найти совпадение и вернуть соответствующую метку из "timeframeLabels". Наконец, функция "describeRsiVariant" использует оператор switch по входному варианту, чтобы вернуть описательную строку для каждого варианта RSI, например "RSI" для базового варианта или "RSI Элерса со сглаживанием" для RSI Элерса; если совпадение не найдено, возвращается пустая строка. Теперь можно использовать эти вспомогательные функции для инициализации индикатора.

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

В обработчике OnInit мы связываем выделенные буферы с конкретными индексами построений и типами данных. Буфер 0 связывается с "areaFillTop" как "INDICATOR_DATA" для верхнего уровня заливки, буфер 1 — с "areaFillBottom" аналогично для нижней заливки, буфер 2 — с "topBoundaryValues" для данных верхней границы, буфер 3 — с "centerLineValues" для средней линии, буфер 4 — с "bottomBoundaryValues" для нижней границы, буфер 5 — с "rsiCurveValues" для основных значений RSI, а буфер 6 — с "rsiHueValues" как INDICATOR_COLOR_INDEX для цветовой индексации; буфер 7 связывается с "processedBarCounts" как "INDICATOR_CALCULATIONS" для отслеживания обработанных баров. Затем мы настраиваем видимость построений с помощью PlotIndexSetInteger, скрывая данные построений 0 и 1, чтобы не отображать сырые значения заливки, и показывая построения 2, 3 и 4 для границ и центральной линии.

Далее мы определяем "chosenTimeframe", выбирая максимум между текущим периодом графика и пользовательским "AnalysisTimeframe" для поддержки мультитаймфреймовых операций. Краткое имя индикатора задаём с помощью IndicatorSetString объединяя текст таймфрейма из "convertTimeframeToText", описание варианта RSI из "describeRsiVariant" и строку ключевых параметров, таких как "RsiLength", "DataSmoothingLength" и "DynamicBoundaryLength", чтобы индикатор было легко распознать на графике. В конце возвращаем INIT_SUCCEEDED чтобы указать на успешную инициализацию. Теперь можно перейти к расчётам индикатора. Начнём с определения таймфрейма для расчётов следующим образом.

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

Сначала мы создаём функцию "checkTimeframeValidity", чтобы проверить, достаточно ли исторических данных доступно для выбранного таймфрейма на текущей временной метке. Мы используем статический флаг "alerted", чтобы отслеживать, было ли зафиксировано некорректное состояние. Если первая временная метка находится раньше даты начала серии, полученной через SeriesInfoInteger с SERIES_FIRSTDATE, мы получаем первую дату терминала с помощью "SERIES_TERMINAL_FIRSTDATE" и копируем время таймфрейма через CopyTime для сравнения дат. Если дата начала некорректна или позже временной метки, мы устанавливаем флаг alerted и возвращаем false; в противном случае сбрасываем флаг, если он был установлен ранее, и возвращаем true.

В обработчике OnCalculate который обрабатывает значения индикатора при появлении нового бара или обновлении данных, сначала проверяем, меньше ли количество доступных баров, чем запрошенное общее количество, с помощью Bars; если данных недостаточно, возвращаем -1, сигнализируя о необходимости пересчёта. Если выбранный таймфрейм отличается от текущего периода, то есть включён мультитаймфреймовый режим, мы объявляем временные массивы для данных и временных меток, затем вызываем "checkTimeframeValidity", чтобы убедиться в доступности данных, и возвращаем 0 при некорректном состоянии.

Если мультитаймфреймовый хэндл некорректен, мы получаем его с помощью заранее определённого макроса "MULTI_FRAME_ACCESS" и возвращаем 0 при неудаче. Количество обработанных баров из буфера 7 копируем в "interimData" с помощью CopyBuffer, также возвращая 0 при ошибке. Мы определяем "FRAME_RATIO" как отношение количества секунд между выбранным и текущим периодами, чтобы масштабировать позиции баров. Начальную позицию "currentPos" рассчитываем с помощью функций минимума и максимума на основе ранее обработанных баров и скорректированного общего количества.

Мы выполняем цикл от "currentPos" до "barTotal", проверяя флаг остановки, и определяем макрос "TRANSFER_MULTI_FRAME", который копирует значение каждого буфера из мультитаймфреймового хэндла в соответствующие массивы, такие как "areaFillTop", "areaFillBottom", границы, центральная линия, "rsiCurveValues" и "rsiHueValues", в текущую позицию через основанный на времени "CopyBuffer"; при ошибке цикл прерывается.

Если включена мультитаймфреймовая интерполяция, мы копируем активную и следующую временные метки. Если это последний бар или временные метки совпадают, участок пропускается. Затем мы подсчитываем "stepsBack", чтобы найти предыдущий бар с более ранней временной меткой. Далее выполняем цикл вперёд от 1 до значения меньше "stepsBack", определяя макрос "SMOOTH_MULTI_FRAME" для линейной интерполяции значений в массивах между текущей и обратной позициями. Он применяется для сглаживания областей заливки, границ, центральной линии и кривой RSI, обеспечивая плавное мультитаймфреймовое отображение.

Наконец, мы возвращаем обновлённое значение "currentPos", чтобы указать обработанные бары. После настройки таймфрейма можно перейти к вычислениям индикатора. Мы определим вспомогательные функции, чтобы код оставался организованным и модульным. Начнём с функции для расчёта скорректированных средних значений данных для методов сглаживания.

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

Мы определяем константы с помощью директив препроцессора: "AVG_VARIANTS" равно 1 и задаёт количество вариантов усреднения, "AVG_ARRAY_X1" равно 1, умноженному на число вариантов, для одноколоночных массивов, а "AVG_ARRAY_X2" равно 2, умноженным на число вариантов, для двухколоночных массивов, чтобы поддерживать разные потребности сглаживания. В функции "computeCustomAverage" мы используем оператор switch по параметру "avgApproach", направляя расчёт к соответствующему методу усреднения — базовому, основанному на приросте, выровненному или линейно-взвешенному. Функция возвращает вычисленное среднее из соответствующей функции либо просто "inputVal" в варианте по умолчанию, если совпадение не найдено.

Мы объявляем "basicAvgArray" как многомерный массив с размерностью "AVG_ARRAY_X2". Функция "computeBasicAvg" сначала проверяет и при необходимости изменяет размер массива, чтобы он соответствовал "barCnt", умножает "varIndex" на 2 для смещения по столбцам, сохраняет "inputVal" в первом столбце, инициализирует среднее значение во втором столбце, затем проходит по предыдущим позициям до "avgLen", суммирует прошлые значения, делит сумму на фактическое количество смещений и возвращает результат.

Аналогично, для усреднения на основе прироста мы объявляем "growthAvgArray" с "AVG_ARRAY_X1", при необходимости изменяем его размер в "computeGrowthAvg", напрямую задаём значение и, если мы находимся дальше первого бара, а "avgLen" больше 1, обновляем его по формуле, которая добавляет взвешенную разницу с предыдущим значением, имитируя экспоненциальное сглаживание, после чего возвращаем результат. Для выровненного усреднения массив "evenedAvgArray" использует "AVG_ARRAY_X1", а функция "computeEvenedAvg" изменяет размер массива, задаёт значение, затем, если пройден второй бар и "avgLen" больше 1, корректирует его, добавляя разницу с предыдущим значением, делённую на длину, для простого инкрементального среднего, и возвращает результат.

Наконец, "linearAvgArray" также использует "AVG_ARRAY_X1", а "computeLinearAvg" изменяет размер массива, задаёт значение и досрочно возвращает его, если "avgLen" равен 1 или меньше. В противном случае функция инициализирует суммы весов и взвешенных значений текущим значением, проходит по предыдущим барам, накапливая убывающие веса и соответствующие значения, и возвращает сумму значений, делённую на сумму весов, то есть линейно-взвешенное среднее. Далее нужно передать функции усреднения набор данных. Вот как мы определяем формирование входного набора данных.

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

Для логики формирования набора данных мы сначала определяем константу "DATA_VARIANTS" как 1, чтобы указать количество вариантов обработки данных, и "DATA_VARIANT_SIZE" как 4, чтобы задать ширину столбцов для каждого варианта в массиве. Мы объявляем "smoothedDataArray" как многомерный массив, предназначенный для хранения сглаженных ценовых данных по барам. В функции "fetchChosenData" выбирается и возвращается соответствующее ценовое значение на основе "dataType" для заданной позиции бара.

Если это сглаженный тип, начиная с "Data_SmoothedClose", мы при необходимости изменяем размер массива под "barCnt", масштабируем "varIndex" на размер варианта и рассчитываем "smoothedOpen" как среднее сохранённых Open и Close предыдущего бара, если это не первый бар, иначе используем среднюю точку между текущими Open и Close. "smoothedClose" рассчитывается как среднее четырёх точек — Open, High, Low и Close. Затем "smoothedHigh" определяется как максимум среди High, smoothedOpen и smoothedClose, а "smoothedLow" — как минимум среди Low, smoothedOpen и smoothedClose. Значения smoothedOpen и smoothedClose сохраняются в массиве со смещениями +2 и +3.

Затем оператор switch возвращает конкретное сглаженное значение на основе "dataType", например среднюю точку для "Data_SmoothedMid", среднее трёх точек для "Data_SmoothedStandard", корректировки по направлению, такие как среднее High-Close, если Close выше Open, для "Data_SmoothedAdjusted", или экстремальное значение High/Low/Close для "Data_SmoothedExtreme". Для несглаженных типов другой оператор switch возвращает прямые значения, например Close для "Data_ClosePrice", или вычисленные средние, такие как середина диапазона High-Low для "Data_MidPoint", типичная цена для "Data_StandardPrice" либо цены с корректировкой по направлению, аналогичные сглаженным случаям, но на основе сырых данных. Если тип не совпадает ни с одним вариантом, возвращаем 0 как резервное значение. В обработчике OnCalculate теперь можно вызвать функции для выполнения начальных расчётов данных.

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

Здесь мы продолжаем обработчик OnCalculate для однотаймфреймового режима: задаём "beginPos" как максимум между "prevProcessed" минус 1 и 0, чтобы начать с последнего обработанного бара или с начала, если ранее ничего не было обработано. Мы проходим по барам от "beginPos" до значения меньше "barTotal", проверяя _StopFlag для возможности прерывания, и для каждого бара рассчитываем "adjustedData": сначала получаем выбранную цену через "fetchChosenData" с использованием типа "SourceData" и OHLC-массивов текущего бара, затем применяем выбранное сглаживание с помощью "computeCustomAverage" на основе "DataSmoothingApproach" и "DataSmoothingLength". После цикла возвращаем "beginPos", чтобы сообщить количество обработанных баров. После компиляции получаем следующий результат.

INITIAL INDICATOR DATA COMPUTATIONS

По изображению видно, что ключевая часть вычислений уже выполнена. Далее нужно рассчитать значения RSI с использованием соответствующих вариантов, чтобы сформировать итоговые кривые индикатора.

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

Здесь мы определяем "RSI_VARIANTS" как 1, чтобы задать количество вариантов расчёта RSI, и объявляем "rsiComputeArray" как многомерный массив с 13 столбцами на каждый вариант для хранения промежуточных расчётов по барам. Также задаются позиционные константы: "DATA_SHIFT_POS" равна 0 для текущих данных, "SHIFT_POS" равна 1 для направленного изменения, "ABS_SHIFT_POS" равна 2 для абсолютного изменения, "DATA_SHIFTS_POS" равна 3 для сглаженных изменений в некоторых вариантах, а "RSI_COMPUTE_POS" (псевдоним к 1) используется для сохранённых значений RSI.

В функции "computeRsiValue" мы при необходимости изменяем размер "rsiComputeArray" под "barCnt", рассчитываем "arrayOffset", умножая "varIndex" на 13, и сохраняем "currData" в позиции текущего изменения данных для текущего бара. Оператор switch по "rsiVariant" позволяет рассчитывать RSI разными способами. Для базового RSI рассчитываем коэффициент alpha как 1, делённое на "rsiLen"; для начальных баров, количество которых меньше длины, суммируем абсолютные изменения и задаём средние направленные и абсолютные изменения. В остальных случаях обновляем их экспоненциально с учётом текущего изменения данных и возвращаем 50, умноженное на отношение net/abs плюс 1.

Для постепенного RSI мы суммируем положительные и отрицательные разницы за период, задаём начальное значение 50, затем инкрементально обновляем предыдущий RSI с помощью взвешенной корректировки на основе относительной силы и возвращаем результат. Для быстрого RSI аналогично суммируем положительные и отрицательные разницы, возвращая 100, умноженное на отношение положительных значений к общей сумме. Для RSI Элерса данные сглаживаются четырёхточечным средним, если пройдено более двух баров; затем суммируются разницы по сглаженным значениям и возвращается 50 плюс 50, умноженное на отношение net к total.

Для стиля Cuttler мы суммируем положительные и отрицательные разницы, вычисляем 100 минус 100, делённое на (1 плюс отношение positives/negatives), сохраняем и возвращаем результат. Для стиля Harris отдельно накапливаем и считаем положительные и отрицательные значения, усредняем их при наличии количества, затем вычисляем формулу, похожую на Cuttler, но на основе средних, сохраняем и возвращаем результат. Для стиля RSX рассчитываем коэффициенты усиления kg и hg, обнуляем смещения массива для начальных баров меньше длины и возвращаем 50; в остальных случаях рассчитываем движение и абсолютное движение, выполняем двойные EMA-подобные обновления в цикле по три прохода для обоих значений и возвращаем ограниченное диапазоном 0–100 значение 50, умноженное на (motion/abs плюс 1). Если вариант не совпал, возвращаем 0. Теперь можно вызвать эту функцию в цикле для расчёта данных RSI.

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

Мы продолжаем цикл по барам в однотаймфреймовом режиме обработчика "OnCalculate", рассчитывая значение RSI для каждого бара. После получения "adjustedData" из сглаживания передаём его в "computeRsiValue" вместе с выбранным "ChosenRsiVariant", "RsiLength" и данными бара, сохраняя результат в "rsiCurveValues" в текущей позиции. Далее определяем уровни границ: если "DynamicBoundaryLength" равен 1 или меньше, то есть используются статические границы, задаём "topBoundaryValues" равным "TopBoundaryPercent", "bottomBoundaryValues" равным "BottomBoundaryPercent", а "centerLineValues" — их среднему значению.

Для динамических границ, когда длина больше 1, мы инициализируем минимальное и максимальное значения текущим RSI, затем проходим по предыдущим барам в пределах заданной длины, обновляя минимум и максимум с помощью функций MathMin и MathMax . Диапазон рассчитывается как highest минус lowest; затем верхняя граница задаётся как lowest плюс верхний процент от диапазона, нижняя — как lowest плюс нижний процент, а центр — как lowest плюс половина диапазона. Затем мы задаём цветовой индекс в "rsiHueValues" на основе "HueAdjustmentOn": при пересечении границ присваиваем 1, если RSI выше верхней границы, 2 — если ниже нижней, иначе 0; при пересечении центральной линии логика аналогична, но относительно центральной линии; по умолчанию сравниваем значение с предыдущим RSI для определения направления — 1 для роста, 2 для падения, 0 в остальных случаях, а для первого бара — 0. В конце настраиваем области заливки: "areaFillTop" получает текущее значение RSI, а "areaFillBottom" — верхнюю границу, если RSI в зоне перекупленности, нижнюю границу, если он в зоне перепроданности, или сам RSI в остальных случаях.

После цикла мы обновляем "processedBarCounts" на индексе последнего бара, записывая максимум количества баров, обработанных в этом вызове, плюс 1 либо просто 1, чтобы отслеживать объём выполненных вычислений. После компиляции получаем следующий результат.

COMPLETE INDICATOR

По изображению видно, что индикатор успешно инициализирован, рассчитан и построен. Осталось обработать оповещения. Мы просто вынесем эту логику в функцию и вызовем её в обработчике событий.

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

Здесь мы добавляем функцию "processAlertTriggers" для обработки уведомлений на основе изменений оттенка кривой RSI. Если "ActivateNotifications" равно false, выходим из функции досрочно. "notifyIndex" задаётся как последний бар либо предыдущий, если "NotifyOnActiveBar" равно false, чтобы не отправлять оповещения по незавершённым барам; затем извлекается временная метка по этому индексу. Мы проверяем, отличается ли значение оттенка в "notifyIndex" от предыдущего, и если да, вызываем "triggerNotification" с временной меткой и направлением "rising" для оттенка 1 или "falling" для оттенка 2.

В функции "triggerNotification" используются статические переменные для отслеживания предыдущего тренда и временной метки. Если текущий тренд или метка отличается от сохранённых, мы обновляем их, формируем текст уведомления с использованием "convertTimeframeToText" для периода, символа, локального времени через TimeToString, описания варианта RSI и сообщения о смене тренда, а затем отправляем его с помощью функции Alert . При вызове этой функции в обработчике событий получаем следующий результат.

ACTIVATED ALERT SYSTEM

По изображению видно, что мы рассчитываем индикатор, связываем построения и активируем систему оповещений, когда она включена, тем самым достигая поставленных целей. Остаётся выполнить бэктестирование программы — это рассматривается в следующем разделе.


Бэктестирование

Мы выполнили тестирование, и ниже представлена итоговая визуализация в виде единого GIF-изображения.

ADVANCED RSI BACKTEST


Заключение

В заключение отметим, что мы разработали улучшенную версию индикатора Relative Strength Index (RSI) в MQL5, которая поддерживает несколько вариантов расчёта, настраиваемые источники данных с подходами к сглаживанию и динамические границы уровней перекупленности и перепроданности. Мы добавили изменение оттенка для цветовой индикации, опциональные уведомления об изменениях тренда и обработку мультитаймфреймовых данных с интерполяцией для приведения значений RSI к разным таймфреймам. С этим динамическим индикатором RSI вы сможете усилить технический анализ и при необходимости дальше настраивать его под свои торговые задачи. Успешной торговли!

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21083

Прикрепленные файлы |
Теория графов: Применение поиска в глубину (DFS) в торговых системах Теория графов: Применение поиска в глубину (DFS) в торговых системах
В этой статье метод поиска в глубину применяется к структуре рынка путем моделирования максимумов и минимумов свингов в виде узлов графа и отслеживания одного структурного пути настолько глубоко, насколько остаются валидными соответствующие условия. Когда ключевой свинг пробит, алгоритм возвращается назад и исследует альтернативную ветвь. Читатели получают практический фреймворк для формализации структурной направленности и проверки того, соответствует ли текущий путь таким целям, как пулы ликвидности или зоны спроса и предложения.
Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (Окончание) Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (Окончание)
Лучше всего здесь работает сочетание вариантов 2 и 4: оно сохраняет техническую точность, не обещает доходность и сразу показывает практический смысл архитектуры. В статье завершается адаптация фреймворка CogDriver к анализу финансовых рынков. Представление рыночной сцены, временная память, прогнозный план и оценка ожидаемой ошибки объединяются в единый торговый контур, при этом прогнозирование отделено от принятия решений. Рассматриваются построение моделей, организация обучения и проверка архитектуры в тестере стратегий MetaTrader 5 с акцентом на снижение избыточной реактивности и дрожания торговых решений.
Создание пользовательских индикаторов в MQL5 (Часть 4): Smart WaveTrend Crossover на основе двух осцилляторов Создание пользовательских индикаторов в MQL5 (Часть 4): Smart WaveTrend Crossover на основе двух осцилляторов
В этой статье мы разработаем пользовательский индикатор MQL5 под названием Smart WaveTrend Crossover, использующий два осциллятора WaveTrend: один — для формирования сигналов пересечения, второй — для фильтрации тренда, с настраиваемыми параметрами длины канала, периода усреднения и скользящей средней. Индикатор отображает цветные свечи в зависимости от направления тренда, отображает стрелки покупки и продажи при пересечениях, а также включает параметры для включения подтверждения по тренду и настройки визуальных элементов, таких как цвета и смещения.
Неопределённость как модель (Часть 7): Стохастические регрессоры Неопределённость как модель (Часть 7): Стохастические регрессоры
Материал — практическое руководство по множественной регрессии со стохастическими факторами в алготрейдинге с акцентом на эндогенность. Показаны EDA/CDA-приёмы, тест Дарбина—Ву—Хаусмана и двухэтапный МНК для получения состоятельных коэффициентов. Все расчёты реализованы в MQL5 и проверены на внутридневной волатильности EURUSD. Читатель получит рабочий конвейер для выявления эндогенности и корректной спецификации моделей.