CTrade::PositionClose() — The Return Value Contract and What It Costs in Production
If you are migrating from MQL4 or debugging a position management bug you cannot reproduce in Strategy Tester, read this before changing anything else.
What the Documentation Actually Says
The MQL5 reference for CTrade::PositionClose() is technically accurate. Both overloads are listed. The return value is documented as a boolean. What the documentation does not state plainly is what true actually means in terms of account state: the request passed internal validation and was sent to the broker. Nothing more.
This is distinct from the old MQL4 contract. OrderClose() in MQL4 was synchronous — when it returned true , the order was settled and the next line of code ran against a consistent account state. Developers who built systems on that contract and are now porting to MT5 carry the assumption forward, often without realizing it.
The Async Gap in Live Conditions
When PositionClose() returns TRADE_RETCODE_DONE , the close request has been acknowledged. The broker confirmation is pending. On ECN brokers under normal load, the time between TRADE_RETCODE_DONE and the position actually disappearing from PositionSelect() is approximately 300–800ms.
Strategy Tester does not replicate this gap. Every backtest will pass cleanly. The failure surfaces at market open on the first live day.
The practical consequence: if the next line of code after PositionClose() calls PositionSelect() to check account state before running entry logic, it will find the old position still present. If entry logic is condition-gated on "no open position," it will either skip the entry or open a second position alongside the one being closed. The account briefly holds double exposure with no error in the log.
Here is the naive pattern that produces this:
The Deferred Verification Pattern
The correct pattern defers any logic that depends on a closed account state until position removal is confirmed on the next tick:
The flag blocks all dependent logic until PositionSelect() returns false , confirming actual removal. No sleep, no arbitrary delays — just a state check on the next tick.
OnTradeTransaction Timing
OnTradeTransaction fires as part of the confirmation sequence, but it fires before the position registry has fully updated. Reading PositionSelect() inside OnTradeTransaction will return stale state in the gap period. Use OnTradeTransaction to set flag state only — never to read position state.
Handling Four Return Codes Correctly
Not all retcodes from PositionClose() are equivalent:
- TRADE_RETCODE_DONE — Request accepted. Apply deferred verification.
- TRADE_RETCODE_REQUOTE — Price changed during request. Retry immediately using the current bid or ask.
- TRADE_RETCODE_REJECTED — Request was invalid. Check symbol, margin requirements, and stop levels before retrying.
- TRADE_RETCODE_TIMEOUT — No confirmation received. This is the dangerous case: the order may have filled and the confirmation was lost in transit. Always verify position state independently before retrying. A blind retry on a TIMEOUT that filled will open a second position.
What This Looks Like in a Production EA
The full pattern combines all of the above: ClosePosition() sets the pending flag, OnTick() checks and clears it after confirming removal, and retcode handling branches to the correct response for each case.
This pattern is not complex. The documentation describes the return contract accurately. The gap between documentation and production reality is in how the migration from MQL4 carries a synchronous assumption into an asynchronous execution environment.
We see this pattern causing account state bugs in roughly a third of the MT5 EA rescue work we handle each year. It passes every backtest. It fails at market open. The fix is fewer than 20 lines.


