The MQL5 reference page for `PositionClose()` is accurate. It documents the signature, the parameters, and the return type. What it does not tell you is that `true` does not mean the position is closed — it means the close request passed an internal validity check. We have worked on rescue projects where that gap was the entire root cause: a developer who read the documentation, implemented every function correctly by every visible standard, and shipped an EA that silently double-exposed the account in live conditions while passing every backtest.
The MQL5 CTradeAPI documentation covers what you need to call functions correctly. It does not cover what happens between calling a function and confirming the result. That gap is where production EAs fail.
What the Documentation Covers — and Where It Stops
The official reference documents `PositionClose()` accurately. Both overloads — by symbol and by ticket — are described. The return value is documented as: `true — successful check of the basic structures`.
That phrase is the problem. “Successful check of the basic structures” means the request passed CTradeAPI’s internal pre-validation — symbol exists, deviation parameter is valid, the request structure is well-formed. It has nothing to do with whether the position was actually closed. To know the execution outcome, you call `ResultRetcode()`. The documentation says this, in a note below the return value section.
Developers porting from MQL4 do not read that note carefully. MQL4’s `OrderClose()` was synchronous: when it returned `true`, the position was closed, the account reflected the change, and the next line of code ran in a settled state. Developers internalize that contract across years of MQL4 development. When they move to MQL5 and see `PositionClose()` return `true`, they apply the same contract. The documentation does not contradict them loudly enough.
`PositionClose()` Returns `true` While the Position Is Still Open
`PositionClose()` returning `true` — and `ResultRetcode()` returning `TRADE_RETCODE_DONE` — means the trade server accepted the close request. The position is not yet closed. In rescue project diagnostics across ECN brokers, the position remains visible via `PositionSelect()` for 300–800ms after `PositionClose()` returns `TRADE_RETCODE_DONE`, while the broker processes the execution and confirms the fill. That window does not exist in Strategy Tester, which is precisely why the problem passes every backtest.
The naive pattern looks like this:
if(trade.PositionClose(_Symbol))
{
// Assume closed — run next logic
OpenNewPosition();
}

What we diagnose in rescue projects: the EA calls `PositionClose()`, gets `TRADE_RETCODE_DONE`, and immediately proceeds to open logic. That logic calls `PositionSelect(_Symbol)` to check whether a position is open before placing a new one. During the async gap, the old position is still present — `PositionSelect()` returns `true`. The EA concludes no position exists in the intended direction (or that the opposing leg needs to be placed), opens a new position, and then the original close confirms 400ms later. The account now holds double exposure. Neither the entry log nor the backtest shows any anomaly.
The fix is not to add a `Sleep()` call — that blocks the terminal and creates different problems. The fix is deferred verification: set a flag after calling `PositionClose()`, and verify the position is actually gone on the next `OnTick()` execution before running any dependent logic.
// OnTick() — deferred verification pattern
if(m_closePending)
{
if(!PositionSelect(_Symbol))
{
m_closePending = false;
// Position confirmed closed — now safe to proceed
}
return; // Do not run entry logic while close is pending
}
`OnTradeTransaction` Timing and the Async Confirmation Race
The logical response to the problem above is to validate position state inside `OnTradeTransaction()`, which fires when a deal is confirmed. The reasoning is correct — but the timing is not.
`OnTradeTransaction` fires on `TRADE_TRANSACTION_DEAL_ADD` immediately when a closing deal is recorded. At that moment, the trade server has recorded the deal, but it has not necessarily finished updating the position registry. `PositionSelect(_Symbol)` called inside `OnTradeTransaction` for a closing deal can return `true` — position still showing — even though the deal that removed it was just the trigger for the event.
The pattern we see in MT4→MT5 migration projects:
void OnTradeTransaction(const MqlTradeTransaction &trans, ...)
{
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
{
if(!PositionSelect(_Symbol))
{
RunCleanup(); // Position appears closed — run cleanup
}
}
}
This fails in two ways. First, `PositionSelect()` inside the transaction handler sometimes returns `true` (position still present) and `RunCleanup()` never fires. Second — and more damaging — `OnTradeTransaction` can fire multiple times for a single deal sequence. If `RunCleanup()` fires on the first transaction and a second transaction fires before the position state settles, the cleanup runs twice, or a guard flag that was cleared by the first run prevents the second from executing, leaving state corrupt.
The production-safe pattern: use `OnTradeTransaction` only to set a flag. Defer all state reads and logic to `OnTick()`.
bool m_transactionReceived = false;
void OnTradeTransaction(const MqlTradeTransaction &trans, ...)
{
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
m_transactionReceived = true; // Flag only — no state reads here
}
void OnTick()
{
if(m_transactionReceived)
{
m_transactionReceived = false;
if(!PositionSelect(_Symbol))
RunCleanup(); // Position state settled — safe to read
}
}
Four `TRADE_RETCODE` Values That Require Different Response Logic
MQL4 developers arrive at CTradeAPI expecting to check error codes explicitly and write their own retry loops. CTradeAPI handles some error paths internally — specifically, it resubmits automatically on `TRADE_RETCODE_REQUOTE` within the deviation configured via `SetDeviationInPoints()`. Developers who don’t know this either add a manual retry loop on top of the internal one (doubling request volume), or assume “CTradeAPI handles everything” and add no retry logic at all for conditions where they do need to intervene.
Four retcodes, four different correct responses:
| RETCODE | What the Server Means | Correct Response |
|---|---|---|
| `TRADE_RETCODE_DONE` | Request accepted | Do not assume execution. Verify position/order state independently on the next tick. |
| `TRADE_RETCODE_REQUOTE` | Price moved past deviation | Do not add a retry loop. CTradeAPI retries internally. If REQUOTE is returned after internal retry, treat it as DONE: set your pending flag and verify close state on the next tick. |
| `TRADE_RETCODE_REJECTED` / `TRADE_RETCODE_ERROR` | Server rejected the request | Log, alert, halt. Do not retry — the rejection condition will not self-resolve. Investigate before the next trade. |
| `TRADE_RETCODE_TIMEOUT` | Trade server did not respond | Verify state before retrying. The order may have filled without sending a response. Query position and order state independently before treating this as a failed request. |

The `TRADE_RETCODE_TIMEOUT` case is the most dangerous. Treating timeout as “the order didn’t go through” and retrying blindly can result in duplicate fills — the order went through, the response was lost, and the retry submits a second order against a position that already exists.
A Production-Safe `PositionClose` Pattern
Combining the three patterns above: deferred verification after close, flag-only `OnTradeTransaction`, and correct retcode routing.
#include <Trade\Trade.mqh>
CTrade trade;
bool m_closePending = false;
bool m_transactionReceived = false;
// Call this to initiate a close
void RequestClose()
{
if(!trade.PositionClose(_Symbol))
{
printf("%s: PositionClose failed pre-validation | Retcode=%u",
__FUNCTION__, trade.ResultRetcode());
return;
}
uint retcode = trade.ResultRetcode();
if(retcode == TRADE_RETCODE_DONE)
{
m_closePending = true; // Request accepted — verify on next tick
}
else if(retcode == TRADE_RETCODE_REQUOTE)
{
// CTradeAPI retries internally on REQUOTE — if this retcode is still
// returned, the internal retry did not resolve within the deviation budget.
// Set the pending flag: tick-level verification will confirm actual state.
m_closePending = true;
printf("%s: REQUOTE returned as final retcode — verifying on next tick | Retcode=%u",
__FUNCTION__, retcode);
}
else
{
printf("%s: Close rejected | Retcode=%u Description=%s",
__FUNCTION__, retcode, trade.ResultRetcodeDescription());
// Alert and halt — do not retry
}
}
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
m_transactionReceived = true; // Flag only — read state in OnTick
}
void OnTick()
{
// Check transaction flag first — position state is settled by tick time
if(m_transactionReceived)
{
m_transactionReceived = false;
if(m_closePending && !PositionSelect(_Symbol))
{
m_closePending = false;
RunCleanup();
}
}
}
This pattern survives the three failure modes in sequence: the `DONE` retcode does not trigger immediate logic, `OnTradeTransaction` does not read position state, and the `OnTick()` guard confirms actual closure before proceeding. The REQUOTE branch no longer silently exits without tracking state — if CTradeAPI’s internal retry exhausts the deviation budget and returns REQUOTE to the caller, the pending flag ensures tick-level verification runs regardless of the final retcode.
This is one of the four most common patterns we diagnose in MT5 EA rescue projects. The others — async order state after `OrderSend()`, `OnTimer()` interference with `OnTick()` trade logic, and static variable reset on terminal reconnect — follow the same structure: code that is correct by documentation standards and silent under live conditions.
If your production EA is showing any of these symptoms — double exposure, failed closes that pass every backtest, or state corruption after reconnect — our free quote form is the starting point. We diagnose production MQL5 failures at the code level and document every root cause.
barmenteros FX has been building and rescuing production MQL4/MQL5 Expert Advisors since 2011. Every pattern in this article comes from real client projects — anonymized but not invented.


Leave a Reply