English Deutsch 日本語
preview
Создание пользовательских индикаторов в MQL5 (Часть 3): Улучшение многошкальной визуализации с секторным и круговым стилями

Создание пользовательских индикаторов в MQL5 (Часть 3): Улучшение многошкальной визуализации с секторным и круговым стилями

MetaTrader 5Трейдинг |
47 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 2) мы разработали отображение Relative Strength Index в формате круговой шкалы со стрелкой на языке MetaQuotes Language 5 (MQL5). В нём использовались Canvas и механизм стрелки: значения Relative Strength Index отображались на круговой шкале с динамической стрелкой, цветовые зоны показывали уровни перекупленности и перепроданности, а настраиваемые текстовые обозначения дополняли визуализацию. Также было интегрировано традиционное линейное построение для более полного анализа импульса. В части 3 мы разрабатываем расширенную мультишкальную версию с секторным и круглым стилями. Эта версия поддерживает несколько осцилляторов, таких как Relative Strength Index (RSI), Commodity Channel Index (CCI) и Money Flow Index (MFI), а пользователь может выбирать отдельные и комбинированные варианты отображения. В статье вводятся производные классы для секторного и круглого дизайна шкал, улучшается отрисовка корпуса с помощью дуг, полигонов и относительного позиционирования, что позволяет получить более аккуратное мультииндикаторное отображение. Мы рассмотрим следующие темы:

  1. Мультишкальная архитектура с секторным и круглым стилями
  2. Реализация в MQL5
  3. Бэктестинг
  4. Заключение

К концу статьи у вас будет рабочий индикатор MQL5 для визуализации нескольких осцилляторов в виде шкал. Он будет готов к дальнейшей настройке — приступим!


Мультишкальная архитектура с секторным и круглым стилями

Мультишкальная архитектура строится на базовом классе для настраиваемых шкал и вводит производные классы для круглого и секторного стилей, чтобы визуализировать такие осцилляторы, как Relative Strength Index, Commodity Channel Index и Money Flow Index. Одиночные или комбинированные варианты отображения выбираются через перечисление, что дает гибкость при анализе импульса рынка по нескольким индикаторам. Круглый стиль сохраняет корпус в виде заполненных окружностей, а секторный стиль улучшает визуальную часть за счет секторов на основе дуг, скругляющих дуг, соединительных линий и полигонов для неполных циферблатов. Такая конструкция адаптируется к диапазонам углов и поддерживает относительное позиционирование, чтобы выравнивать несколько шкал по горизонтали на графике. Идея появилась из того, что иногда может понадобиться лишь небольшой участок шкалы для вывода информации. Поэтому мы решили разделить полную круговую шкалу на половину и четверть, чтобы можно было выбрать вариант, подходящий под конкретный стиль; и в нашем случае мы условно реализуем все три варианта.

Чтобы этого добиться, мы планируем расширить базовый класс шкалы чисто виртуальными методами для расчета и отрисовки корпуса шкалы, что позволит переопределять их в производных классах для логики, специфичной для конкретного стиля. Мы добавим перечисление для выбора шкалы, чтобы условно инициализировать и позиционировать экземпляры для Relative Strength Index (круглая шкала), Commodity Channel Index (секторная шкала) и Money Flow Index (секторная шкала), интегрируя их хендлы и буферы для копирования данных. Они нужны нам для копирования данных, которые затем визуализируются, но в вашем случае можно использовать что угодно: прибыль, динамику индикатора, прогресс или даже показатели счета, не ограничиваясь данными других индикаторов. Архитектура разделяет слои шкалы (с улучшенным заполнением отметок для нулевых позиций) и стрелки (с настраиваемыми множителями хвоста), обеспечивая эффективную перерисовку и относительную привязку на основе предыдущих шкал. Ниже кратко показана визуальная схема наших целей.

ОБЗОР АРХИТЕКТУРЫ ШКАЛ


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

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

#property indicator_buffers 3
#property indicator_plots 3
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrDodgerBlue
#property indicator_style1 STYLE_SOLID
#property indicator_width1 2
#property indicator_label1 "RSI"
#property indicator_type2 DRAW_LINE
#property indicator_color2 clrGreen
#property indicator_style2 STYLE_SOLID
#property indicator_width2 2
#property indicator_label2 "CCI"
#property indicator_type3 DRAW_LINE
#property indicator_color3 clrBlue
#property indicator_style3 STYLE_SOLID
#property indicator_width3 2
#property indicator_label3 "MFI"
#property indicator_minimum 0
#property indicator_maximum 100
#property indicator_level1 30
#property indicator_level2 70
#property indicator_level3 -100
#property indicator_level4 100
#property indicator_level5 0
#property indicator_levelcolor clrGray
#property indicator_levelstyle STYLE_DOT

Сначала мы заново определяем метаданные индикатора с помощью директив #property: выделяем 3 буфера через "indicator_buffers" для хранения данных и настраиваем 3 линии отображения индикатора через indicator_plots, поскольку теперь работаем с 3 индикаторами. Для первой линии отображения задаем тип DRAW_LINE, цвет DodgerBlue, стиль сплошной линии (solid), ширину 2 и метку "RSI", как и раньше. Вторая линия отображения — зеленая сплошная линия шириной 2 с меткой "CCI", а третья — синяя сплошная линия шириной 2 с меткой "MFI".

Мы задаем вертикальную шкалу от 0 до 100 с помощью "indicator_minimum" и "indicator_maximum", а также добавляем пять серых пунктирных уровней: 30, 70, -100, 100 и 0. Для этого используются "indicator_level1"–"indicator_level5", "indicator_levelcolor" и "indicator_levelstyle", чтобы обозначить пороговые значения для разных осцилляторов. Эти свойства обеспечат одновременную линейную визуализацию Relative Strength Index, Commodity Channel Index и Money Flow Index в отдельном окне.

Чтобы через окно входных параметров можно было выбирать, какие шкалы отображать (RSI, CCI, MFI или их комбинации), сделать индикатор более гибким и экономным по ресурсам, а также поддержать расширенную отрисовку шкалы, особенно для таких индикаторов, как CCI (где возможны отрицательные значения и ноль в середине), нужно объявить несколько перечислений. Они помогут определить, где на шкале располагается "нулевая" отметка, повышая точность для шкал, которые не ограничиваются положительными значениями.

//+------------------------------------------------------------------+
//| Gauge Selection Enum                                             |
//+------------------------------------------------------------------+
enum ENUM_GAUGE_SELECTION {                // Define gauge selection enum
   RSI_ONLY,                               // RSI Only
   CCI_ONLY,                               // CCI Only
   MFI_ONLY,                               // MFI Only
   RSI_CCI,                                // RSI CCI
   RSI_MFI,                                // RSI MFI
   CCI_MFI,                                // CCI MFI
   ALL                                     // All
};

// Inputs
input ENUM_GAUGE_SELECTION inpGaugeSelection = ALL; // Gauge Selection


//+------------------------------------------------------------------+
//| Null Mark Position Enum                                          |
//+------------------------------------------------------------------+
enum ENUM_NULLMARK_POS {                   // Define null mark position enum
   NULLMARK_NONE=0,                        // None
   NULLMARK_LEFT=1,                        // Left
   NULLMARK_MIDDLE=2,                      // Middle
   NULLMARK_RIGHT=3                        // Right
};

Мы определяем перечисление "ENUM_GAUGE_SELECTION", чтобы предоставить варианты выбора отображаемых шкал: отдельные варианты "RSI_ONLY", "CCI_ONLY" или "MFI_ONLY", комбинации "RSI_CCI", "RSI_MFI" или "CCI_MFI", а также "ALL" для вывода всех шкал. Затем объявляем входной параметр "inpGaugeSelection" типа "ENUM_GAUGE_SELECTION" со значением по умолчанию "ALL", чтобы пользователь мог выбрать конфигурацию шкал прямо в настройках индикатора. Далее создаем перечисление "ENUM_NULLMARK_POS" для указания положения нулевой отметки на шкале: "NULLMARK_NONE" равно 0, "NULLMARK_LEFT" — 1, "NULLMARK_MIDDLE" — 2, а "NULLMARK_RIGHT" — 3. Это обеспечивает гибкую работу с разметкой шкалы, особенно для индикаторов с отрицательными диапазонами. Мы считаем это важным, поскольку так можно покрыть все возможные сценарии.

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

Старая структура корпуса:

//+------------------------------------------------------------------+
//| Case Structure                                                   |
//+------------------------------------------------------------------+
struct Struct_Case {                       // Define case structure
   bool display;                           // Store display flag
   Struct_Circle circle;                   // Store circle structure
};

Новая структура корпуса:

//+------------------------------------------------------------------+
//| Case Structure                                                   |
//+------------------------------------------------------------------+
struct Struct_Case {                       // Define case structure
   bool display;                           // Store display flag
   Struct_Circle circle;                   // Store circle structure
   int mode;                               // Store mode
   Struct_Arc mainArc;                     // Store main arc
   Struct_Arc secondaryArc;                // Store secondary arc
   Struct_Arc centerArc;                   // Store center arc
   Struct_Arc leftRoundingArc;             // Store left rounding arc
   Struct_Arc rightRoundingArc;            // Store right rounding arc
   Struct_Line leftConnectLine;            // Store left connect line
   Struct_Line rightConnectLine;           // Store right connect line
   Struct_Dot fillDot;                     // Store fill dot
};

Здесь мы улучшаем структуру "Struct_Case", чтобы она поддерживала более сложный корпус шкалы, особенно для секторных стилей. В структуре остается флаг отображения и встроенная "Struct_Circle" для базовых круговых элементов. Мы добавляем целочисленное поле "mode", определяющее конфигурацию корпуса на основе диапазона углов, а также экземпляры "Struct_Arc" для основной дуги, вторичной дуги (для больших углов), центральной дуги и левой/правой скругляющих дуг, сглаживающих края. Кроме того, включаются "Struct_Line" для левой и правой соединительных линий, связывающих компоненты, и "Struct_Dot" для точки заливки, чтобы обеспечить полное покрытие при заполнении полигонов во время отрисовки. Что касается структуры параметров, мы добавляем только входной параметр множителя хвоста, как показано ниже. Для наглядности он выделен.

//+------------------------------------------------------------------+
//| Gauge Input Parameters Structure                                 |
//+------------------------------------------------------------------+
struct Struct_GaugeInputParams {           // Define gauge input parameters structure
   int xOffset;                            // Store x offset
   int yOffset;                            // Store y offset
   int anchorCorner;                       // Store anchor corner
   int relativeMode;                       // Store relative mode
   string relativeObjectName;              // Store relative object name
   int scaleAngleRange;                    // Store scale angle range
   int rotationAngle;                      // Store rotation angle
   color scaleColor;                       // Store scale color
   int scaleStyle;                         // Store scale style
   bool displayScaleArc;                   // Store display scale arc flag
   double minScaleValue;                   // Store minimum scale value
   double maxScaleValue;                   // Store maximum scale value
   int scaleMultiplier;                    // Store scale multiplier
   int tickStyle;                          // Store tick style
   int tickSize;                           // Store tick size
   double majorTickInterval;               // Store major tick interval
   int mediumTicksPerMajor;                // Store medium ticks per major
   int minorTicksPerInterval;              // Store minor ticks per interval
   int tickFontSize;                       // Store tick font size
   string tickFontName;                    // Store tick font name
   bool tickFontItalic;                    // Store tick font italic flag
   bool tickFontBold;                      // Store tick font bold flag
   color tickFontColor;                    // Store tick font color
   Struct_RangeParams ranges[4];           // Store ranges array
   color caseColor;                        // Store case color
   int borderStyle;                        // Store border style
   color borderColor;                      // Store border color
   int borderGapSize;                      // Store border gap size
   Struct_GaugeLegendParams description;   // Store description
   Struct_GaugeLegendParams units;         // Store units
   Struct_GaugeLegendParams multiplier;    // Store multiplier
   Struct_GaugeLegendParams value;         // Store value
   int needleCenterStyle;                  // Store needle center style
   color needleCenterColor;                // Store needle center color
   color needleColor;                      // Store needle color
   int needleFillStyle;                    // Store needle fill style
   double needleTailMultiplier;            // Store needle tail multiplier
};

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

//+------------------------------------------------------------------+
//| Base Gauge Class                                                 |
//+------------------------------------------------------------------+
class CGaugeBase                           // Define base gauge class
{
private:
   int relativeX;                          //--- Store relative X
   int relativeY;                          //--- Store relative Y
   int centerX;                            //--- Store center X
   int centerY;                            //--- Store center Y
   double currentValue;                    //--- Store current value
   bool initializationComplete;            //--- Store initialization complete flag
   void Draw();                            //--- Declare draw method
   void CalculateNeedle();                 //--- Declare calculate needle method
   void RedrawNeedle(double value);        //--- Declare redraw needle method
   void CalculateAndDrawLegends();         //--- Declare calculate and draw legends method
   void CalculateAndDrawLegendString(Struct_GaugeLegendString &legendString); //--- Declare calculate and draw legend string method
   void RedrawScaleMarks(Struct_Case &internalCase, Struct_Arc &scaleArc, int borderGap); //--- Declare redraw scale marks method
   void CalculateRanges(int borderGap);    //--- Declare calculate ranges method
   bool IsValidRange(int index);           //--- Declare check valid range method
   void NormalizeRangeValues(double &minValue, double &maxValue, double val0, double val1); //--- Declare normalize range values method
   void CalculateRangePie(Struct_Range &range, int innerRadius, int radialGap, int outerRadius, double rangeStart, double rangeEnd, color rangeClr, color caseClr); //--- Declare calculate range pie method
   void DrawRanges();                      //--- Declare draw ranges method
   void DrawRange(Struct_Range &range);    //--- Declare draw range method
   void CalculateInnerOuterRadii(int &innerRadius, int &outerRadius, int baseRadius, int tickLength, int tickStyle); //--- Declare calculate inner outer radii method
   bool DrawTick(double angle, int length, Struct_Arc &scaleArc); //--- Declare draw tick method
   double CalculateAngleDelta(double angle1, double angle2, int direction); //--- Declare calculate angle delta method
   bool GetLabelAreaSize(Struct_LabelAreaSize &areaSize, Struct_GaugeLegendString &legendString); //--- Declare get label area size method
   bool EraseLegendString(Struct_GaugeLegendString &legendString, color eraseClr); //--- Declare erase legend string method
   bool RedrawValueDisplay(double value);  //--- Declare redraw value display method
   void SetLegendStringParams(Struct_GaugeLegendString &legendString, Struct_GaugeLegendParams &param, int minRadius, int radiusDelta); //--- Declare set legend string params method
protected:
   Struct_GaugeInputParams inputParams;    //--- Store input parameters
   Struct_ScaleLayer scaleLayer;           //--- Store scale layer
   Struct_NeedleLayer needleLayer;         //--- Store needle layer
   int m_radius;                           //--- Store radius
   virtual void CalculateCaseElements(Struct_Case &externalCase, Struct_Case &internalCase, int borderSize, int borderGap) = 0; //--- Declare calculate case elements method
   virtual void DrawCaseElements(Struct_Case &externalCase, Struct_Case &internalCase) = 0; //--- Declare draw case elements method
public:
   bool Create(string name, int x, int y, int size, string relativeObjectName, int relativeMode, int corner, bool background, uchar scaleTransparency, uchar needleTransparency); //--- Declare create method
   bool CalculateLocation();               //--- Declare calculate location method
   void Redraw();                          //--- Declare redraw method
   void NewValue(double value);            //--- Declare new value method
   void Delete();                          //--- Declare delete method
   void SetScaleParameters(int angleRange, int rotation, double minValue, double maxValue, int multiplier, int style, color scaleClr, bool displayArc = false); //--- Declare set scale parameters method
   void SetTickParameters(int style, int size, double majorInterval, int mediumPerMajor, int minorPerInterval); //--- Declare set tick parameters method
   void SetTickLabelFont(int fontSize, string fontName, bool italic, bool bold, color fontClr = clrBlack); //--- Declare set tick label font method
   void SetCaseParameters(color caseClr, int borderStyle, color borderClr, int borderGapSize); //--- Declare set case parameters method
   void SetLegendParameters(int legendType, bool enable, string text, int radius, double angle, uint fontSize, string fontName, bool italic, bool bold, color textClr = clrDarkGray); //--- Declare set legend parameters method
   void SetLegendParam(Struct_GaugeLegendParams &legendParam, bool enable, string text, int radius, double angle, uint fontSize, string fontName, bool italic, bool bold, color textClr = clrDarkGray); //--- Declare set legend param method
   void SetRangeParameters(int index, bool enable, double start, double end, color rangeClr); //--- Declare set range parameters method
   void SetNeedleParameters(int centerStyle, color centerClr, color needleClr, int fillStyle, double tailMultiplier = 2.0); //--- Declare set needle parameters method
};

