MQL4のOnTimerでFX自動売買の定期処理を組む方法

MQL4のOnTimerは、設定した間隔で監視や表示更新を行うためのイベント処理です。OnInitでEventSetTimerを設定し、OnTimerに定期処理を書き、OnDeinitでEventKillTimerを呼ぶのが基本の構成になります。
FX自動売買では、ティックが来るたびの売買判定と、一定間隔で確認したい状態監視を分けたい場面があります。ただし、OnTimerへ処理を移しても同じEA内で並列に動くわけではなく、タイマーが動いたから価格も更新されたとは限りません。
この記事ではWindows版MT4で動かすEAを前提に、発注しない監視例で開始・実行・解除を確認します。MQL4のストラテジーテスターではTimerイベントが処理されないという制約も踏まえ、デモ環境で何を見ればよいかを整理します。
- OnInitでタイマーを開始しOnDeinitで解除する構成
- 売買を行わず実行回数と気配値の時刻を表示する例
- 長い処理と古い価格をタイマーの停止と混同しない見方
- MT4テスターの制約を踏まえたデモ確認の手順
MQL4のOnTimerで定期処理を始める

OnTickとの違いは処理を始めるきっかけ
OnTickは、EAを設定したチャートの銘柄に新しいティックが届いたときの処理です。OnTimerは、設定したタイマーに基づいて呼ばれる処理なので、その銘柄へのティック到着だけを実行のきっかけにしません。価格更新のたびに行いたい仕事と、一定間隔で見直したい仕事を分けられます。
たとえば、チャート上にEAの状態を表示する、取得済みデータの確認時刻を更新する、次に実行すべき軽い作業があるか調べる、といった監視が候補です。一方、各ティックでの価格条件を扱う仕様なら、OnTimerへ置き換えるだけで同じ動作になるとは考えないでください。
イベント関数は名前だけでなく型や引数も決まっています。MQL4公式のイベント処理関数では、OnTimerは戻り値なし・引数なしのvoid OnTimer()とされ、EAとインジケーターが対象です。スクリプト向けの機能ではありません。本記事のコードはEAとして扱います。
| 確認項目 | OnTick | OnTimer |
|---|---|---|
| 実行のきっかけ | 設定先銘柄の新しいティック | 登録したタイマーのイベント |
| 向く処理の例 | 価格更新を起点とした判定 | 状態表示や一定間隔の監視 |
| 価格の扱い | 受信したティックを起点にできる | 実行のたびに新しい価格があるとは限らない |
| 併用時の注意 | 同じEA内の処理として設計 | 別スレッド化と考えない |
OnTimerで監視するだけのEAなら、OnTickを必ず書かなければならないわけではありません。空のOnTickを足すより、何をきっかけに何を確認するプログラムかを明確にすると読みやすくなります。取引ロジックをすでに持つEAへ組み込む場合は、両方が同じ変数や処理を使う範囲も確認しましょう。

開始と解除を一組にして設計する
最小構成は、初期化で登録し、イベントで処理し、終了時に解除する流れです。OnTimerという関数を用意するだけでは、タイマーのイベントを受け取る設定は完了しません。EventSetTimerを呼んだ結果まで確認して、初期化を成功とするか判断します。
| 段階 | 使う関数 | この記事の例で行うこと |
|---|---|---|
| 初期化 | OnInit | 入力を確認し、監視用の状態を初期化 |
| 登録 | EventSetTimer | 秒単位の間隔を指定し、成功・失敗を確認 |
| 定期処理 | OnTimer | 実行回数、前回からの間隔、気配値の更新時刻を表示 |
| 終了処理 | OnDeinit・EventKillTimer | タイマーを解除し、例で使った表示を消す |
EventSetTimerの引数は秒数で、成功時はtrue、失敗時はfalseです。失敗の情報を調べるときはGetLastErrorを使います。EventSetTimerの公式仕様を基に、以下の例では登録に失敗したらINIT_FAILEDを返し、監視が始まったかのように見せない構成にしています。
一つのプログラムで動かせるタイマーは一つです。「5秒の監視用」と「60秒の更新用」のつもりで登録を増やす設計にはしません。複数の周期が必要な場合は、一つのOnTimerから、それぞれの作業が実行時刻に達したか判断する形を検討します。最初の確認では仕事を一つに絞ると原因を追いやすいです。

