Создание пользовательских индикаторов в MQL5 (Часть 6): Усовершенствование расчётов RSI — сглаживание, динамические цвета и и поддержка нескольких таймфреймов
Введение
В нашей предыдущей статье (Часть 5), мы разработали индикатор WaveTrend Crossover на MetaQuotes Language 5 (MQL5), в котором использовался холст (canvas) для создания дымчатых градиентов, отображались сигнальные пузырьки для выделения ключевых событий пересечения, а также были интегрированы функции управления рисками, включая расчёты Stop Loss и Take Profit, для повышения качества торговых решений. В шестой части мы создадим расширенный индикатор Relative Strength Index (RSI) с поддержкой нескольких методов расчёта RSI, включая классические, сглаженные и адаптивные варианты, различных техник сглаживания данных, динамической изменения оттенка для мгновенной визуальной индикации и мультитаймфреймовой обработки данных с интерполяцией для приведения значений RSI к разным таймфреймам. Мы рассмотрим следующие темы:
- Изучение вариантов RSI, методов сглаживания и динамических возможностей
- Реализация в MQL5
- Бэктестирование
- Заключение
К концу статьи у вас будет полностью рабочий и настраиваемый индикатор 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", чтобы сообщить количество обработанных баров. После компиляции получаем следующий результат.

По изображению видно, что ключевая часть вычислений уже выполнена. Далее нужно рассчитать значения 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, чтобы отслеживать объём выполненных вычислений. После компиляции получаем следующий результат.

По изображению видно, что индикатор успешно инициализирован, рассчитан и построен. Осталось обработать оповещения. Мы просто вынесем эту логику в функцию и вызовем её в обработчике событий.
//+------------------------------------------------------------------+ //| 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 . При вызове этой функции в обработчике событий получаем следующий результат.

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

Заключение
В заключение отметим, что мы разработали улучшенную версию индикатора Relative Strength Index (RSI) в MQL5, которая поддерживает несколько вариантов расчёта, настраиваемые источники данных с подходами к сглаживанию и динамические границы уровней перекупленности и перепроданности. Мы добавили изменение оттенка для цветовой индикации, опциональные уведомления об изменениях тренда и обработку мультитаймфреймовых данных с интерполяцией для приведения значений RSI к разным таймфреймам. С этим динамическим индикатором RSI вы сможете усилить технический анализ и при необходимости дальше настраивать его под свои торговые задачи. Успешной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21083
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Теория графов: Применение поиска в глубину (DFS) в торговых системах
Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (Окончание)
Создание пользовательских индикаторов в MQL5 (Часть 4): Smart WaveTrend Crossover на основе двух осцилляторов
Неопределённость как модель (Часть 7): Стохастические регрессоры
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования