Создание пользовательских индикаторов в MQL5 (Часть 3): Улучшение многошкальной визуализации с секторным и круговым стилями
Введение
В нашей предыдущей статье (Часть 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), а пользователь может выбирать отдельные и комбинированные варианты отображения. В статье вводятся производные классы для секторного и круглого дизайна шкал, улучшается отрисовка корпуса с помощью дуг, полигонов и относительного позиционирования, что позволяет получить более аккуратное мультииндикаторное отображение. Мы рассмотрим следующие темы:
К концу статьи у вас будет рабочий индикатор 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 ¶m, 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).

Заключение
В заключение отметим, что мы расширили индикатор в формате gauge в MQL5, добавив поддержку нескольких осцилляторов, таких как Relative Strength Index, Commodity Channel Index и Money Flow Index, через выбираемые пользователем комбинации. Мы ввели круглый и секторный стили с помощью производных классов и реализовали расширенную отрисовку корпуса с дугами, полигонами и относительным позиционированием для выровненного отображения. Этот индикатор представляет собой универсальный инструмент для анализа нескольких осцилляторов: в нем настраиваются шкалы, диапазоны, легенды и стрелки, адаптирующиеся к разным областям значений. При желании его можно развить дальше и подключить собственные индикаторы данных.
Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20719
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Оптимизация трендовой торговли: Торговля по направлению тренда и с учётом его силы
От начального до среднего уровня: Словно мыльные пузыри
Моделирование рынка: Position View (VIII)
Встраивание торговой дисциплины в код (Часть 3): Принудительное ограничение торговли на уровне символов с помощью белого списка в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования