Why OnTradeTransaction Matters
If you've built an Expert Advisor in MetaTrader 5 that manages multiple positions or pending orders, you've probably run into a frustrating problem: your EA needs to know exactly when a trade happens—not just once per tick. The OnTick() function fires on every price change, but it doesn't tell you what changed in your trade list. Was that a new position opened? A stop loss modified? A pending order triggered? You'd have to scan all positions and orders every tick, compare with previous state, and guess what changed.
That's where MQL5 OnTradeTransaction comes in. This event handler fires only when a trade transaction occurs—placement, modification, deletion, or execution. It hands you a structured MqlTradeTransaction object with the exact type of event, the order or deal ID involved, and the symbol. No polling, no state tracking guesswork. It's the cleanest way to keep your EA's internal state in sync with the trade server.
In this guide, you'll learn how to implement OnTradeTransaction in your EAs, understand each transaction type, handle common edge cases, and avoid the mistakes that trip up most developers. I'll show you real code snippets you can drop into your projects, explain the MetaTrader 5 OnTradeTransaction mechanics, and walk through the trade lifecycle from start to finish. By the end, you'll have a robust event-driven approach that makes your EAs react instantly to server-side changes—not lag behind by a tick or miss events entirely.
Prerequisites
Before we dive in, make sure you have:
- MetaTrader 5 build 2000 or newer – older builds had bugs in transaction handling. Open Help → About to check your build number. If you're below 2000, update via the broker's installer or the MetaQuotes website. Build 4000+ is even better for stability.
- A demo account with at least one symbol active. You don't need real money, but you need a server connection to test trade events. I recommend opening a demo with a broker that offers low spreads and fast execution for realistic testing.
- MetaEditor – the built-in IDE (press F4 in MT5, or navigate Tools → MetaQuotes Language Editor).
- Basic MQL5 knowledge – you should be comfortable with functions, classes, and the
OnTick()structure. If you're brand new to MQL5, start with the official documentation's "Getting Started" section first. You don't need to be an expert, but you should know how to compile and attach an EA.
You don't need any special libraries. The OnTradeTransaction handler is part of the core MQL5 language, available in every Expert Advisor template. It's built-in—no extra includes or DLLs required.
Understanding the Trade Transaction Model
MQL5 uses a three-layer trade model: orders, deals, and positions. A trade transaction is any server-side event that changes one of these layers. The OnTradeTransaction handler receives a MqlTradeTransaction struct containing:
type– anENUM_TRADE_TRANSACTION_TYPEvalue telling you what happened.order– the order ID involved (for pending orders and limit orders).deal– the deal ID involved (for executed trades).position– the position ID (for position-related events).symbol– the trading symbol.
The six transaction types you'll work with most often are:
| Transaction Type | Enum Value | When It Fires |
|---|---|---|
| Order placed | TRADE_TRANSACTION_ORDER_ADD | A pending order or market order is submitted to the server. |
| Order updated | TRADE_TRANSACTION_ORDER_UPDATE | A pending order's price, SL/TP, or volume is modified. |
| Order deleted | TRADE_TRANSACTION_ORDER_DELETE | A pending order is canceled or a market order is rejected by the server. |
| Deal added | TRADE_TRANSACTION_DEAL_ADD | A trade is executed (market order filled, pending triggered, partial fill). |
| Position update | TRADE_TRANSACTION_POSITION | A position's volume, SL/TP, or direction changes (from a deal or modification). |
| History request | TRADE_TRANSACTION_HISTORY_ADD | A completed trade is added to the account history (usually after position close). |
Notice that a single trade action can trigger multiple transaction events. For example, when a pending buy limit order gets triggered by price, you'll see: ORDER_ADD (when the pending was first placed), then ORDER_DELETE (the pending is removed), DEAL_ADD (the execution), and POSITION (the position opens or updates). Your handler needs to handle this cascade correctly. I've seen many developers miss the ORDER_DELETE event and then wonder why their pending order counter is off by one.
Step-by-Step: Implementing OnTradeTransaction
Step 1: Create a New Expert Advisor
Open MetaEditor (F4 in MT5). Click File → New → Expert Advisor. Name it something like "TradeTracker". The wizard generates a skeleton with OnInit(), OnDeinit(), and OnTick(). You'll add OnTradeTransaction() manually. Don't worry—the wizard doesn't include it by default, but it's a standard part of the MQL5 language.
Step 2: Add the Handler Function
After the OnTick() function, add this:
//+------------------------------------------------------------------+
//| TradeTransaction function |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
// We'll fill this in step by step
}
Three parameters: the transaction struct, the original request that caused it, and the server's result. The request and result are only filled for transactions your EA initiated—for manual trades or other EAs, they'll be empty structs. That's an important distinction: if you're building a monitoring tool that reacts to any trade, you'll rely on the trans struct alone.
Step 3: Switch on Transaction Type
The core logic is a switch on trans.type. Here's a complete handler that logs every transaction to the Experts journal:
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
switch(trans.type)
{
case TRADE_TRANSACTION_ORDER_ADD:
Print("Order placed: ", trans.order, " symbol: ", trans.symbol);
// Store pending order info if needed
break;
case TRADE_TRANSACTION_ORDER_UPDATE:
Print("Order updated: ", trans.order);
// Update your pending order tracking
break;
case TRADE_TRANSACTION_ORDER_DELETE:
Print("Order deleted: ", trans.order);
// Remove from pending tracking
break;
case TRADE_TRANSACTION_DEAL_ADD:
Print("Deal executed: ", trans.deal, " position: ", trans.position);
// Process new deal (open, close, partial)
ProcessDeal(trans.deal);
break;
case TRADE_TRANSACTION_POSITION:
Print("Position updated: ", trans.position);
// Update position tracking
break;
case TRADE_TRANSACTION_HISTORY_ADD:
Print("History added: deal ", trans.deal);
// Final cleanup after close
break;
}
}
One thing I often do is add a default case to catch any unexpected transaction types—especially useful during development when you're testing edge cases like partial fills or broker-specific quirks.
Step 4: Extract Deal Details for Execution Events
When you get a TRADE_TRANSACTION_DEAL_ADD, you'll want to know if it was a buy or sell, the volume, price, and whether it opened or closed a position. Use HistoryDealSelect() and HistoryDealGet*() functions:
void ProcessDeal(ulong dealTicket)
{
if(!HistoryDealSelect(dealTicket))
{
Print("Failed to select deal ", dealTicket);
return;
}
long dealType = HistoryDealGetInteger(dealTicket, DEAL_TYPE);
double volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);
double price = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
string symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);
long entry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); // DEAL_ENTRY_IN, DEAL_ENTRY_OUT, DEAL_ENTRY_INOUT
if(entry == DEAL_ENTRY_IN)
Print("Position opened: ", symbol, " vol:", volume, " price:", price);
else if(entry == DEAL_ENTRY_OUT)
Print("Position closed: ", symbol, " vol:", volume, " price:", price);
else if(entry == DEAL_ENTRY_INOUT)
Print("Position reversed: ", symbol, " vol:", volume);
}
I prefer to call HistoryDealSelect() inside the transaction handler rather than in OnTick() because it guarantees the deal data is available. If you try to read deal history from OnTick() right after a trade, you might get stale data—the server might not have finalized the record yet. The transaction handler fires after the server commits the change, so you're safe.
Step 5: Build a Simple Position Tracker
Let's put it together into something useful. This EA tracks the number of open positions and pending orders per symbol, updating them in real time as events occur:
//+------------------------------------------------------------------+
//| TradeTracker EA |
//+------------------------------------------------------------------+
#property strict
int positionsCount = 0;
int pendingCount = 0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Initial sync with current state
positionsCount = PositionsTotal();
pendingCount = OrdersTotal();
Print("Initial state: ", positionsCount, " positions, ", pendingCount, " pending orders");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Your trading logic here
}
//+------------------------------------------------------------------+
//| TradeTransaction function |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
switch(trans.type)
{
case TRADE_TRANSACTION_ORDER_ADD:
pendingCount++;
Print("Pending count: ", pendingCount);
break;
case TRADE_TRANSACTION_ORDER_DELETE:
if(pendingCount > 0) pendingCount--;
Print("Pending count: ", pendingCount);
break;
case TRADE_TRANSACTION_DEAL_ADD:
// Deal executed - re-sync positions
positionsCount = PositionsTotal();
Print("Position count: ", positionsCount);
break;
case TRADE_TRANSACTION_POSITION:
// Position modified - could be SL/TP change
// No count change, but volume might have changed
break;
case TRADE_TRANSACTION_HISTORY_ADD:
// Trade





