MQL5 OnTick vs OnTradeTransaction: Which Event to Use for EA

Struggling with race conditions in your MT5 Expert Advisor? Compare OnTick and OnTradeTransaction event handlers, see code examples, and learn a hybrid.

mql5-ontick-vs-ontradetransaction-which-event-to

Why This Comparison Matters for Your MT5 EA

If you've written more than a few MQL5 Expert Advisors, you've hit this wall: your EA opens a trade, but the next tick arrives too late, or the OnTick() handler fires before the broker confirms the order. Suddenly your position sizing is off, your trailing stop misses the fill price, or you're sending duplicate modification requests. Sound familiar?

The root cause is almost always picking the wrong event handler for order management. MT5 gives you two main highways into your EA's logic — OnTick and OnTradeTransaction — and they serve very different purposes. Most traders treat them as interchangeable, and that's where the bugs creep in.

I've been down this road. Early in my MQL5 days, I spent a weekend debugging a scalping EA that kept over-trading because I was checking position counts inside OnTick() without accounting for the trade event timing. Switching to OnTradeTransaction() fixed it in about ten minutes. That experience taught me something: knowing when to use each handler isn't just academic — it directly impacts whether your EA behaves correctly in live markets.

This post gives you a clear, practical comparison. You'll see code examples for both handlers, learn where each one shines, and get a decision framework you can apply to your next EA. I'll also show you the edge cases that bite most developers, including pending order races and partial fills.

How OnTick Works in MQL5

OnTick is the default entry point for every Expert Advisor. MT5 calls it every time a new price quote arrives for the symbol your EA is attached to. In fast markets, that can mean hundreds or thousands of calls per second. In slow markets, maybe a few per minute. You don't control the frequency — the terminal does, based on incoming data from the broker's price feed.

Here's the skeleton:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // 1. Get current price data
   MqlTick currentTick;
   SymbolInfoTick(_Symbol, currentTick);

   // 2. Check trading conditions
   if(SomeEntryCondition())
     {
      // 3. Open a trade
      OpenBuyTrade();
     }

   // 4. Manage existing positions
   ManageOpenPositions();
  }

The critical thing to understand: OnTick() fires before the broker confirms any order you just sent. When you call OrderSend() inside OnTick(), the function returns immediately with a ticket number — but the order might still be pending, partially filled, or rejected. Your next OnTick() call will see the same position count as before because the trade server hasn't processed the request yet.

This is the single biggest source of logic errors in MT5 EAs. You think you've opened a trade, so you set a stop loss or trailing stop in the same tick — but the trade hasn't actually been placed. Or worse, you try to modify a position that doesn't exist yet. I've seen EAs that send a dozen modification requests in under a second because the developer assumed OrderSend() was synchronous.

When OnTick Makes Sense

  • Pure price-action strategies — You only need to check entry/exit conditions on new price data. No complex order management.
  • Simple market orders — Open a buy when RSI crosses 30, close when it hits 70. You don't care about the exact fill confirmation.
  • Indicators that recalculate on each tick — Moving averages, Bollinger Bands, anything that updates with every new price.
  • High-frequency scalping — You want to react to every tick and don't want the overhead of trade event handling.

How OnTradeTransaction Works in MQL5

OnTradeTransaction is a dedicated event handler that fires when any trade operation changes state — order placement, execution, modification, deletion, or position close. MT5 calls it with a MqlTradeTransaction struct that tells you exactly what happened and what state the trade is in now. This struct contains fields like type, order, deal, position, symbol, and price.

Here's the basic structure:

//+------------------------------------------------------------------+
//| Trade transaction function                                       |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
   // Check the type of transaction
   switch(trans.type)
     {
      case TRADE_TRANSACTION_ORDER_ADD:
         // A new order was added to the market
         HandleNewOrder(trans.order);
         break;

      case TRADE_TRANSACTION_DEAL_ADD:
         // A deal was executed (fill)
         HandleDeal(trans.deal);
         break;

      case TRADE_TRANSACTION_POSITION:
         // Position data changed (size, SL, TP)
         HandlePositionUpdate(trans.position);
         break;

      case TRADE_TRANSACTION_HISTORY_ADD:
         // Order moved to history
         HandleOrderHistory(trans.order);
         break;
     }
  }

