恥は/dev/nullへ by 初心者

~ PC初心者による右往左往の記録 ~

ローソク足の確定を音声で通知する(MQL5)

トレードでは、エントリーできる状況が訪れるまで待つ時間が長いですよね。この時間がもったいないので、節目となる価格や意識されているラインにアラートを仕掛けておいて、アラートが鳴るまでは他の作業をしています。

これ以外で「待つ」というと、ローソク足の確定を待つ場面があります。これも無駄な待ち時間だと思うことが時々あるので、(ローソク足が確定した時にだけチャートを見るために)確定を知らせてくれるインディケーターを作りました。

ざっくり言うと、現在足が確定した時に「足が確定しました」という音声を流すインディケーターです。

ちなみに、相変わらずテストやエラーチェックを行う処理は省いています(本来は大切なことですが、自分しか使わないので横着しています)。

目次


音声ファイルの用意

まず音声ファイルを用意します。私は以下のサイトを利用しました。

音読さん
https://ondoku3.com/ja/

このサイトで、音声として読み上げたいテキストを入力すると、それを音声ファイルにしてくれます。今回は「足が確定しました」という音声ファイルを作成しました。

ところで、MT5から出る効果音の音源はwaveファイルです。一方、「音読さん」で作成されるファイルはmp3ファイルです。そのため、mp3からwaveに変換する必要があります。私はffmpegというソフトで変換しました。

完成したファイル

ファイル名   candle_fixed.wav
中身        「足が確定しました」という音声


最後に、この.wavファイルを以下のフォルダに入れました。

Cドライブ → Program Files → (MT5のフォルダ) → Sounds  


コード

#property copyright "Copyright 2025, PHILOJUAN"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0
#property strict

// ボタンのパラメーター
input int buttonXdis1     = 200;           // ボタンの表示位置(X軸)
input int buttonYdis1     = 5;             // ボタンの表示位置(Y軸)
input color colorOfBorder = clrNONE;       // ボタンの縁の色
input color colorOfButtonText = clrWhite;  // ボタン内のテキストの色

// グローバル変数
bool   notificationIsAvailable = true;     // 制御変数(音声による通知を行うか否か)
string prefix = "BRGZPMQ";                 // オブジェクト名の接頭辞
string buttonName1 = prefix + "_NOTIFY_BUTTON";
datetime timeRecord = D'2000.01.01 00:00';  // ローソク足の発生時刻を記録する変数

// ターミナルのグローバル変数名と代入する値 (tgv は Terminal Global Variableの略)
string tgvName = "MVVKKZ_CANDLE_FIXED_" + IntegerToString(ChartID());
const double TRUE  = 1.0;
const double FALSE = 0.0;

//---------------------------------------------------------------------------------
// インジ挿入時 or TimeFrame変更時
//---------------------------------------------------------------------------------
int OnInit()
{
    // [初期化メモ]
    // notificationIsAvailableは、宣言時にtrueを代入済み。
    // ボタンの押下状態に関しては、CreateButton関数内でOBJPROP_STATEを
    // trueにしている。

    // 現在足の発生時刻でtimeRecordを初期化
    timeRecord = iTime(NULL, 0, 0);

    // 通知のON・OFFを切り替えるボタンを配置 (NotiはNotifyの略)
    CreateButton(buttonName1, "Noti_ON", CORNER_RIGHT_UPPER, buttonXdis1, buttonYdis1,
                 60, 17, 7, colorOfButtonText, colorOfBorder, clrGray);

    // ターミナルのグローバル変数の値を元に、ボタンや変数の状態を復元
    if(GlobalVariableGet(tgvName) == FALSE) {
        ObjectSetInteger(0, buttonName1, OBJPROP_STATE, false);
        ChangeNotifyAndButtonText(buttonName1);
    }

    return INIT_SUCCEEDED;
}

