MQL5 OnTradeTransaction EA: Log Order Status Changes

Build a real-time MQL5 EA using OnTradeTransaction to log order fills, partial fills, and slippage to CSV. Full code, setup steps, and honest pitfalls.

mql5-ontradetransaction-ea-log-order-status

Why Most EAs Miss What Happens Between the Order and the Fill

You've been there. You backtest an EA for weeks, it looks flawless, then you run it live and something feels off. Maybe it's the fills. Maybe the EA opens a position at a price you didn't expect, or it thinks a partial fill didn't happen, or it just keeps trying to modify an order that's already been cancelled. The culprit? Your EA only watches OnTick() and maybe OnTimer(), but it ignores the real-time transaction stream that MetaTrader 5 pumps out with every order event.

The OnTradeTransaction event is MetaTrader 5's way of telling your EA exactly what the trade server is doing with every order, position, and deal. It fires for pending order placements, modifications, deletions, partial fills, full fills, slippage events, and even margin check rejections. Most developers skip it because the documentation is sparse and the event structure feels intimidating. That's a mistake. If you're building any EA that cares about execution quality — and you should — this event is your best tool for logging, debugging, and reacting in real time.

In this post, I'll walk you through a practical implementation: an EA that listens to OnTradeTransaction, logs every order status change to a file, and flags partial fills and slippage. You'll get working MQL5 code, real parameter values you can drop into a test EA, and honest advice on where this approach shines and where it falls short.

I've been burned by this myself. A few years back I had a grid EA that looked perfect in backtest — 90% win rate, smooth equity curve. First week live, it opened six positions instead of three because a partial fill triggered a duplicate order. The EA saw the first fill on tick but not the second. OnTradeTransaction would have caught that immediately. That's why I'm writing this.

Understanding OnTradeTransaction: The Event Structure

Before we write a single line of code, you need to understand what arrives in the event handler. In MQL5, the OnTradeTransaction function signature looks like this:

void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)

Three structures come with every call:

  • MqlTradeTransaction — contains the transaction type, order ticket, position ticket, deal ticket, price, volume, time, and other metadata about what just happened. This is the main event payload.
  • MqlTradeRequest — the original request that triggered this transaction (if your EA sent it). This can be empty if the transaction came from manual trading or another EA. You'll use this to compare requested vs executed prices for slippage.
  • MqlTradeResult — the server's response to the request, including return code, deal ticket, and any error message. I rarely use this for logging, but it's useful for debugging rejected orders.

The key field in MqlTradeTransaction is type, which is an enum of type TradeTransactionType. The most useful values for logging are:

Transaction TypeEnum ValueMeaningWhen It Fires
TRADE_TRANSACTION_ORDER_ADD0A pending order is placed on the serverImmediately after OrderSend() success
TRADE_TRANSACTION_ORDER_UPDATE1Order parameters changed (price, SL, TP)After OrderModify() or server-side adjustment
TRADE_TRANSACTION_ORDER_DELETE2Pending order cancelled or expiredManual cancel, expiry, or OrderDelete()
TRADE_TRANSACTION_DEAL_ADD4A deal (fill or partial fill) was executedEach fill event, including partials
TRADE_TRANSACTION_POSITION6Position updated (size, price, swap, etc.)After any change to an open position

For partial fill detection, you'll focus on TRADE_TRANSACTION_DEAL_ADD and TRADE_TRANSACTION_POSITION. The deal tells you the exact volume and price of each fill; the position update tells you the cumulative effect. Slippage you can calculate by comparing the requested price from MqlTradeRequest with the actual fill price from the deal.

One thing the docs don't tell you: the event can fire multiple times for the same logical operation. A single market order can generate an ORDER_ADD (if it's a pending conversion), then one or more DEAL_ADD events, then a POSITION event. I've seen up to five events for one order during high volatility. Your handler needs to be idempotent — don't double-log the same event.

Building the Logging EA: Step by Step

Let's build a simple EA that logs every trade transaction to a CSV file. I'll keep the trading logic minimal — just a market order on each tick for demo purposes — so you can focus on the event handling. In a real EA you'd replace that with your strategy.

Step 1: Set Up the File Handle and Input Parameters

First, declare a file handle and input parameters for the log file path and verbosity level. I prefer using FILE_COMMON so the log survives terminal reinstalls, but you can use FILE_LOCAL if you want it in the Files folder of the specific data folder.

//+------------------------------------------------------------------+
//|                                                  TradeLogger.mq5 |
//|                                    (c) 2025 TradingBotMaker.com |
//+------------------------------------------------------------------+
#property copyright "TradingBotMaker.com"
#property version   "1.00"
#property description "Logs all OnTradeTransaction events to CSV"

