English Deutsch
preview
MQL5でカスタムインジケーターを作成する(第3回):扇形と円形によるマルチゲージの強化

MQL5でカスタムインジケーターを作成する(第3回):扇形と円形によるマルチゲージの強化

MetaTrader 5トレーディング |
23 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第2回)では、MetaQuotes Language 5 (MQL5)でRSI(相対力指数)をゲージ形式で表示するインジケーターを作成しました。Canvas描画と針の仕組みを使い、RSIの値を円形ゲージとして可視化しています。針は値に応じて動的に変化し、買われすぎと売られすぎのゾーンは色分けで表現しています。さらに、従来の線グラフも併用することで、モメンタムの把握がしやすい構成にしました。第3回となる今回は、この仕組みをさらに拡張し、マルチゲージに対応させます。RSIに加えて、CCI(商品チャネル指数)やMFI(マネーフローインデックス)といった複数のオシレーターを、ユーザーが自由に組み合わせて表示できるようにします。また、ゲージの見た目も強化します。円形に加えて、扇形を扱えるようにし、それぞれを派生クラスとして実装します。円弧や多角形、相対配置を使った描画により、より柔軟で見栄えの良いマルチインジケーター表示を実現します。本記事では以下のトピックを扱います。

  1. 扇形と円形のマルチゲージフレームワーク
  2. MQL5での実装
  3. バックテスト
  4. 結論

最終的には、複数オシレーターを視覚的に扱えるMQL5インジケーターが完成します。今後のカスタマイズにもそのまま活用できるはずです。それでは進めていきます。


扇形と円形のマルチゲージフレームワーク

マルチゲージフレームワークは、カスタマイズ可能なゲージの基盤クラスを土台として構築します。その上で、円形ゲージおよび扇形ゲージのスタイルを実装した派生クラスを導入し、RSI(相対力指数)、CCI(商品チャネル指数)、MFI(マネーフローインデックス)といったオシレーターを可視化できるようにします。表示方法は列挙型で制御し、単体表示と複合表示を切り替えられるようにすることで、複数インジケーターにわたる柔軟なモメンタム分析を実現します。円形ゲージでは、塗りつぶされた円をベースとした従来の円形のケースを維持します。一方、扇形ゲージは、円弧ベースのセクションや丸みを持たせた円弧、接続線、多角形などを組み合わせることで、部分的なダイヤル形状を表現し、より視覚的にリッチな表示を実現します。また、角度の範囲に応じて柔軟に形状を変えられるようにしつつ、相対配置によって複数のゲージをチャート上に横並びで配置できるようにしています。このアイデアは、ゲージ全体ではなく、一部だけを表示したい場合があることから生まれました。そこで、円形のゲージを半分や4分の1に分割し、用途に応じて選べるようにするべきだと考えました。実装では、これらを条件に応じて使い分けられるよう、3種類の表示をサポートします。

これを実現するために、基盤となるゲージクラスには、ケースの計算および描画をおこなう純粋仮想メソッドを定義し、派生クラス側でスタイルごとの処理をオーバーライドできるようにします。また、ゲージ選択用の列挙型を追加し、RSI(円形ゲージ)、CCI(扇形ゲージ)、MFI(扇形ゲージ)の各インスタンスを条件付きで初期化および配置できるようにします。さらに、それぞれのインジケーターからデータを取得するためのハンドルとバッファも統合します。これらは主に可視化のためのデータ取得に使用しますが、用途はこれに限りません。たとえば、損益の表示、インジケーターの流れ、進捗状況や口座情報のメトリクスなど、任意のデータを可視化する用途にも応用できます。アーキテクチャとしては、スケール(ゼロ位置の目盛り配置を強化)と針(テール長を調整可能)のレイヤーを分離しています。これにより、再描画効率を高めつつ、前のゲージを基準にした相対配置(アンカー)も安定しておこなえるようにしています。以下に想定される表示例を示します。

ゲージアーキテクチャの概要


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つのプロットを設定します。1つ目のプロットは従来どおりRSI用で、タイプはDRAW_LINE、色はdodger blue、スタイルは実線、太さ2、ラベルは「RSI」とします。2つ目はCCI用で、緑色の実線、太さ2、ラベルは「CCI」、3つ目はMFI用で、青色の実線、太さ2、ラベルは「MFI」とします。

さらに、indicator_minimumとindicator_maximumを使って縦軸スケールを0〜100に設定し、indicator_level1〜indicator_level5、indicator_levelcolor、indicator_levelstyleを用いて、30、70、-100、100、0の5つのレベルラインを灰色の点線で追加し、各オシレーターの閾値を参照できるようにします。これらの設定により、RSI、CCI、MFIを別ウィンドウ上で同時にグラフ表示できるようになります。

