【MQL4関数】OnTick関数とは?ロウソク足が動いたときに実行されるイベント関数

関数

OnTick関数とは?

OnTick関数は、EAがアタッチされたチャートの通貨ペアに新しいティック(価格変動)が発生するたびに自動的に呼び出されるイベント関数です。MT4のEA(Expert Advisor)において最も基本的かつ重要な関数であり、売買ロジックのほとんどはこの関数の中に記述します。

「ロウソク足が動いたとき」という表現は、正確には「新しい価格データ(ティック)がサーバーから配信されたとき」を意味します。1本のロウソク足の中でも価格が変動するたびに何度も呼び出されます。

基本的な書式

void OnTick()
{
   // ティック発生時に実行したい処理をここに記述
}

引数

OnTick関数は引数を取りません

戻り値

戻り値の型はvoid(なし)です。値を返す必要はありません。

対象ファイル

OnTick関数はEA(.mq4ファイル)専用です。カスタムインジケーターやスクリプトでは使用できません(インジケーターではOnCalculate関数が同様の役割を果たします)。

OnTick関数の呼び出しタイミング

OnTick関数が呼び出される条件を正しく理解することが重要です。

  • 呼び出される条件:EAがアタッチされたチャートの通貨ペアで新しいティックが発生したとき
  • 呼び出し頻度:相場が活発なときは1秒間に何度も呼ばれ、相場が閑散としているときはほとんど呼ばれない
  • 週末・祝日:マーケットが閉まっている間はティックが発生しないため、OnTick関数は呼ばれない
  • 他の通貨ペア:アタッチした通貨ペア以外の価格変動では呼ばれない

プログラム例1:基本的なティック情報の表示

最もシンプルな例として、ティックが発生するたびに現在の価格情報をログに出力するEAです。OnTick関数の動作を確認するのに最適です。

//+------------------------------------------------------------------+
//| 基本的なティック情報表示EA                                         |
//+------------------------------------------------------------------+
void OnTick()
{
   // 現在のBid価格とAsk価格を取得
   double currentBid = Bid;
   double currentAsk = Ask;
   
   // スプレッドをポイント単位で計算
   double spread = (Ask - Bid) / Point;
   
   // ログに価格情報を出力
   Print("通貨ペア: ", Symbol(),
         " | Bid: ", DoubleToString(currentBid, Digits),
         " | Ask: ", DoubleToString(currentAsk, Digits),
         " | スプレッド: ", DoubleToString(spread, 1), " pips");
   
   // チャート上にも情報を表示
   Comment("最終ティック時刻: ", TimeToString(TimeCurrent(), TIME_SECONDS),
           "\nBid: ", DoubleToString(currentBid, Digits),
           "\nAsk: ", DoubleToString(currentAsk, Digits),
           "\nスプレッド: ", DoubleToString(spread, 1), " points");
}

このEAをチャートにアタッチすると、ティックが発生するたびにエキスパートタブにログが出力され、チャート左上に価格情報がリアルタイム表示されます。

プログラム例2:新しいロウソク足の確定を検出する

OnTick関数はティックごとに呼ばれるため、「新しいロウソク足が形成されたとき(=前の足が確定したとき)だけ処理を実行する」というパターンは非常によく使われます。

//+------------------------------------------------------------------+
//| 新しいバー検出EA                                                   |
//+------------------------------------------------------------------+

// 前回のバーの時刻を記録するためのグローバル変数
datetime lastBarTime = 0;

//+------------------------------------------------------------------+
//| 新しいバーが形成されたかを判定する関数                              |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   // 現在のバー(最新の未確定バー)の開始時刻を取得
   datetime currentBarTime = iTime(Symbol(), Period(), 0);
   
   // 前回記録した時刻と異なれば新しいバーが形成された
   if(currentBarTime != lastBarTime)
   {
      lastBarTime = currentBarTime;  // 時刻を更新
      return true;                    // 新しいバーが出現
   }
   
   return false;  // まだ同じバーの中
}

//+------------------------------------------------------------------+
//| OnTick関数                                                        |
//+------------------------------------------------------------------+
void OnTick()
{
   // 新しいバーが形成されたときだけ処理を実行
   if(!IsNewBar())
      return;  // 新しいバーでなければ何もしない
   
   // ここに新しいバー確定時の処理を記述
   // インデックス1が直前に確定したバー
   double closePrice = iClose(Symbol(), Period(), 1);
   double openPrice  = iOpen(Symbol(), Period(), 1);
   double highPrice  = iHigh(Symbol(), Period(), 1);
   double lowPrice   = iLow(Symbol(), Period(), 1);
   
   Print("=== 新しいバー検出 ===");
   Print("確定したバーの情報:");
   Print("  始値: ", DoubleToString(openPrice, Digits));
   Print("  高値: ", DoubleToString(highPrice, Digits));
   Print("  安値: ", DoubleToString(lowPrice, Digits));
   Print("  終値: ", DoubleToString(closePrice, Digits));
   
   // 陽線か陰線かを判定
   if(closePrice > openPrice)
      Print("  → 陽線(上昇)");
   else if(closePrice < openPrice)
      Print("  → 陰線(下降)");
   else
      Print("  → 十字線(同事線)");
}

この「新しいバー検出パターン」は、EAの処理負荷を軽減し、確定した価格データのみで売買判断を行う際に不可欠なテクニックです。

プログラム例3:移動平均線クロスによる売買EA

OnTick関数の中で実際に売買注文を出す実践的な例です。短期移動平均線と長期移動平均線のクロスを検出して売買を行います。

//+------------------------------------------------------------------+
//| 移動平均線クロスEA                                                 |
//+------------------------------------------------------------------+

// 外部パラメータ(ユーザーが変更可能)
extern int    FastMA_Period = 10;     // 短期移動平均線の期間
extern int    SlowMA_Period = 25;     // 長期移動平均線の期間
extern double LotSize       = 0.1;   // 取引ロット数
extern int    Slippage      = 3;     // 許容スリッページ
extern int    MagicNumber   = 12345; // マジックナンバー

// 新しいバー検出用の変数
datetime g_lastBarTime = 0;

//+------------------------------------------------------------------+
//| 新しいバー判定                                                     |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   datetime currentBarTime = iTime(Symbol(), Period(), 0);
   if(currentBarTime != g_lastBarTime)
   {
      g_lastBarTime = currentBarTime;
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| 現在のポジション数を取得する関数                                    |
//+------------------------------------------------------------------+
int CountPositions(int type)
{
   int count = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         // 同じ通貨ペアかつ同じマジックナンバーのポジションをカウント
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
         {
            if(OrderType() == type)
               count++;
         }
      }
   }
   return count;
}

//+------------------------------------------------------------------+
//| 指定タイプのポジションをすべて決済する関数                          |
//+------------------------------------------------------------------+
void ClosePositions(int type)
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
         {
            if(OrderType() == type)
            {
               double closePrice;
               if(type == OP_BUY)
                  closePrice = Bid;   // 買いポジションはBidで決済
               else
                  closePrice = Ask;   // 売りポジションはAskで決済
               
               bool result = OrderClose(OrderTicket(), OrderLots(), closePrice, Slippage, clrNONE);
               if(!result)
                  Print("決済エラー: ", GetLastError());
            }
         }
      }
   }
}

//+------------------------------------------------------------------+
//| OnTick関数 - メインロジック                                        |
//+------------------------------------------------------------------+
void OnTick()
{
   // 新しいバーが確定したときだけ判定を行う
   if(!IsNewBar())
      return;
   
   // 移動平均線の値を取得(バーインデックス1 = 直前の確定バー)
   double fastMA_current  = iMA(Symbol(), Period(), FastMA_Period, 0, MODE_SMA, PRICE_CLOSE, 1);
   double slowMA_current  = iMA(Symbol(), Period(), SlowMA_Period, 0, MODE_SMA, PRICE_CLOSE, 1);
   
   // 1本前の移動平均線の値(バーインデックス2)
   double fastMA_previous = iMA(Symbol(), Period(), FastMA_Period, 0, MODE_SMA, PRICE_CLOSE, 2);
   double slowMA_previous = iMA(Symbol(), Period(), SlowMA_Period, 0, MODE_SMA, PRICE_CLOSE, 2);
   
   // ゴールデンクロス判定(短期が長期を下から上に抜ける)
   bool goldenCross = (fastMA_previous <= slowMA_previous) && (fastMA_current > slowMA_current);
   
   // デッドクロス判定(短期が長期を上から下に抜ける)
   bool deadCross = (fastMA_previous >= slowMA_previous) && (fastMA_current < slowMA_current);
   
   // ゴールデンクロス → 売りポジションを決済して買い注文
   if(goldenCross)
   {
      Print("★ ゴールデンクロス検出!買いエントリー");
      
      // 売りポジションがあれば決済
      ClosePositions(OP_SELL);
      
      // 買いポジションがなければ新規買い注文
      if(CountPositions(OP_BUY) == 0)
      {
         int ticket = OrderSend(Symbol(), OP_BUY, LotSize, Ask, Slippage,
                                0, 0, "MA Cross Buy", MagicNumber, 0, clrBlue);
         if(ticket < 0)
            Print("買い注文エラー: ", GetLastError());
         else
            Print("買い注文成功 チケット: ", ticket, " 価格: ", Ask);
      }
   }
   
   // デッドクロス → 買いポジションを決済して売り注文
   if(deadCross)
   {
      Print("★ デッドクロス検出!売りエントリー");
      
      // 買いポジションがあれば決済
      ClosePositions(OP_BUY);
      
      // 売りポジションがなければ新規売り注文
      if(CountPositions(OP_SELL) == 0)
      {
         int ticket = OrderSend(Symbol(), OP_SELL, LotSize, Bid, Slippage,
                                0, 0, "MA Cross Sell", MagicNumber, 0, clrRed);
         if(ticket < 0)
            Print("売り注文エラー: ", GetLastError());
         else
            Print("売り注文成功 チケット: ", ticket, " 価格: ", Bid);
      }
   }
}