input string LogFileName = "TradeLog.csv";      // Log file name
input bool   LogPartialFills = true;            // Flag partial fills
input bool   LogSlippage = true;                // Log slippage >= 1 pip
input double SlippageThreshold = 1.0;           // Min pips to log slippage
input bool   LogAllEvents = false;              // Log every event, not just deals

int fileHandle = INVALID_HANDLE;

Notice LogAllEvents. By default I only log DEAL_ADD and POSITION events because those are the most actionable. If you're debugging order placement issues, set this to true and you'll see every ORDER_ADD and ORDER_DELETE too. Just be warned — on a busy day with multiple EAs, the file can grow to tens of megabytes quickly.

Step 2: Initialize the Log File with Headers

In OnInit(), open the file in write mode and write the CSV header. I use FILE_CSV|FILE_WRITE|FILE_COMMON for portability. If the file already exists, this overwrites it — you might want to append instead for production. I'll show the append approach in a moment.

int OnInit()
{
   fileHandle = FileOpen(LogFileName, FILE_CSV|FILE_WRITE|FILE_COMMON, ",");
   if(fileHandle == INVALID_HANDLE)
   {
      Print("Failed to create log file: ", GetLastError());
      return(INIT_FAILED);
   }
   // Write header
   FileWrite(fileHandle, "Time", "Type", "OrderTicket", "DealTicket",
             "Volume", "Price", "RequestPrice", "SlippagePips", "PositionTicket");
   FileFlush(fileHandle);
   return(INIT_SUCCEEDED);
}

One subtle issue: if you're running multiple instances of this EA on different charts, they'll fight over the same file. I handle this by appending the chart symbol and timeframe to the filename, like "TradeLog_EURUSD_M15.csv". You can do that with StringFormat("%s_%s_%s.csv", LogPrefix, Symbol(), EnumToString(Period())).

Step 3: The Core Event Handler

Here's where the real work happens. I structure the handler to first identify the transaction type, then extract the relevant data and write it to the file. For partial fills, I check if the deal volume is less than the order volume. For slippage, I compare the request price with the deal price.

void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
{
   // Filter events based on input
   if(!LogAllEvents)
   {
      if(trans.type != TRADE_TRANSACTION_DEAL_ADD &&
         trans.type != TRADE_TRANSACTION_POSITION)
         return;
   }

   // Get symbol info for pip calculation
   string symbol = trans.symbol;
   if(symbol == "")
      symbol = Symbol(); // fallback to current chart

   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   double pipValue = point * 10; // assuming 5-digit broker, adjust if needed

   // Build the log line
   string timeStr = TimeToString(trans.time, TIME_DATE|TIME_SECONDS);
   string typeStr = EnumToString(trans.type);
   string orderTicket = IntegerToString(trans.order);
   string dealTicket = IntegerToString(trans.deal);
   string volume = DoubleToString(trans.volume, 2);
   string price = DoubleToString(trans.price, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
   string requestPrice = (request.price > 0) ? DoubleToString(request.price, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)) : "N/A";
   string positionTicket = IntegerToString(trans.position);

   // Calculate slippage if applicable
   string slippage = "N/A";
   if(LogSlippage && request.price > 0 && trans.price > 0 && trans.type == TRADE_TRANSACTION_DEAL_ADD)
   {
      double diff = MathAbs(trans.price - request.price);
      double pips = diff / pipValue;
      if(pips >= SlippageThreshold)
      {
         slippage = DoubleToString(pips, 1);
         Print("SLIPPAGE: ", pips, " pips on order ", orderTicket);
      }
   }

   // Check for partial fill
   if(LogPartialFills && trans.type == TRADE_TRANSACTION_DEAL_ADD)
   {
      // Get the original order volume from history
      HistorySelect(0, TimeCurrent());
      uint total = HistoryOrdersTotal();
      double orderVolume = 0;
      for(uint i = 0; i < total; i++)
      {
         ulong ticket = HistoryOrderGetTicket(i);
         if(ticket == trans.order)
         {
            orderVolume = HistoryOrderGetDouble(ticket, ORDER_VOLUME_INITIAL);
            break;
         }
      }
      if(orderVolume > 0 && trans.volume < orderVolume)
      {
         Print("PARTIAL FILL: Order ", orderTicket, " filled ", trans.volume,
               " of ", orderVolume, " lots");
      }
   }

   // Write to file
   if(fileHandle != INVALID_HANDLE)
   {
      FileWrite(fileHandle, timeStr, typeStr, orderTicket, dealTicket,
                volume, price, requestPrice, slippage, positionTicket);
      FileFlush(fileHandle);
   }
}

I want to highlight a few decisions here. First, I use HistorySelect() inside the event handler. That's a synchronous call to the trade server, which can introduce latency. On a slow VPS or during high-frequency trading, this could stack up. An alternative is to cache order volumes in a dictionary keyed by ticket number. I'll show that optimization later.

Second, the pip calculation assumes a 5-digit broker. If you're on a 4-digit broker (like some JPY

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