また、インジケーターの入力ダイアログから、RSI、CCI、MFIのいずれか、あるいはその組み合わせを選択して表示できるようにすることで、柔軟性とリソース効率を高めます。さらに、特にCCIのように負の値を取り、ゼロが中央に来るインジケーターに対応したスケール描画を正しくおこなうために、ゼロ位置(null)をどこに配置するかを制御する列挙型も定義しておきます。

//+------------------------------------------------------------------+
//| 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を含めます。また、ENUM_GAUGE_SELECTION型の入力パラメータinpGaugeSelectionを宣言し、デフォルト値をALLに設定することで、ユーザーがインジケーター設定から直接ゲージ構成を選択できるようにします。続いて、スケール上のゼロ位置(null)を指定するための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
};

今回追加した乗数用の変数によって、針のテールの長さを調整できるようになります(たとえば扇形ゲージでは短くするなど)。これにより、見た目のバランスが取りやすくなり、ゲージ形状ごとのフィット感も向上します。続いて、クラス構成を大きく見直し、ポリモーフィズムと継承を導入することで、設計の整理と将来的な拡張性を高めます。そのために、以下のようにprivateヘルパーを持つ2つの派生クラスを追加します。

//+------------------------------------------------------------------+
//| 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クラスを定義し、位置情報、現在値、初期化フラグといったprivateメンバーを保持します。また、描画処理、針の計算、凡例表示、スケールマーク、レンジ、目盛り、ラベルなどのための各種メソッドをこれまで通り宣言しますが、CalculateCaseElementsとDrawCaseElementsについては純粋仮想関数として定義し、サブクラス側での実装を必須とします。一方で、セッターや更新処理などの共通ロジックについては、publicおよびprotectedセクションでデフォルト実装を提供します。次に、CGaugeBaseを継承したCRoundGaugeクラスを作成し、CalculateCaseElementsをオーバーライドして外側および内側の円を構成します。これには枠線サイズを基準とした外周、内周の設定、スケールの円弧から算出される中心座標、半径の調整、入力カラーや表示フラグの反映が含まれます。またDrawCaseElementsもオーバーライドし、表示対象となっている場合にはFillCircleを使用してこれらの円をスケールCanvas上に描画し、ARGB変換された色で塗りつぶします。

一方、CSectorGaugeクラスもCGaugeBaseを継承し、privateメソッドCaseCalculateSectorを追加します。このメソッドではセクター要素の計算をおこない、角度範囲がπを超えるかどうかでmodeを判定し、大きな角度の場合はメイン円弧と補助円弧を設定します。またMathArcsinやNormalizeRadiansを用いて左右の丸み部分を計算し、中心部分の円弧にはオフセットを適用します。さらにsinとcosを用いて左右の接続ラインをmodeに応じて調整し、中間角度位置を点で塗りつぶします。CSectorGaugeではCalculateCaseElementsをオーバーライドし、外部要素および枠線を考慮した全体のオフセットを計算します。その後、枠線が正の場合は外部ケースとして、内部ケースは適切なオフセットを用いてCaseCalculateSectorを呼び出します。さらに、すべての円弧、線、点のコンポーネントに対して枠線またはケースの色を割り当て、それぞれの表示を有効化します。

さらにDrawCaseElementsをオーバーライドし、外側および内側の表示フラグに応じてRedrawSectorCaseを呼び出します。この処理ではPieによるメインおよびサブセクターの描画、FillCircleによる丸い部分の塗りつぶし、そして接続部分を補完する多角形生成(座標配列を用いたFillPolygon)をおこないます。modeが0の場合は左右に分割された多角形、modeが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の場合は即座に0を返します。その後、絶対値に100,000,000を掛けて小数部分を整数側にシフトし、unsigned long型のvとして扱います。次にjを-5から5まで変化させながら微小なオフセットを加えたvtmpを生成し、それぞれについて末尾のゼロ数をカウントします。vtmpを10で割り続け、余りが0である間は最大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

ここでは、ゲージクラスのグローバルインスタンスを宣言します。RSIの可視化には円形スタイルを使用するCRoundGaugeとしてrsiGaugeを定義し、CCIとMFIについては扇形デザインを用いるCSectorGaugeとしてcciGaugeとmfiGaugeを定義します。また、それぞれのテクニカル指標を参照するためのハンドルとしてrsiHandle、cciHandle、mfiHandleを宣言し、初期値はINVALID_HANDLEに設定します。さらに、ゲージ上の表示スケーリングを調整するために、10000から0.0001までの9段階のスケール係数を持つscaleMultipliers配列を定義します。これに対応するラベルとして、scaleMultiplierStrings配列を用意し、凡例表示で現在の倍率を視覚的に確認できるようにします。最後に、各インジケーターの計算結果を保持するためのバッファとして、rsiBuffer、cciBuffer、mfiBufferの各double配列を宣言します。これらは後続の描画処理およびゲージ更新に使用されます。これでゲージの初期化処理に進む準備が整いました。

