恥は/dev/nullへ by 初心者

プログラミング素人がのろのろと学んだことをつづっています♪

[MQL5]MT5で上位足の移動平均線を表示する

MT4からMT5へ移行して以降、MTF移動平均線(以下「MA」)を使っていませんでしたが、5分足チャートや15分足チャートを見ている時に「1時間足のMAも表示されている方が便利だな」と思ったので、上位足のMAを表示するインディケーターを、MQL5の練習を兼ねて作ってみました。



現在分かっている問題点

試しに作ってはみたものの、現在分かっている問題点として次のものがあります。

  • 取引時間外にMAが描画されない。
  • たまに(上位足ではなく)現在足のMAが表示されるケースがある。

こうした点はまだ改善の余地があります。

あと、MT4の時もそうでしたが、普通のMA(STFのMA)に比べると描画に少し時間がかかりますね。「MAを描画する対象を直近のローソク足N本にする」という機能を追加した方がよいかもしれません。


イメージ

この画像では15分足チャートに1時間足のMA(カクカクした青いライン)を表示しています。


MAの時間足を変更する

先ほどの例では1時間足のMAを表示していましたが、他の時間足に変更することもできます。たとえば、「4時間足の20本MA」を「赤色」で表示したい場合、プロパティを次のように変更します。


(備考)MAは「Moving Average」の略です。

コード

#property copyright "Copyright 2023, PHILOJUAN"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1

input ENUM_TIMEFRAMES maTimeFrame = PERIOD_H1; // 表示したいMAの時間足
input int             maPeriod    = 24;        // MAの期間
input color           maColor     = clrBlue;   // MAの色
input int             maWidth     = 2;         // MAの太さ

int chartTFminutes = 0;      // チャートの時間足を「分」で表した数値
int maTFminutes    = 0;      // MAの時間足を「分」で表した数値
int barsToRedraw   = 0;      // Tick受信時に再描画するbar数(maTimeFrameの現在足に相当する部分)
int barsToExclude  = 0;      // MAの計算対象としないbarの数(計算ができない部分)
bool lessThanMATF  = false;  // フラグ(チャートの時間足が「MAの時間足」より下位足)

// インディケーター用配列
double MA_1[];

// ハンドル変数
int h_ma1;