この例では、OnTick関数内で新しいバーの確定を検出し、移動平均線のクロスを判定して自動売買を行っています。実際のEA開発の基本パターンとして参考にしてください。

プログラム例4:複数の処理を整理して管理する構造

実際のEA開発では、OnTick関数の中に全ての処理を詰め込むと可読性が下がります。役割ごとに関数を分割し、OnTick関数はそれらを呼び出すだけのシンプルな構造にするのがベストプラクティスです。

//+------------------------------------------------------------------+
//| 構造化されたEAのテンプレート                                        |
//+------------------------------------------------------------------+

extern double LotSize     = 0.1;
extern int    MagicNumber = 99999;

datetime g_lastBarTime = 0;

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   Print("EA初期化完了: ", Symbol(), " ", EnumToString((ENUM_TIMEFRAMES)Period()));
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| 終了処理関数                                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Print("EA停止: 理由コード = ", reason);
   Comment("");  // チャート上のコメントをクリア
}

//+------------------------------------------------------------------+
//| OnTick関数 - すべての処理の起点                                    |
//+------------------------------------------------------------------+
void OnTick()
{
   // ステップ1: チャート上に情報を表示(毎ティック更新)
   DisplayInfo();
   
   // ステップ2: 新しいバーでなければ以降の処理をスキップ
   if(!IsNewBar())
      return;
   
   // ステップ3: 既存ポジションの管理(トレーリングストップなど)
   ManagePositions();
   
   // ステップ4: エントリーシグナルの確認
   int signal = CheckSignal();
   
   // ステップ5: シグナルに基づいてエントリー
   if(signal != 0)
      ExecuteTrade(signal);
}

//+------------------------------------------------------------------+
//| チャート上に情報を表示する関数                                      |
//+------------------------------------------------------------------+
void DisplayInfo()
{
   int buyCount  = CountMyPositions(OP_BUY);
   int sellCount = CountMyPositions(OP_SELL);
   double totalProfit = CalculateTotalProfit();
   
   string info = "";
   info += "━━━ EA情報パネル ━━━\n";
   info += "通貨ペア: " + Symbol() + "\n";
   info += "時間足: " + IntegerToString(Period()) + "分\n";
   info += "現在時刻: " + TimeToString(TimeCurrent(), TIME_SECONDS) + "\n";
   info += "━━━━━━━━━━━━━━━\n";
   info += "買いポジション: " + IntegerToString(buyCount) + "\n";
   info += "売りポジション: " + IntegerToString(sellCount) + "\n";
   info += "合計損益: " + DoubleToString(totalProfit, 2) + " " + AccountCurrency() + "\n";
   
   Comment(info);
}

//+------------------------------------------------------------------+
//| 新しいバーの判定                                                   |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   datetime currentBarTime = iTime(Symbol(), Period(), 0);
   if(currentBarTime != g_lastBarTime)
   {
      g_lastBarTime = currentBarTime;
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| 既存ポジションの管理                                               |
//+------------------------------------------------------------------+
void ManagePositions()
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;
      if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber)
         continue;
      
      // 例:利益が50pips以上なら建値にストップロスを移動
      double profitPips = 0;
      if(OrderType() == OP_BUY)
         profitPips = (Bid - OrderOpenPrice()) / Point;
      else if(OrderType() == OP_SELL)
         profitPips = (OrderOpenPrice() - Ask) / Point;
      
      // 5桁ブローカー対応(10で割る)
      if(Digits == 3 || Digits == 5)
         profitPips /= 10.0;
      
      if(profitPips >= 50.0 && OrderStopLoss() != OrderOpenPrice())
      {
         bool result = OrderModify(OrderTicket(), OrderOpenPrice(),
                                    OrderOpenPrice(), OrderTakeProfit(), 0, clrGreen);
         if(result)
            Print("建値ストップに移動: チケット ", OrderTicket());
      }
   }
}

