DEV Community

Omais Ali
Omais Ali

Posted on

OrderSend returned true, so where is my trade? Checking results properly in MQL5

A very common bug in MT5 Expert Advisors: the code calls trade.Buy(...), it returns true, and the EA assumes it is now in a position. Then the next tick opens another one, or the EA manages a trade that does not exist.

The reason is that true from CTrade.Buy or OrderSend only means the request passed the local checks and was sent. Whether the trade server accepted it is in the result code.

Check the retcode, not just the return value

#include <Trade\Trade.mqh>
CTrade trade;

bool OpenBuy(double lots, double sl, double tp)
{
   if(!trade.Buy(lots, _Symbol, 0.0, sl, tp, "entry"))
   {
      PrintFormat("Request not sent: error %d", GetLastError());
      return false;
   }
   uint code = trade.ResultRetcode();
   if(code == TRADE_RETCODE_DONE || code == TRADE_RETCODE_PLACED)
      return true;

   PrintFormat("Server rejected buy: %u %s", code, trade.ResultRetcodeDescription());
   return false;
}
Enter fullscreen mode Exit fullscreen mode

TRADE_RETCODE_DONE means a market order was filled. TRADE_RETCODE_PLACED means a pending order was accepted. Anything else is a rejection you should handle.

The rejections you will actually see

  1. TRADE_RETCODE_REQUOTE and TRADE_RETCODE_PRICE_OFF: price moved. Refresh prices and retry a limited number of times.
  2. TRADE_RETCODE_INVALID_STOPS: stop loss or take profit too close. Read SYMBOL_TRADE_STOPS_LEVEL and widen them.
  3. TRADE_RETCODE_NO_MONEY: not enough free margin. Recalculate the lot size, do not retry.
  4. TRADE_RETCODE_MARKET_CLOSED: session is closed. Wait, do not loop.
  5. TRADE_RETCODE_INVALID_FILL: the broker does not support the filling mode you sent. See below.

Set the filling mode the broker supports

Many EAs work on one broker and fail on another only because of the filling mode. Ask the symbol what it allows:

void SetFilling()
{
   long modes = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
   if((modes & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK)
      trade.SetTypeFilling(ORDER_FILLING_FOK);
   else if((modes & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC)
      trade.SetTypeFilling(ORDER_FILLING_IOC);
   else
      trade.SetTypeFilling(ORDER_FILLING_RETURN);
}
Enter fullscreen mode Exit fullscreen mode

Call it once in OnInit after you create the CTrade object.

Confirm the position before you manage it

Even after TRADE_RETCODE_DONE, base your management logic on what is really open: loop over PositionsTotal(), match your magic number and symbol, and only then trail or close. That way a rejected order, a manual close or a restart never leaves the EA believing something that is not true.

Small change, but it removes a whole class of "the EA went crazy" support tickets. If you have hit a retcode that is not on this list, drop it in the comments and I will explain what it means.

Top comments (4)

Collapse
 
stratcorealpha profile image
Arnold Holm •

I would keep TRADE_RETCODE_DONE_PARTIAL as a separate outcome rather than treating everything outside DONE and PLACED as a rejection. With IOC, a partial fill can leave real exposure. Retrying the full requested volume can then add more than intended. Would you add a partial-fill case and compare the filled volume with the requested volume before deciding whether anything remains to send?

Collapse
 
omaisali profile image
Omais Ali •

Yes, that is a real gap in the snippet. TRADE_RETCODE_DONE_PARTIAL should be its own branch. I would read trade.ResultVolume() and compare it with the requested volume, treat the filled part as a live position straight away so its stop and take profit are set, and then decide on the remainder separately. Resending the full original size is exactly how an EA ends up with more exposure than planned. For IOC fills I usually send only requested minus filled, normalized to the volume step, and skip it if that is below the symbol minimum. Thanks for pointing it out.

Collapse
 
stratcorealpha profile image
Arnold Holm •

I would make sending that remainder a fresh decision, rather than an automatic retry. The partial fill may already use the current risk allowance, or the entry condition may have changed before the next request. A test where the first fill succeeds but the remaining size is no longer allowed would check that the EA keeps the filled exposure without adding more.

Thread Thread
 
omaisali profile image
Omais Ali •

Agreed, that is the safer design. The remainder should go back through the same checks as a new entry: current risk allowance including the filled part, spread, and whether the signal is still valid. Your test case is a good one to keep permanently, since it is exactly the situation where an automatic retry would double the exposure without anyone noticing.