The key advantage: OnTradeTransaction() fires after the broker processes the request. When you get a TRADE_TRANSACTION_DEAL_ADD event, the trade is already filled. You can safely modify the position, set trailing stops, or log the exact fill price without race conditions. The function receives three parameters: the transaction struct, the original request struct, and the result struct — giving you full context.

One nuance many developers miss: OnTradeTransaction() fires for all symbols in the terminal, not just the one your EA is attached to. If you're running a multi-symbol EA, you must filter by trans.symbol to avoid reacting to trades on other charts. I'll show you how in the hybrid example below.

When OnTradeTransaction Is Essential

  • Pending order management — You place a stop order at 1.1050, but need to modify it when price approaches. OnTradeTransaction() tells you when the order is actually in the market.
  • Partial fill handling — Your limit order gets filled in two chunks. Each fill fires a separate deal event, letting you track the average fill price.
  • Multi-symbol EAs — You trade EURUSD and GBPUSD. OnTradeTransaction() catches all events regardless of which chart the EA is on.
  • Trailing stops with fill price — You need the exact execution price to calculate your stop distance, not the tick price that may have moved.
  • Audit logging — Every order state change is captured, making it easy to reconstruct exactly what happened.

Head-to-Head Comparison: OnTick vs OnTradeTransaction

AspectOnTickOnTradeTransaction
TriggerEvery new price tickEvery trade state change
Timing vs trade executionBefore confirmationAfter confirmation
Execution certaintyLow — assumes order placedHigh — actual fill confirmed
Partial fill handlingNot possible nativelyEach fill fires a deal event
Pending order trackingManual polling requiredAutomatic ADD/DELETE events
Multi-symbol scopeChart symbol onlyAll symbols in terminal
Performance overheadHigh call frequencyLow call frequency
Entry condition evaluationNatural — price-basedNot designed for this

Practical Implementation: A Hybrid Approach

Here's the truth: most well-designed MT5 EAs use both handlers. OnTick() evaluates entry and exit conditions based on price. OnTradeTransaction() handles the aftermath — position management, trailing stops, and logging. Trying to cram everything into one handler leads to spaghetti code that's hard to debug.

Let me show you a concrete example. I'll build a simple breakout EA that:

  1. Checks for a 20-period high breakout on each tick.
  2. Places a buy stop order 2 pips above the break.
  3. Trails a stop loss based on the fill price.

The OnTick Entry Logic

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // Only check entries if no position exists for this symbol
   if(PositionSelect(_Symbol))
      return;

   // Calculate breakout level
   double high20 = iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, 20, 1));
   double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double entryBuffer = 2 * _Point;

   // Place buy stop if price is near the breakout
   if(currentAsk >= high20 - entryBuffer && currentAsk <= high20 + entryBuffer)
     {
      MqlTradeRequest request = {};
      MqlTradeResult result = {};

      request.action = TRADE_ACTION_PENDING;
      request.symbol = _Symbol;
      request.volume = 0.1;
      request.type = ORDER_TYPE_BUY_STOP;
      request.price = high20 + entryBuffer;
      request.sl = 0;
      request.tp = 0;
      request.deviation = 10;
      request.magic = 123456;

      OrderSend(request, result);
     }
  }

This is clean — just entry logic. No position management clutter. The return on line 5 prevents re-entry while we have a position. Notice I'm using iHighest() to find the highest high over 20 bars. That's a standard MQL5 function that returns the index of the highest value in a series.

The OnTradeTransaction Position Management

//+------------------------------------------------------------------+
//| Trade transaction function                                       |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
   // Only handle deals for our symbol and magic
   if(trans.symbol != _Symbol)
      return;

   // Check if it's a deal (fill) for our order
   if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
     {
      // Get deal details
      MqlDeal deal;
      HistoryDealSelect(trans.deal);
      HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
      if(HistoryDealGetInteger(trans.deal, DEAL_MAGIC) != 123456)
         return;

      // Extract fill price
      double fillPrice = HistoryDealGetDouble(trans.deal, DEAL_PRICE);
      double stopLoss = fillPrice - 20 * _Point;

      // Get the position ticket
      ulong positionTicket = HistoryDealGetInteger(trans.deal, DEAL_POSITION_ID);

      // Modify position with trailing stop based on fill price
      MqlTradeRequest modRequest = {};

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles