English Deutsch
preview
MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張

MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張

MetaTrader 5トレーディングシステム |
12 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第5回)では、MetaQuotes Language 5 (MQL5)でWaveTrendクロスオーバーインジケータを開発しました。このインジケータでは、フォググラデーションを用いたCanvas描画、重要なクロスオーバーイベントを強調するシグナルバブル、さらにストップロスおよびテイクプロフィット計算を含むリスク管理機能を統合し、トレード判断を強化しました。第6回では、より高度なRelative Strength Index(RSI、相対性指数)インジケータを構築します。本インジケータは、複数のRSI計算手法(クラシック、平滑化、アダプティブなど)、様々なデータ平滑化手法、動的な色相シフトによる視覚フィードバック、そしてマルチタイムフレームデータ処理と補間機能をサポートしています。本記事では以下のトピックを扱います。

  1. RSIのバリエーション、平滑化手法、および動的機能の検討
  2. MQL5での実装
  3. バックテスト
  4. 結論

最終的に、複数の計算方式、平滑化、可視化、時間足対応を備えた柔軟なRSI分析ツールを完成させます。それでは始めましょう。


RSIのバリエーション、平滑化手法、および動的機能の検討

通常のRelative Strength Index (RSI)は、価格変動の速度と変化を測定し、買われすぎや売られすぎの状態を識別するための指標であり、一般的には0から100の範囲で推移し、70と30を境界として用います。本記事ではRSIを拡張し、Cuttler、Ehlers、Harris、Quick、Basic、RSX、Gradualといった複数のバリエーションを追加することで、計算方法を調整し、また市場のモメンタムに対する感度や平滑化の特性など、異なる側面を強調できるようにします。データの平滑化には、単純平均、成長ベース、平滑化平均、線形加重といった平均化手法を用い、価格入力(終値、始値、高値、安値、またはそれらから導出される平均値など)に対して前処理をおこなうことで、ノイズを低減し、より明確なシグナルを得られるようにします。

色相シフトは、方向転換、中心線の交差、または境界の突破といった条件に基づいてインジケータの色を変化させ、トレンドの反転や強さを示す視覚的な手がかりを提供します。動的な境界線は、指定された期間における最近のRSIの極値に基づいて買われすぎと売られすぎのレベルを調整しますが、静的な境界線は固定のパーセンテージを使用します。また、マルチタイムフレームのサポートにより、異なる期間にわたる分析が可能になり、オプションの補間機能を使用してより滑らかな視覚化を実現できます。

設計方針としては、RSIのバリエーション、データソース、平滑化手法、色相シフトの条件、境界設定を選択するためのユーザー入力を構成し、それらの設定に基づいてRSI曲線を計算します。その後、境界線および塗りつぶしを描画し、マルチタイムフレームデータを処理し、色相シフトが発生した際には通知を送信します。つまり、本インジケータは従来のRSIよりも柔軟性が高く、さまざまな市場環境や分析スタイルに対応できる高度なカスタマイズ性を備えています。

概念フレームワーク

最終的には、任意の価格系列からモメンタムを計算できる適応型RSIエンジンが得られます。これは終値に限定されるものではありません。さらに、入力データとRSI自体の両方を動的に平滑化できるため、コアロジックを変更することなく、高速、低速、サイクル追従型の挙動を切り替えることが可能になります。そのうえで、境界値と視覚的状態は自動調整され、買われすぎ・売られすぎのシグナルは固定値ではなく市場状況に適応する形になります。それでは実装に進みましょう。


MQL5での実装

MQL5でインジケータを作成するには、まずMetaEditorを開き、ナビゲータでインジケータフォルダを開き、[新規]タブをクリックして、表示される手順に従ってファイルを作成します。作成後は、コーディング画面でバッファバッファ数、プロット数、各ラインの色・太さ・ラベルなどのインジケータの設定を定義します。

//+------------------------------------------------------------------+
//|                              Multi-Method RSI with Smoothing.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property indicator_separate_window
#property indicator_buffers 8
#property indicator_plots   5

#property indicator_label1  "RSI High/Low Area"
#property indicator_type1   DRAW_FILLING
#property indicator_color1  C'209,243,209',C'255,230,183'
#property indicator_label2  "RSI Top Boundary"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrLimeGreen
#property indicator_style2  STYLE_DOT
#property indicator_label3  "RSI Center Line"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrSilver
#property indicator_style3  STYLE_DOT
#property indicator_label4  "RSI Bottom Boundary"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrOrange
#property indicator_style4  STYLE_DOT
#property indicator_label5  "RSI Curve"
#property indicator_type5   DRAW_COLOR_LINE
#property indicator_color5  clrSilver,clrLimeGreen,clrOrange
#property indicator_width5  2

実装は、まずインジケータをメインチャートとは分離されたサブウィンドウに表示するように設定することから始めます。これは#property indicator_separate_windowによっておこない、メインチャートと明確に分離することで可読性を高めます。次に、内部計算用として「#property indicator_buffers 8」を用いて8つのバッファを確保し、さらに可視化要素として「#property indicator_plots 5」により5つのプロットを定義します。プロット構成としては、まずRSIの高値・安値領域を表す塗りつぶし領域を作成し、これは薄い緑およびオレンジ系の色調で表示します。次に、上側境界を示す点線のライムグリーンライン、中央ラインとしての点線シルバーライン、下側境界を示す点線オレンジラインをそれぞれ配置します。さらにRSI本体の曲線は、値の状態に応じてシルバー、ライムグリーン、オレンジの間で色が変化するようにし、視認性を高めるため線幅は2とします。最後に、これらの動作を制御するための入力パラメータを定義していきます。

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum DataSourceType {
   Data_ClosePrice,      // Use closing price
   Data_OpenPrice,       // Use opening price
   Data_HighPrice,       // Use highest price
   Data_LowPrice,        // Use lowest price
   Data_MidPoint,        // Use midpoint price
   Data_StandardPrice,   // Use standard price
   Data_BalancedPrice,   // Use balanced price
   Data_OverallAverage,  // Use overall average price
   Data_MidBodyAverage,  // Use mid-body average
   Data_DirectionAdjusted, // Use direction-adjusted price
   Data_ExtremeAdjusted, // Use extreme-adjusted price
   Data_SmoothedClose,   // Use smoothed close
   Data_SmoothedOpen,    // Use smoothed open
   Data_SmoothedHigh,    // Use smoothed high
   Data_SmoothedLow,     // Use smoothed low
   Data_SmoothedMid,     // Use smoothed midpoint
   Data_SmoothedStandard,// Use smoothed standard
   Data_SmoothedBalanced,// Use smoothed balanced
   Data_SmoothedOverall, // Use smoothed overall
   Data_SmoothedMidBody, // Use smoothed mid-body
   Data_SmoothedAdjusted,// Use smoothed adjusted
   Data_SmoothedExtreme  // Use smoothed extreme
};

enum RsiVariant {
   Variant_CuttlerStyle,  // Cuttler-style RSI
   Variant_EhlersStyle,   // Ehlers-style smoothed RSI
   Variant_HarrisStyle,   // Harris-style RSI
   Variant_QuickStyle,    // Quick RSI
   Variant_BasicStyle,    // Basic RSI
   Variant_RsxStyle,      // RSX-style
   Variant_GradualStyle   // Gradual RSI
};

enum HueShiftCondition {
   Hue_OnDirectionShift,   // Shift hue on direction change
   Hue_OnCenterCrossing,   // Shift hue on center crossing
   Hue_OnBoundaryCrossing  // Shift hue on boundary crossing
};

