Recover MT5 EA state from account facts, not old memory
Key takeaways
- When an EA reloads, process variables such as
lastBarTime = 0reset. Server-side positions, pending orders, and deals may remain, so read server state first and enable new requests later. - Persist the last completed signal boundary separately from an in-flight request. If the terminal stopped during an ambiguous request, reconciliation—not automatic resubmission—is the safe next action.
- A Magic Number is a namespace, not a distributed lock. Keep one active runtime across local terminals and VPS hosting, and fail closed when account state or runtime ownership is unexplained.
How to Forward-Test an MT5 EA on a Demo Account defined a recovery requirement: after restart or reconnect, an EA must recognize existing account state without repeating an entry. This lab turns that requirement into a runtime model.
The goal is not “resume trading automatically under every failure.” A safer and testable goal is: enable new signal processing only when the current account state has a complete explanation; otherwise stop for review.
Four kinds of state must agree after recovery
EA memory and server-side account state have different lifetimes. Reloading an EA or terminal initializes program globals again, while an accepted order, a deal, or an open position can remain on the trade server. A network-only outage may leave the EA process running, but account state may change while the terminal is offline.
| State source | Examples | Recovery treatment |
|---|---|---|
| Process memory | lastBarTime, requestPending, local arrays | Rebuild; never treat as durable evidence |
| Current account | Open positions, pending orders, SL/TP | Query again as current server facts |
| Trade history | Orders, deals, position identifiers | Use to resolve an ambiguous in-flight request |
| Durable marker | Completed bar, in-flight bar, runtime owner | Scope narrowly and compare with server facts |
The recovery order is current account → trade history → durable markers → new memory cache. A durable marker does not outrank the server. It may say that a request began even when the request never reached the server.
The official position-properties reference opens in a new tab distinguishes POSITION_TICKET from POSITION_IDENTIFIER. The identifier stays with the position life cycle and appears as ORDER_POSITION_ID and DEAL_POSITION_ID on related records. In netting mode, service operations or a reversal can change a ticket, so a long-lived ledger should retain the identifier and related history rather than only one ticket.
Put a fail-closed state machine before entry logic
A restart-aware EA needs more than an on/off flag. Three runtime states form a useful minimum.
| Runtime state | Meaning | New requests |
|---|---|---|
RECOVERING | Checking connection, permissions, exposure, orders, and markers | Blocked |
READY | Every declared invariant holds and no ambiguous request remains | Allowed only at a signal boundary |
LOCKED | Duplicate runtime, unknown exposure, or unresolved in-flight request found | Blocked until reviewed |
Keep the control flow obvious enough to audit from a log.
OnInit
-> start in RECOVERING
-> record the Magic Number and runtime identity
OnTick
-> if disconnected: enter RECOVERING and return
-> on the first connected event: reconcile and return
-> if reconciliation passes: READY; otherwise: LOCKED
-> if state is not READY: do not evaluate an entry
-> if READY: check the new-bar boundary and durable markers once
OnTradeTransaction
-> record identifiers and result briefly
-> set needsReconciliation = true
-> rebuild the account snapshot at the next safe event boundaryTerminalInfoInteger(TERMINAL_CONNECTED) reports a trade-server connection. The official terminal-properties table opens in a new tab lists this separately from TERMINAL_TRADE_ALLOWED. Account permissions such as ACCOUNT_TRADE_ALLOWED and ACCOUNT_TRADE_EXPERT are separate again. A connected terminal is not necessarily an EA that can submit a request.
Do not trade on the first tick that reveals a restored connection. Use that event only to move through RECOVERING, capture a snapshot, and return. A later event can confirm that the state is still READY. This creates a visible log boundary between recovery and entry processing.
Reconcile both open positions and pending orders
One call to PositionSelect(_Symbol) is not a complete inventory. A hedging account can hold multiple positions on the same symbol, and pending orders do not belong to the position list. The official trade-functions reference opens in a new tab provides separate position and order iterators.
The following is a shortened read-only inventory helper, not a complete trading EA.
struct ExposureSnapshot
{
int ownedPositions;
int ownedPendingOrders;
double ownedVolume;
bool missingStopLoss;
bool foreignPositionOnSymbol;
};
bool ReadExposure(ExposureSnapshot &snapshot)
{
ZeroMemory(snapshot);
for(int i=0; i<PositionsTotal(); i++)
{
ulong ticket=PositionGetTicket(i);
if(ticket==0)
return false;
string symbol=PositionGetString(POSITION_SYMBOL);
long magic=PositionGetInteger(POSITION_MAGIC);
if(symbol!=_Symbol)
continue;
if(magic!=(long)MagicNumber)
{
snapshot.foreignPositionOnSymbol=true;
continue;
}
snapshot.ownedPositions++;
snapshot.ownedVolume+=PositionGetDouble(POSITION_VOLUME);
if(PositionGetDouble(POSITION_SL)<=0.0)
snapshot.missingStopLoss=true;
}
for(int i=0; i<OrdersTotal(); i++)
{
ulong ticket=OrderGetTicket(i);
if(ticket==0)
return false;
if(OrderGetString(ORDER_SYMBOL)==_Symbol &&
OrderGetInteger(ORDER_MAGIC)==(long)MagicNumber)
snapshot.ownedPendingOrders++;
}
return true;
}An inventory becomes a reconciliation only after it is compared with declared strategy invariants. For the market-order learning EA used in this series, a conservative policy could require all of the following:
- zero or one owned position;
- zero pending orders, because this EA does not create them;
- allowed direction and volume, with required SL/TP present;
- no automatically inferred ownership of a foreign position on the same netting symbol;
- a history lookup whenever an in-flight marker remains.
A different Magic Number does not always make a same-symbol position irrelevant. Netting accounts keep one position per symbol, potentially reflecting several deals. Check ACCOUNT_MARGIN_MODE in the official account-properties reference opens in a new tab. If multiple strategies share one netting symbol, ownership can become ambiguous; separating the strategy by account or symbol produces a cleaner operational boundary.
Recovery code should not silently close an unknown position or rewrite its protection. Record the ticket, identifier, volume, SL/TP, and Magic Number at the moment of conflict, then lock new requests. Preserving the evidence is more useful than forcing the account into an assumed state.
Persist completed and in-flight bars separately
The learning EA uses datetime lastBarTime = 0; in process memory. If the EA restarts after acting on a bar, that variable returns to zero and the current bar can look unprocessed.
An existing position may block another entry, but that is not a complete idempotency rule. The earlier trade may already have closed within the same bar, the server response may have been lost, or only a pending order may remain. “No current position” does not prove that the signal boundary was never processed.
Use at least two durable markers.
| Marker | Meaning | Behaviour after restart |
|---|---|---|
last_processed_bar | Signal boundary fully reconciled | Never evaluate that bar again |
inflight_bar | Request started but final outcome not committed | Never resend automatically; reconcile or lock |
Client-terminal global variables are different from MQL5 program globals. The official terminal-global reference opens in a new tab states that they survive in the terminal and are shared by MQL5 programs inside that client terminal. This shortened helper demonstrates key scoping and the cold-start rule.
datetime lastProcessedBar=0;
string processedKey;
string inflightKey;
string StatePrefix()
{
return StringFormat("Permact.%I64d.%s.%s.%d.%I64u",
AccountInfoInteger(ACCOUNT_LOGIN),
AccountInfoString(ACCOUNT_SERVER),
_Symbol,
(int)_Period,
MagicNumber);
}
bool LoadProcessedBar()
{
processedKey=StatePrefix()+".last_processed_bar";
inflightKey=StatePrefix()+".inflight_bar";
if(!GlobalVariableCheck(processedKey))
{
datetime currentBar=iTime(_Symbol,_Period,0);
if(currentBar==0 || GlobalVariableSet(processedKey,(double)currentBar)==0)
return false;
GlobalVariablesFlush();
lastProcessedBar=currentBar; // consume the current bar on first attach
return true;
}
double stored=0.0;
if(!GlobalVariableGet(processedKey,stored))
return false;
lastProcessedBar=(datetime)(long)stored;
return true;
}Scope a key by account login, server, symbol, timeframe, and Magic Number at minimum. The same Magic Number on a different server or timeframe is a different runtime state. Mask full account identifiers in exported logs and screenshots, or replace them with an observation ID.
On first attach, consuming the current bar is a fail-closed cold-start policy. The EA waits for the next new bar instead of entering late from a bar that began before the EA acquired state.
Before sending a request, persist inflight_bar and call GlobalVariablesFlush(). Only after account and history reconciliation confirms the outcome should the EA advance last_processed_bar and clear the in-flight marker. The official flush reference opens in a new tab describes forced disk persistence for terminal globals that might otherwise be lost during a sudden machine failure.
The ordering defines what each crash window means.
| Failure window | Possible evidence | Recovery policy |
|---|---|---|
| Before in-flight write | No request started | A later normal evaluation is possible |
| After in-flight write, before send | Marker exists but no server request | Lock rather than guess and resend |
| After send, before response | Order, deal, or position may exist | Reconcile current state and history |
| After server completion, before completion marker | Fill exists while only in-flight remains | Match deal and position identifier |
| After completion marker and in-flight clear | Processing committed | Do not evaluate the bar again |
The terminal file and the broker request cannot participate in one atomic transaction. This design deliberately prefers a missed signal in some crash windows over duplicate exposure caused by an automatic resend. Describe the guarantee as at-most-once submission with explicit reconciliation, not exactly-once execution.
Keep OnTradeTransaction short and evidence-focused
An immediate OrderSend() result is not the entire request life cycle. One request can lead to order creation, deals, history movement, and position changes.
The official OnTradeTransaction reference opens in a new tab warns that transaction arrival order is not guaranteed and that a slow handler can allow older events to be displaced from the 1,024-item queue. Record identifiers quickly and schedule a complete snapshot for a later safe boundary.
bool needsReconciliation=false;
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
PrintFormat("trade_tx type=%s order=%I64u deal=%I64u position=%I64u symbol=%s",
EnumToString(trans.type),
trans.order,
trans.deal,
trans.position,
trans.symbol);
if(trans.type==TRADE_TRANSACTION_REQUEST)
PrintFormat("trade_request magic=%I64u symbol=%s retcode=%u order=%I64u deal=%I64u",
request.magic,
request.symbol,
result.retcode,
result.order,
result.deal);
if(trans.symbol==_Symbol ||
(trans.type==TRADE_TRANSACTION_REQUEST && request.symbol==_Symbol))
needsReconciliation=true;
}Interpret the additional request and result fields only for TRADE_TRANSACTION_REQUEST. At a later OnTick or OnTimer boundary, enumerate positions and orders again. A final server snapshot is stronger evidence than a memory model assembled by assuming event order.
If an in-flight marker remains with no current position or order, query trade history. The official HistorySelect reference opens in a new tab creates order and deal lists for a server-time interval. Narrow candidates using symbol, Magic Number, time, volume, and DEAL_POSITION_ID; the deal-properties table opens in a new tab documents DEAL_MAGIC and DEAL_POSITION_ID.
A deal with the same Magic Number around the same time is not automatically proof of one signal. A unique correlation ID in the request comment or an external ledger provides stronger matching, although broker handling of comments can vary. Preserve server tickets and history alongside any application-level ID.
A Magic Number is not a runtime lock
If the same EA runs on a local terminal and a VPS, both processes can evaluate the same bar and submit with the same Magic Number. The number classifies requests and positions; it does not prevent another process from sending.
Inside one client terminal, GlobalVariableSetOnCondition() offers an atomic compare-and-set. The official reference opens in a new tab explicitly describes mutex-style coordination among EAs in the same terminal. A local machine and a remote VPS do not share that terminal-global store.
Use a controlled single-runtime migration procedure.
- Block new entries on the current runtime.
- Record positions, pending orders, last completed bar, and in-flight state.
- Confirm from logs that the old EA is actually stopped.
- Start the target VPS or terminal in
RECOVERING. - Match account, server, symbol, timeframe, Inputs, and Magic Number.
- Enter
READYonly after reconciliation, with no request on the first recovery tick.
Active/passive high availability requires an external lease, a fencing token, expiry rules, and a clock policy. A heartbeat file or shared Magic Number alone cannot prevent split-brain order submission across separate terminals.
Make the recovery decision table explicit
A boolean Reconcile() == false does not explain why the EA stopped. Emit a structured result for every invariant.
| Check | READY condition | LOCKED example |
|---|---|---|
| Connection and permissions | Server connected; terminal and account permit the EA | Flapping connection or permission mismatch |
| Runtime ownership | Exactly one active runtime | Local and VPS heartbeats overlap |
| Current positions | Count, side, volume, and SL/TP fit the strategy | Unknown position or missing required stop |
| Pending orders | Types and count fit the strategy | Pending order exists for a market-only EA |
| Completed bar | Key scope matches and timestamp is not in the future | Marker reused across server or timeframe |
| In-flight request | None, or outcome resolved from history | Request outcome cannot be found |
| Trade history | Orders, deals, and identifier form one explanation | Volume or ownership remains ambiguous |
Keep secrets and full account numbers out of logs. A useful recovery record includes:
observation_id / EA version / source commit
terminal build / server alias / account mode
symbol / timeframe / Magic Number / Inputs hash
runtime state before -> after
owned positions / pending orders / volume / SL/TP
last_processed_bar / inflight_bar
matched order / deal / position identifier
lock reason / operator decision / next replay testIf a code release changes an invariant or key format, add a state-schema version. Migrate the old marker explicitly or lock; do not let new code silently reinterpret old state.
Inject each failure window on demo
One successful startup is not a recovery test. Use separate observation IDs to exercise these windows on an isolated demo account after completing a reproducible Strategy Tester run.
| Test | Injection point | Passing evidence |
|---|---|---|
| Reattach EA | Mid-bar with no position | Current bar is consumed; EA waits for the next bar |
| Restart with position | After server-side protection is visible | Same ticket/identifier recognized; no duplicate entry |
| Reconnect | First tick after quote outage | RECOVERING logged; request delayed beyond recovery tick |
| Ambiguous request | Stop after in-flight persistence | No automatic resend; account and history reconciled |
| Pending order remains | Restart after order acceptance | Order found separately from positions |
| Local-to-VPS move | Stop source, then start target | No overlapping active interval; ownership transfer logged |
| Accidental double attach | Place the EA on two charts | Second runtime cannot enter READY |
Begin without exposure. Tests involving open positions come later, after the EA's protection and stop procedure are understood. Do not use a live account for intentional network cuts or forced-shutdown experiments.
Profit is not a passing criterion. For each signal, connect evaluation count, submission count, order, deal, position change, and completion marker in one evidence chain with no unexplained exposure.
Frequently asked questions
Is persisting lastBarTime enough?
No. Persisting before send can skip a signal after failure; persisting after send can allow a duplicate after an ambiguous response. Track completed and in-flight boundaries separately, then reconcile server state and history.
Can a Magic Number recover ownership after restart?
It helps narrow the search but does not prove the whole ownership story. Include symbol, account mode, order and deal history, position identifier, EA version, and Inputs. Sharing a netting symbol across strategies can remain ambiguous even with separate Magic Numbers.
Does reconnect call OnInit again?
Reloading the EA or terminal and restoring only a network connection are different events. Do not make safety depend on one callback being repeated. Detect the connection transition at event boundaries and return to RECOVERING explicitly.
The in-flight marker remains, but history is empty. Can the EA resend?
Do not infer that automatically. The shutdown may have happened before send, but history synchronization or the selected time range may also be incomplete. Lock the observation, preserve evidence, and resume under a new test ID after the cause is known.
Can a VPS take over automatically when the local runtime fails?
Automatic takeover without an external lease and fencing can create split brain, where both runtimes believe they are active. This lab uses controlled manual migration and one active runtime.
Risk note
Recovery logic is a software control intended to reduce duplicate requests and operational ambiguity. It cannot remove network, broker-server, liquidity, partial-fill, venue, operating-system, or human risk, and it cannot guarantee exactly-once execution.
The snippets in this article explain a design; they are not a complete EA. This lab does not recommend a broker, instrument, parameter set, or trade. Leveraged products can produce rapid and substantial losses. Validate invariants, evidence, and stop procedures in Strategy Tester and an isolated demo environment.
Up next
Continue with Build a Testable MT5 EA with AI: MQL5 Specs and Prompts for Codex or Claude. Instead of asking AI to “make a profitable strategy,” the lab begins with a human-written specification: market hypothesis, instrument and timeframe, entry and exit rules, position sizing and loss limits, prohibited behaviour, recovery and logging requirements, plus pass and stop criteria for backtests and demo forward tests.
From that specification, AI can help implement MQL5, generate test scenarios, and challenge failure cases and overfitting risk. The human remains responsible for deciding whether to adopt, revise, or reject the strategy from reproducible evidence.