//+------------------------------------------------------------------+
//| Round Gauge Class                                                |
//+------------------------------------------------------------------+
class CRoundGauge : public CGaugeBase      // Define round gauge class
{
protected:
   void CalculateCaseElements(Struct_Case &externalCase, Struct_Case &internalCase, int borderSize, int borderGap) override //--- Override calculate case elements
   {
      if(borderSize > 0) {                 //--- Check border size
         externalCase.circle.centerX = scaleLayer.scaleArc.centerX; //--- Set external center X
         externalCase.circle.centerY = scaleLayer.scaleArc.centerY; //--- Set external center Y
         externalCase.circle.radius = m_radius; //--- Set external radius
         externalCase.circle.clr = inputParams.borderColor; //--- Set external color
         externalCase.display = true;      //--- Set display flag
      } else                               //--- Handle no border
         externalCase.display = false;     //--- Set display flag false
      internalCase.circle.centerX = scaleLayer.scaleArc.centerX; //--- Set internal center X
      internalCase.circle.centerY = scaleLayer.scaleArc.centerY; //--- Set internal center Y
      internalCase.circle.radius = m_radius - borderSize; //--- Set internal radius
      internalCase.circle.clr = inputParams.caseColor; //--- Set internal color
      internalCase.display = true;         //--- Set display flag
   }
   void DrawCaseElements(Struct_Case &externalCase, Struct_Case &internalCase) override //--- Override draw case elements
   {
      if(externalCase.display)             //--- Check external display
         scaleLayer.obj_Canvas.FillCircle(externalCase.circle.centerX, externalCase.circle.centerY, externalCase.circle.radius, ColorToARGB(externalCase.circle.clr, scaleLayer.transparency)); //--- Fill external circle
      if(internalCase.display)             //--- Check internal display
         scaleLayer.obj_Canvas.FillCircle(internalCase.circle.centerX, internalCase.circle.centerY, internalCase.circle.radius, ColorToARGB(internalCase.circle.clr, scaleLayer.transparency)); //--- Fill internal circle
   }
};