enum AveragingApproach {
   Avg_Basic,          // Basic averaging
   Avg_GrowthBased,    // Growth-based averaging
   Avg_EvenedOut,      // Evened-out averaging
   Avg_WeightedLinear  // Weighted linear averaging
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "Chart and Calculation Settings";
input ENUM_TIMEFRAMES AnalysisTimeframe    = PERIOD_CURRENT;     // Choose timeframe for data analysis
input int             RsiLength            = 14;                 // Length for RSI computation

input group "Data Source and Variant Options";
input DataSourceType  SourceData           = Data_ClosePrice;    // Select data source for calculations
input RsiVariant      ChosenRsiVariant     = Variant_BasicStyle; // Select RSI computation variant
input int             DataSmoothingLength  = 0;                  // Smoothing length for data (0 or 1 disables)
input AveragingApproach DataSmoothingApproach = Avg_GrowthBased; // Approach for data smoothing

input group "Hue Adjustment Settings";
input HueShiftCondition HueAdjustmentOn    = Hue_OnBoundaryCrossing; // Condition for hue adjustment

input group "Boundary Configuration";
input int             DynamicBoundaryLength = 50;                // Length for dynamic boundaries (1 or less for static)
input double          TopBoundaryPercent   = 80.0;               // Top boundary percentage
input double          BottomBoundaryPercent = 20.0;              // Bottom boundary percentage

input group "Notification Preferences";
input bool            ActivateNotifications = false;             // Activate notifications?
input bool            NotifyOnActiveBar     = true;              // Notify on active bar?
input bool            InterpolateMultiFrame = true;              // Smooth multi-frame data?

次に、ユーザーの選択肢を体系化し、設定の柔軟性を高めるために列挙型を定義していきます。まずDataSourceType列挙型では、ユーザーが選択できる価格データソースを分類します。これには、終値や始値といった基本的な価格だけでなく、中間値やバランス価格といった派生的な平均値、さらにノイズ低減のための平滑化済みバージョンも含まれます。次にRsiVariant列挙型では、RSIの計算方式のバリエーションを定義します。標準的なBasicに加えて、RSX、Cuttler、Ehlers、Harris、Quick、Gradualといった異なる計算スタイルを選択できるようにし、目的に応じた計算方法を切り替え可能にします。

さらにHueShiftCondition列挙型を用いて、RSI曲線の色が変化するタイミングを制御します。ここでは、方向転換時、中心線の交差時、または境界値の交差時といった条件を指定できます。加えてAveragingApproach列挙型では、データ前処理に使用する平滑化手法を定義します。単純平均、成長ベース、平滑化平均、線形加重などの手法を選択できるようにし、入力データのノイズを抑えるための方法を制御します。ユーザー入力はセクションごとに整理されており、まずチャートおよび計算設定としてAnalysisTimeframeによる時間足の選択と、RsiLengthによるRSI期間の設定をおこないます。これらにはコメントを付与し、理解しやすい形にしています。この結果として、以下のようなインプット設定ウィンドウが構成されます。

インジケータ入力ウィンドウ

次に初期化処理に入る前に、まずグローバル変数を定義していきます。

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
double rsiCurveValues[], rsiHueValues[], areaFillTop[], areaFillBottom[], topBoundaryValues[], centerLineValues[], bottomBoundaryValues[], processedBarCounts[]; //--- Declare buffers
int    multiFrameDataHandle = INVALID_HANDLE;                    //--- Initialize multi-frame handle
ENUM_TIMEFRAMES chosenTimeframe;                                 //--- Declare chosen timeframe

#define MULTI_FRAME_ACCESS iCustom(_Symbol, chosenTimeframe, __FILE__, PERIOD_CURRENT, RsiLength, SourceData, ChosenRsiVariant, DataSmoothingLength, DataSmoothingApproach, HueAdjustmentOn, DynamicBoundaryLength, TopBoundaryPercent, BottomBoundaryPercent, ActivateNotifications, NotifyOnActiveBar, InterpolateMultiFrame) //--- Define multi-frame access


int timeframeCodes[] = {PERIOD_M1, PERIOD_M2, PERIOD_M3, PERIOD_M4, PERIOD_M5, PERIOD_M6, PERIOD_M10, PERIOD_M12, PERIOD_M15, PERIOD_M20, PERIOD_M30, PERIOD_H1, PERIOD_H2, PERIOD_H3, PERIOD_H4, PERIOD_H6, PERIOD_H8, PERIOD_H12, PERIOD_D1, PERIOD_W1, PERIOD_MN1}; //--- Define timeframe codes
string timeframeLabels[] = {"1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "6 minutes", "10 minutes", "12 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours", "daily", "weekly", "monthly"}; //--- Define timeframe labels

//+------------------------------------------------------------------+
//| Convert timeframe to text                                        |
//+------------------------------------------------------------------+
string convertTimeframeToText(int code) {
   if (code == PERIOD_CURRENT)                                   //--- Check current
      code = _Period;                                            //--- Set period
   int pos;                                                      //--- Declare pos
   for (pos = 0; pos < ArraySize(timeframeCodes); pos++)         //--- Loop codes
      if (code == timeframeCodes[pos]) break;                    //--- Break on match
   return(timeframeLabels[pos]);                                 //--- Return label
}

//+------------------------------------------------------------------+
//| Describe RSI variant                                             |
//+------------------------------------------------------------------+
string describeRsiVariant(int variant) {
   switch (variant) {                                            //--- Switch variant
   case Variant_BasicStyle:                                      //--- Handle basic
      return("RSI");                                             //--- Return RSI
   case Variant_RsxStyle:                                        //--- Handle RSX
      return("RSX");                                             //--- Return RSX
   case Variant_CuttlerStyle:                                    //--- Handle Cuttler
      return("Cuttler-style RSI");                               //--- Return Cuttler
   case Variant_HarrisStyle:                                     //--- Handle Harris
      return("Harris-style RSI");                                //--- Return Harris
   case Variant_QuickStyle:                                      //--- Handle Quick
      return("Quick RSI");                                       //--- Return Quick
   case Variant_GradualStyle:                                    //--- Handle Gradual
      return("Gradual RSI");                                     //--- Return Gradual
   case Variant_EhlersStyle:                                     //--- Handle Ehlers
      return("Ehlers-style smoothed RSI");                       //--- Return Ehlers
   default:                                                      //--- Handle default
      return("");                                                //--- Return empty
   }
}

ここでは、インジケータの各種バッファを保持するためのグローバル配列を宣言します。主なRSIラインを格納するrsiCurveValues、色分け用インデックスを保持するrsiHueValues、塗りつぶし領域の上側および下側を表すareaFillTopとareaFillBottom、さらに境界線描画用としてtopBoundaryValues、centerLineValues、bottomBoundaryValuesを定義します。加えて、計算済みバー数を追跡するためのprocessedBarCountsも用意します。また、マルチタイムフレームデータ管理用としてmultiFrameDataHandleを初期値INVALID_HANDLEで初期化し、選択された時間足を保持するためのchosenTimeframeを宣言します。次にプリプロセッサディレクティブを用いてMULTI_FRAME_ACCESSを定義します。これはiCustomを呼び出す形で構成され、銘柄、選択された時間足、ファイル名、そしてすべての入力パラメータを渡すことで、異なる時間足のデータへアクセスできるようにします。

続いて、時間足コードとその表示用ラベルを対応付けるための配列を準備します。timeframeCodesにはPERIOD_M1からPERIOD_MN1までの標準的な時間足定数を格納し、timeframeLabelsにはそれぞれに対応する表示用文字列を格納します。convertTimeframeToText関数では、時間足コードを対応するラベルへ変換します。まずPERIOD_CURRENTが指定されている場合は実際の現在時間足に変換し、その後timeframeCodesを順に走査して一致するコードを探し、対応するtimeframeLabelsから文字列を返します。さらにdescribeRsiVariant関数では、入力されたRSIバリエーションに応じて説明文字列を返します。例えば標準のBasicであれば「RSI」、Ehlers方式であれば「Ehlers-style smoothed RSI」といった説明を返し、該当しない場合は空文字列を返すようにしています。これで、これらのヘルパー関数を用いてインジケータ初期化処理へ進む準備が整いました。

//+------------------------------------------------------------------+
//| Initialize indicator                                             |
//+------------------------------------------------------------------+
int OnInit() {
   SetIndexBuffer(0, areaFillTop, INDICATOR_DATA);               //--- Set top fill buffer
   SetIndexBuffer(1, areaFillBottom, INDICATOR_DATA);            //--- Set bottom fill buffer
   SetIndexBuffer(2, topBoundaryValues, INDICATOR_DATA);         //--- Set top boundary buffer
   SetIndexBuffer(3, centerLineValues, INDICATOR_DATA);          //--- Set center line buffer
   SetIndexBuffer(4, bottomBoundaryValues, INDICATOR_DATA);      //--- Set bottom boundary buffer
   SetIndexBuffer(5, rsiCurveValues, INDICATOR_DATA);            //--- Set RSI curve buffer
   SetIndexBuffer(6, rsiHueValues, INDICATOR_COLOR_INDEX);       //--- Set hue buffer
   SetIndexBuffer(7, processedBarCounts, INDICATOR_CALCULATIONS); //--- Set processed counts buffer

   PlotIndexSetInteger(0, PLOT_SHOW_DATA, false);                //--- Hide filling data
   PlotIndexSetInteger(1, PLOT_SHOW_DATA, false);                //--- Hide bottom data
   PlotIndexSetInteger(2, PLOT_SHOW_DATA, true);                 //--- Show top boundary
   PlotIndexSetInteger(3, PLOT_SHOW_DATA, true);                 //--- Show center line
   PlotIndexSetInteger(4, PLOT_SHOW_DATA, true);                 //--- Show bottom boundary

   chosenTimeframe = MathMax(_Period, AnalysisTimeframe);        //--- Set chosen timeframe
   IndicatorSetString(INDICATOR_SHORTNAME, convertTimeframeToText(chosenTimeframe) + " " + describeRsiVariant(ChosenRsiVariant) + " with Adjustment (" + (string)RsiLength + "," + (string)DataSmoothingLength + "," + (string)DynamicBoundaryLength + ")"); //--- Set short name

   return(INIT_SUCCEEDED);                                       //--- Return success
}

OnInitイベントハンドラでは、まず確保した各バッファをそれぞれ対応するプロットインデックスおよびデータ種別に関連付けます。バッファ0は上側塗りつぶし領域であるareaFillTopに割り当てられ、INDICATOR_DATAとして上側塗りつぶしの描画に使用されます。バッファ1は下側塗りつぶし領域であるareaFillBottomに割り当てられ、同様にINDICATOR_DATAとして下側塗りつぶしの描画に使用されます。バッファ2は上側境界ラインであるtopBoundaryValues、バッファ3は中央線であるcenterLineValues、バッファ4は下側境界ラインであるbottomBoundaryValuesにそれぞれ対応します。バッファ5はRSI本体の値を保持するrsiCurveValues、バッファ6は色分け制御用のインデックスであるrsiHueValuesに割り当てられ、INDICATOR_COLOR_INDEXとして扱われます。さらにバッファ7は計算済みバー数を管理するprocessedBarCountsに割り当てられ、内部計算用(INDICATOR_CALCULATIONS)として使用されます。次に、プロットの表示設定をPlotIndexSetIntegerでおこないます。塗りつぶし領域に対応するプロット0および1は、描画データとしては使用するものの直接は表示しないため非表示に設定します。一方で、境界ラインおよび中央線に対応するプロット2、3、4はチャート上に表示するように設定します。

続いて、計算に使用する時間足を決定します。chosenTimeframeは、現在のチャート時間足とユーザー入力であるAnalysisTimeframeを比較し、より大きい時間足を採用することでマルチタイムフレーム処理に対応します。その後、インジケータの表示名をIndicatorSetStringで設定します。この名称には、convertTimeframeToTextによる時間足の文字列、describeRsiVariantによるRSI計算方式の説明、さらにRsiLength、DataSmoothingLength、DynamicBoundaryLengthといった主要パラメータを組み合わせて含めることで、チャート上でインジケータの設定内容を一目で識別できるようにします。最後にINIT_SUCCEEDEDを返して初期化が正常に完了したことを示します。それでは、インジケータの計算に進みましょう。まず、計算の期間を以下のように決定します。

//+------------------------------------------------------------------+
//| Check timeframe validity                                         |
//+------------------------------------------------------------------+
bool checkTimeframeValidity(ENUM_TIMEFRAMES frame, const datetime& timeStamps[]) {
   static bool alerted = false;                                    //--- Set alerted flag
   if (timeStamps[0] < SeriesInfoInteger(_Symbol, frame, SERIES_FIRSTDATE)) { //--- Check first date
      datetime startDate, checkDate[];                             //--- Declare dates
      if (SeriesInfoInteger(_Symbol, PERIOD_M1, SERIES_TERMINAL_FIRSTDATE, startDate)) //--- Get terminal date
         if (startDate > 0) {                                      //--- Check date
            CopyTime(_Symbol, frame, timeStamps[0], 1, checkDate); //--- Copy time
            SeriesInfoInteger(_Symbol, frame, SERIES_FIRSTDATE, startDate); //--- Get series date
         }
      if (startDate <= 0 || startDate > timeStamps[0]) {           //--- Check invalid
         alerted = true;                                           //--- Set alerted
         return(false);                                            //--- Return false
      }
   }
   if (alerted) {                                                  //--- Check alerted
      alerted = false;                                             //--- Reset alerted
   }
   return(true);                                                   //--- Return true
}

//+------------------------------------------------------------------+
//| Calculate indicator                                              |
//+------------------------------------------------------------------+
int OnCalculate(const int barTotal,
                const int prevProcessed,
                const datetime& timeStamps[],
                const double& opens[],
                const double& highs[],
                const double& lows[],
                const double& closes[],
                const long& volumeTicks[],
                const long& actualVolumes[],
                const int& spreadValues[]) {
   if (Bars(_Symbol, _Period) < barTotal) return(-1);             //--- Check insufficient bars

   if (chosenTimeframe != _Period) {                              //--- Check multi-frame
      double interimData[];                                       //--- Declare interim data
      datetime activeTimeStamp[], followingTimeStamp[];           //--- Declare timestamps
      if (!checkTimeframeValidity(chosenTimeframe, timeStamps)) return(0); //--- Check validity
      if (multiFrameDataHandle == INVALID_HANDLE) multiFrameDataHandle = MULTI_FRAME_ACCESS; //--- Get handle
      if (multiFrameDataHandle == INVALID_HANDLE) return(0);      //--- Check handle
      if (CopyBuffer(multiFrameDataHandle, 7, 0, 1, interimData) == -1) return(0); //--- Copy processed

#define FRAME_RATIO PeriodSeconds(chosenTimeframe) / PeriodSeconds(_Period) //--- Define frame ratio
      int currentPos = MathMin(MathMax(prevProcessed - 1, 0), MathMax(barTotal - (int)interimData[0] * FRAME_RATIO - 1, 0)); //--- Compute pos
      for (; currentPos < barTotal && !_StopFlag; currentPos++) { //--- Loop positions
#define TRANSFER_MULTI_FRAME(_array, _pos) if (CopyBuffer(multiFrameDataHandle, _pos, timeStamps[currentPos], 1, interimData) == -1) break; _array[currentPos] = interimData[0] //--- Define transfer
         TRANSFER_MULTI_FRAME(areaFillTop, 0);                    //--- Transfer top fill
         TRANSFER_MULTI_FRAME(areaFillBottom, 1);                 //--- Transfer bottom fill
         TRANSFER_MULTI_FRAME(topBoundaryValues, 2);              //--- Transfer top boundary
         TRANSFER_MULTI_FRAME(centerLineValues, 3);               //--- Transfer center line
         TRANSFER_MULTI_FRAME(bottomBoundaryValues, 4);           //--- Transfer bottom boundary
         TRANSFER_MULTI_FRAME(rsiCurveValues, 5);                 //--- Transfer RSI curve
         TRANSFER_MULTI_FRAME(rsiHueValues, 6);                   //--- Transfer hue

         if (!InterpolateMultiFrame) continue;                    //--- Skip if no interpolate
         CopyTime(_Symbol, chosenTimeframe, timeStamps[currentPos], 1, activeTimeStamp); //--- Copy active time
         if (currentPos < (barTotal - 1)) {                       //--- Check not last
            CopyTime(_Symbol, chosenTimeframe, timeStamps[currentPos + 1], 1, followingTimeStamp); //--- Copy following time
            if (activeTimeStamp[0] == followingTimeStamp[0]) continue; //--- Skip same time
         }

         int stepsBack = 1;                                       //--- Initialize steps back
         while ((currentPos - stepsBack) > 0 && timeStamps[currentPos - stepsBack] >= activeTimeStamp[0]) stepsBack++; //--- Count back

         for (int stepsForward = 1; (currentPos - stepsForward) >= 0 && stepsForward < stepsBack; stepsForward++) { //--- Loop forward
#define SMOOTH_MULTI_FRAME(_array) _array[currentPos - stepsForward] = _array[currentPos] + (_array[currentPos - stepsBack] - _array[currentPos]) * stepsForward / stepsBack //--- Define smooth
            SMOOTH_MULTI_FRAME(areaFillTop);                      //--- Smooth top fill
            SMOOTH_MULTI_FRAME(areaFillBottom);                   //--- Smooth bottom fill
            SMOOTH_MULTI_FRAME(topBoundaryValues);                //--- Smooth top boundary
            SMOOTH_MULTI_FRAME(bottomBoundaryValues);             //--- Smooth bottom boundary
            SMOOTH_MULTI_FRAME(centerLineValues);                 //--- Smooth center line
            SMOOTH_MULTI_FRAME(rsiCurveValues);                   //--- Smooth RSI curve
         }
      }
      return(currentPos);                                        //--- Return pos
   }
}

まずcheckTimeframeValidity関数を作成し、選択された時間足に対して、現在の時刻において十分な履歴データが利用可能かどうかを検証します。この関数では、無効状態を一度だけ通知するために静的なalertedフラグを使用します。まず、系列データの開始日時よりも最初のタイムスタンプが前になっている場合を確認するため、SeriesInfoIntegerを用いてSERIES_FIRSTDATEを取得し、さらにターミナル側の最初の日時をSERIES_TERMINAL_FIRSTDATEで取得します。その上でCopyTimeを使用して時間足データの時刻を取得し、比較します。開始日時が無効であるか、またはタイムスタンプより後になっている場合は、alertedフラグを設定しfalseを返します。一方で、以前に無効状態が通知されていた場合はフラグをリセットし、trueを返します。

次にOnCalculateイベントハンドラでは、新しいバーの生成またはデータ更新のたびにインジケータ値を計算します。まずBars関数を用いて利用可能なバー数を確認し、要求された総バー数より少ない場合は-1を返して再計算が必要であることを示します。選択された時間足が現在のチャート時間足と異なる場合、すなわちマルチタイムフレームモードの場合は、一時的なデータ配列とタイムスタンプ配列を宣言し、checkTimeframeValidityを呼び出してデータの有効性を確認します。無効であれば0を返します。

続いて、マルチタイムフレーム用ハンドルが無効である場合は、事前に定義されたMULTI_FRAME_ACCESSマクロを用いて取得し、失敗した場合も0を返します。その後、バッファ7に保存されている処理済みバー数をCopyBufferでinterimDataにコピーし、これに失敗した場合も0を返します。時間スケール調整のためにFRAME_RATIOを定義し、選択された時間足と現在の時間足の秒数比を計算します。この値を用いてバー位置を正規化します。次に、すでに処理済みのバー数と調整後の総バー数を基に、currentPosをminおよびmax関数で算出し、ループ開始位置を決定します。

その後、currentPosからbarTotalまでループし、途中で停止フラグを確認しながら処理を進めます。各反復ではTRANSFER_MULTI_FRAMEマクロを定義し、マルチタイムフレームハンドルから各バッファの値を取得し、areaFillTop、areaFillBottom、各境界ライン、中央線、rsiCurveValues、rsiHueValuesへ現在位置に対応する値をコピーします。コピーに失敗した場合はループを終了します。

マルチタイムフレーム補間が有効な場合は、現在バーと次のバーのタイムスタンプを取得します。最後のバーである場合、またはタイムスタンプが一致する場合は補間をスキップします。その後、過去方向にさかのぼりstepsBackを算出し、より古いバー位置を特定します。続いて1からstepsBack未満までループして、SMOOTH_MULTI_FRAMEマクロを用いて現在値と過去値の間を線形補間します。この処理を塗りつぶし領域、境界ライン、中央線、RSI曲線に適用することで、マルチタイムフレーム表示を滑らかにします。

最後に、処理済み位置としてcurrentPosを返し、次回の計算開始位置を示します。時間足の処理が整ったため、次はインジケータ計算本体へ進みます。まずは平滑化手法に対応するためのヘルパー関数から実装していきます。まずは、平滑化手法における調整済みデータ平均を計算する関数から始めましょう。

#define AVG_VARIANTS 1                                           //--- Define avg variants
#define AVG_ARRAY_X1 1 * AVG_VARIANTS                            //--- Define array x1
#define AVG_ARRAY_X2 2 * AVG_VARIANTS                            //--- Define array x2

//+------------------------------------------------------------------+
//| Compute custom average                                           |
//+------------------------------------------------------------------+
double computeCustomAverage(int avgApproach, double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   switch (avgApproach) {                                        //--- Switch approach
   case Avg_Basic:                                               //--- Handle basic
      return(computeBasicAvg(inputVal, (int)avgLen, pos, barCnt, varIndex)); //--- Return basic avg
   case Avg_GrowthBased:                                         //--- Handle growth
      return(computeGrowthAvg(inputVal, avgLen, pos, barCnt, varIndex)); //--- Return growth avg
   case Avg_EvenedOut:                                           //--- Handle evened
      return(computeEvenedAvg(inputVal, avgLen, pos, barCnt, varIndex)); //--- Return evened avg
   case Avg_WeightedLinear:                                      //--- Handle weighted
      return(computeLinearAvg(inputVal, avgLen, pos, barCnt, varIndex)); //--- Return linear avg
   default:                                                      //--- Handle default
      return(inputVal);                                          //--- Return input
   }
}

double basicAvgArray[][AVG_ARRAY_X2];                            //--- Declare basic avg array

//+------------------------------------------------------------------+
//| Compute basic average                                            |
//+------------------------------------------------------------------+
double computeBasicAvg(double inputVal, int avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(basicAvgArray, 0) != barCnt) ArrayResize(basicAvgArray, barCnt); //--- Resize array
   varIndex *= 2;                                                //--- Adjust index
   int offset;                                                   //--- Declare offset

   basicAvgArray[pos][varIndex + 0] = inputVal;                  //--- Set value
   basicAvgArray[pos][varIndex + 1] = inputVal;                  //--- Set avg
   for (offset = 1; offset < avgLen && (pos - offset) >= 0; offset++) //--- Loop offsets
      basicAvgArray[pos][varIndex + 1] += basicAvgArray[pos - offset][varIndex + 0]; //--- Accumulate avg
   basicAvgArray[pos][varIndex + 1] /= 1.0 * offset;            //--- Average
   return(basicAvgArray[pos][varIndex + 1]);                     //--- Return avg
}

double growthAvgArray[][AVG_ARRAY_X1];                           //--- Declare growth avg array

//+------------------------------------------------------------------+
//| Compute growth average                                           |
//+------------------------------------------------------------------+
double computeGrowthAvg(double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(growthAvgArray, 0) != barCnt) ArrayResize(growthAvgArray, barCnt); //--- Resize array

