MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張
はじめに
前回の記事(第5回)では、MetaQuotes Language 5 (MQL5)でWaveTrendクロスオーバーインジケータを開発しました。このインジケータでは、フォググラデーションを用いたCanvas描画、重要なクロスオーバーイベントを強調するシグナルバブル、さらにストップロスおよびテイクプロフィット計算を含むリスク管理機能を統合し、トレード判断を強化しました。第6回では、より高度なRelative Strength Index(RSI、相対性指数)インジケータを構築します。本インジケータは、複数のRSI計算手法(クラシック、平滑化、アダプティブなど)、様々なデータ平滑化手法、動的な色相シフトによる視覚フィードバック、そしてマルチタイムフレームデータ処理と補間機能をサポートしています。本記事では以下のトピックを扱います。
最終的に、複数の計算方式、平滑化、可視化、時間足対応を備えた柔軟な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)ビットマップ画像形式で示したものです。

結論
本記事では、MQL5でRelative Strength Index (RSI)を拡張し、複数の計算方式に対応した高度なRSIインジケータを開発しました。このインジケータは、複数のRSIバリアント、選択可能な価格データソース、各種平滑化手法、そして買われすぎ・売られすぎ水準を動的に調整する境界値機能を備えています。 さらに、状態変化を視覚的に把握しやすくする色相シフト機能、トレンド転換を通知するオプションのアラート機能、そして補間処理を伴うマルチタイムフレーム対応を実装することで、より幅広い市場分析に対応できるようになりました。この動的RSIインジケータを活用することで、テクニカル分析の柔軟性を高めることができます。また、本記事で構築した設計は拡張性も高く、今後の取引戦略や独自機能の追加に向けた土台としても利用できます。取引をお楽しみください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21083
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5取引ツール(第14回):アンチエイリアシングと角丸スクロールバーを備えたピクセルパーフェクトなスクロール対応テキストキャンバス
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
MQL5入門(第37回):MQL5のAPIとWebRequest関数の習得(XI)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索