※当サイトはアフィリエイト広告(Amazonアソシエイツ等)を利用しています。
こんにちは。trade-engineer.com 運営者のHです。MQL4でインジケーターを自作していると、「エントリー条件に達したらアラートを鳴らしたいが、1本の足で1回だけ鳴れば十分」という場面に必ずぶつかります。ところが素直にAlert()を書くと、同じ条件が成立しているあいだ、ティックが更新されるたびに何度も何度も鳴り続けてしまいます。
この挙動はMQL4の仕様そのもので、OnCalculate(旧API ではstart)関数はティックごとに呼ばれるからです。解決策は「今の足ですでにアラートを鳴らしたかどうか」を記憶するグローバル変数を用意し、iTime関数で比較することです。仕組みはシンプルですが、応用範囲は広く、タイムフレーム指定や複数条件の独立管理、MQL5への移植まで使い回せます。本記事ではその実装を基礎から体系的に解説します。
- ティック更新のたびにアラートが鳴り続ける根本原因を理解できる
- iTime関数を使った同一足1回制限のコードをそのままコピペして使える
- タイムフレーム指定・複数条件の独立管理など応用パターンがわかる
- MQL5移植で注意すべき変数スコープの違いを事前に把握できる
MQL4でアラートを同一足1回に制限する基本実装
まず問題の構造を正確に把握し、それに対応した最小限のコードで解決します。
ティック更新で毎回アラートが鳴り続ける原因
MQL4のプログラム実行モデルを理解しておくと、この問題の原因は一目瞭然です。カスタムインジケーターではOnCalculate関数、旧API形式のEAや古いインジケーターではstart関数が、ブローカーからティックデータが届くたびに呼び出されます。1分足であっても、相場が動いているあいだは1分間に数十回から数百回ティックが来ることがあります。
条件判定コードがOnCalculateの中に素直に書かれていると、その条件が成立している時間帯はすべてのティックでAlert()が実行されます。足が確定するのは次の足が始まるときですが、それまでのあいだに条件が崩れない限り延々と鳴り続けます。たとえば5分足でRSIが70を超えたときにアラートを鳴らす設定にしていたとして、RSIが70を超えたまま2分間相場が動けば、その2分間に来た全ティック分だけアラートが鳴ります。相場が荒れているときは1分間に100ティック以上来ることもあり、スピーカーから連続音が鳴り響くことになります。これは「条件成立ごとに1回」を意図したコードではなく、「条件が成立している間ずっと」になってしまっています。
根本的な解決策は、「その足でアラートをすでに鳴らしたか否か」を何らかの変数で記憶し、鳴らし済みであればAlertをスキップする仕組みを作ることです。状態を記憶するには、関数の外——グローバルスコープ——に変数を置く必要があります。
ティック頻度はブローカーとネット環境によって大きく異なります。スキャルピング業者では1秒に10ティック以上来ることもあります。アラートの過剰発火は特にスプレッドが狭い業者で顕著です。
iTime関数で現在足を識別するフラグ変数の仕組み
MQL4にはiTime関数があり、指定したシンボルと時間軸の指定バー番号の開始時刻をdatetime型で返します。構文は以下の通りです。
datetime iTime(string symbol, int timeframe, int shift)
現在チャートの最新足(確定前の形成中の足)はshift=0で取得できます。足が新しく始まると、iTime(NULL, NULL, 0)が返す値は変わります。逆に言えば、同じ足のあいだはどのティックで呼ばれてもiTime(NULL, NULL, 0)は同じ値を返し続けます。
この性質を使ってフラグを実装します。グローバル変数SoundTimeをdatetime型で宣言しておきます。初期値はMQL4では自動的に0になります(1970-01-01 00:00:00)。アラートを鳴らすとき、SoundTimeにiTime(NULL, NULL, 0)の値を代入します。次のティックが来たとき、SoundTimeは「前回アラートを鳴らした足の開始時刻」を保持しているので、現在のiTime(NULL, NULL, 0)と比較すれば「同じ足か否か」が一瞬で判定できます。足が変わって新しい足が始まればiTime(NULL, NULL, 0)の値が変わるので、比較結果がfalseになりアラートが鳴ります。
このパターンは「前回処理した足のタイムスタンプを保存しておく」という汎用的な手法で、アラート以外にも「新しい足が始まったタイミングで1回だけ処理したい」あらゆるケースに使えます。
SoundTimeグローバル変数の宣言と初期化
コードの骨格をインジケーター形式で示します。まずファイルの先頭(すべての関数の外)でグローバル変数を宣言します。
#property indicator_chart_window #property indicator_buffers 0 datetime SoundTime;
datetime型はMQL4において64ビット整数として実装されており、1970-01-01 00:00:00 UTCからの経過秒数を保持します。グローバルスコープで宣言した場合、MT4起動時またはインジケーター初期化時に0で初期化されます。これは1970年の時刻を指しますが、実際のチャートデータの時刻と一致することはないので、初期状態で条件判定をした場合は必ずtrueになります。つまり「最初のティックが来たとき、まだ1度も鳴らしていないとみなしてアラートを鳴らす」という自然な動きになります。
ローカル変数(関数内の変数)では、関数が呼ばれるたびに初期化されるため状態を保持できません。必ずグローバルスコープで宣言することが必要です。複数の条件に対して別々にアラート管理したい場合は、SoundTime_A、SoundTime_Bのように変数を増やします。名前は自由ですが、何の足タイムスタンプを記憶しているのかが一目でわかる命名にするとコードの可読性が上がります。
OnInit関数内で明示的にSoundTime = 0;と初期化するコードを書くこともできます。これはインジケーターをチャートに追加したとき、またはパラメータを変更してリセットしたときに確実にゼロに戻す意図を明示する書き方です。動作上は省略しても同じですが、チームで開発する場合や後でコードを読み返す際の可読性向上を考えると書いておく価値はあります。
グローバル変数はプログラム全体で共有されます。複数のインジケーターを同じチャートに適用している場合、変数名が衝突することはありません(それぞれの.ex4ファイルが独立したメモリ空間を持つため)。ただし同一インジケーター内で複数の条件を管理するときは変数名を分けてください。
動作確認:ビジュアルモードとログでの検証手順
コードを書いたら動作確認が必要です。アラート系のコードは特に「鳴りすぎ」「鳴らなすぎ」どちらの問題も起きやすいため、再現性のある確認手順を持っておくと効率的です。
最もわかりやすい確認方法はMT4のストラテジーテスターをビジュアルモードで実行することです。ただしAlert()はストラテジーテスター中はポップアップが出ないため、代わりにPrint()でエキスパートログに出力してから確認します。アラート処理が入る部分にPrint("Alert鳴らし: " + TimeToStr(SoundTime));を追加しておくと、「どの足でアラートが1回だけ実行されたか」をログで追えます。
ログはMT4の「エキスパート」タブに出力されます。同じタイムスタンプが複数行出力されていれば、同一足で複数回アラートが発火していることになります。SoundTime制御が正しく機能していれば、各足のタイムスタンプは1行だけ表示されるはずです。確認が終わったらPrint()は削除するかコメントアウトしてください。デモ・本番環境でPrintが大量に走るとエキスパートタブが見づらくなります。
もう一つの確認方法は、デモ口座でリアルタイムに動かしてみることです。条件成立時にアラートが1回だけ鳴り、次の足が始まるまで再度鳴らないことを確認します。条件が成立している間に複数ティックを待って確認するには、M1(1分足)などの短い足で試験するとすぐに1足分を消化できます。MT4で音が出ない場合の設定確認も合わせてチェックしておくと、「実装は正しいが音が出ない」というケースを切り分けられます。
MQL4コード全文(インジケーター・EA両対応)
コピペで動く完全なコードを示します。インジケーター形式とEA形式の両方を掲載します。
インジケーター形式(.mq4)
#property indicator_chart_window
#property indicator_buffers 0
// グローバル変数:最後にアラートを鳴らした足の開始時刻
datetime SoundTime;
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[])
{
// ここにエントリー条件を記述する(例:直近足の高値が前足高値を超えた)
bool condition = (High[0] > High
);
if(condition)
{
// 現在の足のタイムスタンプと前回アラートを鳴らした足を比較
if(SoundTime != iTime(NULL, NULL, 0))
{
Alert("条件成立: " + Symbol() + " " + TimeToStr(Time[0]));
SoundTime = iTime(NULL, NULL, 0); // 鳴らした足を記憶
}
}
return(rates_total);
}
EA(Expert Advisor)形式(.mq4)
datetime SoundTime;
int OnTick()
{
bool condition = (iRSI(NULL, 0, 14, PRICE_CLOSE, 0) > 70);
if(condition)
{
if(SoundTime != iTime(NULL, NULL, 0))
{
Alert("RSI過買いシグナル: " + Symbol());
SoundTime = iTime(NULL, NULL, 0);
}
}
return(0);
}
インジケーターとEAでは関数名が異なりますが(OnCalculate vs OnTick)、フラグ変数の仕組みはまったく同じです。conditionの中身は自分のエントリーロジックに置き換えてください。
応用パターンと実運用での組み合わせ事例
基本実装を理解したら、実際の運用で使えるバリエーションを見ていきます。
任意タイムフレームで1回だけ鳴らす実装
デフォルトのiTime(NULL, NULL, 0)は現在チャートの時間軸を基準にします。たとえばM5チャートで動かしているインジケーターなら5分足の足開始時刻が使われます。これとは別の時間軸を基準にしたい場合はPERIOD定数を指定します。
// 5分足ベースで1回だけ鳴らす(チャート時間軸に関係なく)
if(SoundTime != iTime(NULL, PERIOD_M5, 0))
{
Alert("5分足シグナル: " + Symbol());
SoundTime = iTime(NULL, PERIOD_M5, 0);
}
// 1時間足ベースで1回だけ鳴らす
if(SoundTime_H1 != iTime(NULL, PERIOD_H1, 0))
{
Alert("H1シグナル: " + Symbol());
SoundTime_H1 = iTime(NULL, PERIOD_H1, 0);
}
上記の例ではPERIOD_M5(5分足)とPERIOD_H1(1時間足)を使っています。使えるPERIOD定数はMT4で定義されており、PERIOD_M1(1分)、PERIOD_M5(5分)、PERIOD_M15(15分)、PERIOD_M30(30分)、PERIOD_H1(1時間)、PERIOD_H4(4時間)、PERIOD_D1(日足)、PERIOD_W1(週足)、PERIOD_MN1(月足)です。
この手法はマルチタイムフレーム分析をインジケーター化するときに特に役立ちます。たとえばM5チャートを見ながらH1足が確定したタイミングでアラートを鳴らしたい場合、iTime(NULL, PERIOD_H1, 0)を基準に使えばH1足に1回だけ鳴らすことができます。異なる時間軸のシグナルを複数管理する場合は、それぞれ別のSoundTime変数(例:SoundTime_M5、SoundTime_H1)を用意して独立して管理します。
なおiTimeの第1引数は通貨ペア名の文字列ですが、NULLを渡すと現在チャートのシンボルが使われます。クロスペアの監視など別シンボルの足時刻を取得したい場合は"EURUSD"のように明示指定します。
複数アラート条件を独立管理するパターン
インジケーター1本に複数のシグナル条件を持たせる場合、条件ごとに独立したSoundTime変数を用意するのがもっとも安全で明快な実装です。
// グローバル変数(条件ごとに分ける)
datetime SoundTime_Buy; // 買いシグナル用
datetime SoundTime_Sell; // 売りシグナル用
// OnCalculate / OnTick 内
bool buyCondition = (iRSI(NULL, 0, 14, PRICE_CLOSE, 0) < 30);
bool sellCondition = (iRSI(NULL, 0, 14, PRICE_CLOSE, 0) > 70);
if(buyCondition && SoundTime_Buy != iTime(NULL, NULL, 0))
{
Alert("買いシグナル: " + Symbol());
SoundTime_Buy = iTime(NULL, NULL, 0);
}
if(sellCondition && SoundTime_Sell != iTime(NULL, NULL, 0))
{
Alert("売りシグナル: " + Symbol());
SoundTime_Sell = iTime(NULL, NULL, 0);
}
SoundTime変数を1つに共有してしまうと、買いシグナルでアラートを鳴らした後に同じ足で売りシグナルが成立しても鳴らない、という問題が起きます。条件の数だけ変数を用意するのが正解です。
さらにシグナルをチャート上にアローオブジェクトで視覚表示したい場合は、MT4上にアローオブジェクトを表示する実装と組み合わせると便利です。アラートが鳴ったタイミングで矢印をプロットすれば、バックテスト後のチャート確認でどのポイントでシグナルが出たかを視覚的に追えます。
また、SoundPlayを使って音声ファイルを鳴らす場合も同じパターンが使えます。Alert()をPlaySound("alert.wav");に置き換えるだけです。MT4のsoundsフォルダに.wavファイルを置いておく必要があります。
MQL5への移植:OnCalculate対応と変数スコープ
MT5とMQL5でも基本的な考え方はまったく同じです。ただし細かい仕様の違いがいくつかあるので移植時に注意します。
MQL5でのiTime相当はbars配列から取得する方法とiTime()関数を直接使う方法があります。MQL5のiTimeはMQL4と同じシグネチャで使えます。
// MQL5 カスタムインジケーター形式
datetime SoundTime; // グローバル変数
int OnCalculate(const int rates_total,
const int prev_calculated,
const int begin,
const double &price[])
{
bool condition = (price[rates_total-1] > price[rates_total-2]);
if(condition)
{
if(SoundTime != iTime(NULL, 0, 0))
{
Alert("条件成立: " + Symbol());
SoundTime = iTime(NULL, 0, 0);
}
}
return(rates_total);
}
MQL5でのOnCalculateは引数の形式が複数バリエーションあります。上記は価格配列を直接受け取るシンプルな形式です。OHLCV配列を使う場合は引数が増えますが、SoundTime制御のロジック部分は変わりません。
変数スコープについてはMQL4とMQL5で大きな違いはありませんが、MQL5のEA(OnTick内)で確定足ベースのシグナルを判定する場合はrates_totalの扱いと配列のインデックス方向に注意が必要です。MQL5のデフォルトは時系列順(インデックス0が最古)なので、最新バーはrates_total-1です。MQL4は逆(インデックス0が最新)なので、移植時にインデックスを修正し忘れるミスが多いです。
MQL5に移植する際、配列のインデックス方向が逆になる点に注意してください。MQL4ではHigh[0]が現在足ですが、MQL5のOnCalculateに渡される配列ではhigh[rates_total-1]が最新足です(ArraySetAsSeries等で変更可能)。
よくあるエラーと対処法
この実装で詰まりやすいポイントをまとめます。実際に何度も目にしたエラーパターンです。
エラー1: SoundTimeが未宣言でコンパイルエラー
OnCalculate内でSoundTimeを使っているのに、グローバルスコープでの宣言を忘れているケースです。undeclared identifier 'SoundTime'というコンパイルエラーが出ます。必ずすべての関数の外(先頭付近)でdatetime SoundTime;を宣言してください。
エラー2: int型やdouble型で宣言してしまうint SoundTime = 0;と書いてしまうミスです。iTime()が返すのはdatetime型なので、型の不一致で暗黙キャストが発生し、思わぬ動作につながることがあります。コンパイル自体は通る場合がありますが、datetime型で統一するのが正しいです。
エラー3: iTimeの引数を間違えるiTime(0, 0, 0)のように第1引数に整数0を渡してしまうケースがあります。第1引数はstring型のシンボル名なのでNULLまたはSymbol()を使います。整数0を渡してもコンパイルエラーにならない場合がありますが、結果は未定義動作になります。
エラー4: 確定足ベースで判定したいのに未確定足を見ているshift=0は現在形成中の足です。1本前の確定足を基準にしたい場合はiTime(NULL, NULL, 1)を使います。エントリー条件が確定足ベースのロジックなのにalertだけshift=0で判定していると、意図しないタイミングで発火することがあります。MT4からLINEへのアラート通知と組み合わせる場合も、確定足か未確定足かの基準を揃えておくことが重要です。
まとめ:インジケーターとEAへの展開
今回解説したiTimeを使ったアラート制御パターンは、「同一足で1回だけ処理を実行したい」というあらゆるシーンに汎用的に使える手法です。アラート以外にも、OrderSend(発注)を同一足で1回だけ行う制御や、ラベルオブジェクトを新しい足が始まったタイミングで更新する処理にも同じパターンが使えます。
実際の私の開発フローでは、シグナル検出ロジックとアラート発火ロジックを関数として分離しています。bool IsEntrySignal()のような関数でエントリー条件を返し、OnCalculate側でSoundTime制御と合わせて呼び出す構造にすると、ロジック変更時の修正範囲が限定されてバグが混入しにくくなります。
バックテストで検証したロジックをシグナルインジケーター化するステップとして、今回の手法は必ず必要になります。USDJPY M5で自作シグナルインジケーターを運用した際、このSoundTimeパターンでアラート発火を管理し、1日あたりの誤発火をゼロに抑えた実績があります。ぜひ自分のインジケーターに組み込んでみてください。


);
if(condition)
{
// 現在の足のタイムスタンプと前回アラートを鳴らした足を比較
if(SoundTime != iTime(NULL, NULL, 0))
{
Alert("条件成立: " + Symbol() + " " + TimeToStr(Time[0]));
SoundTime = iTime(NULL, NULL, 0); // 鳴らした足を記憶
}
}
return(rates_total);
}
コメント