//+------------------------------------------------------------------+
//| 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の入力値に基づいて各ゲージの表示フラグを決定します。RSIを含む選択であればshowRSIをtrueとし、同様にCCIおよびMFIについてもshowCCI、showMFIをそれぞれ設定し、対応するインジケーターの表示を条件分岐できるようにします。次に相対配置用の初期値としてprevNameを空文字で初期化し、基準座標としてbaseXを30、baseYを30に設定します。また、IndicatorSetIntegerを用いて5段階のインジケーターレベルを設定し、SetIndexBufferによりrsiBuffer、cciBuffer、mfiBufferをそれぞれインデックス0、1、2にバインドしてインジケーターデータとして扱えるようにします。さらにPlotIndexSetDoubleで3つのプロットすべての空値をEMPTY_VALUEに設定し、PlotIndexSetIntegerで描画開始位置をバー13に指定することで14期間計算との整合性を取ります。このあたりは設計方針によって変更される可能性があります。

showRSIがtrueの場合、rsiGaugeをCreateでbase位置およびサイズ230、相対なしで生成し、SetCaseParametersで枠色をmintcream、枠線スタイル1、枠線の色をlight sky blue、ギャップを3に設定します。続いてSetScaleParametersで250度スケール、値範囲0〜100、倍率4、black、円弧なしを指定します。SetTickParametersではスタイル0、サイズ2、主目盛り10、中間目盛り1、補助目盛り4を設定し、SetTickLabelFontでフォントArial、サイズ1、装飾なし、blackを指定します。SetRangeParametersでは0〜30をlimegreen、70〜100をcoral、30〜70をyellowとし、無効領域をgrayに設定します。SetLegendParametersではRSIラベルを半径8、角度-180、サイズ20、Arial、blue violetで表示し、値表示は半径4、角度180、サイズ13、Arialイタリック、redで設定します。SetNeedleParametersでセンター1、black、シャフトdimgray、塗りあり、テール2.0を指定し、RedrawとNewValue(0)を実行した後、prevNameをrsi_gaugeに更新し、iRSI(14,close)でrsiHandleを取得、無効ならINIT_FAILEDを返します。

showCCIがtrueの場合はprevNameを参照して相対配置をおこない、prevNameが空でなければmode1として扱い、xを0またはbase、yをbase+90に設定してcciGaugeを相対配置で生成します。枠設定はRSIと同様ですがギャップは1とし、スケールは-200〜200の200度構成にします。目盛りとフォント設定は共通とし、レンジは-200〜-100をcoral、-100〜100をdodgerblue、100〜200をlimegreen、その他を無効色とします。凡例はCCIラベルを中心に配置し、値表示や装飾もRSIと同様の構造で設定します。針はテール1.5とし、描画後に初期値0で更新し、prevNameをcci_gaugeに更新、iCCI(14,typical price)でcciHandleを取得し無効チェックをおこないます。

showMFIがtrueの場合も同様に相対配置をおこない、yをbase+90へ配置してmfiGaugeを生成します。サイズは250、枠ギャップは3、スケールは100〜0の逆方向120度構成、倍率3とします。目盛りとフォントは共通設定で、レンジは80〜100をred、20〜80をmagenta、0〜20をgreen、それ以外を無効色とします。凡例はMFIラベルを角度-15付近に配置し、追加の倍率表示や値表示も補助ラベルとして配置します。針のテールの長さは1.2とし、描画後に0で初期化、iMFI(14,tick volume)でmfiHandleを取得しチェックします。

最後にすべて正常に初期化された場合はINIT_SUCCEEDEDを返し、初期化処理を完了します。初期化後、次のような結果が表示されます。

ゲージの初期化

ここではバッファはまだ空のままで、インジケーター自体もプロットされていませんが、ゲージの表示自体は問題なく動作しています。次に必要なのは、OnCalculateイベントハンドラ内で、選択されたインジケーターに対してのみ条件付きで計算処理をおこない、その結果をバッファへ反映させることです。