//+------------------------------------------------------------------+
//| Sector Gauge Class                                               |
//+------------------------------------------------------------------+
class CSectorGauge : public CGaugeBase     // Define sector gauge class
{
private:
   void CaseCalculateSector(Struct_Case &caseStruct, int gap) //--- Declare calculate sector method
   {
      double fi0,fi1,fi2;                  //--- Declare angles
      double sa;                           //--- Declare scale range
      Struct_Arc referenceArc = scaleLayer.scaleArc; //--- Set reference arc
      if(referenceArc.endAngle > referenceArc.startAngle) //--- Check end > start
         sa = NormalizeRadians(referenceArc.endAngle - referenceArc.startAngle); //--- Set sa
      else                                 //--- Handle wrap
         sa = NormalizeRadians(referenceArc.endAngle + (2 * M_PI - referenceArc.startAngle)); //--- Set sa
      if(sa > M_PI)                        //--- Check > PI
         caseStruct.mode = 1;              //--- Set mode 1
      else                                 //--- Handle <= PI
         caseStruct.mode = 0;              //--- Set mode 0
      if(sa > M_PI) {                      //--- Check > PI
         caseStruct.mainArc.centerX = referenceArc.centerX; //--- Set main center X
         caseStruct.mainArc.centerY = referenceArc.centerY; //--- Set main center Y
         caseStruct.mainArc.radius = referenceArc.radius + gap; //--- Set main radius
         caseStruct.mainArc.startAngle = NormalizeRadians(referenceArc.startAngle); //--- Set main start angle
         caseStruct.mainArc.endAngle = NormalizeRadians(referenceArc.startAngle + sa * 0.55); //--- Set main end angle
         caseStruct.mainArc.clr = clrNONE; //--- Set main color
         caseStruct.secondaryArc.display = true; //--- Set secondary display
         caseStruct.secondaryArc.centerX = referenceArc.centerX; //--- Set secondary center X
         caseStruct.secondaryArc.centerY = referenceArc.centerY; //--- Set secondary center Y
         caseStruct.secondaryArc.radius = referenceArc.radius + gap; //--- Set secondary radius
         caseStruct.secondaryArc.startAngle = NormalizeRadians(referenceArc.endAngle - sa * 0.55); //--- Set secondary start angle
         caseStruct.secondaryArc.endAngle = NormalizeRadians(referenceArc.endAngle); //--- Set secondary end angle
         caseStruct.secondaryArc.clr = clrNONE; //--- Set secondary color
      } else {                             //--- Handle <= PI
         caseStruct.mainArc.centerX = referenceArc.centerX; //--- Set main center X
         caseStruct.mainArc.centerY = referenceArc.centerY; //--- Set main center Y
         caseStruct.mainArc.radius = referenceArc.radius + gap; //--- Set main radius
         caseStruct.mainArc.startAngle = NormalizeRadians(referenceArc.startAngle); //--- Set main start angle
         caseStruct.mainArc.endAngle = NormalizeRadians(referenceArc.endAngle); //--- Set main end angle
         caseStruct.mainArc.clr = clrNONE; //--- Set main color
      }
      caseStruct.leftRoundingArc.radius = gap; //--- Set left rounding radius
      caseStruct.leftRoundingArc.centerX = (int)(referenceArc.centerX - referenceArc.radius * MathCos(M_PI - referenceArc.endAngle)); //--- Set left rounding center X
      caseStruct.leftRoundingArc.centerY = (int)(referenceArc.centerY - referenceArc.radius * MathSin(M_PI - referenceArc.endAngle)); //--- Set left rounding center Y
      if(caseStruct.mode == 1)             //--- Check mode 1
         fi1 = referenceArc.endAngle + (2 * M_PI - sa) / 2; //--- Set fi1
      else                                 //--- Handle mode 0
         fi1 = referenceArc.endAngle + M_PI * 0.5; //--- Set fi1
      caseStruct.leftRoundingArc.startAngle = referenceArc.endAngle; //--- Set left start angle
      caseStruct.leftRoundingArc.endAngle = NormalizeRadians(fi1); //--- Set left end angle
      caseStruct.leftRoundingArc.clr = clrNONE; //--- Set left color
      caseStruct.rightRoundingArc.radius = gap; //--- Set right rounding radius
      caseStruct.rightRoundingArc.centerX = (int)(referenceArc.centerX - referenceArc.radius * MathCos(M_PI - referenceArc.startAngle)); //--- Set right rounding center X
      caseStruct.rightRoundingArc.centerY = (int)(referenceArc.centerY - referenceArc.radius * MathSin(M_PI - referenceArc.startAngle)); //--- Set right rounding center Y
      if(caseStruct.mode == 1)             //--- Check mode 1
         fi0 = referenceArc.startAngle - (2 * M_PI - sa) / 2; //--- Set fi0
      else                                 //--- Handle mode 0
         fi0 = referenceArc.startAngle - M_PI * 0.5; //--- Set fi0
      caseStruct.rightRoundingArc.startAngle = NormalizeRadians(fi0); //--- Set right start angle
      caseStruct.rightRoundingArc.endAngle = referenceArc.startAngle; //--- Set right end angle
      caseStruct.rightRoundingArc.clr = clrNONE; //--- Set right color
      caseStruct.centerArc.centerX = referenceArc.centerX; //--- Set center arc center X
      caseStruct.centerArc.centerY = referenceArc.centerY; //--- Set center arc center Y
      caseStruct.centerArc.radius = needleLayer.needleCenter.radius + gap; //--- Set center arc radius
      fi2 = MathArcsin(((double)caseStruct.leftRoundingArc.radius / (double)caseStruct.centerArc.radius)); //--- Calculate fi2
      fi1 = NormalizeRadians(referenceArc.endAngle + fi2); //--- Set fi1
      caseStruct.centerArc.startAngle = fi1; //--- Set center start angle
      fi1 = NormalizeRadians(referenceArc.startAngle - fi2); //--- Set fi1
      caseStruct.centerArc.endAngle = fi1; //--- Set center end angle
      caseStruct.centerArc.clr = clrNONE;  //--- Set center color
      if(caseStruct.mode == 1) {           //--- Check mode 1
         double angleOffset = M_PI - (sa / 2); //--- Calculate offset
         caseStruct.leftConnectLine.startX = (int)(caseStruct.leftRoundingArc.centerX + caseStruct.leftRoundingArc.radius * MathSin((caseStruct.secondaryArc.endAngle + angleOffset) - M_PI * 1.5)); //--- Set left start X
         caseStruct.leftConnectLine.startY = (int)(caseStruct.leftRoundingArc.centerY + caseStruct.leftRoundingArc.radius * MathCos((caseStruct.secondaryArc.endAngle + angleOffset) - M_PI * 1.5)); //--- Set left start Y
         caseStruct.leftConnectLine.endX = (int)(caseStruct.rightRoundingArc.centerX + caseStruct.rightRoundingArc.radius * MathSin((caseStruct.mainArc.startAngle - angleOffset) - M_PI * 1.5)); //--- Set left end X
         caseStruct.leftConnectLine.endY = (int)(caseStruct.rightRoundingArc.centerY + caseStruct.rightRoundingArc.radius * MathCos((caseStruct.mainArc.startAngle - angleOffset) - M_PI * 1.5)); //--- Set left end Y
         caseStruct.leftConnectLine.clr = clrNONE; //--- Set left color
      } else {                             //--- Handle mode 0
         caseStruct.leftConnectLine.startX = (int)(caseStruct.leftRoundingArc.centerX + caseStruct.leftRoundingArc.radius * MathSin(caseStruct.leftRoundingArc.startAngle - M_PI)); //--- Set left start X
         caseStruct.leftConnectLine.startY = (int)(caseStruct.leftRoundingArc.centerY + caseStruct.leftRoundingArc.radius * MathCos(caseStruct.leftRoundingArc.startAngle - M_PI)); //--- Set left start Y
         fi2 = MathArcsin(((double)caseStruct.leftRoundingArc.radius / (double)caseStruct.centerArc.radius)); //--- Calculate fi2
         fi1 = NormalizeRadians(caseStruct.mainArc.endAngle + fi2); //--- Set fi1
         caseStruct.leftConnectLine.endX = (int)(referenceArc.centerX - caseStruct.centerArc.radius * MathCos(M_PI - fi1)); //--- Set left end X
         caseStruct.leftConnectLine.endY = (int)(referenceArc.centerY - caseStruct.centerArc.radius * MathSin(M_PI - fi1)); //--- Set left end Y
         caseStruct.leftConnectLine.clr = clrNONE; //--- Set left color
         caseStruct.rightConnectLine.startX = (int)(caseStruct.rightRoundingArc.centerX + caseStruct.rightRoundingArc.radius * MathSin(caseStruct.rightRoundingArc.endAngle)); //--- Set right start X
         caseStruct.rightConnectLine.startY = (int)(caseStruct.rightRoundingArc.centerY + caseStruct.rightRoundingArc.radius * MathCos(caseStruct.rightRoundingArc.endAngle)); //--- Set right start Y
         fi1 = NormalizeRadians(caseStruct.mainArc.startAngle - fi2); //--- Set fi1
         caseStruct.rightConnectLine.endX = (int)(referenceArc.centerX - caseStruct.centerArc.radius * MathCos(M_PI - fi1)); //--- Set right end X
         caseStruct.rightConnectLine.endY = (int)(referenceArc.centerY - caseStruct.centerArc.radius * MathSin(M_PI - fi1)); //--- Set right end Y
         caseStruct.rightConnectLine.clr = clrNONE; //--- Set right color
      }
      fi1 = M_PI - NormalizeRadians(referenceArc.endAngle - (sa / 2)); //--- Set fi1
      caseStruct.fillDot.x = (int)(referenceArc.centerX - caseStruct.centerArc.radius * MathCos(fi1)); //--- Set fill dot X
      caseStruct.fillDot.y = (int)(referenceArc.centerY - caseStruct.centerArc.radius * MathSin(fi1)); //--- Set fill dot Y
      caseStruct.fillDot.clr = clrNONE;    //--- Set fill dot color
   }
   void RedrawSectorCase(Struct_Case &caseStruct) //--- Declare redraw sector case method
   {
      int polygonX[5];                     //--- Declare polygon X
      int polygonY[5];                     //--- Declare polygon Y
      scaleLayer.obj_Canvas.Pie(caseStruct.mainArc.centerX, caseStruct.mainArc.centerY, caseStruct.mainArc.radius, caseStruct.mainArc.radius, caseStruct.mainArc.startAngle, caseStruct.mainArc.endAngle, ColorToARGB(caseStruct.mainArc.clr, scaleLayer.transparency), ColorToARGB(caseStruct.mainArc.clr, scaleLayer.transparency)); //--- Draw main pie
      if(caseStruct.secondaryArc.display)  //--- Check secondary display
         scaleLayer.obj_Canvas.Pie(caseStruct.secondaryArc.centerX, caseStruct.secondaryArc.centerY, caseStruct.secondaryArc.radius, caseStruct.secondaryArc.radius, caseStruct.secondaryArc.startAngle, caseStruct.secondaryArc.endAngle, ColorToARGB(caseStruct.secondaryArc.clr, scaleLayer.transparency), ColorToARGB(caseStruct.secondaryArc.clr, scaleLayer.transparency)); //--- Draw secondary pie
      scaleLayer.obj_Canvas.FillCircle(caseStruct.leftRoundingArc.centerX, caseStruct.leftRoundingArc.centerY, caseStruct.leftRoundingArc.radius, ColorToARGB(caseStruct.leftRoundingArc.clr, scaleLayer.transparency)); //--- Fill left rounding
      scaleLayer.obj_Canvas.FillCircle(caseStruct.rightRoundingArc.centerX, caseStruct.rightRoundingArc.centerY, caseStruct.rightRoundingArc.radius, ColorToARGB(caseStruct.rightRoundingArc.clr, scaleLayer.transparency)); //--- Fill right rounding
      if(caseStruct.mode == 0)             //--- Check mode 0
         scaleLayer.obj_Canvas.FillCircle(caseStruct.centerArc.centerX, caseStruct.centerArc.centerY, caseStruct.centerArc.radius, ColorToARGB(caseStruct.centerArc.clr, scaleLayer.transparency)); //--- Fill center arc
      if(caseStruct.mode == 0) {           //--- Check mode 0
         caseStruct.secondaryArc.display = false; //--- Set secondary display false
         polygonX[1] = caseStruct.leftConnectLine.startX; //--- Set polygonX1
         polygonX[0] = caseStruct.leftConnectLine.endX; //--- Set polygonX0
         polygonX[2] = caseStruct.leftRoundingArc.centerX; //--- Set polygonX2
         polygonX[4] = caseStruct.mainArc.centerX; //--- Set polygonX4
         polygonX[3] = caseStruct.fillDot.x; //--- Set polygonX3
         polygonY[1] = caseStruct.leftConnectLine.startY; //--- Set polygonY1
         polygonY[0] = caseStruct.leftConnectLine.endY; //--- Set polygonY0
         polygonY[2] = caseStruct.leftRoundingArc.centerY; //--- Set polygonY2
         polygonY[4] = caseStruct.mainArc.centerY; //--- Set polygonY4
         polygonY[3] = caseStruct.fillDot.y; //--- Set polygonY3
         scaleLayer.obj_Canvas.FillPolygon(polygonX, polygonY, ColorToARGB(caseStruct.leftConnectLine.clr, scaleLayer.transparency)); //--- Fill left polygon
         polygonX[3] = caseStruct.rightConnectLine.startX; //--- Set polygonX3
         polygonX[4] = caseStruct.rightConnectLine.endX; //--- Set polygonX4
         polygonX[2] = caseStruct.rightRoundingArc.centerX; //--- Set polygonX2
         polygonX[0] = caseStruct.mainArc.centerX; //--- Set polygonX0
         polygonX[1] = caseStruct.fillDot.x; //--- Set polygonX1
         polygonY[3] = caseStruct.rightConnectLine.startY; //--- Set polygonY3
         polygonY[4] = caseStruct.rightConnectLine.endY; //--- Set polygonY4
         polygonY[2] = caseStruct.rightRoundingArc.centerY; //--- Set polygonY2
         polygonY[0] = caseStruct.mainArc.centerY; //--- Set polygonY0
         polygonY[1] = caseStruct.fillDot.y; //--- Set polygonY1
         scaleLayer.obj_Canvas.FillPolygon(polygonX, polygonY, ColorToARGB(caseStruct.rightConnectLine.clr, scaleLayer.transparency)); //--- Fill right polygon
      } else {                             //--- Handle mode 1
         polygonX[0] = caseStruct.leftConnectLine.endX; //--- Set polygonX0
         polygonX[1] = caseStruct.leftConnectLine.startX; //--- Set polygonX1
         polygonX[2] = caseStruct.leftRoundingArc.centerX; //--- Set polygonX2
         polygonX[3] = caseStruct.fillDot.x; //--- Set polygonX3
         polygonX[4] = caseStruct.rightRoundingArc.centerX; //--- Set polygonX4
         polygonY[0] = caseStruct.leftConnectLine.endY; //--- Set polygonY0
         polygonY[1] = caseStruct.leftConnectLine.startY; //--- Set polygonY1
         polygonY[2] = caseStruct.leftRoundingArc.centerY; //--- Set polygonY2
         polygonY[3] = caseStruct.fillDot.y; //--- Set polygonY3
         polygonY[4] = caseStruct.rightRoundingArc.centerY; //--- Set polygonY4
         scaleLayer.obj_Canvas.FillPolygon(polygonX, polygonY, ColorToARGB(caseStruct.leftConnectLine.clr, scaleLayer.transparency)); //--- Fill polygon
      }
   }
protected:
   void CalculateCaseElements(Struct_Case &externalCase, Struct_Case &internalCase, int borderSize, int borderGap) override //--- Override calculate case elements
   {
      int totalGap = scaleLayer.externalScaleGap + scaleLayer.externalLabelArea + scaleLayer.borderGap; //--- Calculate total gap
      if(borderSize > 0) {                 //--- Check border size
         CaseCalculateSector(externalCase, totalGap + borderSize); //--- Calculate external sector
         externalCase.mainArc.clr = inputParams.borderColor; //--- Set main color
         externalCase.secondaryArc.clr = inputParams.borderColor; //--- Set secondary color
         externalCase.centerArc.clr = inputParams.borderColor; //--- Set center color
         externalCase.leftRoundingArc.clr = inputParams.borderColor; //--- Set left rounding color
         externalCase.rightRoundingArc.clr = inputParams.borderColor; //--- Set right rounding color
         externalCase.leftConnectLine.clr = inputParams.borderColor; //--- Set left connect color
         externalCase.rightConnectLine.clr = inputParams.borderColor; //--- Set right connect color
         externalCase.fillDot.clr = inputParams.borderColor; //--- Set fill dot color
         externalCase.display = true;      //--- Set display flag
      } else                               //--- Handle no border
         externalCase.display = false;     //--- Set display flag false
      CaseCalculateSector(internalCase, totalGap); //--- Calculate internal sector
      internalCase.mainArc.clr = inputParams.caseColor; //--- Set main color
      internalCase.secondaryArc.clr = inputParams.caseColor; //--- Set secondary color
      internalCase.centerArc.clr = inputParams.caseColor; //--- Set center color
      internalCase.leftRoundingArc.clr = inputParams.caseColor; //--- Set left rounding color
      internalCase.rightRoundingArc.clr = inputParams.caseColor; //--- Set right rounding color
      internalCase.leftConnectLine.clr = inputParams.caseColor; //--- Set left connect color
      internalCase.rightConnectLine.clr = inputParams.caseColor; //--- Set right connect color
      internalCase.fillDot.clr = inputParams.caseColor; //--- Set fill dot color
      internalCase.display = true;         //--- Set display flag
   }
   void DrawCaseElements(Struct_Case &externalCase, Struct_Case &internalCase) override //--- Override draw case elements
   {
      if(externalCase.display)             //--- Check external display
         RedrawSectorCase(externalCase);   //--- Redraw external case
      if(internalCase.display)             //--- Check internal display
         RedrawSectorCase(internalCase);   //--- Redraw internal case
   }
};