発注しない監視EAのコード例
以下は、実行回数と前回のOnTimer開始からの経過ミリ秒、対象銘柄の最終気配値時刻をチャートへ表示する例です。注文、決済、外部通信、ファイルへの書き出しは含みません。5秒は仕組みを確認するための例で、売買に有利な間隔や推奨設定を示す数字ではありません。
このコードは説明用で、この記事の作成時点ではMT4実機でのコンパイルと動作を未検証です。使用する場合は、既存EAへそのまま追記せず、別の監視用EAとしてデモ環境で確認してください。既存コードにOnInitやOnTimerがある場合は、同名関数を二重に定義できないため、統合の設計が必要です。
#property strict
input int PollSeconds = 5;
uint previousMs = 0;
long timerCalls = 0;
int OnInit()
{
if(PollSeconds < 1) return(INIT_PARAMETERS_INCORRECT);
previousMs = GetTickCount();
timerCalls = 0;
ResetLastError();
if(!EventSetTimer(PollSeconds))
{
Print("Timer registration failed: ", GetLastError());
return(INIT_FAILED);
}
Comment("Timer registered. Waiting for first event.");
return(INIT_SUCCEEDED);
}
void OnTimer()
{
uint nowMs = GetTickCount();
uint elapsedMs = nowMs - previousMs;
previousMs = nowMs;
timerCalls++;
MqlTick quote;
ResetLastError();
if(!SymbolInfoTick(Symbol(), quote) || quote.time == 0)
{
Comment("Timer calls=", timerCalls,
" / quote unavailable / error=", GetLastError());
return;
}
Comment("Timer calls=", timerCalls, " / elapsed ms=", elapsedMs,
"\nPC local=", TimeToString(TimeLocal(), TIME_DATE|TIME_SECONDS),
"\nQuote time=", TimeToString(quote.time, TIME_DATE|TIME_SECONDS),
"\nBid=", DoubleToString(quote.bid, _Digits),
" / Ask=", DoubleToString(quote.ask, _Digits));
}
void OnDeinit(const int reason)
{
EventKillTimer();
Comment("");
}
登録済みの表示は、EventSetTimerが成功したことを示すものです。OnTimerの実行確認は、最初の待機表示からTimer callsへ変わり、その回数が増えることを見ます。quote unavailableと出る場合も、回数が増えているなら、タイマーの実行と気配値の取得を別々に切り分けられます。
気配値はSymbolInfoTickの公式仕様に沿ってMqlTickへ読み込みます。成功・失敗と最終更新時刻を取得できますが、この例は価格の鮮度が十分かを自動判定しません。取れた値を表示することと、売買してよいと判定することは別の処理です。

設定秒数と実行間隔を分けて読む
PollSecondsへ入れる秒数は、タイマーの設定間隔です。elapsed msは、今回のOnTimerが始まった時点と前回の開始時点の差を表示します。5秒を設定したから、観察値が常に正確に5000になると決め付けないでください。他のイベント処理などを含む実行環境の影響を受けます。
この例のGetTickCountは、名前にTickがあっても相場のティック回数を数える関数ではありません。公式のGetTickCountはシステム起動からの経過ミリ秒を返します。uint型の値には周回があるため、例では符号なしの差として短い実行間隔を見ます。長期間の累積時間を無条件に正確に記録する用途にはしません。
一回目のelapsed msは、OnInitで基準を用意してから最初のOnTimerが始まるまでです。二回目以降は、前回のOnTimer開始から今回の開始までになります。処理本体に何ミリ秒かかったかを測る値ではないので、所要時間を調べたい場合は処理の前後で別に計測します。
定期処理には、少なくとも「どれくらいの頻度で確認したいか」「一回にどこまで処理するか」「遅れた場合はどうするか」を決めます。間隔だけを短くしても、長い処理は軽くなりません。監視内容を増やす前に、軽い表示例で実行を確認し、必要な処理を一つずつ追加する方が判断しやすくなります。
解除と再初期化まで確認する
タイマーを使い終わるときは、OnDeinitでEventKillTimerを呼びます。EventKillTimerの公式説明でも、OnInitで開始したタイマーをOnDeinitで止める使い方が示されています。プログラム終了時には残ったタイマーも強制的に破棄されますが、だから解除処理を書かなくてよい、と考えないようにします。
入力値やチャートの銘柄・時間足を変えた場合などにも、初期化と終了処理の流れを意識します。タイマーの設定だけでなく、回数や前回時刻をどこでリセットするかも決めてください。例ではOnInitでtimerCallsを0へ戻すので、再初期化後の回数は以前の続きではありません。
OnDeinitは、引数を含めてvoid OnDeinit(const int reason)という形で書きます。解除のあとに長い待機や通信を始める構成は避けます。公式の端末イベント仕様には終了処理の時間制限もあるため、終了時には必要な後片付けを短く済ませる設計にします。
例のCommentはチャート上の文字表示を使うため、終了時に空文字で消しています。既存EAも同じ表示領域を使っているなら、表示が上書きされる可能性を確認してください。まず専用のデモ用チャートで開始・更新・解除を一巡させれば、他の表示処理に隠れて動作が分からなくなる状況を減らせます。
MQL4のOnTimerをFX自動売買で検証する

