DEV Community

Omais Ali
Omais Ali

Posted on

Confirming an MQL5 fill with OnTradeTransaction instead of guessing from OrderSend

OrderSend returning true, or CTrade returning a done retcode, means the trade server accepted the request. It does not always mean the position you wanted is open at the volume you wanted. Partial fills, closings that leave a remainder, and netting account quirks all show up after the request. The reliable place to learn what actually happened is OnTradeTransaction.

What to listen for

OnTradeTransaction receives three structures. The one that matters for fills is the transaction of type TRADE_TRANSACTION_DEAL_ADD. That fires when a deal is added to history. From there you can read the deal ticket, then the deal properties: symbol, volume, price, deal type, position identifier, magic and entry type.

#include <Trade/Trade.mqh>

CTrade trade;
ulong  g_waitOrder = 0;

void OnTick()
{
   if(g_waitOrder != 0)
      return; // still waiting for the deal that belongs to the last request

   if(!ReadyToEnter())
      return;

   if(!trade.Buy(0.10, _Symbol, 0.0, 0.0, 0.0, "ctb"))
   {
      Print("Buy request failed: ", trade.ResultRetcode(), " ", trade.ResultRetcodeDescription());
      return;
   }

   g_waitOrder = trade.ResultOrder();
   Print("Buy request accepted, order ", g_waitOrder);
}

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))
      return;

   long magic = HistoryDealGetInteger(deal, DEAL_MAGIC);
   if(magic != trade.RequestMagic())
      return;

   string sym = HistoryDealGetString(deal, DEAL_SYMBOL);
   double vol = HistoryDealGetDouble(deal, DEAL_VOLUME);
   double px  = HistoryDealGetDouble(deal, DEAL_PRICE);
   long   entry = HistoryDealGetInteger(deal, DEAL_ENTRY);
   ulong  order = (ulong)HistoryDealGetInteger(deal, DEAL_ORDER);

   Print("Deal ", deal, " ", sym, " vol=", vol, " px=", px, " entry=", entry);

   if(g_waitOrder != 0 && order == g_waitOrder)
      g_waitOrder = 0;
}
Enter fullscreen mode Exit fullscreen mode

Why this is safer than polling

  1. ResultRetcode DONE on a market order usually means a fill, but on some brokers and account modes you still want the deal before you size the next order or move a stop.
  2. Partial fills arrive as more than one deal for one order. Summing DEAL_VOLUME for that order tells you how much is done.
  3. On a netting account a "Buy" can reduce or flip an existing opposite position. The deal entry type tells you whether volume was added, closed or reversed.
  4. Matching by magic and symbol keeps other EAs on the same account from clearing your wait state.

Practical tips

  1. Store the order ticket from ResultOrder() when the request is accepted, then clear your wait state when a deal for that order arrives.
  2. Use HistorySelectByPosition when you later need every deal that belongs to one position identifier.
  3. Do not call heavy logic inside OnTradeTransaction. Set flags and let OnTick or OnTimer do the work.
  4. In the Strategy Tester, transactions still fire in real ticks mode. In open prices mode some deal details are thinner, so prefer real ticks when you are testing fill handling.

Treat OrderSend as the request and OnTradeTransaction as the receipt. The receipt is what your risk logic should trust.

Top comments (0)