Здесь мы определяем класс "CGaugeBase" с закрытыми членами для координат, текущего значения и флага завершения инициализации, а также с объявленными методами для отрисовки, расчета стрелки, легенд, отметок шкалы, диапазонов, делений и подписей, как и раньше. При этом "CalculateCaseElements" и "DrawCaseElements" оставлены чисто виртуальными, чтобы требовать реализации в подклассах, а остальные сеттеры и методы обновления получают базовое поведение в public- и protected-разделах. Далее создается класс "CRoundGauge", наследующий от "CGaugeBase", который реализует круглую шкалу: "CalculateCaseElements" настраивает внешнюю и внутреннюю окружности корпуса по размеру рамки, радиусу и цветам, а "DrawCaseElements" заполняет эти окружности через "FillCircle", если они должны отображаться.

Для класса "CSectorGauge", который также наследуется от "CGaugeBase", добавляется закрытый метод "CaseCalculateSector" для расчета секторных элементов: он определяет режим по тому, превышает ли диапазон угла число pi, задает основную и вторичную дуги для больших углов, вычисляет левую и правую скругляющие дуги с использованием тригонометрических функций, таких как MathArcsin и "NormalizeRadians", задает центральную дугу со смещением, рассчитывает соединительные линии через поправки по синусу и косинусу, зависящие от режима, а также размещает точку заливки в середине угла. Мы переопределяем "CalculateCaseElements", чтобы задавать параметры внешнего и внутреннего корпуса через "CaseCalculateSector", используя соответствующие зазоры. Метод "RedrawSectorCase" отвечает за фактическую отрисовку секторного корпуса.

Наконец, мы переопределяем "DrawCaseElements", чтобы условно вызывать "RedrawSectorCase" для внешнего и внутреннего корпуса, если они должны отображаться. Внутри "RedrawSectorCase" основные и вторичные секторы отрисовываются через "Pie", скругляющие и центральные окружности заполняются с помощью "FillCircle", а соединения заполняются полигонами через массивы координат и "FillPolygon" в ARGB-цветах. При этом массивы адаптируются под режим 0 (отдельные левый и правый полигоны) или режим 1 (один общий полигон).

В целом чисто виртуальные методы обеспечивают полиморфизм: RSI использует круглую шкалу, а CCI/MFI — секторные шкалы для разнообразия и лучшего соответствия форме (например, симметричной шкале CCI относительно нуля). Относительное позиционирование в "Create" позволит автоматически выравнивать шкалы рядом друг с другом, а переработанный "RedrawScaleMarks" поддержит индикаторы с отрицательными диапазонами (CCI) или инвертированными шкалами (MFI: от 100 к 0), обеспечивая точное размещение делений и подписей. Функция "calc_digits" будет динамически определять количество десятичных знаков для аккуратного отображения значений.

//+------------------------------------------------------------------+
//| Calculate Digits                                                 |
//+------------------------------------------------------------------+
int calc_digits(double value) {
   int i, j, max_nulls = 0, nulls = 0;     //--- Declare variables
   if(value == 0) return(0);               //--- Return 0 if zero
   ulong v = ulong(MathAbs(value) * 100000000); //--- Calculate v
   ulong vtmp;                             //--- Declare vtmp
   for(j = -5; j <= 5; j++) {              //--- Loop j
      nulls = 0;                           //--- Reset nulls
      vtmp = v + (ulong)j;                 //--- Set vtmp
      for(i = 0; i < 8; i++) {             //--- Loop i
         if(vtmp % 10 == 0) {              //--- Check mod 10 == 0
            vtmp = vtmp / 10;              //--- Divide vtmp
            nulls++;                       //--- Increment nulls
         } else break;                     //--- Break else
      }
      if(max_nulls < nulls) max_nulls = nulls; //--- Update max nulls
   }
   return(8 - max_nulls);                  //--- Return digits
}