int OnInit()
{
    //----------------------
    // パラメーターチェック
    //----------------------
    if(maPeriod < 1) {
        Alert("MAの期間は1以上にしてください");
        RemoveIndicator();
        return INIT_FAILED;
    }
    if(maWidth < 1 || maWidth > 5) {
        Alert("MAの太さに指定できるのは 1 ~ 5 です");
        RemoveIndicator();
        return INIT_FAILED;
    }

    // チャートの時間足やMAの時間足を「分」に変換したものを用意
    ENUM_TIMEFRAMES currentTF = _Period;              // チャートの時間足
    chartTFminutes = TimeFrameToMinutes(currentTF);   // チャートの時間足を「分」にしたもの(int型)
    maTFminutes    = TimeFrameToMinutes(maTimeFrame); // MAの時間足を「分」にしたもの(int型)

    // チャートの時間足が「MAの時間足と同じ OR MAの時間足よりも上位足」の時
    if(chartTFminutes >= maTFminutes) {
        SetIndexBuffer(0, MA_1, INDICATOR_CALCULATIONS);
        PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
            // chartTFminutes と maTFminutesが同じなのにMAが描画されるケース
            // が見受けられたので、実験的に上記2行を記述。
        return INIT_SUCCEEDED;
    }
        // [memo]
        //   SetIndexBufferの第3引数は、通常INDICATOR_DATA。
        //   INDICATOR_CALCULATIONSは中間計算用で描画用ではない。


    //  ★★★★ チャートの時間足がMAの時間足以上なら以下のOnInit処理は省略 ★★★★
    //*********************************************************************************** 

    lessThanMATF = true; // チャートの時間足はMAの時間足よりも下位足

    // バッファと配列を関連付け、MAの外観を設定
    SetBufferAndStyle(0, MA_1, DRAW_LINE, STYLE_SOLID, maColor, maWidth);

    //--------------------------------------------
    // barsToRedraw と barsToExclude の計算
    //--------------------------------------------
    int totalBars = Bars(_Symbol, currentTF);  // チャート上に存在するローソク足の本数

    // barsToRedraw (剰余があれば + 1)
    barsToRedraw = maTFminutes / chartTFminutes;
    if(MathMod(maTFminutes, chartTFminutes) != 0.0) barsToRedraw = barsToRedraw + 1;

    // barsToExclude (剰余があれば + 1)
    barsToExclude = maTFminutes * maPeriod / chartTFminutes;
    if(MathMod(maTFminutes * maPeriod, chartTFminutes) != 0.0) barsToExclude = barsToExclude + 1;

    //--------------------------------------------------------------------
    // MAを描画できない場合、アラートを出してインディケーターを除去
    //--------------------------------------------------------------------
    // 考えられるのは、チャート上にあるローソク足の本数が少なすぎる場合や
    // MAの計算期間が大きすぎる場合。
    bool errorA = false;
    bool errorB = false;

    if(barsToExclude > totalBars) errorA = true;
    if((barsToExclude + barsToRedraw - 1) > totalBars) errorB = true;

    if(errorA || errorB) {
        string symbolPeriod = "[" + _Symbol + "," + TimeFrameToString(currentTF) + "]";
        Alert(symbolPeriod, " ERROR! Cannot calculate moving average.");
        Print("totalBars = ", IntegerToString(totalBars));
        Print("maPeriod = ", IntegerToString(maPeriod));

        if(errorA)
            Print("[TYPE A] barsToExclude = ", IntegerToString(barsToExclude));
        if(errorB)
            Print("[TYPE B] barsToExclude + barsToRedraw - 1 = ", IntegerToString(barsToExclude + barsToRedraw - 1));

        // errorAにもerrorBにも該当しないMA期間を教示
        double maxPeriodA = 1.0 * totalBars * chartTFminutes / maTFminutes;
        double maxPeriodB = 1.0 * (totalBars - barsToRedraw + 1) * chartTFminutes / maTFminutes;
        Print("maximum MA period you can specify = ", DoubleToString(MathMin(maxPeriodA, maxPeriodB), 0));

        RemoveIndicator(); // インディケーターを除去
        return INIT_FAILED;
    }

    //--------------------------------------------
    // その他
    //--------------------------------------------
    // MAハンドルの取得
    h_ma1 = iMA(NULL, maTimeFrame, maPeriod, 0, MODE_SMA, PRICE_CLOSE);

    // MA_1[]のインデックスを「最新足から古い足へ」という順序で付け直す
    ArraySetAsSeries(MA_1, true);

    // 最古足から「barsToExclude - 1」の範囲はMAを描画しない(MAを計算できない)
    PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, barsToExclude - 1);

    // データウィンドウの見出し
    string tfString = TimeFrameToString(maTimeFrame);
    PlotIndexSetString(0, PLOT_LABEL, "MA_" + tfString + "(" + IntegerToString(maPeriod) + ")");

    // データウィンドウに表示される値の精度
    IndicatorSetInteger(INDICATOR_DIGITS, 6);

    return(INIT_SUCCEEDED);
}


void OnDeinit(const int reason)
{
    // MAハンドルを削除(MA計算に使われていた領域を解放)
    if(lessThanMATF) IndicatorRelease(h_ma1);
}


int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
    // チャートの時間足が「MAの時間足」と同じ又は上位足の場合、以下の処理をskip
    if(lessThanMATF == false) return rates_total;

    static bool executeFlag = false;

    //--------------------------------------------------------------------------------------
    // インディケーター挿入直後(OR チャートの時間足を切り替えた直後)
    //--------------------------------------------------------------------------------------
    if(prev_calculated == 0) { ; }  // インディケーター挿入直後は処理なし

    //--------------------------------------------------------------------------------------
    // インディケーター挿入直後ではなく、executeFlag が false
    //--------------------------------------------------------------------------------------
    else if(executeFlag == false) {
        ArraySetAsSeries(time, true);

        // [失敗メモ]
        // インディケーター挿入直後、MTFのMA値は計算されていない(prev_calculated == 0 の
        // 時点で、BarsCalculated(h_ma1)の戻り値は「-1」)。
        // そこで、prev_calculated == 0でない時に1回だけfor文を実行する方法を試したが、
        // 一部の区間にMAが描画されないケースがあった。

        // [現状]
        // 描画範囲のMA_1[i]が全てEMPTY_VALUEでなくなるまで、Tickのたびに、
        // else if(executeFlag == false) { }ブロックを実行(実質、1~2回)

        bool emptyValueNotExist = true;  // EMPTY_VALUEが無いことを表すフラグ

        for(int i = rates_total - barsToExclude; i >= 0; i--) {

            MA_1[i] = ValueOnSpecifiedBarTime(h_ma1, time[i]);
            if(MA_1[i] == EMPTY_VALUE) {
                emptyValueNotExist = false;
                break;
            }
        }

        if(emptyValueNotExist) executeFlag = true;
    }
    //--------------------------------------------------------------------------------------
    // インディケーター挿入直後ではなく、executeFlag が true
    //--------------------------------------------------------------------------------------
    else {
        //「MAの時間足における最新足」に相当する部分を再描画
        for(int i = barsToRedraw - 1; i >= 0; i--) {
            MA_1[i] = ValueOnSpecifiedBarTime(h_ma1, iTime(_Symbol, _Period, i));
        }
    }

    return(rates_total);
}