イベントは同じEA内で順番に処理される
OnTimerは、処理を後ろで並列実行するための機能ではありません。MQL4の各プログラムにはイベントの待ち行列があり、同じプログラムのイベントは受け取った順に処理されます。OnTickが長く動いている間に、同じEAのOnTimerだけが別スレッドで自由に進む、という構造ではありません。
また、Timerイベントがすでに待機中、または処理中なら、新しいTimerイベントはそのプログラムの待ち行列へ追加されません。MQL4公式のプログラム実行仕様に、この逐次処理とキューの扱いが説明されています。遅れた分がすべて蓄積し、後から回数どおり実行されることは前提にできません。
説明用に5秒間隔のタイマーで、一回の監視処理に12秒かかったとします。このとき、5秒ごとの処理が12秒の処理と重なって複数並列に進むわけではありません。処理中に来たタイマー通知も、そのまますべて後続実行へ残るわけではありません。この数字は挙動を考えるための仮例です。
したがって、Timer callsが12回増えたから正確に1分が経過した、という時間計算もしません。一定時刻に実行したい仕事では、回数に秒数を掛けるだけでなく、基準となる時計と実行予定時刻を定義します。遅れて気付いた場合に一回だけ実行するのか、見送るのかも、仕事ごとに決める必要があります。
同じ処理をOnTickとOnTimerの両方から呼ぶ場合は、逐次処理でも重複実行の条件は整理します。たとえば一度確認済みの対象を再び処理しないために、対象IDや最終処理時刻を持つ方法があります。並列の競合がないことと、業務上の二重処理が起こらないことは別です。
長いループ、待機、外部通信は、タイマーへ移しただけでは軽くなりません。Sleepは現在のEAやスクリプトを指定時間待機させます。さらにWebRequestは同期処理で、応答を待つ関数です。外部通信の許可や失敗の確認は、MT4のWebRequest設定と通信確認で詳しく扱っています。
長い一覧を監視するなら一回に処理する件数を区切り、次回に続きを扱うなど、戻りの早い処理を検討します。通信を含める場合は、必要な頻度、タイムアウト、失敗時の再試行条件を別に定義します。短い間隔で失敗を繰り返す構成にせず、一回のイベントから制御が戻ることを優先してください。

タイマー実行と価格の鮮度を分ける
OnTimerの回数が増えても、銘柄のBidやAskが新しくなっているとは限りません。ティックを待たずに監視できることと、新しい市場データを作れることは別です。監視処理が動いた時刻、対象銘柄の気配値が更新された時刻、価格そのものを分けて見ます。
特にTimeCurrentを「タイマーが動いた現在時刻」と思い込まないことが大切です。TimeCurrentの公式説明では、OnTimer内では気配値表示にあるいずれかの銘柄の、最後に分かっている気配受信時刻を返すとされています。対象銘柄の最終更新時刻とは限らず、新しい受信がなければ同じ値のままになることがあります。
| 見たい情報 | 例で使う値 | 読み方 |
|---|---|---|
| 処理の進行 | timerCalls・elapsedMs | OnTimerの実行と前回開始からの間隔 |
| PC側の表示時刻 | TimeLocal | 端末を動かすコンピューターのローカル時刻 |
| 対象銘柄の価格更新時刻 | SymbolInfoTickで得たquote.time | その銘柄の最終気配更新の時刻 |
| 最新既知のサーバー時刻 | TimeCurrent | OnTimer内では他銘柄の受信時刻も反映し得る |
例のPC localとQuote timeは、時計の基準も異なり得ます。両者をそのまま引いて「価格が何秒古い」と判定しないでください。PCの時刻設定とサーバー側の時刻の対応、対象銘柄の取引時間、許容する更新間隔を整理してから、鮮度を判定する条件を作ります。
価格が更新されないときも、すぐにEAの故障とは決められません。監視の実行回数が進むのか、気配取得が成功するのか、対象銘柄の更新時刻が変わるのかを分けます。例では価格取得に失敗した場合にも回数を表示するため、表示された古い価格を繰り返し新着データとして扱いません。
確定足に基づく別時間足の判定を追加する場合は、タイマーの周期だけで足の確定を判断しません。参照する足の開始時刻と、判定できる時点をそろえる必要があります。足の対応はEAのマルチタイムフレームと確定足のそろえ方で確認し、本記事ではイベントの実行管理と分けて扱います。