   growthAvgArray[pos][varIndex] = inputVal;                     //--- Set value
   if (pos > 0 && avgLen > 1)                                    //--- Check pos and len
      growthAvgArray[pos][varIndex] = growthAvgArray[pos - 1][varIndex] + (2.0 / (1.0 + avgLen)) * (inputVal - growthAvgArray[pos - 1][varIndex]); //--- Compute growth
   return(growthAvgArray[pos][varIndex]);                        //--- Return avg
}

double evenedAvgArray[][AVG_ARRAY_X1];                           //--- Declare evened avg array

//+------------------------------------------------------------------+
//| Compute evened average                                           |
//+------------------------------------------------------------------+
double computeEvenedAvg(double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(evenedAvgArray, 0) != barCnt) ArrayResize(evenedAvgArray, barCnt); //--- Resize array

   evenedAvgArray[pos][varIndex] = inputVal;                     //--- Set value
   if (pos > 1 && avgLen > 1)                                    //--- Check pos and len
      evenedAvgArray[pos][varIndex] = evenedAvgArray[pos - 1][varIndex] + (inputVal - evenedAvgArray[pos - 1][varIndex]) / avgLen; //--- Compute evened
   return(evenedAvgArray[pos][varIndex]);                        //--- Return avg
}

double linearAvgArray[][AVG_ARRAY_X1];                           //--- Declare linear avg array

//+------------------------------------------------------------------+
//| Compute linear average                                           |
//+------------------------------------------------------------------+
double computeLinearAvg(double inputVal, double avgLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(linearAvgArray, 0) != barCnt) ArrayResize(linearAvgArray, barCnt); //--- Resize array

   linearAvgArray[pos][varIndex] = inputVal;                     //--- Set value
   if (avgLen <= 1) return(inputVal);                            //--- Return if no avg

   double totalWeights = avgLen;                                 //--- Set total weights
   double totalValues = avgLen * inputVal;                       //--- Set total values

   for (int offset = 1; offset < avgLen && (pos - offset) >= 0; offset++) { //--- Loop offsets
      double currWeight = avgLen - offset;                       //--- Compute weight
      totalWeights += currWeight;                                //--- Accumulate weights
      totalValues += currWeight * linearAvgArray[pos - offset][varIndex]; //--- Accumulate values
   }
   return(totalValues / totalWeights);                           //--- Return avg
}

定数はプリプロセッサディレクティブを用いて定義します。AVG_VARIANTSは平滑化バリアントの数として1に設定し、AVG_ARRAY_X1は単一列配列用としてバリアント数×1、AVG_ARRAY_X2は二列配列用としてバリアント数×2に設定します。これにより、異なる平滑化処理に対応できる配列構造を準備します。次にcomputeCustomAverage関数では、avgApproachパラメータに対してswitch文を用い、適切な平滑化手法へ振り分けます。基本平均、成長ベース平均、均等化平均、線形加重のいずれかにルーティングし、それぞれ対応する関数から計算結果を返します。いずれにも該当しない場合は、デフォルトとして入力値inputValをそのまま返します。

