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 Type | Enum Value | Meaning | When It Fires |
|---|---|---|---|
| TRADE_TRANSACTION_ORDER_ADD | 0 | A pending order is placed on the server | Immediately after OrderSend() success |
| TRADE_TRANSACTION_ORDER_UPDATE | 1 | Order parameters changed (price, SL, TP) | After OrderModify() or server-side adjustment |
| TRADE_TRANSACTION_ORDER_DELETE | 2 | Pending order cancelled or expired | Manual cancel, expiry, or OrderDelete() |
| TRADE_TRANSACTION_DEAL_ADD | 4 | A deal (fill or partial fill) was executed | Each fill event, including partials |
| TRADE_TRANSACTION_POSITION | 6 | Position 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