MT4テスターではTimerイベントを検証できない
MQL4で特に見落としたくないのがテスターの制約です。MQL4公式のストラテジーテスター仕様では、TimerイベントとChartEventはテスター内で処理されないと明記されています。OnInitで登録処理が通ったように見えても、通常のチャートと同じOnTimerの呼び出しを期待してはいけません。
そのため、今回の監視EAをテスターへ載せて回数が増えない場合、最初からTimer対応を前提に原因探しをしないようにします。「OnTimerがないときでも呼ばれるはず」「ビジュアルモードなら同じ」と推測せず、MQL4の実行環境として仕様を確認してください。
MQL5やMT5の説明には、テスターでのタイマーの扱いが異なるものがあります。関数名やコードの形が似ていても、MQL4のテスターの根拠にはできません。検索結果の見出しだけで判断せず、資料がどの言語・端末・実行環境を説明しているか確かめることが必要です。
判定ロジックを関数へ分離すれば、テスターで検証できる部分とデモ環境で確認する部分を分ける設計は可能です。ただし、その関数をOnTickから呼んだ結果は、タイマーの周期や待ち行列まで再現した証明にはなりません。関数の計算結果の確認と、イベントの動作確認を別に報告します。
デモ環境で開始から停止まで確認する
最初は、既存EAを動かしていないデモ用チャートで確認します。MetaEditorでEA用のファイルを用意し、コード全体を読み、注文関数や外部送信が追加されていないことを確かめてからコンパイルします。実機未検証の例なので、エラーや警告が出た場合は、その内容を解消してから動作を見ます。
デモ用チャートへEAを設定し、例の間隔が正の秒数になっていることと、登録後の待機表示を確認します。
Timer callsが増えるかを見ます。気配の取得結果と更新時刻も別々に読み、同じ価格でも回数が進むかを区別します。
入力間隔を変えた後の再初期化を確認し、最後にEAを外します。例の表示が消え、監視を終了できるところまで確かめます。

動かない場合は、EAとして設定したか、OnTimerの型や引数が正しいか、EventSetTimerが成功したかの順に見ます。そのうえで、MT4とEAが動作しているか、他の処理が戻らなくなっていないかを調べます。コンパイル成功だけでは登録や実行を確認したことにはなりません。
回数は増えるのに価格欄が更新されない場合は、タイマーの登録を繰り返し直すのではなく、銘柄の気配値取得と更新時刻へ確認対象を移します。間隔がばらつく場合は、同じEA内の重い処理がないか、処理時間を計測して確かめます。症状に対応する場所だけを見直すと、設定変更を増やさずに切り分けられます。
監視例で開始・定期更新・解除を確認できたら、実際に必要な仕事を一つ追加し、同じ確認を繰り返します。複数銘柄、通信、複数周期の処理をまとめて足すと、どの変更で動作が変わったか分かりにくくなります。実行間隔と価格の鮮度を確認したことを、売買成績の評価へ読み替えないようにしてください。
MQL4のOnTimerは短い処理から確かめる
MQL4のOnTimerは、OnInitでEventSetTimerを登録し、OnTimerで必要な仕事を行い、OnDeinitでEventKillTimerを呼ぶ流れで設計します。登録結果を確認し、状態の初期化と解除まで一組にすると、定期処理が動かなかったときの確認箇所も明確になります。
FX自動売買へ組み込むときは、同じEA内のイベントが順番に処理されること、処理中のTimerイベントは追加され続けないこと、実行できても価格が新しいとは限らないことを押さえます。長時間処理をタイマーへ移すだけでは、待ち時間や負荷の問題は解決しません。
最初の目標は、発注しない監視例で回数と時刻を確認し、デモ環境で終了まで確かめることです。MT4テスターで確認できる部分と、OnTimerの実際の動作を確認する部分を分けたうえで、必要な監視へ少しずつ広げていきましょう。