//--------------------------------------------------------------------
// 指定したbarの発生時刻におけるインディケーターの値を返す関数
//--------------------------------------------------------------------
double ValueOnSpecifiedBarTime(int handle, datetime startTime)
{
    double buffer[1];

    if(CopyBuffer(handle, 0, startTime, 1, buffer) > 0)
        return(buffer[0]);

    else return(EMPTY_VALUE);
    // else行は、CopyBufferの戻り値が -1(エラー)又は 0 の場合
}


//--------------------------------------------------------------------
// バッファと配列を関連付け、インディケーターの外観を設定する関数
//--------------------------------------------------------------------
void SetBufferAndStyle(int bufNumber,
                       double &bufArray[],
                       ENUM_DRAW_TYPE drawType,    // DRAW_LINEや DRAW_NONE
                       ENUM_LINE_STYLE drawStyle,
                       color drawColor,
                       int   drawWidth
                      )
{
    SetIndexBuffer(bufNumber, bufArray, INDICATOR_DATA);
    PlotIndexSetInteger(bufNumber, PLOT_DRAW_TYPE , drawType);
    PlotIndexSetInteger(bufNumber, PLOT_LINE_STYLE, drawStyle);
    PlotIndexSetInteger(bufNumber, PLOT_LINE_COLOR, drawColor);
    PlotIndexSetInteger(bufNumber, PLOT_LINE_WIDTH, drawWidth);
}


//--------------------------------------------------------------------
// インディケーターを除去する関数
//--------------------------------------------------------------------
void RemoveIndicator()
{
    string shortname = "indicator_to_remove";
    IndicatorSetString(INDICATOR_SHORTNAME, shortname);
    ChartIndicatorDelete(0, 0, shortname);
}


//--------------------------------------------------------------------
// 時間足を「H1」等の短縮表現に変換する関数
//--------------------------------------------------------------------
// 例 PERIOD_H4   →   H4
string TimeFrameToString(ENUM_TIMEFRAMES intTF)
{
    if(intTF == PERIOD_CURRENT) intTF = Period();

    return StringSubstr(EnumToString(intTF), 7);
}


//--------------------------------------------------------------------
// MQL5の時間軸をint型の「分」に変換する関数
//--------------------------------------------------------------------
// [備考] MQL4と違い、PERIOD_H1以上は分数に対応していない
int TimeFrameToMinutes(ENUM_TIMEFRAMES timeframe)
{
    int minutes = 0;

    if(timeframe == PERIOD_CURRENT) timeframe = _Period;

    if(timeframe < PERIOD_H1)
        // MT5の現行の仕様なら、単に minutes = timeframe; でもOK。
        minutes = (int)StringToInteger(StringSubstr(EnumToString(timeframe), 8));
    else if(timeframe < PERIOD_D1)
        minutes = 60 * (int)StringToInteger(StringSubstr(EnumToString(timeframe), 8));
    else {
        switch(timeframe) {
            case PERIOD_D1:
                minutes = 1440;
                break;
            case PERIOD_W1:
                minutes = 10080;
                break;
            case PERIOD_MN1:
                minutes = 43200;  // 43200分は 30日に相当(MQL4のMN1を踏襲)
                break;
            default:
                minutes = -1;   // ERROR
        }
    }
    return minutes;
}


iTime()関数の落とし穴?

OnCalculate()関数の中でローソク足の発生時刻を取得する際