basicAvgArrayは多次元配列としてAVG_ARRAY_X2サイズで定義します。computeBasicAvg関数では、まずバー数barCntに応じて配列サイズを確認し、必要に応じてリサイズします。varIndexを2倍して列オフセットを作成し、第1列にinputValを格納、第2列に平均値を保持します。その後、過去avgLen分の値をループで加算し、実際の参照数で割ることで単純平均を算出し、その結果を返します。

同様に、成長ベース平滑化ではgrowthAvgArrayをAVG_ARRAY_X1で定義し、computeGrowthAvgで必要に応じてサイズ変更します。入力値は直接格納され、バーが2本目以降かつavgLenが1より大きい場合には、直前の値との差分に重みをかけて加算する式を用いて更新されます。これは指数的な平滑化の挙動を模したものであり、その結果を返します。均等化平滑化ではevenedAvgArrayをAVG_ARRAY_X1で定義し、computeEvenedAvg内でサイズ変更と値の格納をおこないます。2本目以降かつavgLenが1より大きい場合には、前値との差分をavgLenで割って加算することで、単純な逐次平均に近い形で補正し、その結果を返します。

最後に線形重み付き平均ではlinearAvgArrayをAVG_ARRAY_X1で定義し、computeLinearAvgでサイズ変更と初期値設定をおこないます。avgLenが1以下の場合は早期に終了し、それ以外の場合は現在値を含めた総和と重み総和を初期化します。その後、過去データをループで走査し、減衰する重みを掛けながら値を加算し、最終的に総和を重み総和で割ることで線形重み付き平均を算出します。次に、平均化関数にデータセットを入力する必要があります。データセットの生成方法の定義は以下のとおりです。

#define DATA_VARIANTS 1                                          //--- Define data variants
#define DATA_VARIANT_SIZE 4                                      //--- Define variant size

double smoothedDataArray[][DATA_VARIANTS * DATA_VARIANT_SIZE];   //--- Declare smoothed data array

//+------------------------------------------------------------------+
//| Fetch chosen data                                                |
//+------------------------------------------------------------------+
double fetchChosenData(int dataType, const double& opens[], const double& closes[], const double& highs[], const double& lows[], int pos, int barCnt, int varIndex = 0) {
   if (dataType >= Data_SmoothedClose) {                         //--- Check smoothed
      if (ArrayRange(smoothedDataArray, 0) != barCnt) ArrayResize(smoothedDataArray, barCnt); //--- Resize array
      varIndex *= DATA_VARIANT_SIZE;                             //--- Adjust index

      double smoothedOpen;                                       //--- Declare smoothed open
      if (pos > 0)                                               //--- Check pos
         smoothedOpen = (smoothedDataArray[pos - 1][varIndex + 2] + smoothedDataArray[pos - 1][varIndex + 3]) / 2.0; //--- Compute smoothed open
      else                                                       //--- Handle initial
         smoothedOpen = (opens[pos] + closes[pos]) / 2;          //--- Set initial open

      double smoothedClose = (opens[pos] + highs[pos] + lows[pos] + closes[pos]) / 4.0; //--- Compute smoothed close
      double smoothedHigh = MathMax(highs[pos], MathMax(smoothedOpen, smoothedClose)); //--- Compute smoothed high
      double smoothedLow = MathMin(lows[pos], MathMin(smoothedOpen, smoothedClose)); //--- Compute smoothed low

      smoothedDataArray[pos][varIndex + 2] = smoothedOpen;       //--- Set smoothed open
      smoothedDataArray[pos][varIndex + 3] = smoothedClose;      //--- Set smoothed close

      switch (dataType) {                                        //--- Switch data type
      case Data_SmoothedClose:                                   //--- Handle smoothed close
         return(smoothedClose);                                  //--- Return close
      case Data_SmoothedOpen:                                    //--- Handle smoothed open
         return(smoothedOpen);                                   //--- Return open
      case Data_SmoothedHigh:                                    //--- Handle smoothed high
         return(smoothedHigh);                                   //--- Return high
      case Data_SmoothedLow:                                     //--- Handle smoothed low
         return(smoothedLow);                                    //--- Return low
      case Data_SmoothedMid:                                     //--- Handle smoothed mid
         return((smoothedHigh + smoothedLow) / 2.0);             //--- Return mid
      case Data_SmoothedMidBody:                                 //--- Handle smoothed mid body
         return((smoothedOpen + smoothedClose) / 2.0);           //--- Return mid body
      case Data_SmoothedStandard:                                //--- Handle smoothed standard
         return((smoothedHigh + smoothedLow + smoothedClose) / 3.0); //--- Return standard
      case Data_SmoothedBalanced:                                //--- Handle smoothed balanced
         return((smoothedHigh + smoothedLow + smoothedClose + smoothedClose) / 4.0); //--- Return balanced
      case Data_SmoothedOverall:                                 //--- Handle smoothed overall
         return((smoothedHigh + smoothedLow + smoothedClose + smoothedOpen) / 4.0); //--- Return overall
      case Data_SmoothedAdjusted:                                //--- Handle smoothed adjusted
         if (smoothedClose > smoothedOpen) return((smoothedHigh + smoothedClose) / 2.0); //--- Return high close
         else return((smoothedLow + smoothedClose) / 2.0);       //--- Return low close
      case Data_SmoothedExtreme:                                 //--- Handle smoothed extreme
         if (smoothedClose > smoothedOpen) return(smoothedHigh); //--- Return high
         if (smoothedClose < smoothedOpen) return(smoothedLow);  //--- Return low
         return(smoothedClose);                                  //--- Return close
      }
   }

   switch (dataType) {                                           //--- Switch data type
   case Data_ClosePrice:                                         //--- Handle close
      return(closes[pos]);                                       //--- Return close
   case Data_OpenPrice:                                          //--- Handle open
      return(opens[pos]);                                        //--- Return open
   case Data_HighPrice:                                          //--- Handle high
      return(highs[pos]);                                        //--- Return high
   case Data_LowPrice:                                           //--- Handle low
      return(lows[pos]);                                         //--- Return low
   case Data_MidPoint:                                           //--- Handle mid point
      return((highs[pos] + lows[pos]) / 2.0);                    //--- Return mid
   case Data_MidBodyAverage:                                     //--- Handle mid body
      return((opens[pos] + closes[pos]) / 2.0);                  //--- Return mid body
   case Data_StandardPrice:                                      //--- Handle standard
      return((highs[pos] + lows[pos] + closes[pos]) / 3.0);      //--- Return standard
   case Data_BalancedPrice:                                      //--- Handle balanced
      return((highs[pos] + lows[pos] + closes[pos] + closes[pos]) / 4.0); //--- Return balanced
   case Data_OverallAverage:                                     //--- Handle overall
      return((highs[pos] + lows[pos] + closes[pos] + opens[pos]) / 4.0); //--- Return overall
   case Data_DirectionAdjusted:                                  //--- Handle direction adjusted
      if (closes[pos] > opens[pos]) return((highs[pos] + closes[pos]) / 2.0); //--- Return high close
      else return((lows[pos] + closes[pos]) / 2.0);              //--- Return low close
   case Data_ExtremeAdjusted:                                    //--- Handle extreme adjusted
      if (closes[pos] > opens[pos]) return(highs[pos]);          //--- Return high
      if (closes[pos] < opens[pos]) return(lows[pos]);           //--- Return low
      return(closes[pos]);                                       //--- Return close
   }
   return(0);                                                    //--- Return zero
}

データセット生成ロジックでは、まず定数としてDATA_VARIANTSを1に設定し、データ処理バリアントの数を表します。さらにDATA_VARIANT_SIZEを4に設定し、各バリアントごとの配列における列幅を定義します。これにより、複数種類のデータ変換結果を同一配列内で管理できる構造を準備します。次にsmoothedDataArrayを多次元配列として宣言し、バー全体にわたる平滑化済み価格データを格納できるようにします。fetchChosenData関数では、dataTypeに基づいて指定されたバー位置の適切な価格値を選択し、返却します。

平滑化系のデータ型(Data_SmoothedClose以降)では、まず必要に応じて配列サイズをbarCntに合わせてリサイズします。その際、varIndexをバリアントサイズでスケーリングし、配列内の列オフセットを決定します。smoothedOpenは、2本目以降のバーであれば直前バーにおけるopenとcloseの値から平均を取り、初期バーであれば現在のopenとcloseの中間値として計算します。smoothedCloseはOHLC(始値、高値、安値、終値)の4値平均として算出し、そこからsmoothedHighはhigh、smoothedOpen、smoothedCloseの最大値、smoothedLowはlow、smoothedOpen、smoothedCloseの最小値として導出します。その後、smoothedOpenとsmoothedCloseを配列のオフセット+2および+3に格納します。

続くswitch文では、dataTypeに応じて平滑化後の値を返します。例えばData_SmoothedMidでは中間値を返し、Data_SmoothedStandardでは3点平均を返します。またData_SmoothedAdjustedでは、終値が始値を上回る場合は高値と終値の平均、そうでない場合は安値と終値の平均を取るなど、方向性に応じて調整します。Data_SmoothedExtremeでは高値、安値、終値の極値を用いて計算します。非平滑化データ型についても同様にswitch文で処理されます。Data_ClosePriceでは終値をそのまま返し、Data_MidPointでは高値と安値の中間値を返します。Data_StandardPriceでは高値、安値、終値の代表値を用いた標準価格を返します。また方向依存型のケースでは、平滑化版と同様のロジックを生値に対して適用します。該当するデータ型が存在しない場合は、既定値として0を返します。この処理により、OnCalculateイベントハンドラ内で初期データ計算を呼び出す準備が整います。

int beginPos = (int)MathMax(prevProcessed - 1, 0);            //--- Set begin pos
for (; beginPos < barTotal && !_StopFlag; beginPos++) {       //--- Loop bars
   double adjustedData = computeCustomAverage(DataSmoothingApproach, fetchChosenData(SourceData, opens, closes, highs, lows, beginPos, barTotal), DataSmoothingLength, beginPos, barTotal); //--- Compute adjusted data
}

return(beginPos);                                             //--- Return begin pos

ここでは、OnCalculateイベントハンドラのシングルタイムフレームモード処理を続けます。まずbeginPosをprevProcessedから1を引いた値と0の最大値として設定し、前回処理済みのバーから開始するか、未処理の場合は先頭から開始するようにします。次にbeginPosからbarTotal未満までループを実行し、途中で_StopFlagを確認して処理の中断が可能な状態にします。各バーごとにadjustedDataを計算しますが、まずfetchChosenData関数を用いて現在のバーのOHLC配列とSourceDataタイプに基づいた選択価格を取得します。その後、その値に対してcomputeCustomAverage関数を適用し、DataSmoothingApproachおよびDataSmoothingLengthに基づいて平滑化処理をおこないます。ループ終了後、処理済みバー位置としてbeginPosを返し、計算がどこまで完了したかを報告します。コンパイルすると、次の結果が得られます。

初期インジケータデータ計算

画像から分かるように、インジケータの基礎となるデータ処理部分はすでに完了しています。次にするべきは、各RSIバリアントを用いてRSI値そのものを計算することです。これにより、最終的な曲線描画に必要な主要ロジックが完成します。

#define RSI_VARIANTS 1                                           //--- Define RSI variants

double rsiComputeArray[][RSI_VARIANTS * 13];                     //--- Declare RSI compute array

#define DATA_SHIFT_POS 0                                         //--- Define data shift pos
#define DATA_SHIFTS_POS 3                                        //--- Define data shifts pos
#define SHIFT_POS 1                                              //--- Define shift pos
#define ABS_SHIFT_POS 2                                          //--- Define abs shift pos
#define RSI_COMPUTE_POS 1                                        //--- Define RSI compute pos
#define RS_COMPUTE_POS 1                                         //--- Define RS compute pos

//+------------------------------------------------------------------+
//| Compute RSI value                                                |
//+------------------------------------------------------------------+
double computeRsiValue(int rsiVariant, double currData, double rsiLen, int pos, int barCnt, int varIndex = 0) {
   if (ArrayRange(rsiComputeArray, 0) != barCnt) ArrayResize(rsiComputeArray, barCnt); //--- Resize array
   int arrayOffset = varIndex * 13;                              //--- Compute offset

   rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] = currData; //--- Set data shift

   switch (rsiVariant) {                                         //--- Switch variant
   case Variant_BasicStyle: {                                    //--- Handle basic
      double factorAlpha = 1.0 / MathMax(rsiLen, 1);             //--- Compute alpha
      if (pos < rsiLen) {                                        //--- Check initial
         int cnt;                                                //--- Initialize count
         double totalAbsShifts = 0;                              //--- Initialize total abs
         for (cnt = 0; cnt < rsiLen && (pos - cnt - 1) >= 0; cnt++) //--- Loop shifts
            totalAbsShifts += MathAbs(rsiComputeArray[pos - cnt][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - cnt - 1][arrayOffset + DATA_SHIFT_POS]); //--- Accumulate abs shifts
         rsiComputeArray[pos][arrayOffset + SHIFT_POS] = (rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[0][arrayOffset + DATA_SHIFT_POS]) / MathMax(cnt, 1); //--- Set shift
         rsiComputeArray[pos][arrayOffset + ABS_SHIFT_POS] = totalAbsShifts / MathMax(cnt, 1); //--- Set abs shift
      } else {                                                   //--- Handle non-initial
         double dataShift = rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute shift
         rsiComputeArray[pos][arrayOffset + SHIFT_POS] = rsiComputeArray[pos - 1][arrayOffset + SHIFT_POS] + factorAlpha * (dataShift - rsiComputeArray[pos - 1][arrayOffset + SHIFT_POS]); //--- Update shift
         rsiComputeArray[pos][arrayOffset + ABS_SHIFT_POS] = rsiComputeArray[pos - 1][arrayOffset + ABS_SHIFT_POS] + factorAlpha * (MathAbs(dataShift) - rsiComputeArray[pos - 1][arrayOffset + ABS_SHIFT_POS]); //--- Update abs shift
      }
      return(50.0 * (rsiComputeArray[pos][arrayOffset + SHIFT_POS] / MathMax(rsiComputeArray[pos][arrayOffset + ABS_SHIFT_POS], DBL_MIN) + 1)); //--- Return RSI
   }

   case Variant_GradualStyle: {                                  //--- Handle gradual
      double posSum = 0, negSum = 0;                             //--- Initialize sums
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      if (pos < 1) rsiComputeArray[pos][arrayOffset + RSI_COMPUTE_POS] = 50; //--- Set initial
      else rsiComputeArray[pos][arrayOffset + RSI_COMPUTE_POS] = rsiComputeArray[pos - 1][arrayOffset + RSI_COMPUTE_POS] + (1 / MathMax(rsiLen, 1)) * (100 * posSum / MathMax(posSum + negSum, DBL_MIN) - rsiComputeArray[pos - 1][arrayOffset + RSI_COMPUTE_POS]); //--- Compute gradual
      return(rsiComputeArray[pos][arrayOffset + RSI_COMPUTE_POS]); //--- Return value
   }

   case Variant_QuickStyle: {                                    //--- Handle quick
      double posSum = 0, negSum = 0;                             //--- Initialize sums
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      return(100 * posSum / MathMax(posSum + negSum, DBL_MIN));  //--- Return quick RSI
   }

   case Variant_EhlersStyle: {                                   //--- Handle Ehlers
      double posSum = 0, negSum = 0;                             //--- Initialize sums
      rsiComputeArray[pos][arrayOffset + DATA_SHIFTS_POS] = (pos > 2) ? (rsiComputeArray[pos][arrayOffset + DATA_SHIFT_POS] + 2.0 * rsiComputeArray[pos - 1][arrayOffset + DATA_SHIFT_POS] + rsiComputeArray[pos - 2][arrayOffset + DATA_SHIFT_POS]) / 4.0 : currData; //--- Compute shifts
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFTS_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFTS_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      return(50 * (posSum - negSum) / MathMax(posSum + negSum, DBL_MIN) + 50); //--- Return Ehlers RSI
   }

   case Variant_CuttlerStyle: {                                  //--- Handle Cuttler
      double posSum = 0;                                         //--- Initialize positive sum
      double negSum = 0;                                         //--- Initialize negative sum
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][arrayOffset + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][arrayOffset + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) posSum += dataDiff;                   //--- Accumulate positive
         else negSum -= dataDiff;                                //--- Accumulate negative
      }
      rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS] = 100.0 - 100.0 / (1.0 + posSum / MathMax(negSum, DBL_MIN)); //--- Compute Cuttler
      return(rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS]);  //--- Return value
   }

   case Variant_HarrisStyle: {                                   //--- Handle Harris
      double avgPos = 0, avgNeg = 0, posCnt = 0, negCnt = 0;     //--- Initialize averages and counts
      for (int offset = 0; offset < (int)rsiLen && (pos - offset - 1) >= 0; offset++) { //--- Loop offsets
         double dataDiff = rsiComputeArray[pos - offset][varIndex + DATA_SHIFT_POS] - rsiComputeArray[pos - offset - 1][varIndex + DATA_SHIFT_POS]; //--- Compute diff
         if (dataDiff > 0) {                                     //--- Handle positive
            avgPos += dataDiff;                                  //--- Accumulate positive
            posCnt++;                                            //--- Increment positive count
         } else {                                                //--- Handle negative
            avgNeg -= dataDiff;                                  //--- Accumulate negative
            negCnt++;                                            //--- Increment negative count
         }
      }
      if (posCnt != 0) avgPos /= posCnt;                         //--- Average positive
      if (negCnt != 0) avgNeg /= negCnt;                         //--- Average negative
      rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS] = 100 - 100 / (1.0 + (avgPos / MathMax(avgNeg, DBL_MIN))); //--- Compute Harris
      return(rsiComputeArray[pos][varIndex + RSI_COMPUTE_POS]);  //--- Return value
   }

   case Variant_RsxStyle: {                                      //--- Handle RSX
      double kgVal = 3.0 / (2.0 + rsiLen), hgVal = 1.0 - kgVal;  //--- Compute kg and hg
      if (pos < rsiLen) {                                        //--- Check initial
         for (int offset = 1; offset < 13; offset++) rsiComputeArray[pos][offset + arrayOffset] = 0; //--- Zero offsets
         return(50);                                             //--- Return initial
      }

      double motion = rsiComputeArray[pos][DATA_SHIFT_POS + arrayOffset] - rsiComputeArray[pos - 1][DATA_SHIFT_POS + arrayOffset]; //--- Compute motion
      double absMotion = MathAbs(motion);                        //--- Compute abs motion
      for (int offset = 0; offset < 3; offset++) {               //--- Loop offsets
         int subOffset = offset * 2;                             //--- Compute sub offset
         rsiComputeArray[pos][arrayOffset + subOffset + 1] = kgVal * motion + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 1]; //--- Update 1
         rsiComputeArray[pos][arrayOffset + subOffset + 2] = kgVal * rsiComputeArray[pos][arrayOffset + subOffset + 1] + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 2]; //--- Update 2
         motion = 1.5 * rsiComputeArray[pos][arrayOffset + subOffset + 1] - 0.5 * rsiComputeArray[pos][arrayOffset + subOffset + 2]; //--- Update motion

         rsiComputeArray[pos][arrayOffset + subOffset + 7] = kgVal * absMotion + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 7]; //--- Update 7
         rsiComputeArray[pos][arrayOffset + subOffset + 8] = kgVal * rsiComputeArray[pos][arrayOffset + subOffset + 7] + hgVal * rsiComputeArray[pos - 1][arrayOffset + subOffset + 8]; //--- Update 8
         absMotion = 1.5 * rsiComputeArray[pos][arrayOffset + subOffset + 7] - 0.5 * rsiComputeArray[pos][arrayOffset + subOffset + 8]; //--- Update abs motion
      }
      return(MathMax(MathMin((motion / MathMax(absMotion, DBL_MIN) + 1.0) * 50.0, 100.00), 0.00)); //--- Return RSX
   }
   }
   return(0);                                                    //--- Return zero
}