//---------------------------------------------------------------------------------
// インジ除去時 or TimeFrame変更時
//---------------------------------------------------------------------------------
void OnDeinit(const int reason)
{
    // ボタンを削除(オブジェクトを増やした場合に備えて、prefixで消去)
    ObjectsDeleteAll(0, prefix);
    ChartRedraw();
}


//---------------------------------------------------------------------------------
// Tick受信時
//---------------------------------------------------------------------------------
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[])
{
    // 通知設定がOFFの場合、以降のプロセスを省略する
    if(notificationIsAvailable == false) return 0;

    // 最新足の発生時刻
    datetime generatedTime = time[rates_total - 1];

    // timeRecordの更新 (ローソク足1本分よりも長い時間が経過していた場合)
    if(generatedTime - timeRecord > PeriodSeconds(_Period)) {
        timeRecord = generatedTime;
        return 0;
    }
    // [目的] 通知をONにしたままMT5を終了し、しばらく経ってから起動した場合、
    //        起動と同時にwaveファイルが再生される。これを回避する。

    // 新たなローソク足が発生したら、waveファイルを再生する
    if(timeRecord != generatedTime) {
        PlaySound("candle_fixed.wav");
        timeRecord = generatedTime;
    }

    return 0;
}


//---------------------------------------------------------------------------------
// ボタン押下時
//---------------------------------------------------------------------------------
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
    if(id == CHARTEVENT_OBJECT_CLICK && sparam == buttonName1) {

        // timeRecordの更新
        timeRecord = iTime(NULL, 0, 0);

        // ボタンの状態に応じて各種変更を行う
        ChangeNotifyAndButtonText(buttonName1);
    }
}

//----------------------------------------------------------------------------
// ボタンの押下状態に応じて、変数の内容やボタンテキストを変更する関数
//----------------------------------------------------------------------------
void ChangeNotifyAndButtonText(string objName)
{
    // ボタンの押下状態を取得
    long buttonState = ObjectGetInteger(0, objName, OBJPROP_STATE);

    // 通知のON・OFFを変更
    notificationIsAvailable = buttonState ? true : false;

    // ターミナルのグローバル変数にボタンの押下状態を保存
    double num = buttonState ? TRUE : FALSE;
    GlobalVariableSet(tgvName, num);

    // ボタン内のテキストを変更
    string onText    = "Noti_ON";
    string offText   = "Noti_OFF";
    ObjectSetString (0, objName, OBJPROP_TEXT, buttonState ? onText : offText);
    ChartRedraw();
}


//----------------------------------------------------------------------------
// ボタンを作成する関数
//----------------------------------------------------------------------------
void CreateButton(string name,        // オブジェクト名
                  string caption,     // ボタン内に表示するテキスト
                  int corner,         // 基点(ボタンを配置する時に基準とする場所)
                  int xshift,         // 基点からの距離(横方向)
                  int yshift,         // 基点からの距離(縦方向)
                  int xsize,          // ボタンのサイズ(横)
                  int ysize,          // ボタンのサイズ(縦)
                  int font_Size,      // ボタン内に表示するテキストのサイズ
                  color textColor,    // ボタン内に表示するテキストの色
                  color bo_color,     // ボタンの縁の色
                  color bg_Color)     // ボタンの色
{
    ObjectCreate(    0, name, OBJ_BUTTON, 0, 0, 0);
    ObjectSetInteger(0, name, OBJPROP_CORNER, corner);
    ObjectSetInteger(0, name, OBJPROP_XDISTANCE, xshift);
    ObjectSetInteger(0, name, OBJPROP_YDISTANCE, yshift);
    ObjectSetInteger(0, name, OBJPROP_XSIZE, xsize);
    ObjectSetInteger(0, name, OBJPROP_YSIZE, ysize);

    ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, bo_color);
    ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg_Color);

    ObjectSetString (0, name, OBJPROP_TEXT, caption);
    ObjectSetString (0, name, OBJPROP_FONT, "Arial");
    ObjectSetInteger(0, name, OBJPROP_FONTSIZE, font_Size);
    ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);

    ObjectSetInteger(0, name, OBJPROP_STATE, true);     // このインジではボタン設置時に trueにしている。
    // ObjectSetInteger(0, name, OBJPROP_STATE, false);
    ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);

    ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
    ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
    ObjectSetInteger(0, name, OBJPROP_ZORDER, 1);
}