//+------------------------------------------------------------------+
//| 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イベントハンドラでは、まずinpGaugeSelectionの入力に基づいてshowRSI、showCCI、showMFIの各フラグを再評価し、この計算サイクルで処理対象となるインジケーターとゲージを決定します。次にstatic datetime lastBarTimeを用いて新しいバーの発生を検出し、最新のtime[0]と比較して異なればisNewBarをtrueとし、lastBarTimeを更新します。showRSIがtrueでかつハンドルが有効な場合は、CopyBufferを用いてRelative Strength Indexの履歴全体をrsiBufferへコピーし、失敗した場合は表示を隠すためにArrayFillでrsiBufferをEMPTY_VALUEで埋めます。同様にshowCCIおよびshowMFIについても、それぞれcciBufferおよびmfiBufferへCopyBufferでデータを転送し、失敗時はEMPTY_VALUEでクリアします。

同様にshowCCIおよびshowMFIについても、それぞれcciBufferおよびmfiBufferへCopyBufferでデータを転送し、失敗時はEMPTY_VALUEでクリアします。さらに新規バーが発生した場合で、かつ各インジケーターが有効かつハンドルが正常な場合には、CopyBufferでシフト0、取得数1の最新値を一時配列valへ取得し、成功時には対応するゲージへ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イベントハンドラでは、rsiGauge、cciGauge、mfiGaugeそれぞれに対してDeleteメソッドを呼び出し、スケールや針などのレイヤーオブジェクトをチャート上から削除してクリーンアップをおこないます。その後ChartRedrawを呼び出して、インジケーター終了後に視覚的な残留物が一切残らないようチャートを更新します。これにより、デアクティベーション時の後処理が正しく完了します。コンパイルすると、次の結果が得られます。

最終複合ゲージ結果

この可視化から分かるように、インジケーターの計算、ゲージの描画、各種パラメータ設定、そして不要時の削除まで一連の処理が正しく機能しており、当初の目的は達成されています。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

最終フルゲージバックテストGIF


結論

今回は、MQL5のゲージ型インジケーターを拡張し、RSI、CCI、MFIといった複数のオシレーターを、ユーザーが選択可能な組み合わせで扱えるようにしました。あわせて、円形ゲージと扇形ゲージを派生クラスとして導入し、円弧、多角形、相対配置を用いた枠描画により、複数ゲージを整列させた状態でより柔軟に表示できる構成にしています。このインジケーターは、スケールやレンジ、凡例、針といった各要素を柔軟に設定でき、異なる値のドメインを持つオシレーターにも対応できる実用的なマルチ分析ツールになっています。また、設計自体は拡張性を意識しているため、任意のカスタムデータ指標へ置き換えることも可能です。

取引をお楽しみください。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20719

添付されたファイル |
ラリー・ウィリアムズの『市場の秘密』(第3回):MQL5で非ランダムな市場の動きを証明する ラリー・ウィリアムズの『市場の秘密』(第3回):MQL5で非ランダムな市場の動きを証明する
MQL5を使用してラリー・ウィリアムズによる市場挙動の実験を再現することで、金融市場が本当にランダムなのかどうかを検証します。本記事では、カスタムエキスパートアドバイザー(EA)を用い、シンプルなプライスアクションテストを通じて統計的な市場バイアスを明らかにする方法を解説します。
MQL5で他の言語の実用的なモジュールを実装する(第6回):MQL5におけるPython風ファイルI/O操作 MQL5で他の言語の実用的なモジュールを実装する(第6回):MQL5におけるPython風ファイルI/O操作
複雑なMQL5ファイル操作を簡素化するために、読み書きを容易にするPythonスタイルのインターフェースを構築する方法を紹介します。カスタム関数とクラスを用いて、Pythonの直感的なファイル処理パターンを再現する方法を解説します。その結果、MQL5のファイルI/Oにおいて、よりクリーンで信頼性の高いアプローチが実現しました。
MQL5でのAI搭載取引システムの構築(第8回):アニメーション、タイミング指標、応答管理ツールによるUIの改善 MQL5でのAI搭載取引システムの構築(第8回):アニメーション、タイミング指標、応答管理ツールによるUIの改善
本記事では、MQL5におけるAI駆動取引システムを、ユーザーインターフェースの改善によって強化します。具体的には、リクエストの準備フェーズおよび思考フェーズにおけるローディングアニメーションの追加や、レスポンスに表示される処理時間(タイミングメトリクス)による応答の向上などを実装します。さらに、AIへの再クエリを行うための再生成ボタンや、最新の応答をファイルとして保存できるエクスポート機能などのレスポンス管理ツールを追加し、操作性を向上させます。
データサイエンスとML(第47回):DeepARモデルによるPythonでの市場予測 データサイエンスとML(第47回):DeepARモデルによるPythonでの市場予測
DeepARと呼ばれる時系列予測のための優れたモデルを用いて、市場の予測を試みます。DeepARは、ARIMA(自己回帰和分移動平均)やVAR(ベクトル自己回帰)のようなモデルに見られる自己回帰的な性質とディープニューラルネットワークを組み合わせたモデルです。