【MQL4関数】OnTimer関数とは?一定時間ごとに実行されるイベント関数

関数

OnTimer関数の基本

OnTimer関数は、一定時間(タイマー間隔)が経過するたびに自動的に呼び出されるイベントハンドラ関数です。チャートのティック(価格変動)に依存せず、指定した時間間隔で定期的に処理を実行できるため、定期的なデータ取得、ログ出力、監視処理などに非常に便利です。

関数のシグネチャ

void OnTimer(void);

OnTimer関数には引数も戻り値もありません。この関数を使うためには、事前にEventSetTimer()またはEventSetMillisecondTimer()でタイマーを起動しておく必要があります。

タイマーの起動と停止

関数名 説明
EventSetTimer(int seconds) 秒単位でタイマー間隔を設定(最小1秒)
EventSetMillisecondTimer(int milliseconds) ミリ秒単位でタイマー間隔を設定(最小数十ms程度)
EventKillTimer() タイマーを停止する

基本的な使い方の流れ

  1. OnInit()内でEventSetTimer()を呼び出してタイマーを起動する
  2. 指定した間隔ごとにOnTimer()が自動的に呼ばれる
  3. OnDeinit()内でEventKillTimer()を呼び出してタイマーを停止する
// OnTimer関数の最も基本的な構造
int OnInit()
{
   // 5秒ごとにOnTimerを呼び出すよう設定
   EventSetTimer(5);
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
   // タイマーを停止(リソース解放)
   EventKillTimer();
}

void OnTimer()
{
   // 5秒ごとにここが実行される
   Print("タイマーイベント発生: ", TimeToString(TimeCurrent(), TIME_SECONDS));
}

プログラム例1:定期的にスプレッドを記録するインジケーター

最もシンプルな例として、一定間隔でスプレッドをエキスパートログに記録するインジケーターです。ティックが発生しない閑散相場でも確実にスプレッド情報を取得できます。

//+------------------------------------------------------------------+
//| スプレッド定期記録インジケーター                                    |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property strict

// 入力パラメータ:記録間隔(秒)
input int InpInterval = 10; // 記録間隔(秒)

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // 指定秒数ごとにタイマーイベントを発生させる
   EventSetTimer(InpInterval);
   
   Print("スプレッド記録開始 - 間隔: ", InpInterval, "秒");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理関数                                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // タイマーを確実に停止する
   EventKillTimer();
   Print("スプレッド記録終了");
}

//+------------------------------------------------------------------+
//| タイマーイベントハンドラ                                           |
//+------------------------------------------------------------------+
void OnTimer()
{
   // 現在のスプレッドをポイント単位で取得
   int spreadPoints = (int)MarketInfo(Symbol(), MODE_SPREAD);
   
   // Bid/Askからスプレッドを実数値で計算
   double spreadValue = MarketInfo(Symbol(), MODE_ASK) - MarketInfo(Symbol(), MODE_BID);
   
   // ログに出力
   Print(Symbol(), " スプレッド: ", spreadPoints, " points (", 
         DoubleToString(spreadValue, (int)MarketInfo(Symbol(), MODE_DIGITS)), ")");
}

//+------------------------------------------------------------------+
//| OnCalculate(インジケーター必須関数)                              |
//+------------------------------------------------------------------+
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[])
{
   return(rates_total);
}

この例ではEventSetTimer(10)により10秒ごとにスプレッドを記録しています。ティックベースのOnCalculateでは価格変動がないとデータを取れませんが、タイマーを使えば一定間隔で確実にデータを収集できます。

プログラム例2:タイマーでチャート上に時計を表示するインジケーター

チャート上にサーバー時間とローカル時間をリアルタイム表示する実用的なインジケーターです。1秒ごとにタイマーで更新するため、ティックがなくても時計が動き続けます。

//+------------------------------------------------------------------+
//| チャート時計インジケーター                                         |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property strict

// 入力パラメータ
input color InpColorServer = clrWhite;  // サーバー時間の文字色
input color InpColorLocal  = clrYellow; // ローカル時間の文字色
input int   InpFontSize    = 12;        // フォントサイズ
input ENUM_BASE_CORNER InpCorner = CORNER_RIGHT_UPPER; // 表示コーナー

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   // 1秒ごとに更新
   EventSetTimer(1);
   
   // サーバー時間ラベルを作成
   CreateLabel("LabelServerTime", "Server: --:--:--", InpColorServer, 10, 20);
   // ローカル時間ラベルを作成
   CreateLabel("LabelLocalTime", "Local: --:--:--", InpColorLocal, 10, 40);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   
   // ラベルオブジェクトを削除
   ObjectDelete(0, "LabelServerTime");
   ObjectDelete(0, "LabelLocalTime");
}

//+------------------------------------------------------------------+
//| タイマーイベント:1秒ごとに時計を更新                              |
//+------------------------------------------------------------------+
void OnTimer()
{
   // サーバー時間を取得して表示を更新
   string serverTimeStr = "Server: " + TimeToString(TimeCurrent(), TIME_SECONDS);
   ObjectSetString(0, "LabelServerTime", OBJPROP_TEXT, serverTimeStr);
   
   // ローカル時間を取得して表示を更新
   string localTimeStr = "Local: " + TimeToString(TimeLocal(), TIME_SECONDS);
   ObjectSetString(0, "LabelLocalTime", OBJPROP_TEXT, localTimeStr);
   
   // チャートを再描画して即座に反映
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| ラベルオブジェクト作成用のヘルパー関数                              |
//+------------------------------------------------------------------+
void CreateLabel(string name, string text, color clr, int xDist, int yDist)
{
   // 既存のオブジェクトがあれば削除
   ObjectDelete(0, name);
   
   // ラベルオブジェクトを作成
   ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, InpCorner);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, xDist);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, yDist);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetString(0, name, OBJPROP_FONT, "Arial Bold");
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, InpFontSize);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
}

//+------------------------------------------------------------------+
//| OnCalculate(インジケーター必須)                                  |
//+------------------------------------------------------------------+
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[])
{
   return(rates_total);
}

このインジケーターはタイマーの代表的な活用例です。EventSetTimer(1)で毎秒更新することで、秒単位の時計をチャート上に実現しています。ChartRedraw()を呼ぶことで、オブジェクトの変更が即座に画面に反映されます。

プログラム例3:一定時間ごとにトレーリングストップを実行するEA

ティックベースではなくタイマーベースでトレーリングストップを管理するEAです。ティックが少ない通貨ペアや時間帯でも、確実にストップロスの調整が行われるメリットがあります。

//+------------------------------------------------------------------+
//| タイマー式トレーリングストップEA                                    |
//+------------------------------------------------------------------+
#property strict

// 入力パラメータ
input int    InpCheckInterval  = 3;     // チェック間隔(秒)
input double InpTrailDistance  = 30.0;  // トレーリング距離(pips)
input double InpTrailStep     = 10.0;  // トレーリングステップ(pips)
input int    InpMagicNumber   = 12345; // マジックナンバー

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   // 指定秒数ごとにトレーリングストップをチェック
   EventSetTimer(InpCheckInterval);
   
   Print("タイマー式トレーリングストップ起動 - チェック間隔: ", InpCheckInterval, "秒");
   Print("トレーリング距離: ", InpTrailDistance, " pips, ステップ: ", InpTrailStep, " pips");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   Print("タイマー式トレーリングストップ停止");
}

//+------------------------------------------------------------------+
//| タイマーイベント:定期的にトレーリングストップを適用                 |
//+------------------------------------------------------------------+
void OnTimer()
{
   // 全オーダーをスキャンしてトレーリングストップを適用
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      // オーダーを選択
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;
      
      // 対象通貨ペアとマジックナンバーをフィルタ
      if(OrderSymbol() != Symbol())
         continue;
      if(OrderMagicNumber() != InpMagicNumber)
         continue;
      
      // トレーリングストップ処理を実行
      ApplyTrailingStop(OrderTicket(), OrderType(), OrderOpenPrice(), OrderStopLoss());
   }
}

//+------------------------------------------------------------------+
//| トレーリングストップの適用                                         |
//+------------------------------------------------------------------+
void ApplyTrailingStop(int ticket, int orderType, double openPrice, double currentSL)
{
   double point = MarketInfo(Symbol(), MODE_POINT);
   int digits   = (int)MarketInfo(Symbol(), MODE_DIGITS);
   
   // pipsをポイントに変換(3桁/5桁対応)
   double pipMultiplier = (digits == 3 || digits == 5) ? 10.0 : 1.0;
   double trailPoints   = InpTrailDistance * pipMultiplier * point;
   double stepPoints    = InpTrailStep * pipMultiplier * point;
   
   double newSL = 0;
   
   if(orderType == OP_BUY)
   {
      double bid = MarketInfo(Symbol(), MODE_BID);
      
      // 現在価格がトレーリング距離以上の利益に達しているか
      if(bid - openPrice >= trailPoints)
      {
         // 新しいストップロスを計算
         newSL = NormalizeDouble(bid - trailPoints, digits);
         
         // 現在のSLよりステップ分以上改善される場合のみ変更
         if(newSL > currentSL + stepPoints || currentSL == 0)
         {
            if(OrderModify(ticket, openPrice, newSL, OrderTakeProfit(), 0, clrGreen))
               Print("Buy #", ticket, " トレーリングSL更新: ", DoubleToString(newSL, digits));
            else
               Print("トレーリングSL更新失敗 #", ticket, " Error: ", GetLastError());
         }
      }
   }
   else if(orderType == OP_SELL)
   {
      double ask = MarketInfo(Symbol(), MODE_ASK);
      
      // 現在価格がトレーリング距離以上の利益に達しているか
      if(openPrice - ask >= trailPoints)
      {
         // 新しいストップロスを計算
         newSL = NormalizeDouble(ask + trailPoints, digits);
         
         // 現在のSLよりステップ分以上改善される場合のみ変更
         if(newSL < currentSL - stepPoints || currentSL == 0)
         {
            if(OrderModify(ticket, openPrice, newSL, OrderTakeProfit(), 0, clrRed))
               Print("Sell #", ticket, " トレーリングSL更新: ", DoubleToString(newSL, digits));
            else
               Print("トレーリングSL更新失敗 #", ticket, " Error: ", GetLastError());
         }
      }
   }
}

//+------------------------------------------------------------------+
//| OnTick(EA必須関数 - ここではエントリーロジック等を記述可能)       |
//+------------------------------------------------------------------+
void OnTick()
{
   // エントリーロジックはOnTickに記述
   // トレーリングストップはOnTimerで処理しているため、ここでは不要
}

OnTickでトレーリングストップを処理する一般的な方法と異なり、タイマー方式では価格変動がなくても定期的にチェックが行われます。特に流動性の低い通貨ペアや早朝の時間帯において、ストップロスの調整漏れを防ぐ効果があります。

プログラム例4:ミリ秒タイマーで高頻度監視を行うEA

EventSetMillisecondTimer()を使い、ミリ秒単位の高頻度で複数通貨ペアの価格を監視する例です。急変検知やアラート通知などの用途に適しています。

//+------------------------------------------------------------------+
//| 高頻度マルチ通貨監視EA                                             |
//+------------------------------------------------------------------+
#property strict

// 入力パラメータ
input int    InpTimerMs       = 500;    // タイマー間隔(ミリ秒)
input double InpAlertPips     = 5.0;    // アラート閾値(pips)
input bool   InpSendPush      = false;  // プッシュ通知を送信するか

// 監視対象通貨ペア
string WatchSymbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD"};

// 前回の価格を保存する配列
double PrevBid[];
// 最後にアラートを出した時刻(連続アラート防止用)
datetime LastAlertTime[];

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   int count = ArraySize(WatchSymbols);
   
   // 配列を初期化
   ArrayResize(PrevBid, count);
   ArrayResize(LastAlertTime, count);
   
   // 各通貨ペアの初期Bid価格を取得
   for(int i = 0; i < count; i++)
   {
      PrevBid[i] = MarketInfo(WatchSymbols[i], MODE_BID);
      LastAlertTime[i] = 0;
      
      // 通貨ペアがマーケットウォッチに存在するか確認
      if(PrevBid[i] == 0)
         Print("警告: ", WatchSymbols[i], " の価格を取得できません。マーケットウォッチに追加してください。");
   }
   
   // ミリ秒単位でタイマーを設定
   if(!EventSetMillisecondTimer(InpTimerMs))
   {
      Print("ミリ秒タイマーの設定に失敗しました");
      return(INIT_FAILED);
   }
   
   Print("マルチ通貨監視開始 - 間隔: ", InpTimerMs, "ms, 閾値: ", InpAlertPips, " pips");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   Print("マルチ通貨監視終了");
}