ここではRSI_VARIANTSを1に設定し、RSI計算バリアントの数を定義します。さらにrsiComputeArrayを多次元配列として宣言し、バリアントごとに13列を確保することで、バー全体にわたる中間計算結果を保持できる構造を用意します。次に位置定数を定義します。DATA_SHIFT_POSは現在データ用として0、SHIFT_POSは価格変化量(ネット変化)用として1、ABS_SHIFT_POSは絶対変化量用として2、DATA_SHIFTS_POSは一部バリアントで使用する平滑化された変化量用として3に設定します。またRSI_COMPUTE_POSは計算済みRSI値の格納位置として1に対応させます。

computeRsiValue関数では、まずrsiComputeArrayを必要に応じてbarCntに合わせてリサイズします。次にarrayOffsetをvarIndex×13として算出し、バリアントごとの配列領域を確保します。現在のバーのデータであるcurrDataはDATA_SHIFT_POSに格納されます。続いてrsiVariantに基づくswitch文により、RSIの計算方式を切り替えます。Basicスタイルでは、まずα係数を1 / rsiLenとして定義します。バー数がrsiLen未満の場合は、絶対変化量とネット変化量を累積して平均を計算します。一方でそれ以降のバーでは、現在の変化量を用いて指数的に更新し、ネット変化量と絶対変化量を更新します。最終的に50 × (net / abs + 1)としてRSI値を返します。

Gradualスタイルでは、期間内の正の変化と負の変化を合計し、初期値として50を設定します。その後、前回のRSI値に対して、相対的な強さに基づいた重み付き調整を逐次的に加算しながら更新します。Quickスタイルでは、正の変化と負の変化を同様に累積し、最終的に100 × (positives / total)として高速応答型のRSIを返します。Ehlersスタイルでは、まずバー数が2を超える場合に4点平均による平滑化を適用します。その後、平滑化された値に基づいて差分を計算し、正負の合計から50 + 50 × (net / total)としてRSIを算出します。

Cuttlerスタイルでは、正負の変化量を累積し、100 - 100 / (1 + positives / negatives)という式でRSIを計算し、結果を格納して返します。Harrisスタイルでは、正負の変化量をそれぞれ回数とともに累積し、平均値を算出した上でCuttlerと同様の式を適用します。その結果を保存し返却します。RSXスタイルでは、まず増加係数kgおよび減少係数hgを定義します。初期バーでは配列領域をゼロクリアし、RSI値として50を返します。それ以降はモーション量および絶対モーション量を計算し、3回のループによるEMA的更新処理を両方に対して適用します。最終的に50 × (motion / abs + 1)を0〜100の範囲にクランプして返します。該当するバリアントが存在しない場合は、既定値として0を返します。この関数をループ内で呼び出すことで、RSIデータの計算処理を実行できるようになります。

for (; beginPos < barTotal && !_StopFlag; beginPos++) {       //--- Loop bars
   double adjustedData = computeCustomAverage(DataSmoothingApproach, fetchChosenData(SourceData, opens, closes, highs, lows, beginPos, barTotal), DataSmoothingLength, beginPos, barTotal); //--- Compute adjusted data
   rsiCurveValues[beginPos] = computeRsiValue(ChosenRsiVariant, adjustedData, RsiLength, beginPos, barTotal); //--- Compute RSI value

   if (DynamicBoundaryLength <= 1) {                          //--- Check static boundary
      topBoundaryValues[beginPos] = TopBoundaryPercent;       //--- Set top boundary
      bottomBoundaryValues[beginPos] = BottomBoundaryPercent; //--- Set bottom boundary
      centerLineValues[beginPos] = (topBoundaryValues[beginPos] + bottomBoundaryValues[beginPos]) / 2; //--- Set center line
   } else {                                                   //--- Handle dynamic
      double lowestVal = rsiCurveValues[beginPos];            //--- Set initial low
      double highestVal = rsiCurveValues[beginPos];           //--- Set initial high
      for (int offset = 1; offset < DynamicBoundaryLength && beginPos - offset >= 0; offset++) { //--- Loop offsets
         lowestVal = MathMin(rsiCurveValues[beginPos - offset], lowestVal); //--- Update low
         highestVal = MathMax(rsiCurveValues[beginPos - offset], highestVal); //--- Update high
      }
      double valRange = highestVal - lowestVal;               //--- Compute range
      topBoundaryValues[beginPos] = lowestVal + TopBoundaryPercent * valRange / 100.0; //--- Set top boundary
      bottomBoundaryValues[beginPos] = lowestVal + BottomBoundaryPercent * valRange / 100.0; //--- Set bottom boundary
      centerLineValues[beginPos] = lowestVal + 0.5 * valRange; //--- Set center line
   }

   switch (HueAdjustmentOn) {                                 //--- Switch hue condition
   case Hue_OnBoundaryCrossing:                               //--- Handle boundary crossing
      rsiHueValues[beginPos] = (rsiCurveValues[beginPos] > topBoundaryValues[beginPos]) ? 1 : (rsiCurveValues[beginPos] < bottomBoundaryValues[beginPos]) ? 2 : 0; //--- Set hue
      break;
   case Hue_OnCenterCrossing:                                 //--- Handle center crossing
      rsiHueValues[beginPos] = (rsiCurveValues[beginPos] > centerLineValues[beginPos]) ? 1 : (rsiCurveValues[beginPos] < centerLineValues[beginPos]) ? 2 : 0; //--- Set hue
      break;
   default:                                                   //--- Handle default
      rsiHueValues[beginPos] = (beginPos > 0) ? (rsiCurveValues[beginPos] > rsiCurveValues[beginPos - 1]) ? 1 : (rsiCurveValues[beginPos] < rsiCurveValues[beginPos - 1]) ? 2 : 0 : 0; //--- Set hue
   }

   areaFillTop[beginPos] = rsiCurveValues[beginPos];          //--- Set top fill
   areaFillBottom[beginPos] = (rsiCurveValues[beginPos] > topBoundaryValues[beginPos]) ? topBoundaryValues[beginPos] : (rsiCurveValues[beginPos] < bottomBoundaryValues[beginPos]) ? bottomBoundaryValues[beginPos] : rsiCurveValues[beginPos]; //--- Set bottom fill
}

