A common question in Expert Advisor code is "did my trade just hit its stop loss, its take profit, or did someone close it by hand?" Many EAs answer it by comparing the list of open positions on every tick and guessing. That works until two things happen on the same tick, or the EA restarts in the middle.
MQL5 gives you a better tool: OnTradeTransaction. The terminal calls it for every change on the account, and once a deal is added to history you can read exactly why it happened.
The event you care about
A trade request produces several transactions: the order is added, updated, removed, a deal is added, the position changes. For "why did this position close", the one to watch is TRADE_TRANSACTION_DEAL_ADD. A deal is the actual execution, and it carries a reason code.
input long InpMagic = 240101; // the magic number this EA trades with
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
return;
ulong deal = trans.deal;
if(deal == 0 || !HistoryDealSelect(deal))
{
PrintFormat("Deal %I64u not in history yet, will reconcile later", deal);
return;
}
if(HistoryDealGetInteger(deal, DEAL_MAGIC) != InpMagic)
return;
ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal, DEAL_ENTRY);
if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_OUT_BY && entry != DEAL_ENTRY_INOUT)
return; // an opening deal, not a close
ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(deal, DEAL_REASON);
ulong positionId = HistoryDealGetInteger(deal, DEAL_POSITION_ID);
double net = HistoryDealGetDouble(deal, DEAL_PROFIT)
+ HistoryDealGetDouble(deal, DEAL_SWAP)
+ HistoryDealGetDouble(deal, DEAL_COMMISSION);
PrintFormat("Position %I64u closed by %s, volume %.2f, net %.2f",
positionId, ReasonText(reason),
HistoryDealGetDouble(deal, DEAL_VOLUME), net);
OnPositionClosed(positionId, reason, net);
}
A few notes on that code:
- The magic number filter matters. Without it, a manual trade or another EA on the same account will run through your logic.
-
DEAL_ENTRY_OUT_BYcovers close by an opposite position, andDEAL_ENTRY_INOUTcovers a reversal on netting accounts, where one deal closes and opens in the same step. - Net result is profit plus swap plus commission. Using
DEAL_PROFITalone makes a scratch trade look like a small win.
Turning the reason code into something readable
string ReasonText(ENUM_DEAL_REASON r)
{
switch(r)
{
case DEAL_REASON_SL: return "stop loss";
case DEAL_REASON_TP: return "take profit";
case DEAL_REASON_SO: return "stop out";
case DEAL_REASON_EXPERT: return "expert advisor";
case DEAL_REASON_CLIENT: return "manual (desktop)";
case DEAL_REASON_MOBILE: return "manual (mobile)";
case DEAL_REASON_WEB: return "manual (web)";
default: return "other (" + IntegerToString(r) + ")";
}
}
This is where the event pays for itself. Once you know the reason, the EA can behave differently:
- After a stop loss, start a cooldown before the next entry.
- After a take profit, allow the next signal as normal.
- After a stop out, stop trading completely and alert, because the risk model has already failed.
- After a manual close, respect it. Do not reopen the same trade on the next tick just because the signal is still true.
That last one is the bug I see most often. The trader closes a position on their phone, and the EA immediately opens it again.
Partial closes
If a position is partly closed, you still get a DEAL_ENTRY_OUT deal, but the position stays open with less volume. Check whether the position still exists before you treat it as finished:
void OnPositionClosed(ulong positionId, ENUM_DEAL_REASON reason, double net)
{
if(PositionSelectByTicket(positionId))
{
PrintFormat("Position %I64u partly closed, %.2f lots remain",
positionId, PositionGetDouble(POSITION_VOLUME));
return;
}
// fully closed: update stats, cooldowns, daily loss counters here
}
On hedging accounts the position ticket equals the position identifier, so PositionSelectByTicket works with the value from DEAL_POSITION_ID.
The limits you must design around
OnTradeTransaction is not a guaranteed delivery system, and two cases break EAs that trust it blindly:
The EA was not running. If the terminal was closed, or the EA was removed and attached again, every close in that time happened without an event. On startup, read recent history with
HistorySelectand rebuild your state from deals instead of from memory.The queue can overflow. Transactions are queued for the EA, and if the EA is busy for a long time, old events can be dropped. Keep the handler short, and do heavy work elsewhere.
So the pattern that holds up is: react to events for speed, and reconcile from history on start and every few minutes for correctness. If the two disagree, history wins.
Testing it
The Strategy Tester calls OnTradeTransaction too, so stop loss and take profit reasons can be checked there. Manual reasons cannot, so test those on a demo account: open a position with the EA, close it from the mobile app, and confirm the journal shows "manual (mobile)" and that the EA does not reopen it.
Once the EA knows why each trade ended, a lot of guesswork in the rest of the code goes away.
Top comments (0)