else if(executeFlag == false) { 

ブロックの中ではtime[ ]を使い、

else {

ブロックの中ではiTime()を使っています。これは以下の問題があったからです。

time[ ]を使う場合、インデックスを時系列にするためにArraySetAsSeries()を実行する必要があります。一方、iTime()を使えばその必要はありません。そこで、両方のブロックにおいてiTime()を使おうとしました。しかし、「else if」ブロック内でiTime()を使用したところ、15分足チャートではMAが普通に表示されましたが、5分足チャートでは表示されませんでした。

そこで、MAの計算に失敗したローソク足を調べると次のようになっていました。

iTime() → 1970.01.01 00:00
time[]  → 2019.10.30 04:55


次に全ローソク足の発生時刻をiTime()で出力しました。すると、現在足から見て249999本前まではiTime()が適切な発生時刻を返していましたが、それよりも古いローソク足ではiTime()の戻り値が全て「1970.01.01 00:00」となっていました。

これが原因で古いローソク足時点のMAを計算できなかったようです。そこで全ローソク足に処理を行う場面ではtime[]を使い、新しいローソク足に対して処理を行う場面ではiTime()を使用することにしました。


取引時間外にMTFのMAが表示されないのは何故か?

相場が動いていない日曜日のことです。15分足チャートにこのインディケーターを入れ、1時間足のMAを表示させようとしました。しかし、チャートに1時間足のMAは表示されませんでした。

当初は「取引時間外だからTickの受信がない。だから、OnCalculate関数の中に記述されたコードが実行されず、MAが描画されないのだろう。」と考えました。しかし、MT5に標準で搭載されている「MACD」というインディケーターをチャートに入れるとMACDがチャートに描画されました。

MACD」のコードを見たらOnCalculate関数を使っている部分があったので、取引時間外にインディケーターをチャートに入れても、少なくとも1回はOnCalculate関数が実行されるようです。

よって、「1時間足のMAが表示されなかったのはOnCalculate関数が実行されないからではない」と言えます。


考えた結果、「チャートの時間足のMA値は計算されているけれど、それ以外の時間足のMA値は計算されていないのでは?」という可能性が頭に浮かびました。この可能性を確認するために以下の実験をしてみました。

まず、h_ma2という変数を用意して、OnInit関数の中に次の記述をしました。

h_ma2 = iMA(NULL, 0, maPeriod, 0, MODE_SMA, PRICE_CLOSE);  // 第2引数が 0 なのでチャートの時間足のMAを計算します

次に、OnCalculate関数の先頭に次の記述をしました。

printf("BarsCalculated of h_ma1 = %d", BarsCalculated(h_ma1));
printf("BarsCalculated of h_ma2 = %d", BarsCalculated(h_ma2));

ファイルをコンパイルしてチャートに入れてみると、エキスパートに次のように表示されました。

BarsCalculated of h_ma1 = -1
BarsCalculated of h_ma2 = 250176


この結果から2つのことが分かりました。

1. OnCalculate関数が1回実行された
2. チャートの時間足のMA値は計算されているが、1時間足のMA値はまだ計算されていない


というわけで、

・取引時間外にチャートにインディケーターを入れた場合、OnCalculateは少なくとも1回は実行される。
・この時点で、チャートの時間足のMA値は計算済みなので、画面に表示される。
・この時点で、MTFのMA値はまだ計算されていないので、画面に表示されない。

ということのようです。

なお、OnCalculate関数の実行回数については不明な部分があります。もしインディケーターをチャートに入れたままずっと放置しておけば、たとえば10分に1回とか1時間に1回とか、何らかのタイミングでOnCalculateが実行されるのかもしれません。私は数分くらい様子を見ただけで、インディケーターをチャートから除去してしまったので、その辺りが分かりません。そこで「少なくとも1回は」と表現した次第です。


ついでに、MA_1[ ]の要素数についても調べてみました。OnInit関数の最後に

printf("array size of MA_1 (OnInit) = %d", ArraySize(MA_1));

と記述して、OnCalculateの先頭に

printf("array size of MA_1 (OnCalculate) = %d", ArraySize(MA_1));

と記述したところ、次のように表示されました。

array size of MA_1 (OnInit) = 0
array size of MA_1 (OnCalculate) = 250176

これを見ると、OnInit関数の時点では、MA_1[ ]の要素数がゼロなので、MA_1[ ]を使えませんね。 仮にiMA関数を使わずにMA値を計算しMA_1[ ]にデータを入れようとしても、array out of rangeになってしまいます。

また、MA_1[ ]はSetIndexBuffer関数により0番バッファに関連づけられていますから(ターミナルの管理下に置かれていますから)、ArrayResizeで要素数を変更することもできません。

というわけで、「Tick受信が無い時間帯」「(SetIndexBuffer関数によって)ターミナルの管理下に置かれた配列」を使ってMTFの」MAを画面に表示させるのは難しいようです。

ただ、OnCalculate関数が実行された時点ではMA_1[ ]の要素数が250176となっていますので、この段階なら、iMA関数に頼らずに上位足のMA値を計算し、その結果をMA_1[ ]に代入すればMTFのMAが表示されるかもしれません。たとえば、もし5分足チャートに1時間足のMAを表示するのであれば、〇時55分に発生した5分足の終値は1時間足の終値と同じですから、それを集計するとか。あるいは、iClose関数で1時間足の終値を取得できればそれを使っても良いかもしれません。しかし、そこまで作り込むメリットを余り感じませんね(^^;。