processedBarCounts[barTotal - 1] = MathMax(barTotal - prevProcessed + 1, 1); //--- Set processed counts

シングルタイムフレームモードのOnCalculateイベントハンドラ内ループを続け、各バーのRSI値を計算していきます。まず平滑化済みデータであるadjustedDataを取得した後、それをcomputeRsiValue関数へ渡し、選択されたChosenRsiVariant、RsiLengthおよびバー情報とともにRSIを算出します。その結果をrsiCurveValuesの現在位置に格納します。次に境界レベルを決定します。DynamicBoundaryLengthが1以下の場合は静的境界として扱い、topBoundaryValuesにはTopBoundaryPercent、bottomBoundaryValuesにはBottomBoundaryPercentを設定し、中央線であるcenterLineValuesはその平均値として算出します。

一方で、DynamicBoundaryLengthが1を超える場合は動的境界として処理します。この場合、まず現在のRSI値を初期値として最小値と最大値を初期化し、過去バーを指定期間分ループしてMathMinおよびMathMaxを用いてそれぞれ最小値と最大値を更新します。その後、レンジを「最大値−最小値」として算出し、上側境界は「最小値+レンジ×上側割合」、下側境界は「最小値+レンジ×下側割合」、中央線は「最小値+レンジ×0.5」として設定します。次にrsiHueValuesへ色インデックスを設定します。これはHueAdjustmentOnの条件に基づき決定されます。境界クロス判定の場合:RSIが上側境界を超えれば1、下側境界を下回れば2、それ以外は0、中央線クロス判定の場合:中央線基準で同様に判定、 デフォルト(方向判定)の場合:前バーのRSIと比較し、上昇なら1、下降なら2、それ以外は0、初期バーは常に0です。続いて塗りつぶし領域を設定します。areaFillTopには現在のRSI値を設定し、areaFillBottomには条件に応じて値を切り替えます。具体的には、RSIが買われすぎ領域にある場合は上側境界、売られすぎ領域にある場合は下側境界、それ以外の場合はRSI自身を設定します。

ループ終了後、processedBarCountsを更新し、今回の呼び出しで処理したバー数の最大値に1を加えた値、または単純に1を記録することで、計算の進行状況を管理します。コンパイルすると、次の結果が得られます。

完全なインジケータ

画像から分かる通り、インジケータは初期化、計算、描画まで正常に動作しています。残る作業はアラート処理のみです。このロジックは関数としてまとめ、イベントハンドラ内から呼び出す形で実装します。このロジックは関数にまとめ、イベントハンドラから呼び出します。

//+------------------------------------------------------------------+
//| Process alert triggers                                           |
//+------------------------------------------------------------------+
void processAlertTriggers(const datetime& timeStamps[], double& hueTrends[], int barTotal) {
   if (!ActivateNotifications) return;                           //--- Check notifications
   int notifyIndex = barTotal - 1;                               //--- Set notify index
   if (!NotifyOnActiveBar) notifyIndex = barTotal - 2;           //--- Adjust if not active
   datetime notifyStamp = timeStamps[notifyIndex];               //--- Get stamp

   if (hueTrends[notifyIndex] != hueTrends[notifyIndex - 1]) {   //--- Check hue change
      if (hueTrends[notifyIndex] == 1) triggerNotification(notifyStamp, "rising"); //--- Trigger rising
      if (hueTrends[notifyIndex] == 2) triggerNotification(notifyStamp, "falling"); //--- Trigger falling
   }
}

//+------------------------------------------------------------------+
//| Trigger notification                                             |
//+------------------------------------------------------------------+
void triggerNotification(datetime stamp, string trend) {
   static string prevTrend = "none";                             //--- Initialize previous trend
   static datetime prevStamp;                                    //--- Initialize previous stamp

   if (prevTrend != trend || prevStamp != stamp) {               //--- Check change
      prevTrend = trend;                                         //--- Update trend
      prevStamp = stamp;                                         //--- Update stamp

      string notifyText = convertTimeframeToText(_Period) + " " + _Symbol + " at " + TimeToString(TimeLocal(), TIME_SECONDS) + describeRsiVariant(ChosenRsiVariant) + " trend shifted to " + trend; //--- Format text
      Alert(notifyText);                                         //--- Send alert
   }
}

ここでは、RSI曲線の色変化に基づいて通知するためのprocessAlertTriggers関数を追加します。まずActivateNotificationsがfalseの場合は何も実行せず、そのまま終了します。通知対象となるバー位置はnotifyIndexで決定します。通常は最新バーを使用しますが、NotifyOnActiveBarがfalseの場合は、未確定バーによる誤通知を避けるために一つ前の確定バーを対象とします。その後、その位置のタイムスタンプを取得します。続いて、notifyIndexにおける色インデックスが直前バーの色インデックスと異なるかどうかを確認します。変化が検出された場合は、色インデックスが1であれば上昇方向を意味する"rising"、2であれば下降方向を意味する"falling"を指定してtriggerNotification関数を呼び出します。

triggerNotification関数では、前回通知したトレンド方向とタイムスタンプを保持するために静的変数を使用します。現在のトレンドまたはタイムスタンプが前回保存された値と異なる場合のみ通知することで、同じシグナルが繰り返し送信されることを防ぎます。条件を満たした場合は保存値を更新し、通知メッセージを生成します。通知メッセージには、convertTimeframeToTextで取得した時間足名、銘柄名、TimeToStringによるローカル時刻、describeRsiVariantで取得したRSI計算方式の説明、およびトレンド変化の内容を含めます。作成したメッセージはAlert関数を用いて送信します。この関数をイベントハンドラから呼び出すことで、以下のような結果が得られます。

アラートシステム作動

画像から分かるように、インジケータの計算、プロットへの反映、そして通知機能の有効化まで正しく動作しています。これにより、本記事で設定した目標は達成できました。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

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

高度なRSIバックテスト


結論

本記事では、MQL5Relative Strength Index (RSI)を拡張し、複数の計算方式に対応した高度なRSIインジケータを開発しました。このインジケータは、複数のRSIバリアント、選択可能な価格データソース、各種平滑化手法、そして買われすぎ・売られすぎ水準を動的に調整する境界値機能を備えています。 さらに、状態変化を視覚的に把握しやすくする色相シフト機能、トレンド転換を通知するオプションのアラート機能、そして補間処理を伴うマルチタイムフレーム対応を実装することで、より幅広い市場分析に対応できるようになりました。この動的RSIインジケータを活用することで、テクニカル分析の柔軟性を高めることができます。また、本記事で構築した設計は拡張性も高く、今後の取引戦略や独自機能の追加に向けた土台としても利用できます。取引をお楽しみください。

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

添付されたファイル |
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
MQL5取引ツール(第14回):アンチエイリアシングと角丸スクロールバーを備えたピクセルパーフェクトなスクロール対応テキストキャンバス MQL5取引ツール(第14回):アンチエイリアシングと角丸スクロールバーを備えたピクセルパーフェクトなスクロール対応テキストキャンバス
本記事では、MQL5のCCanvasベース価格ダッシュボードを拡張し、利用ガイドを表示するためのピクセルパーフェクトなスクロール可能テキストパネルを追加します。これにより、ネイティブのスクロール機能の制限を回避しつつ、カスタムアンチエイリアス処理と角丸デザインのスクロールバーを実現します。テキストパネルは、不透明度を設定可能なテーマ対応背景をサポートし、説明文や連絡先情報などのコンテンツを動的に改行表示できます。また、上下ボタン、スライダーのドラッグ操作、本文領域内でのマウスホイール操作によるインタラクティブなナビゲーションにも対応しています。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5入門(第37回):MQL5のAPIとWebRequest関数の習得(XI) MQL5入門(第37回):MQL5のAPIとWebRequest関数の習得(XI)
MQL5を使用してBinance APIに認証付きリクエストを送信し、アカウント内の全資産の残高情報を取得する方法を解説します。APIキー、サーバー時刻、署名を利用して安全にアカウント情報へアクセスし、そのレスポンスをファイルへ保存して後で活用する方法を学びます。