//+------------------------------------------------------------------+
//| エントリーシグナルの確認                                            |
//| 戻り値: 1=買い, -1=売り, 0=シグナルなし                            |
//+------------------------------------------------------------------+
int CheckSignal()
{
   // RSIを使った簡単なシグナル例
   double rsi = iRSI(Symbol(), Period(), 14, PRICE_CLOSE, 1);
   
   // RSIが30以下で買いシグナル
   if(rsi < 30.0)
   {
      Print("買いシグナル検出 RSI=", DoubleToString(rsi, 2));
      return 1;
   }
   
   // RSIが70以上で売りシグナル
   if(rsi > 70.0)
   {
      Print("売りシグナル検出 RSI=", DoubleToString(rsi, 2));
      return -1;
   }
   
   return 0;  // シグナルなし
}

//+------------------------------------------------------------------+
//| 売買を実行する関数                                                 |
//+------------------------------------------------------------------+
void ExecuteTrade(int signal)
{
   // 既にポジションがある場合はエントリーしない
   if(CountMyPositions(OP_BUY) + CountMyPositions(OP_SELL) > 0)
      return;
   
   int ticket = -1;
   
   if(signal == 1)  // 買い
   {
      ticket = OrderSend(Symbol(), OP_BUY, LotSize, Ask, 3,
                          0, 0, "RSI Buy", MagicNumber, 0, clrBlue);
   }
   else if(signal == -1)  // 売り
   {
      ticket = OrderSend(Symbol(), OP_SELL, LotSize, Bid, 3,
                          0, 0, "RSI Sell", MagicNumber, 0, clrRed);
   }
   
   if(ticket < 0)
      Print("注文エラー: ", GetLastError());
   else
      Print("注文成功: チケット ", ticket);
}

//+------------------------------------------------------------------+
//| 自分のポジション数を数える関数                                      |
//+------------------------------------------------------------------+
int CountMyPositions(int type)
{
   int count = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber && OrderType() == type)
            count++;
      }
   }
   return count;
}

//+------------------------------------------------------------------+
//| 全ポジションの合計損益を計算する関数                                |
//+------------------------------------------------------------------+
double CalculateTotalProfit()
{
   double profit = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
            profit += OrderProfit() + OrderSwap() + OrderCommission();
      }
   }
   return profit;
}

この構造では、OnTick関数は5つのステップ(情報表示→新バー判定→ポジション管理→シグナル確認→売買実行)を順に呼び出すだけのシンプルな形になっています。各処理が独立した関数に分離されているため、メンテナンスや機能追加が容易です。

OnTick関数の重要なポイントと注意事項

1. ティック頻度は一定ではない

OnTick関数の呼び出し頻度は相場の活発さに依存します。ロンドン・ニューヨーク時間は1秒に何十回も呼ばれることがありますが、週末前やアジア早朝は数秒〜数十秒に1回しか呼ばれないこともあります。「一定間隔で処理を実行したい」場合はOnTimer関数の使用を検討してください。

2. 他の通貨ペアのティックでは呼ばれない

EAをEURUSDのチャートにアタッチした場合、USDJPYの価格が変動してもOnTick関数は呼ばれません。複数通貨ペアを監視したい場合は工夫が必要です。

3. 重い処理を入れない

OnTick関数内の処理が完了するまで次のティックの処理は待機状態になります。大量のループや複雑な計算を毎ティック実行すると、ティックの取りこぼしや動作の遅延が発生します。重い処理は新しいバー確定時だけ実行するなど、工夫しましょう。

4. バックテストでの動作

ストラテジーテスターでは、テストモデル(全ティック、コントロールポイント、始値のみ)によってOnTick関数の呼び出し頻度が大きく異なります。「始値のみ」モデルでは各バーにつき1回しか呼ばれないため、ティックごとの処理は正しくテストできません。

5. エラー処理を忘れない

OnTick関数内で注文を出す場合、通信エラーやスリッページなどで失敗することがあります。OrderSend関数やOrderClose関数の戻り値を必ず確認し、エラー時の処理(リトライやログ出力)を実装しましょう。

6. OnTick関数とOnInit/OnDeinitの関係

EA全体のライフサイクルは以下の順序で進みます。

  • OnInit() → EAがチャートにアタッチされたとき1回だけ実行(初期化処理)
  • OnTick() → ティック発生のたびに繰り返し実行(メイン処理)
  • OnDeinit() → EAが停止・削除されるとき1回だけ実行(終了処理)

まとめ

OnTick関数はMQL4のEA開発における心臓部であり、価格変動に応じた自動売買ロジックのすべてがこの関数を起点に動作します。以下のポイントを押さえておきましょう。

  • ティック(価格変動)が発生するたびに自動的に呼ばれるイベント関数
  • 引数なし、戻り値なし(void)のシンプルな関数
  • EA専用の関数であり、インジケーターやスクリプトでは使えない
  • 新しいバーの確定検出と組み合わせて使うのが定番パターン
  • 処理の構造化(関数分割)を意識して、保守しやすいコードを書く