DEV Community

Omais Ali
Omais Ali

Posted on

Closing every position your MQL5 EA owns without skipping any

Sooner or later every Expert Advisor needs a "close everything" routine. A daily loss limit is hit, the weekend is coming, or the trader presses a panic button. It sounds like five lines of code, and that is exactly why it so often ships with bugs that only show up when real money is on the line.

This tutorial builds a close routine that does not skip positions, only touches positions the EA owns, retries sensibly and tells you when something is left open.

The classic bug: looping forward while closing

Here is the version many people write first.

for(int i = 0; i < PositionsTotal(); i++)
{
   ulong ticket = PositionGetTicket(i);
   trade.PositionClose(ticket);
}
Enter fullscreen mode Exit fullscreen mode

Say there are four positions. The loop closes the one at index 0. The list shrinks, so the position that was at index 1 is now at index 0. The loop moves on to index 1 and closes what used to be index 2. You end up closing roughly half of the positions and leaving the rest open, with no error at all.

A common fix is to count down from the last index. That works, but there is a cleaner pattern that also helps with retries: take a snapshot of the tickets first, then close them one by one by ticket.

Step 1: collect the tickets this EA owns

#include <Trade\Trade.mqh>

input long InpMagic = 20261024;

CTrade trade;

int CollectMyTickets(ulong &tickets[])
{
   ArrayResize(tickets, 0);
   int total = PositionsTotal();
   for(int i = 0; i < total; i++)
   {
      ulong ticket = PositionGetTicket(i);   // also selects the position
      if(ticket == 0)
         continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;                           // not ours, leave it alone
      int n = ArraySize(tickets);
      ArrayResize(tickets, n + 1);
      tickets[n] = ticket;
   }
   return ArraySize(tickets);
}
Enter fullscreen mode Exit fullscreen mode

Nothing is closed inside this loop, so the index shifting problem cannot happen. The magic number filter matters: a trader may run several EAs or trade manually on the same account, and a panic button that closes someone else's positions is its own kind of disaster. Add a symbol filter here too if the EA should only manage its own chart symbol.

Step 2: decide which errors are worth retrying

Not every failure deserves another attempt. A requote or a moved price usually succeeds a moment later. A closed market or disabled trading will not.

bool IsRetryable(uint rc)
{
   return rc == TRADE_RETCODE_REQUOTE       ||
          rc == TRADE_RETCODE_PRICE_CHANGED ||
          rc == TRADE_RETCODE_PRICE_OFF     ||
          rc == TRADE_RETCODE_TIMEOUT       ||
          rc == TRADE_RETCODE_CONNECTION;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: close one ticket with limited retries

bool CloseTicket(ulong ticket, int maxAttempts)
{
   for(int attempt = 1; attempt <= maxAttempts; attempt++)
   {
      if(!PositionSelectByTicket(ticket))
         return true;                        // already closed, by us or by a stop

      string sym = PositionGetString(POSITION_SYMBOL);
      trade.SetTypeFillingBySymbol(sym);

      trade.PositionClose(ticket);
      uint rc = trade.ResultRetcode();

      if(rc == TRADE_RETCODE_DONE)
         return true;

      if(rc == TRADE_RETCODE_DONE_PARTIAL)
         continue;                           // remainder is still open, go again

      PrintFormat("Close %I64u attempt %d failed: %u %s",
                  ticket, attempt, rc, trade.ResultRetcodeDescription());

      if(!IsRetryable(rc))
         return false;

      Sleep(300);
   }
   return !PositionSelectByTicket(ticket);
}
Enter fullscreen mode Exit fullscreen mode

A few details are doing real work here.

  1. The position is selected again on every attempt. Between attempts it may have been closed by its own stop loss or take profit, and trying to close a ticket that no longer exists only produces confusing errors.
  2. CTrade reads a fresh price on every PositionClose call, so a retry after a requote uses the current Bid or Ask rather than the stale one.
  3. The filling mode is set per symbol, because a multi symbol EA can hold positions on symbols with different filling rules.
  4. A partial fill does not count as success. The loop goes around again and closes whatever volume is left.

Step 4: close everything and report what is left

int CloseAllMine()
{
   ulong tickets[];
   int count = CollectMyTickets(tickets);
   int failed = 0;

   for(int i = 0; i < count; i++)
   {
      if(!CloseTicket(tickets[i], 3))
         failed++;
   }

   if(failed > 0)
      PrintFormat("CloseAllMine: %d of %d positions still open", failed, count);

   return failed;
}

int OnInit()
{
   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(20);
   return INIT_SUCCEEDED;
}
Enter fullscreen mode Exit fullscreen mode

The return value is the important part. The caller now knows whether the account is actually flat. A daily loss guard, for example, should keep blocking new entries and try again on the next tick or timer event if anything is still open, instead of assuming the job is done.

Do not forget pending orders

Closing positions while leaving pending orders in place often means the EA is back in the market a few minutes later. Use the same snapshot idea with OrdersTotal, OrderGetTicket and ORDER_MAGIC, and remove each one with trade.OrderDelete(ticket). Usually it is best to delete pending orders first, so nothing new can fill while you are closing.

A note on hedging accounts

On a hedging account, if the EA holds a buy and a sell of the same symbol, trade.PositionCloseBy(ticket, oppositeTicket) closes one against the other and saves paying the spread twice. It is an optimisation, not a requirement, but worth knowing when an EA builds hedged baskets.

How to test it

On a demo account, open four or five positions with the EA's magic number and one manual position. Run the routine and check that every EA position is closed and the manual one is untouched. Then run it again when the market is closed and confirm that it stops quickly, logs TRADE_RETCODE_MARKET_CLOSED and reports the positions as still open rather than retrying forever.

Top comments (0)