//+------------------------------------------------------------------+
//| タイマーイベント:ミリ秒間隔で価格変動を監視                       |
//+------------------------------------------------------------------+
void OnTimer()
{
   int count = ArraySize(WatchSymbols);
   
   for(int i = 0; i < count; i++)
   {
      // 現在のBid価格を取得
      double currentBid = MarketInfo(WatchSymbols[i], MODE_BID);
      
      // 価格が取得できない場合はスキップ
      if(currentBid == 0)
         continue;
      
      // 前回からの変動幅を計算
      double change = currentBid - PrevBid[i];
      
      // pipsに変換
      int digits = (int)MarketInfo(WatchSymbols[i], MODE_DIGITS);
      double point = MarketInfo(WatchSymbols[i], MODE_POINT);
      double pipMultiplier = (digits == 3 || digits == 5) ? 10.0 : 1.0;
      double changePips = change / (point * pipMultiplier);
      
      // 変動幅が閾値を超えた場合にアラート
      if(MathAbs(changePips) >= InpAlertPips)
      {
         // 連続アラート防止(最低30秒間隔)
         if(TimeCurrent() - LastAlertTime[i] >= 30)
         {
            string direction = (changePips > 0) ? "上昇" : "下落";
            string msg = StringFormat("%s 急変検知! %s %.1f pips (%.5f → %.5f)",
                                      WatchSymbols[i], direction, 
                                      MathAbs(changePips), PrevBid[i], currentBid);
            
            // ログとアラート出力
            Print(msg);
            Alert(msg);
            
            // プッシュ通知(設定が有効な場合)
            if(InpSendPush)
               SendNotification(msg);
            
            LastAlertTime[i] = TimeCurrent();
         }
      }
      
      // 現在の価格を次回比較用に保存
      PrevBid[i] = currentBid;
   }
}

//+------------------------------------------------------------------+
//| OnTick(EA必須関数)                                              |
//+------------------------------------------------------------------+
void OnTick()
{
   // メインのトレードロジックはここに記述可能
   // 監視処理はOnTimerで独立して動作する
}

EventSetMillisecondTimer(500)により0.5秒ごとに複数通貨ペアの価格を監視し、急激な変動を検知してアラートを出します。連続アラート防止のために最低30秒の間隔を設けている点がポイントです。

OnTimer関数のポイントと注意事項

タイマーの基本ルール

  • 1つのEA/インジケーターにつきタイマーは1つだけEventSetTimer()を複数回呼ぶと、最後の設定で上書きされます。複数の異なる間隔で処理を行いたい場合は、最小間隔でタイマーを設定し、OnTimer内でカウンタを使って分岐させます。
  • OnDeinit()でEventKillTimer()を必ず呼ぶ — タイマーの停止を忘れると、EAやインジケーターを削除した後もリソースが解放されない可能性があります。
  • タイマーの精度には限界がある — 特にミリ秒タイマーの場合、OSやMT4の処理負荷により正確な間隔にならないことがあります。

複数間隔の処理をOnTimerで実現する方法

// 1秒タイマーで複数の処理間隔を実現する例
int timerCount = 0; // タイマーカウンター

int OnInit()
{
   // 最小間隔(1秒)でタイマーを設定
   EventSetTimer(1);
   timerCount = 0;
   return(INIT_SUCCEEDED);
}

void OnTimer()
{
   timerCount++;
   
   // 毎秒実行する処理
   UpdateClockDisplay();
   
   // 5秒ごとに実行する処理
   if(timerCount % 5 == 0)
   {
      CheckSpread();
   }
   
   // 60秒(1分)ごとに実行する処理
   if(timerCount % 60 == 0)
   {
      LogAccountStatus();
   }
   
   // 300秒(5分)ごとに実行する処理
   if(timerCount % 300 == 0)
   {
      CheckMultiTimeframeSignals();
      timerCount = 0; // オーバーフロー防止のためリセット
   }
}

OnTimerとOnTickの使い分け

項目 OnTimer OnTick
呼び出しタイミング 指定した時間間隔ごと 新しいティック(価格変動)が来るたび
閑散時の動作 価格変動がなくても実行される 価格変動がないと実行されない
適した用途 定期監視、UI更新、時刻ベース処理 価格ベースの売買判断
バックテスト ストラテジーテスターでは動作しない 正常にテスト可能

重要な注意事項

  • ストラテジーテスターでは動作しない — OnTimerはバックテスト中に呼び出されません。タイマーに依存するロジックは、バックテストでは別の方法(OnTick内での時刻チェック等)で代替する必要があります。
  • 重い処理は避ける — OnTimer内で重い処理(大量のオーダー検索、複雑な計算など)を頻繁に実行すると、MT4全体のパフォーマンスに影響します。特にミリ秒タイマーを使う場合は処理を軽量に保ちましょう。
  • MarketInfo()で最新価格を取得する — OnTimer内ではBid/Ask変数が更新されない場合があるため、MarketInfo(Symbol(), MODE_BID)等で明示的に取得するのが確実です。
  • 週末・市場クローズ中もタイマーは動作する — MT4が起動している限りタイマーイベントは発生します。市場が閉まっている間に不要な処理が走らないよう、必要に応じて曜日や時刻のチェックを入れましょう。