Position vs order in MQL5: why your close loop misses trades
Two loops that look almost identical in MQL5 can behave completely differently: one closes every position, the other closes every second one and leaves the rest running. The problem is rarely the closing call itself — it is how you enumerate what needs closing. Here are the ways that enumeration goes wrong, and the code that fixes each.
Orders and positions are not the same object
In MQL4, an order *was* your trade. OrdersTotal() and OrderSelect() were how you found open trades, and OrderClose() shut them.
MQL5 splits that into two lists:
- Orders — requests that have been sent and are waiting to be filled. Market orders pass through this state in milliseconds; pending orders sit here until triggered or cancelled.
OrdersTotal()counts these. - Positions — the resulting net exposure.
PositionsTotal(),PositionGetTicket()and friends.
So a loop over OrdersTotal() looking for trades will find nothing on most accounts, and will find your limit orders on the ones where it finds anything at all. A straight port of an MQL4 close loop will not compile. If you ported it by renaming functions in a hurry, check that OrdersTotal() became PositionsTotal(). That one substitution is behind a lot of this.
Symptom: exactly half your positions survive
The more interesting bug compiles cleanly.
// Compiles. Skips roughly every second position.
void CloseAllWrong()
{
for(int i = 0; i < PositionsTotal(); i++)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) == _Symbol)
trade.PositionClose(ticket);
}
}
Trace it with three positions on the chart symbol, [A, B, C]:
i = 0→ ticket A, closed. The list is now [B, C].i = 1→ index 1 is now C, closed. The list is [B].i = 2→2 < PositionsTotal()is false. Loop ends. B is still open.
The index is a moving target because the list shrinks underneath you. Note also that PositionsTotal() is re-evaluated on every iteration, so it never produces an out-of-range error — it just quietly stops. With one position you never notice. With two you never notice. With five you close one and leave four.
The fix: snapshot the tickets, then close
Do the enumeration in one pass and the closing in another. Nothing you do in the second pass can disturb the first.
#include <Trade\Trade.mqh>
CTrade trade;
input ulong InpMagic = 20240101;
void CloseAllMine()
{
ulong tickets[];
int total = PositionsTotal();
for(int i = 0; i < total; i++)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol)
continue;
if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
continue;
int n = ArraySize(tickets);
ArrayResize(tickets, n + 1);
tickets[n] = ticket;
}
for(int i = 0; i < ArraySize(tickets); i++)
{
if(!trade.PositionClose(tickets[i]))
Print("close failed ticket=", tickets[i],
" ret=", trade.ResultRetcode(), " ",
trade.ResultRetcodeDescription());
}
}
PositionGetTicket(i) both selects the position and returns its ticket, so the PositionGetString and PositionGetInteger calls straight after it refer to the right one. Filtering by magic matters on a hedging account, where a manual trade on the same symbol would otherwise get swept up.
Symptom: only one position ever closes
// Hedging account: touches exactly one position per call
if(PositionSelect(_Symbol))
trade.PositionClose(_Symbol);
PositionSelect() takes a *symbol name*, and on a hedging account there can be several positions for one symbol. MQL5 resolves the ambiguity by picking the one with the lowest ticket. Wrapped in a while loop it will work — until one close fails, at which point it spins forever on the same ticket. PositionClose(symbol) also closes the full volume of whatever it selects, which is not what you want if the intent was per-ticket.
Iterate by ticket. PositionSelectByTicket() exists for when you already know which one you mean.
Closing is not instant
PositionClose() returns once the server has accepted the request. The local position list is updated from trade transaction events, which arrive separately. Consequences:
- Never verify a close by re-reading
PositionsTotal()and assuming a smaller number means success. Check the specific ticket withPositionSelectByTicket(). - In async mode,
PositionClose()returns before the result is known. Do not fire a second close for the same ticket in the same tick. - Never re-open immediately after closing and assume the old position is gone.
Exit handling bugs rarely announce themselves. They show up as a plausible win rate and a balance curve that does not match it — the shape you can [see a real backtest](https://mql.ranartech.com/r/6V4RG96MUmMI) of: an AUDUSD M15 EA, 565 trades, 60% winners, and -$2,517 net. Entry logic and exit logic that disagree about what is currently open will produce exactly that.
Checklist
PositionsTotal()for trades,OrdersTotal()for pending orders. Never the other way round.- Snapshot tickets before closing anything.
- Filter by symbol and magic.
- Iterate by ticket, not by symbol name.
- Verify with
PositionSelectByTicket(), not with counts.