Функция "calc_digits" определяет количество десятичных знаков, необходимое для отображения значения, с учетом проблем точности чисел с плавающей точкой. Если значение равно нулю, сразу возвращается 0. Затем вычисляется беззнаковое длинное целое "v": берется абсолютное значение, умноженное на 100 000 000, чтобы перенести дробную часть в целое число. Далее в цикле по небольшим смещениям "j" от -5 до 5 к "v" добавляется каждое смещение и получается "vtmp". После этого подсчитываются конечные нули путем деления на 10, пока остаток равен нулю, но не более 8 раз. Максимальное количество конечных нулей сохраняется в "max_nulls", а итоговое число десятичных знаков возвращается как 8 минус "max_nulls". Так функция стабильно определяет точность для подписей шкалы и значений.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CRoundGauge rsiGauge;                      //--- Declare RSI gauge
CSectorGauge cciGauge;                     //--- Declare CCI gauge
CSectorGauge mfiGauge;                     //--- Declare MFI gauge
int rsiHandle = INVALID_HANDLE;            //--- Initialize RSI handle
int cciHandle = INVALID_HANDLE;            //--- Initialize CCI handle
int mfiHandle = INVALID_HANDLE;            //--- Initialize MFI handle
double scaleMultipliers[9] = {10000, 1000, 100, 10, 1, 0.1, 0.01, 0.001, 0.0001}; //--- Define scale multipliers array
string scaleMultiplierStrings[9] = {"x10k", "x1k", "x100", "x10", " ", "/10", "/100", "/1k", "/10k"}; //--- Define multiplier strings array
double rsiBuffer[], cciBuffer[], mfiBuffer[]; //--- Declare buffers

Здесь мы объявляем глобальные экземпляры классов шкал: "rsiGauge" как "CRoundGauge" для визуализации Relative Strength Index в круговом стиле, а "cciGauge" и "mfiGauge" как "CSectorGauge" для Commodity Channel Index и Money Flow Index в секторном дизайне. Целочисленные хендлы "rsiHandle", "cciHandle" и "mfiHandle" инициализируются значением INVALID_HANDLE, чтобы позднее ссылаться на соответствующие технические индикаторы. Массив "scaleMultipliers" типа double содержит 9 масштабных множителей от 10000 до 0.0001, а строковый массив "scaleMultiplierStrings" содержит соответствующие подписи: "x10k", "x1k", "x100", "x10", пробел для масштаба 1, а также "/10", "/100", "/1k" и "/10k". Эти массивы помогают отображать множители шкалы в легендах.

//+------------------------------------------------------------------+
//| Initialize Indicator                                             |
//+------------------------------------------------------------------+
int OnInit() {
   bool showRSI = (inpGaugeSelection == RSI_ONLY || inpGaugeSelection == RSI_CCI || inpGaugeSelection == RSI_MFI || inpGaugeSelection == ALL); //--- Set show RSI
   bool showCCI = (inpGaugeSelection == CCI_ONLY || inpGaugeSelection == RSI_CCI || inpGaugeSelection == CCI_MFI || inpGaugeSelection == ALL); //--- Set show CCI
   bool showMFI = (inpGaugeSelection == MFI_ONLY || inpGaugeSelection == RSI_MFI || inpGaugeSelection == CCI_MFI || inpGaugeSelection == ALL); //--- Set show MFI
   string prevName = "";                   //--- Initialize prev name
   int baseX = 30;                         //--- Set base X
   int baseY = 30;                         //--- Set base Y
   IndicatorSetInteger(INDICATOR_LEVELS, 5); //--- Set levels
   SetIndexBuffer(0, rsiBuffer, INDICATOR_DATA); //--- Set RSI buffer
   SetIndexBuffer(1, cciBuffer, INDICATOR_DATA); //--- Set CCI buffer
   SetIndexBuffer(2, mfiBuffer, INDICATOR_DATA); //--- Set MFI buffer
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE); //--- Set RSI empty
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE); //--- Set CCI empty
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE); //--- Set MFI empty
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, 14 - 1); //--- Set RSI draw begin
   PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, 14 - 1); //--- Set CCI draw begin
   PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, 14 - 1); //--- Set MFI draw begin
   if(showRSI) {                           //--- Check show RSI
      if(!rsiGauge.Create("rsi_gauge", baseX, baseY, 230, "", 0, 0, false, 0, 0)) return(INIT_FAILED); //--- Create RSI gauge
      rsiGauge.SetCaseParameters(clrMintCream, 1, clrLightSkyBlue, 3); //--- Set RSI case
      rsiGauge.SetScaleParameters(250, 0, 0, 100, 4, 0, clrBlack, false); //--- Set RSI scale
      rsiGauge.SetTickParameters(0, 2, 10, 1, 4); //--- Set RSI ticks
      rsiGauge.SetTickLabelFont(1, "Arial", false, false, clrBlack); //--- Set RSI font
      rsiGauge.SetRangeParameters(0, true, 0, 30, clrLimeGreen); //--- Set RSI range 0
      rsiGauge.SetRangeParameters(1, true, 70, 100, clrCoral); //--- Set RSI range 1
      rsiGauge.SetRangeParameters(2, true, 30, 70, clrYellow); //--- Set RSI range 2
      rsiGauge.SetRangeParameters(3, false, 0, 0, clrGray); //--- Set RSI range 3
      rsiGauge.SetLegendParameters(0, true, "RSI", 8, -180, 20, "Arial", false, false, clrBlueViolet); //--- Set RSI legend 0
      rsiGauge.SetLegendParameters(3, true, "2", 4, 180, 13, "Arial", true, false, clrRed); //--- Set RSI legend 3
      rsiGauge.SetNeedleParameters(1, clrBlack, clrDimGray, 1, 2.0); //--- Set RSI needle
      rsiGauge.Redraw();                   //--- Redraw RSI
      rsiGauge.NewValue(0);                //--- Set RSI value 0
      prevName = "rsi_gauge";              //--- Set prev name
      rsiHandle = iRSI(_Symbol, _Period, 14, PRICE_CLOSE); //--- Get RSI handle
      if(rsiHandle == INVALID_HANDLE) return(INIT_FAILED); //--- Check RSI handle
   }
   if(showCCI) {                           //--- Check show CCI
      string relName = prevName;           //--- Set rel name
      int relMode = (prevName != "") ? 1 : 0; //--- Set rel mode
      int posX = (prevName != "") ? 0 : baseX; //--- Set pos X
      int posY = baseY + 90;               //--- Set pos Y
      if(!cciGauge.Create("cci_gauge", posX, posY, 230, relName, relMode, 0, false, 0, 0)) return(INIT_FAILED); //--- Create CCI gauge
      cciGauge.SetCaseParameters(clrMintCream, 1, clrLightSkyBlue, 1); //--- Set CCI case
      cciGauge.SetScaleParameters(200, 0, -200, 200, 4, 0, clrBlack, false); //--- Set CCI scale
      cciGauge.SetTickParameters(0, 2, 100, 1, 4); //--- Set CCI ticks
      cciGauge.SetTickLabelFont(1, "Arial", false, false, clrBlack); //--- Set CCI font
      cciGauge.SetRangeParameters(0, true, -200, -100, clrCoral); //--- Set CCI range 0
      cciGauge.SetRangeParameters(1, true, -100, 100, clrDodgerBlue); //--- Set CCI range 1
      cciGauge.SetRangeParameters(2, true, 100, 200, clrLimeGreen); //--- Set CCI range 2
      cciGauge.SetRangeParameters(3, false, 0, 0, clrGray); //--- Set CCI range 3
      cciGauge.SetLegendParameters(0, true, "CCI", 4, 0, 16, "Arial", false, false, clrBlueViolet); //--- Set CCI legend 0
      cciGauge.SetLegendParameters(3, true, "1", 1, 0, 12, "Arial", true, false, clrRed); //--- Set CCI legend 3
      cciGauge.SetNeedleParameters(1, clrBlack, clrDimGray, 1, 1.5); //--- Set CCI needle
      cciGauge.Redraw();                   //--- Redraw CCI
      cciGauge.NewValue(0);                //--- Set CCI value 0
      prevName = "cci_gauge";              //--- Set prev name
      cciHandle = iCCI(_Symbol, _Period, 14, PRICE_TYPICAL); //--- Get CCI handle
      if(cciHandle == INVALID_HANDLE) return(INIT_FAILED); //--- Check CCI handle
   }
   if(showMFI) {                           //--- Check show MFI
      string relName = prevName;           //--- Set rel name
      int relMode = (prevName != "") ? 1 : 0; //--- Set rel mode
      int posX = (prevName != "") ? 0 : baseX; //--- Set pos X
      int posY = baseY + 90;               //--- Set pos Y
      if(!mfiGauge.Create("mfi_gauge", posX, posY, 250, relName, relMode, 0, false, 0, 0)) return(INIT_FAILED); //--- Create MFI gauge
      mfiGauge.SetCaseParameters(clrMintCream, 1, clrLightSkyBlue, 3); //--- Set MFI case
      mfiGauge.SetScaleParameters(120, -35, 100, 0, 3, 0, clrBlack, false); //--- Set MFI scale
      mfiGauge.SetTickParameters(0, 2, 10, 1, 4); //--- Set MFI ticks
      mfiGauge.SetTickLabelFont(0, "Arial", false, false, clrBlack); //--- Set MFI font
      mfiGauge.SetRangeParameters(0, true, 80, 100, clrRed); //--- Set MFI range 0
      mfiGauge.SetRangeParameters(1, true, 20, 80, clrMagenta); //--- Set MFI range 1
      mfiGauge.SetRangeParameters(2, true, 0, 20, clrGreen); //--- Set MFI range 2
      mfiGauge.SetRangeParameters(3, false, 0, 0, clrGray); //--- Set MFI range 3
      mfiGauge.SetLegendParameters(0, true, "MFI", 4, -15, 14, "Arial", false, false, clrBlueViolet); //--- Set MFI legend 0
      mfiGauge.SetLegendParameters(2, true, "", 4, -50, 10, "Arial", false, false, clrDimGray); //--- Set MFI legend 2
      mfiGauge.SetLegendParameters(3, true, "0", 3, -80, 16, "Arial", true, false, clrRed); //--- Set MFI legend 3
      mfiGauge.SetNeedleParameters(1, clrBlack, clrDimGray, 1, 1.2); //--- Set MFI needle
      mfiGauge.Redraw();                   //--- Redraw MFI
      mfiGauge.NewValue(0);                //--- Set MFI value 0
      mfiHandle = iMFI(_Symbol, _Period, 14, VOLUME_TICK); //--- Get MFI handle
      if(mfiHandle == INVALID_HANDLE) return(INIT_FAILED); //--- Check MFI handle
   }
   return(INIT_SUCCEEDED);                 //--- Return succeeded
}