OnChartEvent関数にある「timeRecordの更新」

1週間後には忘れてしまいそうなので、OnChartEvent関数の中にある次の行の目的を書いておきます。

// timeRecordの更新
timeRecord = iTime(NULL, 0, 0);


チャート上に配置したボタンをクリックして音声通知をONにすると、ローソク足がまだ形成途中なのに(確定していないのに)waveファイルが再生されるケースがありました。これを回避することが目的です。

<状況>
「通知をOFFにした時点のローソク足」の次足が確定するまでの間に通知をONにすると、即座に(Tick受信時に)waveファイルが再生されます。

文字で読むと分かりにくいので、図を描きました。

A足時点で音声通知をOFFにします。それから、A足の次に登場したB足が確定する前に(B足が伸びたり縮んだりしている間に)音声通知をONにします。すると、ONにした後、最初のTickを受信した時点で「足が確定しました」と音声が流れます。
一方、C足が登場してからONにした場合は「ローソク足が確定していないのに音声が流れる」という事態は生じません。

<原因>
原因は次のとおりです(1分足を例にしています)。

1分足チャートで、11時20分台(11:20:00 ~ 11:20:59)に通知をOFFにして、11時21分台(11:21:00 ~ 11:21:59)にONにした場合、変数の中身は次のようになっています。

timeRecord      =   11:20:00  
generatedTime   =   11:21:00  

よって、OnCalculate関数内の

if(timeRecord != generatedTime) {
        PlaySound("candle_fixed.wav");
        timeRecord = generatedTime;
    }

が実行されます(音声ファイルが再生されます)。

一方、11時22分台(11:22:00 ~ 11:22:59)にONにした場合、変数の中身は

timeRecord      =   11:20:00  
generatedTime   =   11:22:00  

となっていますが、両者の差がローソク足1本分(60秒)よりも大きいので、OnCalculate関数内にある以下のif文が実行されます。

if(generatedTime - timeRecord > PeriodSeconds(_Period)) {
    timeRecord = generatedTime;
    return 0;
}


この結果、2つの変数の中身は同じになります。

timeRecord      =   11:22:00  
generatedTime   =   11:22:00  


このため、ONにした時点の現在足が確定して次足が発生するまで(11:23:00になるまで)、音声ファイルが再生されることはありません。


<OnCalculate関数における「timeRecordの更新」>
上述の例において、11時21分台にONにした場合、時間としては最大1分59秒という幅があります。しかし、ローソク足の発生時刻は常に00秒なので(timeRecordは11:20:00 で、 generatedTimeは11:21:00)、timeRecordとgeneratedTimeの差は足1本分(60秒)です。

このため、OnCalculate関数内にある以下のif文は実行されません(timeRecordは更新されません)。

if(generatedTime - timeRecord > PeriodSeconds(_Period)) {
    timeRecord = generatedTime;
    return 0;
}


かといって、このif文の「 > 」を「 >= 」にすると、ローソク足が確定した時に音声ファイルが再生されなくなります。


そこで、ひとまずボタン押下時に以下の処理を入れました。

// timeRecordの更新
timeRecord = iTime(NULL, 0, 0);


ただ、どうにもビミョーだと感じているので、もっとマシなロジックを考えたいところです。

余談

1分足を例にしてOnChartEvent関数の中でtimeRecordを更新している理由を書きましたが、1分足チャートでこのインディケーターを使うことはまずありません(^^;。