В обработчике события OnInit мы сначала определяем флаги видимости каждой шкалы на основе входного параметра "inpGaugeSelection": "showRSI" равен true для вариантов, включающих Relative Strength Index, аналогично "showCCI" и "showMFI" условно отображают соответствующие индикаторы. Мы инициализируем пустую строку "prevName" для относительного позиционирования, задаем базовые координаты "baseX" и "baseY" равными 30, настраиваем пять уровней индикатора через "IndicatorSetInteger" и привязываем буферы с помощью "SetIndexBuffer": индекс 0 для "rsiBuffer", 1 для "cciBuffer" и 2 для "mfiBuffer" как данные индикатора. Затем для всех трех графических построений задаются пустые значения EMPTY_VALUE через PlotIndexSetDouble, а начало отрисовки устанавливается на баре 13 с помощью PlotIndexSetInteger, чтобы согласовать его с расчетами за 14 периодов. При другом подходе ваша логика может измениться.

Если "showRSI" равен true, мы создаем экземпляр "rsiGauge" методом "Create" в базовой позиции с размером 230 без относительной привязки, а затем настраиваем его: "SetCaseParameters" задает корпус цвета mint cream, стиль рамки 1, рамку light sky blue и зазор 3; "SetScaleParameters" задает диапазон 250 градусов, значения от 0 до 100, множитель 4, черный цвет и отсутствие дуги; "SetTickParameters" — стиль 0, размер 2, основной интервал 10, 1 среднее деление и 4 малых; "SetTickLabelFont" — размер 1, Arial, без курсива и полужирного начертания, черный цвет; диапазоны через "SetRangeParameters" — 0–30 lime green, 70–100 coral, 30–70 yellow и отключенный серый; легенды через "SetLegendParameters" — описание "RSI" на радиусе 8, угол -180, размер 20, Arial, без курсива и полужирного начертания, blue violet, а значение "2" на радиусе 4, угол 180, размер 13, Arial, курсив true, жирный false, red. Затем "SetNeedleParameters" задает центр 1, black, dim gray, заливку 1 и хвост 2.0; вызываются "Redraw" и "NewValue" со значением 0; "prevName" обновляется до "rsi_gauge"; "rsiHandle" инициализируется через iRSI для текущего символа, периода 14 и цен закрытия, а при недействительном хендле возвращается INIT_FAILED.

Если "showCCI" равен true, имя относительной привязки берется из "prevName", а режим устанавливается в 1, если оно не пустое; позиция по X равна 0 или базовой, по Y — base+90. "cciGauge" создается относительно предыдущей шкалы; корпус настраивается так же, как у RSI, но с зазором 1; шкала задается на 200 градусов от -200 до 200; деления и шрифт используются те же; диапазоны: -200–-100 coral, -100–100 dodger blue, 100–200 lime green, четвертый отключен; легенды: "CCI" с радиусом 4, углом 0, размером 16, Arial, без курсива и полужирного начертания, blue violet, а значение "1" с радиусом 1, углом 0, размером 12, Arial, курсив true, жирный false, red; хвост стрелки — 1.5; выполняются перерисовка и инициализация значением 0; обновляется "prevName"; хендл "cciHandle" создается через iCCI с периодом 14 и typical price, после чего проверяется на недействительность.

Для "showMFI" при значении true используется похожая относительная настройка, позиция по Y равна base+90; создается "mfiGauge" размером 250; корпус имеет зазор 3; шкала — 120 градусов, от 100 к 0 (обратное направление), множитель 3; деления те же; размер шрифта 0; диапазоны: 80–100 red, 20–80 magenta, 0–20 green, четвертый отключен; легенды: "MFI" с радиусом 4, углом -15, размером 14, Arial, без курсива и полужирного начертания, blue violet; множитель — пустой текст с радиусом 4, углом -50, размером 10, dim gray; значение "0" с радиусом 3, углом -80, размером 16, Arial, курсив true, жирный false, red; хвост стрелки — 1.2; выполняется перерисовка и инициализация значением 0; хендл "mfiHandle" создается через iMFI с периодом 14 и тиковым объемом, затем проверяется на недействительность.

Мы возвращаем INIT_SUCCEEDED, чтобы завершить инициализацию. После инициализации получаем следующий результат.

ИНИЦИАЛИЗАЦИЯ ШКАЛ

Видно, что буферы пока пустые и индикаторы не построены, но шкалы отображаются корректно. Теперь нужно выполнить расчеты в обработчике события расчета для выбранных индикаторов — условно, в зависимости от настроек.

//+------------------------------------------------------------------+
//| Calculate Indicator                                              |
//+------------------------------------------------------------------+
int OnCalculate(const int ratesTotal,
                const int prevCalculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tickVolume[],
                const long &volume[],
                const int &spread[]) {
   bool showRSI = (inpGaugeSelection == RSI_ONLY || inpGaugeSelection == RSI_CCI || inpGaugeSelection == RSI_MFI || inpGaugeSelection == ALL); //--- Set show RSI
   bool showCCI = (inpGaugeSelection == CCI_ONLY || inpGaugeSelection == RSI_CCI || inpGaugeSelection == CCI_MFI || inpGaugeSelection == ALL); //--- Set show CCI
   bool showMFI = (inpGaugeSelection == MFI_ONLY || inpGaugeSelection == RSI_MFI || inpGaugeSelection == CCI_MFI || inpGaugeSelection == ALL); //--- Set show MFI
   static datetime lastBarTime = 0;        //--- Declare last bar time
   bool isNewBar = (ratesTotal > 0 && time[ratesTotal - 1] != lastBarTime); //--- Check new bar
   if(isNewBar) lastBarTime = time[ratesTotal - 1]; //--- Update last bar time
   if(showRSI) {                           //--- Check show RSI
      if(rsiHandle != INVALID_HANDLE && CopyBuffer(rsiHandle, 0, 0, ratesTotal, rsiBuffer) < 0) return(0); //--- Copy RSI buffer
   } else {                                //--- Handle no show
      ArrayFill(rsiBuffer, 0, ratesTotal, EMPTY_VALUE); //--- Fill RSI empty
   }
   if(showCCI) {                           //--- Check show CCI
      if(cciHandle != INVALID_HANDLE && CopyBuffer(cciHandle, 0, 0, ratesTotal, cciBuffer) < 0) return(0); //--- Copy CCI buffer
   } else {                                //--- Handle no show
      ArrayFill(cciBuffer, 0, ratesTotal, EMPTY_VALUE); //--- Fill CCI empty
   }
   if(showMFI) {                           //--- Check show MFI
      if(mfiHandle != INVALID_HANDLE && CopyBuffer(mfiHandle, 0, 0, ratesTotal, mfiBuffer) < 0) return(0); //--- Copy MFI buffer
   } else {                                //--- Handle no show
      ArrayFill(mfiBuffer, 0, ratesTotal, EMPTY_VALUE); //--- Fill MFI empty
   }
   if(isNewBar) {                          //--- Check new bar
      if(showRSI && rsiHandle != INVALID_HANDLE) { //--- Check RSI
         double val[1];                    //--- Declare val
         if(CopyBuffer(rsiHandle, 0, 0, 1, val) > 0) rsiGauge.NewValue(val[0]); //--- Set RSI value
      }
      if(showCCI && cciHandle != INVALID_HANDLE) { //--- Check CCI
         double val[1];                    //--- Declare val
         if(CopyBuffer(cciHandle, 0, 0, 1, val) > 0) cciGauge.NewValue(val[0]); //--- Set CCI value
      }
      if(showMFI && mfiHandle != INVALID_HANDLE) { //--- Check MFI
         double val[1];                    //--- Declare val
         if(CopyBuffer(mfiHandle, 0, 0, 1, val) > 0) mfiGauge.NewValue(val[0]); //--- Set MFI value
      }
   }
   return(ratesTotal);                     //--- Return rates total
}

В обработчике события OnCalculate мы заново определяем флаги видимости "showRSI", "showCCI" и "showMFI" на основе входного параметра "inpGaugeSelection", чтобы понять, какие индикаторы и шкалы нужно обрабатывать в текущем цикле расчета. Для обнаружения новых баров используется статическая переменная "datetime" "lastBarTime": "isNewBar" становится true, если последняя временная метка изменилась, после чего "lastBarTime" обновляется. Если "showRSI" равен true и хендл действителен, вся история из буфера Relative Strength Index копируется в "rsiBuffer" с помощью CopyBuffer; при ошибке возвращается 0. Иначе "rsiBuffer" заполняется значением EMPTY_VALUE через ArrayFill, чтобы скрыть графическое построение.

Аналогично для Commodity Channel Index данные копируются в "cciBuffer" или буфер заполняется пустыми значениями; для Money Flow Index — в "mfiBuffer" или также пустыми значениями. На новом баре, если соответствующая шкала отображается и хендл действителен, последнее одиночное значение копируется во временный массив "val" через "CopyBuffer" со сдвигом 0 и количеством 1. Если копирование успешно, соответствующая шкала обновляется методом "NewValue" с передачей "val[0]". Затем возвращается "ratesTotal", чтобы продолжить обработку. Наконец, не забывайте удалять шкалы, чтобы не засорять график объектами.

//+------------------------------------------------------------------+
//| Deinitialize Indicator                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   rsiGauge.Delete();                      //--- Delete RSI gauge
   cciGauge.Delete();                      //--- Delete CCI gauge
   mfiGauge.Delete();                      //--- Delete MFI gauge
   ChartRedraw();                          //--- Redraw chart
}

В обработчике события OnDeinit мы вызываем метод "Delete" для "rsiGauge", "cciGauge" и "mfiGauge", чтобы удалить с графика объекты слоев шкалы и стрелки. Затем вызывается ChartRedraw для обновления графика, чтобы после деинициализации индикатора не оставалось визуальных следов. После компиляции получаем следующий результат.

ФИНАЛЬНОЕ ОБЪЕДИНЕННОЕ ОТОБРАЖЕНИЕ ШКАЛ

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


Бэктестинг

Мы выполнили тестирование, и ниже показана итоговая визуализация в виде одного растрового изображения формата Graphics Interchange Format (GIF).

ИТОГОВЫЙ GIF БЭКТЕСТА С ПОЛНЫМИ ШКАЛАМИ


Заключение

В заключение отметим, что мы расширили индикатор в формате gauge в MQL5, добавив поддержку нескольких осцилляторов, таких как Relative Strength Index, Commodity Channel Index и Money Flow Index, через выбираемые пользователем комбинации. Мы ввели круглый и секторный стили с помощью производных классов и реализовали расширенную отрисовку корпуса с дугами, полигонами и относительным позиционированием для выровненного отображения. Этот индикатор представляет собой универсальный инструмент для анализа нескольких осцилляторов: в нем настраиваются шкалы, диапазоны, легенды и стрелки, адаптирующиеся к разным областям значений. При желании его можно развить дальше и подключить собственные индикаторы данных.

Удачной торговли!

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

Прикрепленные файлы |
Оптимизация трендовой торговли: Торговля по направлению тренда и с учётом его силы Оптимизация трендовой торговли: Торговля по направлению тренда и с учётом его силы
Это специализированный трендовый советник, который выполняет краткосрочный и долгосрочный анализ, принимает торговые решения и исполняет сделки на основе общего тренда и его силы. В статье подробно рассматривается советник, специально разработанный для терпеливых и дисциплинированных трейдеров, способных последовательно придерживаться выбранного торгового направления, открывать сделки и удерживать позиции только тогда, когда рынок движется уверенно и по тренду, не меняя часто свой рыночный уклон, особенно против тренда, пока не будут достигнуты цели по тейк-профиту.
От начального до среднего уровня: Словно мыльные пузыри От начального до среднего уровня: Словно мыльные пузыри
В этой статье будет объяснен очень простой и легкий для понимания механизм, целью которого является сортировка любого массива. В ней мы увидим, что полученный результат не всегда соответствует ожиданиям, поэтому для достижения нужных показателей потребуется адаптировать саму реализацию.
Моделирование рынка: Position View (VIII) Моделирование рынка: Position View (VIII)
В предыдущей статье мы рассмотрели, как можно реализовать индикатор позиции для закрытия открытой позиции непосредственно с графика, взаимодействуя с доступным на нем объектом. После завершения и проверки работы первого механизма мы приступили к внесению изменений, чтобы обеспечить возможность удаления линий тейк-профита и стоп-лосса для открытой позиции. Однако, поскольку необходимые изменения требовали подробных объяснений, в той же статье я показал только те изменения, которые необходимо было внести в советник, но всё ещё требовалось показать изменения, которые необходимо было внести в индикатор позиции.
Встраивание торговой дисциплины в код (Часть 3): Принудительное ограничение торговли на уровне символов с помощью белого списка в MQL5 Встраивание торговой дисциплины в код (Часть 3): Принудительное ограничение торговли на уровне символов с помощью белого списка в MQL5
В этой статье подробно рассматривается фреймворк на MQL5, который ограничивает торговлю утвержденным набором символов. Это решение сочетает в себе общую библиотеку, дашборд управления конфигурацией и советника-принудителя, проверяющего каждую сделку на соответствие белому списку символов и логирующего заблокированные попытки. Оно включает в себя полностью функциональные примеры кода, четкое объяснение решений по структурному проектированию и тесты проверки, подтверждающие надежную фильтрацию символов, контролируемая рыночная экспозиция и прозрачный мониторинг соблюдения правил.